@myagentroam/node 0.1.0 → 0.1.2
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/README.md +19 -2
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +58 -3
- package/dist/capabilities.js.map +1 -1
- package/dist/claude-agent-sdk.d.ts +1 -0
- package/dist/claude-agent-sdk.js +36 -2
- package/dist/claude-agent-sdk.js.map +1 -1
- package/dist/codex-app-server.d.ts +1 -0
- package/dist/codex-app-server.js +23 -5
- package/dist/codex-app-server.js.map +1 -1
- package/dist/connector.d.ts +41 -3
- package/dist/connector.js +625 -134
- package/dist/connector.js.map +1 -1
- package/dist/database.d.ts +13 -0
- package/dist/database.js +284 -239
- package/dist/database.js.map +1 -1
- package/dist/migrations/v001.d.ts +3 -0
- package/dist/migrations/v001.js +249 -0
- package/dist/migrations/v001.js.map +1 -0
- package/dist/migrations/v002.d.ts +4 -0
- package/dist/migrations/v002.js +144 -0
- package/dist/migrations/v002.js.map +1 -0
- package/dist/operational.d.ts +9 -0
- package/dist/operational.js +30 -0
- package/dist/operational.js.map +1 -1
- package/dist/workspace.d.ts +36 -8
- package/dist/workspace.js +315 -99
- package/dist/workspace.js.map +1 -1
- package/package.json +3 -2
package/dist/connector.js
CHANGED
|
@@ -8,8 +8,8 @@ import { createEnvelope, nodeCapabilitiesSchema, nodeEnvelopeSchema, runnerDefau
|
|
|
8
8
|
import { loadNodeConfig, nodeDatabasePath, saveNodeConfig } from './config.js';
|
|
9
9
|
import { NodeMetrics, nodeLog } from './operational.js';
|
|
10
10
|
import { assertLocalSqlitePath } from './storage.js';
|
|
11
|
-
import {
|
|
12
|
-
import { inspectWorkspace, listWorkspaceDirectories, listWorkspaceFiles, preflightWorkspaceUpload, readCurrentChangeDiff, readCurrentChangesSummary, readGitSummary, readRestrictedDiff, readAllowedTextFile, readWorkspaceTextFile, restoreCurrentChange,
|
|
11
|
+
import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
|
|
12
|
+
import { inspectWorkspace, isWorkspaceChangeVisible, listWorkspaceDirectories, listWorkspaceFiles, preflightWorkspaceUpload, readCurrentChangeDiff, readCurrentChangesSummary, readGitSummary, readRestrictedDiff, readAllowedTextFile, readWorkspaceTextFile, restoreCurrentChange, WorkspaceFileIndex, workspaceUploadTemporaryName } from './workspace.js';
|
|
13
13
|
import { ReliableWorkbenchEventBuffer } from './event-buffer.js';
|
|
14
14
|
import { CodexAppServerClient, codexSessionControl } from './codex-app-server.js';
|
|
15
15
|
import { ClaudeAgentSdkAdapter, denyPermission } from './claude-agent-sdk.js';
|
|
@@ -22,9 +22,16 @@ import { NodeRuntimeState } from './runtime-state.js';
|
|
|
22
22
|
import { CommandStateStore, parseCommandInvocation } from './runner-command-engine.js';
|
|
23
23
|
import { RunnerUsageReader } from './runner-usage.js';
|
|
24
24
|
const HEARTBEAT_MS = 15_000;
|
|
25
|
+
const WORKSPACE_FILE_INDEX_CACHE_LIMIT = 8;
|
|
26
|
+
const WORKSPACE_WATCH_TTL_MS = 45_000;
|
|
27
|
+
const WORKSPACE_SESSION_WATCH_INTERVAL_MS = 30_000;
|
|
28
|
+
const WORKSPACE_SESSION_CACHE_TTL_MS = 15_000;
|
|
29
|
+
const WORKSPACE_CHANGES_CACHE_TTL_MS = 5_000;
|
|
30
|
+
const CAPABILITY_REFRESH_MS = 60_000;
|
|
25
31
|
const SESSION_WATCH_INTERVAL_MS = 2_000;
|
|
26
32
|
const SESSION_WATCH_TTL_MS = 45_000;
|
|
27
33
|
const SESSION_WATCH_MAX_FAILURES = 3;
|
|
34
|
+
const SESSION_WATCH_INITIAL_TURN_LIMIT = 30;
|
|
28
35
|
const RETRY_MIN_MS = 500;
|
|
29
36
|
const RETRY_MAX_MS = 30_000;
|
|
30
37
|
const CLAUDE_APPROVAL_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -87,12 +94,17 @@ export class NodeConnector {
|
|
|
87
94
|
socket;
|
|
88
95
|
terminalSocket;
|
|
89
96
|
heartbeat;
|
|
97
|
+
capabilityRefreshTimer;
|
|
98
|
+
capabilityRefreshInFlight = false;
|
|
90
99
|
reconnectTimer;
|
|
91
100
|
terminalReconnectTimer;
|
|
92
101
|
attempts = 0;
|
|
102
|
+
terminalAttempts = 0;
|
|
93
103
|
stopped = false;
|
|
94
104
|
config;
|
|
105
|
+
capabilitiesProvided;
|
|
95
106
|
capabilities;
|
|
107
|
+
capabilityDetector;
|
|
96
108
|
configPath;
|
|
97
109
|
reconnect;
|
|
98
110
|
claudeApprovalTimeoutMs;
|
|
@@ -158,6 +170,18 @@ export class NodeConnector {
|
|
|
158
170
|
composerAttachments = new Map();
|
|
159
171
|
/** Identical refreshes share one Git scan; separate Workspaces use bounded parallelism. */
|
|
160
172
|
workspaceChangesReads = new Map();
|
|
173
|
+
workspaceChangesCache = new Map();
|
|
174
|
+
workspaceChangesVersions = new Map();
|
|
175
|
+
workspaceFileIndexes = new Map();
|
|
176
|
+
/** Tab-scoped Workspace observation leases; no state survives a Node restart. */
|
|
177
|
+
workspaceWatches = new Map();
|
|
178
|
+
workspaceWatchRevisions = new Map();
|
|
179
|
+
workspaceSessionWatchTimers = new Map();
|
|
180
|
+
workspaceSessionWatchSignatures = new Map();
|
|
181
|
+
workspaceSessionWatchReads = new Map();
|
|
182
|
+
/** Runner-native session discovery is expensive; cache only its short-lived baseline. */
|
|
183
|
+
workspaceSessionCache = new Map();
|
|
184
|
+
workspaceSessionCacheReads = new Map();
|
|
161
185
|
workspaceChangesLimiter = new AsyncLimiter(3);
|
|
162
186
|
database;
|
|
163
187
|
/** Runtime/session state is intentionally not persisted to Node SQLite. */
|
|
@@ -166,7 +190,9 @@ export class NodeConnector {
|
|
|
166
190
|
constructor(options = {}) {
|
|
167
191
|
this.config = options.config;
|
|
168
192
|
this.configPath = options.configPath;
|
|
169
|
-
this.
|
|
193
|
+
this.capabilitiesProvided = options.capabilities !== undefined;
|
|
194
|
+
this.capabilities = options.capabilities ?? unavailableCapabilities();
|
|
195
|
+
this.capabilityDetector = options.capabilityDetector ?? detectCapabilitiesAsync;
|
|
170
196
|
this.reconnect = options.reconnect ?? true;
|
|
171
197
|
this.fakeRunner = options.fakeRunner;
|
|
172
198
|
this.fakeScript = options.fakeScript;
|
|
@@ -179,7 +205,7 @@ export class NodeConnector {
|
|
|
179
205
|
this.terminalManager =
|
|
180
206
|
options.terminalManager ??
|
|
181
207
|
new TerminalManager({
|
|
182
|
-
platform: this.capabilities.platform
|
|
208
|
+
platform: this.capabilities.platform === 'windows' ? 'win32' : 'linux'
|
|
183
209
|
});
|
|
184
210
|
this.terminalManager.onSummary((summary) => this.persistTerminalSession(summary));
|
|
185
211
|
this.terminalManager.onFrame((frame) => this.publishTerminalFrame(frame));
|
|
@@ -191,12 +217,15 @@ export class NodeConnector {
|
|
|
191
217
|
this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
|
|
192
218
|
this.database.closeRunningTerminalSessions();
|
|
193
219
|
this.reconcilePersistedRuns();
|
|
220
|
+
this.startCapabilityRefresh();
|
|
194
221
|
this.connect();
|
|
195
222
|
}
|
|
196
223
|
stop() {
|
|
197
224
|
this.stopped = true;
|
|
198
225
|
if (this.heartbeat !== undefined)
|
|
199
226
|
clearInterval(this.heartbeat);
|
|
227
|
+
if (this.capabilityRefreshTimer !== undefined)
|
|
228
|
+
clearInterval(this.capabilityRefreshTimer);
|
|
200
229
|
if (this.reconnectTimer !== undefined)
|
|
201
230
|
clearTimeout(this.reconnectTimer);
|
|
202
231
|
if (this.terminalReconnectTimer !== undefined)
|
|
@@ -233,6 +262,22 @@ export class NodeConnector {
|
|
|
233
262
|
for (const upload of this.workspaceUploads.values())
|
|
234
263
|
void this.removeWorkspaceUpload(upload);
|
|
235
264
|
this.workspaceUploads.clear();
|
|
265
|
+
for (const index of this.workspaceFileIndexes.values())
|
|
266
|
+
index.dispose();
|
|
267
|
+
this.workspaceFileIndexes.clear();
|
|
268
|
+
for (const watches of this.workspaceWatches.values())
|
|
269
|
+
for (const watch of watches.values())
|
|
270
|
+
clearTimeout(watch.timer);
|
|
271
|
+
this.workspaceWatches.clear();
|
|
272
|
+
for (const timer of this.workspaceSessionWatchTimers.values())
|
|
273
|
+
clearInterval(timer);
|
|
274
|
+
this.workspaceSessionWatchTimers.clear();
|
|
275
|
+
this.workspaceSessionWatchSignatures.clear();
|
|
276
|
+
this.workspaceSessionWatchReads.clear();
|
|
277
|
+
this.workspaceSessionCache.clear();
|
|
278
|
+
this.workspaceSessionCacheReads.clear();
|
|
279
|
+
this.workspaceChangesCache.clear();
|
|
280
|
+
this.workspaceChangesVersions.clear();
|
|
236
281
|
for (const attachment of this.composerAttachments.values())
|
|
237
282
|
void rm(dirname(attachment.targetPath), { recursive: true, force: true });
|
|
238
283
|
this.composerAttachments.clear();
|
|
@@ -245,6 +290,39 @@ export class NodeConnector {
|
|
|
245
290
|
this.database?.close();
|
|
246
291
|
this.database = undefined;
|
|
247
292
|
}
|
|
293
|
+
startCapabilityRefresh() {
|
|
294
|
+
if (this.capabilityRefreshTimer !== undefined)
|
|
295
|
+
clearInterval(this.capabilityRefreshTimer);
|
|
296
|
+
if (!this.capabilitiesProvided)
|
|
297
|
+
void this.refreshCapabilities();
|
|
298
|
+
this.capabilityRefreshTimer = setInterval(() => void this.refreshCapabilities(), CAPABILITY_REFRESH_MS);
|
|
299
|
+
}
|
|
300
|
+
async refreshCapabilities() {
|
|
301
|
+
if (this.stopped || this.capabilityRefreshInFlight)
|
|
302
|
+
return;
|
|
303
|
+
this.capabilityRefreshInFlight = true;
|
|
304
|
+
let next;
|
|
305
|
+
try {
|
|
306
|
+
next = await this.capabilityDetector();
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// A failed local probe keeps Node metadata but safely disables both Runner capabilities.
|
|
310
|
+
next = {
|
|
311
|
+
...this.capabilities,
|
|
312
|
+
codex: { available: false },
|
|
313
|
+
claudeCode: { available: false }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
if (this.stopped || JSON.stringify(next) === JSON.stringify(this.capabilities))
|
|
318
|
+
return;
|
|
319
|
+
this.capabilities = next;
|
|
320
|
+
this.send('capabilities', next);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
this.capabilityRefreshInFlight = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
248
326
|
connect() {
|
|
249
327
|
const config = this.config;
|
|
250
328
|
if (config === undefined)
|
|
@@ -333,7 +411,7 @@ export class NodeConnector {
|
|
|
333
411
|
this.terminalReconnectTimer = setTimeout(() => {
|
|
334
412
|
this.terminalReconnectTimer = undefined;
|
|
335
413
|
this.connectTerminal();
|
|
336
|
-
},
|
|
414
|
+
}, nextReconnectDelay(this.terminalAttempts++));
|
|
337
415
|
}
|
|
338
416
|
handleTerminalMessage(raw) {
|
|
339
417
|
let message;
|
|
@@ -353,6 +431,9 @@ export class NodeConnector {
|
|
|
353
431
|
Object.keys(message).some((key) => key !== 'type' && key !== 'nodeId')) {
|
|
354
432
|
this.terminalSocket?.close(4002, 'TERMINAL_MESSAGE_INVALID');
|
|
355
433
|
}
|
|
434
|
+
else {
|
|
435
|
+
this.terminalAttempts = 0;
|
|
436
|
+
}
|
|
356
437
|
return;
|
|
357
438
|
}
|
|
358
439
|
if (message.type === 'attach') {
|
|
@@ -570,7 +651,7 @@ export class NodeConnector {
|
|
|
570
651
|
// available. Do not leave the local lease in CANCELLING indefinitely:
|
|
571
652
|
// an already-created, but not yet controllable, Codex run is checked
|
|
572
653
|
// again immediately after turn/start resolves.
|
|
573
|
-
if (
|
|
654
|
+
if (codexRun?.turnId === undefined && claudeRun === undefined) {
|
|
574
655
|
const run = this.runtime.getRun(runId);
|
|
575
656
|
if (run?.status === 'CANCELLING')
|
|
576
657
|
this.completeCancelledRun(runId, undefined);
|
|
@@ -612,7 +693,14 @@ export class NodeConnector {
|
|
|
612
693
|
return;
|
|
613
694
|
}
|
|
614
695
|
try {
|
|
615
|
-
const
|
|
696
|
+
const data = payload.data !== null && typeof payload.data === 'object'
|
|
697
|
+
? {
|
|
698
|
+
...payload.data,
|
|
699
|
+
__workspaceWatchSessionHash: payload.userAuthorization
|
|
700
|
+
.sessionHash
|
|
701
|
+
}
|
|
702
|
+
: payload.data;
|
|
703
|
+
const result = await this.executeNodeOperation(payload.operation, data);
|
|
616
704
|
this.send('node.response', { result }, envelope.id);
|
|
617
705
|
}
|
|
618
706
|
catch (error) {
|
|
@@ -661,6 +749,12 @@ export class NodeConnector {
|
|
|
661
749
|
throw new Error('WORKSPACE_BUSY');
|
|
662
750
|
const workspace = database.removeWorkspace(input.workspaceId);
|
|
663
751
|
this.runtime.removeWorkspace(input.workspaceId);
|
|
752
|
+
this.workspaceFileIndexes.get(workspace.id)?.dispose();
|
|
753
|
+
this.workspaceFileIndexes.delete(workspace.id);
|
|
754
|
+
this.invalidateWorkspaceChanges(workspace.id);
|
|
755
|
+
this.workspaceSessionCache.delete(workspace.id);
|
|
756
|
+
this.workspaceSessionCacheReads.delete(workspace.id);
|
|
757
|
+
this.clearWorkspaceWatches(workspace.id);
|
|
664
758
|
return { workspace };
|
|
665
759
|
}
|
|
666
760
|
if (operation === 'workspace.list')
|
|
@@ -737,6 +831,10 @@ export class NodeConnector {
|
|
|
737
831
|
typeof input.customTitle !== 'string') ||
|
|
738
832
|
(input.pinned !== undefined && typeof input.pinned !== 'boolean'))
|
|
739
833
|
throw new Error('SESSION_INVALID');
|
|
834
|
+
if (input.customTitle === null || input.customTitle?.trim().length === 0)
|
|
835
|
+
throw new Error('SESSION_TITLE_REQUIRED');
|
|
836
|
+
if (input.customTitle !== undefined && input.customTitle.trim().length > 160)
|
|
837
|
+
throw new Error('SESSION_TITLE_INVALID');
|
|
740
838
|
const current = this.runtime.getAgentSession(input.sessionId) ??
|
|
741
839
|
(await this.createMetadataProjection(input.sessionId));
|
|
742
840
|
if (current === undefined)
|
|
@@ -760,6 +858,13 @@ export class NodeConnector {
|
|
|
760
858
|
: updated)
|
|
761
859
|
};
|
|
762
860
|
}
|
|
861
|
+
if (operation === 'session.pin.move') {
|
|
862
|
+
if (typeof input.sessionId !== 'string' ||
|
|
863
|
+
(input.direction !== 'up' && input.direction !== 'down'))
|
|
864
|
+
throw new Error('SESSION_PIN_MOVE_INVALID');
|
|
865
|
+
database.movePinnedSession(input.sessionId, input.direction);
|
|
866
|
+
return { moved: true };
|
|
867
|
+
}
|
|
763
868
|
if (operation === 'session.remove') {
|
|
764
869
|
if (typeof input.sessionId !== 'string')
|
|
765
870
|
throw new Error('SESSION_INVALID');
|
|
@@ -777,8 +882,16 @@ export class NodeConnector {
|
|
|
777
882
|
throw new Error('SESSION_ACTIVE');
|
|
778
883
|
}
|
|
779
884
|
const nativeSessionIds = this.nativeSessionIdsForProjection(session);
|
|
780
|
-
for (const externalSessionId of nativeSessionIds)
|
|
781
|
-
|
|
885
|
+
for (const externalSessionId of nativeSessionIds) {
|
|
886
|
+
try {
|
|
887
|
+
await removeNativeSession(session.runner, session.cwd, externalSessionId);
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
if (!(error instanceof Error) || error.message !== 'NATIVE_TRANSCRIPT_UNAVAILABLE')
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
database.deleteSessionRecord(session.id);
|
|
782
895
|
this.forgetNativeSessionProjection(session, nativeSessionIds);
|
|
783
896
|
return { deleted: true };
|
|
784
897
|
}
|
|
@@ -882,6 +995,50 @@ export class NodeConnector {
|
|
|
882
995
|
...(typeof input.limit === 'number' ? { limit: input.limit } : {})
|
|
883
996
|
});
|
|
884
997
|
}
|
|
998
|
+
if (operation === 'workspace.watch') {
|
|
999
|
+
if (typeof input.workspaceId !== 'string' ||
|
|
1000
|
+
typeof input.watchId !== 'string' ||
|
|
1001
|
+
!/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId) ||
|
|
1002
|
+
(input.mode !== 'initial' && input.mode !== 'renew'))
|
|
1003
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
1004
|
+
const workspace = this.requireWorkspace(input.workspaceId);
|
|
1005
|
+
if (input.mode === 'renew') {
|
|
1006
|
+
const watch = this.renewWorkspaceWatch(workspace.id, this.workspaceWatchKey(input));
|
|
1007
|
+
if (watch === undefined)
|
|
1008
|
+
throw new Error('WORKSPACE_WATCH_NOT_FOUND');
|
|
1009
|
+
return { renewed: true };
|
|
1010
|
+
}
|
|
1011
|
+
const topics = this.parseWorkspaceWatchTopics(input);
|
|
1012
|
+
const snapshot = {};
|
|
1013
|
+
if (topics.sessions)
|
|
1014
|
+
snapshot.sessions = await this.executeNodeOperation('session.list', {
|
|
1015
|
+
workspaceId: workspace.id,
|
|
1016
|
+
limit: 20
|
|
1017
|
+
});
|
|
1018
|
+
if (topics.files !== undefined)
|
|
1019
|
+
snapshot.files = {
|
|
1020
|
+
files: await this.listFilesForWorkspace(workspace, topics.files)
|
|
1021
|
+
};
|
|
1022
|
+
if (topics.changes)
|
|
1023
|
+
snapshot.changes =
|
|
1024
|
+
workspace.kind === 'GIT_WORKSPACE'
|
|
1025
|
+
? await this.currentChangesForWorkspace(workspace)
|
|
1026
|
+
: null;
|
|
1027
|
+
// An initial snapshot is the observation baseline. Do not retain a
|
|
1028
|
+
// background lease when producing that baseline has failed.
|
|
1029
|
+
if (topics.files !== undefined || topics.changes)
|
|
1030
|
+
await this.workspaceFileIndex(workspace).observe();
|
|
1031
|
+
const watch = this.createWorkspaceWatch(workspace.id, input.watchId, this.workspaceWatchSessionHash(input), topics);
|
|
1032
|
+
return { watch: { expiresAt: watch.expiresAt }, snapshot };
|
|
1033
|
+
}
|
|
1034
|
+
if (operation === 'workspace.unwatch') {
|
|
1035
|
+
if (typeof input.workspaceId !== 'string' ||
|
|
1036
|
+
typeof input.watchId !== 'string' ||
|
|
1037
|
+
!/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId))
|
|
1038
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
1039
|
+
this.removeWorkspaceWatch(input.workspaceId, this.workspaceWatchKey(input));
|
|
1040
|
+
return { stopped: true };
|
|
1041
|
+
}
|
|
885
1042
|
if (operation === 'workspace.files.list') {
|
|
886
1043
|
const workspace = this.requireWorkspace(input.workspaceId);
|
|
887
1044
|
return { files: await this.listFilesForWorkspace(workspace, input) };
|
|
@@ -941,9 +1098,9 @@ export class NodeConnector {
|
|
|
941
1098
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
942
1099
|
if (this.runtime.hasActiveWorkspaceLease(workspace.id))
|
|
943
1100
|
throw new Error('WORKSPACE_BUSY');
|
|
944
|
-
await restoreCurrentChange(workspace.path, input.path, input.state
|
|
1101
|
+
await restoreCurrentChange(workspace.path, input.path, input.state);
|
|
945
1102
|
// A read started before the write must not satisfy the explicit post-write refresh.
|
|
946
|
-
this.
|
|
1103
|
+
this.invalidateWorkspaceWatchTopics(workspace.id);
|
|
947
1104
|
return { restored: true };
|
|
948
1105
|
}
|
|
949
1106
|
if (operation === 'workspace.diff') {
|
|
@@ -999,16 +1156,6 @@ export class NodeConnector {
|
|
|
999
1156
|
if (input.bootstrapOnly === true)
|
|
1000
1157
|
this.bootstrapSessionIds.add(session.id);
|
|
1001
1158
|
database.saveRunnerDefaults(configuration);
|
|
1002
|
-
if (session.externalSessionId !== null) {
|
|
1003
|
-
database.saveNativeSessionMetadata({
|
|
1004
|
-
runner: session.runner,
|
|
1005
|
-
externalSessionId: session.externalSessionId,
|
|
1006
|
-
workspaceId: session.workspaceId,
|
|
1007
|
-
model: configuration.model,
|
|
1008
|
-
effort: configuration.effort,
|
|
1009
|
-
access: configuration.access
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
1159
|
if (input.bootstrapOnly !== true)
|
|
1013
1160
|
this.emitWorkbenchEvent('session', { session: this.presentSession(session) });
|
|
1014
1161
|
return {
|
|
@@ -1144,11 +1291,14 @@ export class NodeConnector {
|
|
|
1144
1291
|
};
|
|
1145
1292
|
}
|
|
1146
1293
|
if (operation === 'session.list') {
|
|
1147
|
-
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
const
|
|
1294
|
+
const workspaceId = typeof input.workspaceId === 'string' ? input.workspaceId : undefined;
|
|
1295
|
+
// Pagination is only correct after persisted records and the current
|
|
1296
|
+
// Runner discovery have been merged. A selected Workspace therefore
|
|
1297
|
+
// performs discovery inside this authoritative list operation.
|
|
1298
|
+
const nativeSessions = workspaceId === undefined
|
|
1299
|
+
? [...this.directNativeSessions.values()]
|
|
1300
|
+
: await this.cachedWorkspaceSessions(workspaceId);
|
|
1301
|
+
const native = nativeSessions.map((session) => this.presentSession({ ...session, externalDiscovered: true }));
|
|
1152
1302
|
const projected = this.runtime
|
|
1153
1303
|
.listAgentSessions()
|
|
1154
1304
|
.filter((session) => session.nativeControl === 'MAR_MANAGED' ||
|
|
@@ -1160,17 +1310,51 @@ export class NodeConnector {
|
|
|
1160
1310
|
: {
|
|
1161
1311
|
...session,
|
|
1162
1312
|
runnerTitle: source.runnerTitle,
|
|
1313
|
+
pinnedAt: source.pinnedAt,
|
|
1314
|
+
pinOrder: source.pinOrder ?? null,
|
|
1315
|
+
externalDiscovered: true,
|
|
1163
1316
|
titleSource: session.customTitle === null ? source.titleSource : session.titleSource,
|
|
1164
1317
|
lastActivityAt: source.lastActivityAt
|
|
1165
1318
|
});
|
|
1166
1319
|
});
|
|
1167
|
-
const
|
|
1320
|
+
const merged = new Map([
|
|
1168
1321
|
...projected,
|
|
1169
1322
|
...native.filter((native) => native.externalSessionId === null ||
|
|
1170
1323
|
this.sessionProjectionForNativeRecord(native) === undefined)
|
|
1171
|
-
];
|
|
1172
|
-
|
|
1173
|
-
|
|
1324
|
+
].map((session) => [session.id, session]));
|
|
1325
|
+
if (workspaceId !== undefined) {
|
|
1326
|
+
const workspace = database.getWorkspace(workspaceId);
|
|
1327
|
+
if (workspace === undefined)
|
|
1328
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
1329
|
+
for (const record of database.listSessionRecords(workspaceId)) {
|
|
1330
|
+
if (merged.has(record.id))
|
|
1331
|
+
continue;
|
|
1332
|
+
merged.set(record.id, this.presentSession({
|
|
1333
|
+
id: record.id,
|
|
1334
|
+
nodeId: this.config?.nodeId ?? 'recorded-session',
|
|
1335
|
+
workspaceId,
|
|
1336
|
+
runner: record.runner,
|
|
1337
|
+
externalSessionId: record.id,
|
|
1338
|
+
nativeControl: 'EXTERNAL',
|
|
1339
|
+
channelToken: null,
|
|
1340
|
+
cwd: workspace.path,
|
|
1341
|
+
model: record.metadata.model,
|
|
1342
|
+
effort: record.metadata.effort,
|
|
1343
|
+
access: record.metadata.access,
|
|
1344
|
+
customTitle: record.metadata.customTitle,
|
|
1345
|
+
runnerTitle: record.metadata.runnerTitle,
|
|
1346
|
+
pinnedAt: record.pinOrder === null ? null : 1,
|
|
1347
|
+
pinOrder: record.pinOrder,
|
|
1348
|
+
externalDiscovered: false,
|
|
1349
|
+
titleSource: record.metadata.customTitle === null ? 'RUNNER' : 'CUSTOM',
|
|
1350
|
+
lastActivityAt: 0,
|
|
1351
|
+
createdAt: 0
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const allSessions = [...merged.values()];
|
|
1356
|
+
return sessionPage(workspaceId !== undefined
|
|
1357
|
+
? allSessions.filter((session) => session.workspaceId === workspaceId)
|
|
1174
1358
|
: allSessions, input);
|
|
1175
1359
|
}
|
|
1176
1360
|
if (operation === 'session.discover') {
|
|
@@ -1220,18 +1404,26 @@ export class NodeConnector {
|
|
|
1220
1404
|
(await this.resolveDirectNativeSession(input.sessionId));
|
|
1221
1405
|
if (value === undefined)
|
|
1222
1406
|
throw new Error('SESSION_NOT_FOUND');
|
|
1223
|
-
await this.refreshCodexExternalActivity(value);
|
|
1224
1407
|
return { session: this.presentSession(value) };
|
|
1225
1408
|
}
|
|
1226
1409
|
if (operation === 'session.watch') {
|
|
1227
|
-
if (typeof input.sessionId !== 'string'
|
|
1410
|
+
if (typeof input.sessionId !== 'string' ||
|
|
1411
|
+
(input.mode !== 'initial' && input.mode !== 'renew'))
|
|
1228
1412
|
throw new Error('SESSION_INVALID');
|
|
1229
1413
|
const session = this.runtime.getAgentSession(input.sessionId) ??
|
|
1230
1414
|
(await this.resolveDirectNativeSession(input.sessionId));
|
|
1231
1415
|
if (session === undefined || session.externalSessionId === null)
|
|
1232
1416
|
throw new Error('SESSION_NOT_FOUND');
|
|
1417
|
+
if (input.mode === 'renew') {
|
|
1418
|
+
const watch = this.renewNativeSessionWatch(session);
|
|
1419
|
+
if (watch === undefined)
|
|
1420
|
+
throw new Error('SESSION_WATCH_NOT_FOUND');
|
|
1421
|
+
return { renewed: true };
|
|
1422
|
+
}
|
|
1233
1423
|
const watch = this.watchNativeSession(session);
|
|
1234
|
-
const snapshot = watch
|
|
1424
|
+
const snapshot = watch !== undefined
|
|
1425
|
+
? await this.refreshWatchedSession(session.id, SESSION_WATCH_INITIAL_TURN_LIMIT)
|
|
1426
|
+
: undefined;
|
|
1235
1427
|
if (watch !== undefined && snapshot === undefined)
|
|
1236
1428
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
1237
1429
|
return {
|
|
@@ -1650,12 +1842,173 @@ export class NodeConnector {
|
|
|
1650
1842
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
1651
1843
|
return workspace;
|
|
1652
1844
|
}
|
|
1653
|
-
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1845
|
+
parseWorkspaceWatchTopics(input) {
|
|
1846
|
+
if (!isPlainRecord(input.topics))
|
|
1847
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
1848
|
+
const sessions = input.topics.sessions === true;
|
|
1849
|
+
const changes = input.topics.changes === true;
|
|
1850
|
+
const rawFiles = input.topics.files;
|
|
1851
|
+
let files;
|
|
1852
|
+
if (rawFiles !== undefined) {
|
|
1853
|
+
if (!isPlainRecord(rawFiles) || typeof rawFiles.path !== 'string')
|
|
1854
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
1855
|
+
const limit = rawFiles.limit;
|
|
1856
|
+
if ((rawFiles.cursor !== undefined && typeof rawFiles.cursor !== 'string') ||
|
|
1857
|
+
(limit !== undefined &&
|
|
1858
|
+
(typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 200)))
|
|
1859
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
1860
|
+
files = {
|
|
1861
|
+
path: rawFiles.path,
|
|
1862
|
+
...(typeof rawFiles.cursor === 'string' ? { cursor: rawFiles.cursor } : {}),
|
|
1863
|
+
...(typeof rawFiles.limit === 'number' ? { limit: rawFiles.limit } : {})
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
if (!sessions && !changes && files === undefined)
|
|
1867
|
+
throw new Error('WORKSPACE_WATCH_INVALID');
|
|
1868
|
+
return { sessions, changes, files };
|
|
1869
|
+
}
|
|
1870
|
+
workspaceWatchKey(input) {
|
|
1871
|
+
return `${this.workspaceWatchSessionHash(input)}:${input.watchId}`;
|
|
1872
|
+
}
|
|
1873
|
+
workspaceWatchSessionHash(input) {
|
|
1874
|
+
return typeof input.__workspaceWatchSessionHash === 'string'
|
|
1875
|
+
? input.__workspaceWatchSessionHash
|
|
1876
|
+
: 'local-test';
|
|
1877
|
+
}
|
|
1878
|
+
createWorkspaceWatch(workspaceId, watchId, sessionHash, topics) {
|
|
1879
|
+
const key = `${sessionHash}:${watchId}`;
|
|
1880
|
+
this.removeWorkspaceWatch(workspaceId, key);
|
|
1881
|
+
const watch = {
|
|
1882
|
+
workspaceId,
|
|
1883
|
+
watchId,
|
|
1884
|
+
sessionHash,
|
|
1885
|
+
expiresAt: Date.now() + WORKSPACE_WATCH_TTL_MS,
|
|
1886
|
+
topics,
|
|
1887
|
+
timer: undefined
|
|
1888
|
+
};
|
|
1889
|
+
watch.timer = setTimeout(() => this.removeWorkspaceWatch(workspaceId, key), WORKSPACE_WATCH_TTL_MS);
|
|
1890
|
+
watch.timer.unref();
|
|
1891
|
+
const watches = this.workspaceWatches.get(workspaceId) ?? new Map();
|
|
1892
|
+
watches.set(key, watch);
|
|
1893
|
+
this.workspaceWatches.set(workspaceId, watches);
|
|
1894
|
+
if (topics.sessions)
|
|
1895
|
+
this.ensureWorkspaceSessionWatch(workspaceId);
|
|
1896
|
+
return watch;
|
|
1897
|
+
}
|
|
1898
|
+
renewWorkspaceWatch(workspaceId, watchId) {
|
|
1899
|
+
const watch = this.workspaceWatches.get(workspaceId)?.get(watchId);
|
|
1900
|
+
if (watch === undefined || watch.expiresAt <= Date.now()) {
|
|
1901
|
+
this.removeWorkspaceWatch(workspaceId, watchId);
|
|
1902
|
+
return undefined;
|
|
1903
|
+
}
|
|
1904
|
+
clearTimeout(watch.timer);
|
|
1905
|
+
watch.expiresAt = Date.now() + WORKSPACE_WATCH_TTL_MS;
|
|
1906
|
+
watch.timer = setTimeout(() => this.removeWorkspaceWatch(workspaceId, watchId), WORKSPACE_WATCH_TTL_MS);
|
|
1907
|
+
watch.timer.unref();
|
|
1908
|
+
return watch;
|
|
1909
|
+
}
|
|
1910
|
+
removeWorkspaceWatch(workspaceId, watchId) {
|
|
1911
|
+
const watches = this.workspaceWatches.get(workspaceId);
|
|
1912
|
+
const watch = watches?.get(watchId);
|
|
1913
|
+
if (watch === undefined)
|
|
1914
|
+
return;
|
|
1915
|
+
clearTimeout(watch.timer);
|
|
1916
|
+
watches?.delete(watchId);
|
|
1917
|
+
if (watches?.size === 0)
|
|
1918
|
+
this.workspaceWatches.delete(workspaceId);
|
|
1919
|
+
if (!this.workspaceWatchTopicActive(workspaceId, 'sessions'))
|
|
1920
|
+
this.stopWorkspaceSessionWatch(workspaceId);
|
|
1921
|
+
}
|
|
1922
|
+
clearWorkspaceWatches(workspaceId) {
|
|
1923
|
+
const watches = this.workspaceWatches.get(workspaceId);
|
|
1924
|
+
if (watches === undefined)
|
|
1925
|
+
return;
|
|
1926
|
+
for (const watch of watches.values())
|
|
1927
|
+
clearTimeout(watch.timer);
|
|
1928
|
+
this.workspaceWatches.delete(workspaceId);
|
|
1929
|
+
this.workspaceWatchRevisions.delete(workspaceId);
|
|
1930
|
+
this.stopWorkspaceSessionWatch(workspaceId);
|
|
1931
|
+
}
|
|
1932
|
+
ensureWorkspaceSessionWatch(workspaceId) {
|
|
1933
|
+
if (this.workspaceSessionWatchTimers.has(workspaceId))
|
|
1934
|
+
return;
|
|
1935
|
+
const timer = setInterval(() => void this.refreshWorkspaceSessionWatch(workspaceId), WORKSPACE_SESSION_WATCH_INTERVAL_MS);
|
|
1936
|
+
timer.unref();
|
|
1937
|
+
this.workspaceSessionWatchTimers.set(workspaceId, timer);
|
|
1938
|
+
void this.refreshWorkspaceSessionWatch(workspaceId);
|
|
1939
|
+
}
|
|
1940
|
+
stopWorkspaceSessionWatch(workspaceId) {
|
|
1941
|
+
const timer = this.workspaceSessionWatchTimers.get(workspaceId);
|
|
1942
|
+
if (timer !== undefined)
|
|
1943
|
+
clearInterval(timer);
|
|
1944
|
+
this.workspaceSessionWatchTimers.delete(workspaceId);
|
|
1945
|
+
this.workspaceSessionWatchSignatures.delete(workspaceId);
|
|
1946
|
+
this.workspaceSessionWatchReads.delete(workspaceId);
|
|
1947
|
+
}
|
|
1948
|
+
async cachedWorkspaceSessions(workspaceId) {
|
|
1949
|
+
const cached = this.workspaceSessionCache.get(workspaceId);
|
|
1950
|
+
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
1951
|
+
return cached.sessions;
|
|
1952
|
+
const inFlight = this.workspaceSessionCacheReads.get(workspaceId);
|
|
1953
|
+
if (inFlight !== undefined)
|
|
1954
|
+
return inFlight;
|
|
1955
|
+
const discovery = this.discoverWorkspaceSessions(workspaceId)
|
|
1956
|
+
.then((sessions) => {
|
|
1957
|
+
this.workspaceSessionCache.set(workspaceId, {
|
|
1958
|
+
sessions,
|
|
1959
|
+
expiresAt: Date.now() + WORKSPACE_SESSION_CACHE_TTL_MS
|
|
1960
|
+
});
|
|
1961
|
+
return sessions;
|
|
1962
|
+
})
|
|
1963
|
+
.finally(() => this.workspaceSessionCacheReads.delete(workspaceId));
|
|
1964
|
+
this.workspaceSessionCacheReads.set(workspaceId, discovery);
|
|
1965
|
+
return discovery;
|
|
1966
|
+
}
|
|
1967
|
+
async refreshWorkspaceSessionWatch(workspaceId) {
|
|
1968
|
+
const current = this.workspaceSessionWatchReads.get(workspaceId);
|
|
1969
|
+
if (current !== undefined)
|
|
1970
|
+
return current;
|
|
1971
|
+
const refresh = this.performWorkspaceSessionWatchRefresh(workspaceId).finally(() => {
|
|
1972
|
+
this.workspaceSessionWatchReads.delete(workspaceId);
|
|
1973
|
+
});
|
|
1974
|
+
this.workspaceSessionWatchReads.set(workspaceId, refresh);
|
|
1975
|
+
return refresh;
|
|
1976
|
+
}
|
|
1977
|
+
async performWorkspaceSessionWatchRefresh(workspaceId) {
|
|
1978
|
+
if (!this.workspaceWatchTopicActive(workspaceId, 'sessions'))
|
|
1979
|
+
return this.stopWorkspaceSessionWatch(workspaceId);
|
|
1980
|
+
try {
|
|
1981
|
+
const page = await this.executeNodeOperation('session.list', { workspaceId, limit: 20 });
|
|
1982
|
+
const signature = JSON.stringify(page);
|
|
1983
|
+
const previous = this.workspaceSessionWatchSignatures.get(workspaceId);
|
|
1984
|
+
this.workspaceSessionWatchSignatures.set(workspaceId, signature);
|
|
1985
|
+
if (previous === undefined || previous === signature)
|
|
1986
|
+
return;
|
|
1987
|
+
const revision = (this.workspaceWatchRevisions.get(workspaceId) ?? 0) + 1;
|
|
1988
|
+
this.workspaceWatchRevisions.set(workspaceId, revision);
|
|
1989
|
+
this.emitWorkbenchEvent('workspace', { workspaceId, topic: 'sessions', revision });
|
|
1990
|
+
}
|
|
1991
|
+
catch {
|
|
1992
|
+
// A later bounded interval retries; no stale session state is fabricated.
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
workspaceWatchTopicActive(workspaceId, topic) {
|
|
1996
|
+
return [...(this.workspaceWatches.get(workspaceId)?.values() ?? [])].some((watch) => topic === 'files' ? watch.topics.files !== undefined : watch.topics[topic]);
|
|
1997
|
+
}
|
|
1998
|
+
invalidateWorkspaceWatchTopics(workspaceId) {
|
|
1999
|
+
this.invalidateWorkspaceChanges(workspaceId);
|
|
2000
|
+
for (const topic of ['files', 'changes']) {
|
|
2001
|
+
if (!this.workspaceWatchTopicActive(workspaceId, topic))
|
|
2002
|
+
continue;
|
|
2003
|
+
const revision = (this.workspaceWatchRevisions.get(workspaceId) ?? 0) + 1;
|
|
2004
|
+
this.workspaceWatchRevisions.set(workspaceId, revision);
|
|
2005
|
+
this.emitWorkbenchEvent('workspace', { workspaceId, topic, revision });
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
invalidateWorkspaceChanges(workspaceId) {
|
|
2009
|
+
this.workspaceChangesReads.delete(workspaceId);
|
|
2010
|
+
this.workspaceChangesCache.delete(workspaceId);
|
|
2011
|
+
this.workspaceChangesVersions.set(workspaceId, (this.workspaceChangesVersions.get(workspaceId) ?? 0) + 1);
|
|
1659
2012
|
}
|
|
1660
2013
|
async listFilesForWorkspace(workspace, input) {
|
|
1661
2014
|
if ((input.path !== undefined && typeof input.path !== 'string') ||
|
|
@@ -1665,7 +2018,7 @@ export class NodeConnector {
|
|
|
1665
2018
|
input.limit < 1 ||
|
|
1666
2019
|
input.limit > 200)))
|
|
1667
2020
|
throw new Error('DIRECTORY_CURSOR_INVALID');
|
|
1668
|
-
return listWorkspaceFiles(workspace.path, typeof input.path === 'string' ? input.path : '.', typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined
|
|
2021
|
+
return listWorkspaceFiles(workspace.path, typeof input.path === 'string' ? input.path : '.', typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined);
|
|
1669
2022
|
}
|
|
1670
2023
|
async readFileForWorkspace(workspace, input) {
|
|
1671
2024
|
if (typeof input.path !== 'string' ||
|
|
@@ -1677,8 +2030,8 @@ export class NodeConnector {
|
|
|
1677
2030
|
input.limit > 512 * 1024)))
|
|
1678
2031
|
throw new Error('FILE_RANGE_INVALID');
|
|
1679
2032
|
if (isAbsolute(input.path))
|
|
1680
|
-
return readAllowedTextFile(input.path, this.config?.allowedRoots ?? [], typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024
|
|
1681
|
-
return readWorkspaceTextFile(workspace.path, input.path, typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024
|
|
2033
|
+
return readAllowedTextFile(input.path, this.config?.allowedRoots ?? [], typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024);
|
|
2034
|
+
return readWorkspaceTextFile(workspace.path, input.path, typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024);
|
|
1682
2035
|
}
|
|
1683
2036
|
async searchFilesForWorkspace(workspace, input) {
|
|
1684
2037
|
if (typeof input.query !== 'string' ||
|
|
@@ -1688,7 +2041,27 @@ export class NodeConnector {
|
|
|
1688
2041
|
input.limit < 1 ||
|
|
1689
2042
|
input.limit > 200)))
|
|
1690
2043
|
throw new Error('FILE_SEARCH_LIMIT_EXCEEDED');
|
|
1691
|
-
|
|
2044
|
+
const index = this.workspaceFileIndex(workspace);
|
|
2045
|
+
return index.search(input.query, typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined);
|
|
2046
|
+
}
|
|
2047
|
+
workspaceFileIndex(workspace) {
|
|
2048
|
+
let index = this.workspaceFileIndexes.get(workspace.id);
|
|
2049
|
+
if (index === undefined) {
|
|
2050
|
+
if (this.workspaceFileIndexes.size >= WORKSPACE_FILE_INDEX_CACHE_LIMIT) {
|
|
2051
|
+
const oldest = this.workspaceFileIndexes.entries().next().value;
|
|
2052
|
+
if (oldest !== undefined) {
|
|
2053
|
+
const [oldestWorkspaceId, oldestIndex] = oldest;
|
|
2054
|
+
oldestIndex.dispose();
|
|
2055
|
+
this.workspaceFileIndexes.delete(oldestWorkspaceId);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
index = new WorkspaceFileIndex(workspace.path, {
|
|
2059
|
+
onInvalidated: () => this.invalidateWorkspaceWatchTopics(workspace.id)
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
this.workspaceFileIndexes.delete(workspace.id);
|
|
2063
|
+
this.workspaceFileIndexes.set(workspace.id, index);
|
|
2064
|
+
return index;
|
|
1692
2065
|
}
|
|
1693
2066
|
async preflightWorkspaceUploads(workspace, input) {
|
|
1694
2067
|
if ((input.directory !== undefined && typeof input.directory !== 'string') ||
|
|
@@ -1702,7 +2075,7 @@ export class NodeConnector {
|
|
|
1702
2075
|
if (seen.has(name))
|
|
1703
2076
|
return { name, status: 'INVALID_NAME', path: null };
|
|
1704
2077
|
seen.add(name);
|
|
1705
|
-
const target = await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', name
|
|
2078
|
+
const target = await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', name);
|
|
1706
2079
|
return { name, status: target.status, path: target.path || null };
|
|
1707
2080
|
}));
|
|
1708
2081
|
}
|
|
@@ -1725,7 +2098,7 @@ export class NodeConnector {
|
|
|
1725
2098
|
throw new Error('COMPOSER_ATTACHMENT_INVALID');
|
|
1726
2099
|
}
|
|
1727
2100
|
const target = composerAttachment === undefined
|
|
1728
|
-
? await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', input.name
|
|
2101
|
+
? await preflightWorkspaceUpload(workspace.path, typeof input.directory === 'string' ? input.directory : '.', input.name)
|
|
1729
2102
|
: await this.composerAttachmentTarget(workspace.path, composerAttachment.id, input.name);
|
|
1730
2103
|
if (target.status === 'INVALID_NAME')
|
|
1731
2104
|
throw new Error('UPLOAD_NAME_INVALID');
|
|
@@ -1816,6 +2189,7 @@ export class NodeConnector {
|
|
|
1816
2189
|
}
|
|
1817
2190
|
clearTimeout(upload.timer);
|
|
1818
2191
|
this.workspaceUploads.delete(upload.id);
|
|
2192
|
+
this.invalidateWorkspaceWatchTopics(upload.workspaceId);
|
|
1819
2193
|
if (upload.composerAttachment !== undefined) {
|
|
1820
2194
|
this.composerAttachments.set(upload.composerAttachment.id, {
|
|
1821
2195
|
id: upload.composerAttachment.id,
|
|
@@ -1918,14 +2292,24 @@ export class NodeConnector {
|
|
|
1918
2292
|
async currentChangesForWorkspace(workspace) {
|
|
1919
2293
|
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
1920
2294
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
2295
|
+
const cached = this.workspaceChangesCache.get(workspace.id);
|
|
2296
|
+
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
2297
|
+
return cached.value;
|
|
1921
2298
|
const existing = this.workspaceChangesReads.get(workspace.id);
|
|
1922
2299
|
if (existing !== undefined)
|
|
1923
2300
|
return existing;
|
|
2301
|
+
const version = this.workspaceChangesVersions.get(workspace.id) ?? 0;
|
|
1924
2302
|
const request = this.workspaceChangesLimiter.run(async () => {
|
|
1925
2303
|
const summary = await readCurrentChangesSummary(workspace.path);
|
|
1926
|
-
|
|
2304
|
+
const visibleChanges = (await Promise.all(summary.changes.map(async (change) => ({
|
|
2305
|
+
change,
|
|
2306
|
+
visible: await isWorkspaceChangeVisible(workspace.path, change.path)
|
|
2307
|
+
}))))
|
|
2308
|
+
.filter(({ visible }) => visible)
|
|
2309
|
+
.map(({ change }) => change);
|
|
2310
|
+
const value = {
|
|
1927
2311
|
branch: summary.branch,
|
|
1928
|
-
changes:
|
|
2312
|
+
changes: visibleChanges.map((change) => ({
|
|
1929
2313
|
...change,
|
|
1930
2314
|
additions: null,
|
|
1931
2315
|
deletions: null,
|
|
@@ -1936,6 +2320,12 @@ export class NodeConnector {
|
|
|
1936
2320
|
diffAvailable: change.state !== 'UNTRACKED'
|
|
1937
2321
|
}))
|
|
1938
2322
|
};
|
|
2323
|
+
if ((this.workspaceChangesVersions.get(workspace.id) ?? 0) === version)
|
|
2324
|
+
this.workspaceChangesCache.set(workspace.id, {
|
|
2325
|
+
value,
|
|
2326
|
+
expiresAt: Date.now() + WORKSPACE_CHANGES_CACHE_TTL_MS
|
|
2327
|
+
});
|
|
2328
|
+
return value;
|
|
1939
2329
|
});
|
|
1940
2330
|
this.workspaceChangesReads.set(workspace.id, request);
|
|
1941
2331
|
try {
|
|
@@ -2005,34 +2395,19 @@ export class NodeConnector {
|
|
|
2005
2395
|
officialCandidates.push({ thread, workspace: targetWorkspace, cwd });
|
|
2006
2396
|
}
|
|
2007
2397
|
// A selected Workspace still needs the complete native directory scan for
|
|
2008
|
-
// source coverage, but it is independent of App Server
|
|
2009
|
-
// both paths together
|
|
2010
|
-
//
|
|
2398
|
+
// source coverage, but it is independent of App Server discovery. Start
|
|
2399
|
+
// both paths together. The list response is the discovery source; do not
|
|
2400
|
+
// read every thread here because most external threads are not loaded by
|
|
2401
|
+
// this App Server and would be reported as unavailable anyway.
|
|
2011
2402
|
const nativePromise = discoverAllNativeSessions('codex');
|
|
2012
|
-
const
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2021
|
-
catch {
|
|
2022
|
-
// A listing can race archival/deletion. It must not manufacture a
|
|
2023
|
-
// session entry whose native history is no longer readable.
|
|
2024
|
-
return undefined;
|
|
2025
|
-
}
|
|
2026
|
-
return this.directNativeSession({
|
|
2027
|
-
workspace: candidate.workspace,
|
|
2028
|
-
runner: 'codex',
|
|
2029
|
-
externalSessionId: candidate.thread.id,
|
|
2030
|
-
cwd: candidate.cwd,
|
|
2031
|
-
...(candidate.thread.title === undefined ? {} : { title: candidate.thread.title }),
|
|
2032
|
-
titleOrigin: 'OFFICIAL'
|
|
2033
|
-
});
|
|
2034
|
-
});
|
|
2035
|
-
const sessions = official.filter((session) => session !== undefined);
|
|
2403
|
+
const sessions = officialCandidates.map((candidate) => this.directNativeSession({
|
|
2404
|
+
workspace: candidate.workspace,
|
|
2405
|
+
runner: 'codex',
|
|
2406
|
+
externalSessionId: candidate.thread.id,
|
|
2407
|
+
cwd: candidate.cwd,
|
|
2408
|
+
...(candidate.thread.title === undefined ? {} : { title: candidate.thread.title }),
|
|
2409
|
+
titleOrigin: 'OFFICIAL'
|
|
2410
|
+
}));
|
|
2036
2411
|
const native = await nativePromise;
|
|
2037
2412
|
for (const entry of native) {
|
|
2038
2413
|
const targetWorkspace = workspace ??
|
|
@@ -2132,11 +2507,74 @@ export class NodeConnector {
|
|
|
2132
2507
|
this.rememberDirectNativeSessions(sessions);
|
|
2133
2508
|
return sessions;
|
|
2134
2509
|
}
|
|
2510
|
+
async discoverWorkspaceSessions(workspaceId) {
|
|
2511
|
+
const database = this.database;
|
|
2512
|
+
if (database === undefined)
|
|
2513
|
+
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
2514
|
+
const workspace = database.getWorkspace(workspaceId);
|
|
2515
|
+
if (workspace === undefined)
|
|
2516
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
2517
|
+
const [codex, claude] = await Promise.all([
|
|
2518
|
+
this.discoverCodexSessions(workspaceId).then((result) => result.sessions),
|
|
2519
|
+
(async () => {
|
|
2520
|
+
let official = [];
|
|
2521
|
+
try {
|
|
2522
|
+
official = await this.claudeClient.listSessions(workspace.path);
|
|
2523
|
+
}
|
|
2524
|
+
catch {
|
|
2525
|
+
// Local transcript discovery remains authoritative when the SDK list is unavailable.
|
|
2526
|
+
}
|
|
2527
|
+
const native = await discoverNativeSessions('claude-code', workspace.path);
|
|
2528
|
+
return this.directClaudeSessions(workspace, 'claude-code', [
|
|
2529
|
+
...official.flatMap((item) => {
|
|
2530
|
+
const parsed = claudeDiscoveredSession(item, workspace.path);
|
|
2531
|
+
return parsed === undefined ? [] : [parsed];
|
|
2532
|
+
}),
|
|
2533
|
+
...native
|
|
2534
|
+
]);
|
|
2535
|
+
})()
|
|
2536
|
+
]);
|
|
2537
|
+
const sessions = deduplicateDirectSessions([...codex, ...claude]);
|
|
2538
|
+
this.rememberDirectNativeSessions(sessions);
|
|
2539
|
+
return sessions;
|
|
2540
|
+
}
|
|
2135
2541
|
async resolveDirectNativeSession(sessionId) {
|
|
2136
2542
|
const cached = this.directNativeSessions.get(sessionId);
|
|
2137
2543
|
if (cached !== undefined)
|
|
2138
2544
|
return cached;
|
|
2139
|
-
|
|
2545
|
+
const discovered = (await this.listDirectNativeSessions()).find((session) => session.id === sessionId);
|
|
2546
|
+
if (discovered !== undefined)
|
|
2547
|
+
return discovered;
|
|
2548
|
+
const record = this.database
|
|
2549
|
+
?.listWorkspaces()
|
|
2550
|
+
.flatMap((workspace) => this.database?.listSessionRecords(workspace.id) ?? [])
|
|
2551
|
+
.find((candidate) => candidate.id === sessionId);
|
|
2552
|
+
if (record === undefined)
|
|
2553
|
+
return undefined;
|
|
2554
|
+
const workspace = this.database?.getWorkspace(record.workspaceId);
|
|
2555
|
+
if (workspace === undefined)
|
|
2556
|
+
return undefined;
|
|
2557
|
+
return {
|
|
2558
|
+
id: record.id,
|
|
2559
|
+
nodeId: this.config?.nodeId ?? 'recorded-session',
|
|
2560
|
+
workspaceId: record.workspaceId,
|
|
2561
|
+
runner: record.runner,
|
|
2562
|
+
externalSessionId: record.id,
|
|
2563
|
+
nativeControl: 'EXTERNAL',
|
|
2564
|
+
channelToken: null,
|
|
2565
|
+
cwd: workspace.path,
|
|
2566
|
+
model: record.metadata.model,
|
|
2567
|
+
effort: record.metadata.effort,
|
|
2568
|
+
access: record.metadata.access,
|
|
2569
|
+
customTitle: record.metadata.customTitle,
|
|
2570
|
+
runnerTitle: record.metadata.runnerTitle,
|
|
2571
|
+
pinnedAt: record.pinOrder === null ? null : 1,
|
|
2572
|
+
pinOrder: record.pinOrder,
|
|
2573
|
+
externalDiscovered: false,
|
|
2574
|
+
titleSource: record.metadata.customTitle === null ? 'RUNNER' : 'CUSTOM',
|
|
2575
|
+
lastActivityAt: 0,
|
|
2576
|
+
createdAt: 0
|
|
2577
|
+
};
|
|
2140
2578
|
}
|
|
2141
2579
|
/**
|
|
2142
2580
|
* A rename is Node-local metadata, so an external transcript receives a
|
|
@@ -2419,6 +2857,19 @@ export class NodeConnector {
|
|
|
2419
2857
|
this.sessionWatches.set(session.id, watch);
|
|
2420
2858
|
return { expiresAt: watch.expiresAt };
|
|
2421
2859
|
}
|
|
2860
|
+
renewNativeSessionWatch(session) {
|
|
2861
|
+
if (this.hasManagedActiveRun(session)) {
|
|
2862
|
+
this.stopNativeSessionWatch(session.id);
|
|
2863
|
+
return undefined;
|
|
2864
|
+
}
|
|
2865
|
+
const watch = this.sessionWatches.get(session.id);
|
|
2866
|
+
if (watch === undefined || watch.expiresAt <= Date.now()) {
|
|
2867
|
+
this.stopNativeSessionWatch(session.id);
|
|
2868
|
+
return undefined;
|
|
2869
|
+
}
|
|
2870
|
+
watch.expiresAt = Date.now() + SESSION_WATCH_TTL_MS;
|
|
2871
|
+
return { expiresAt: watch.expiresAt };
|
|
2872
|
+
}
|
|
2422
2873
|
stopNativeSessionWatch(sessionId) {
|
|
2423
2874
|
const watch = this.sessionWatches.get(sessionId);
|
|
2424
2875
|
if (watch === undefined)
|
|
@@ -2426,7 +2877,7 @@ export class NodeConnector {
|
|
|
2426
2877
|
clearInterval(watch.timer);
|
|
2427
2878
|
this.sessionWatches.delete(sessionId);
|
|
2428
2879
|
}
|
|
2429
|
-
async refreshWatchedSession(sessionId) {
|
|
2880
|
+
async refreshWatchedSession(sessionId, limit = 10) {
|
|
2430
2881
|
const watch = this.sessionWatches.get(sessionId);
|
|
2431
2882
|
if (watch === undefined)
|
|
2432
2883
|
return undefined;
|
|
@@ -2436,7 +2887,7 @@ export class NodeConnector {
|
|
|
2436
2887
|
this.stopNativeSessionWatch(sessionId);
|
|
2437
2888
|
return undefined;
|
|
2438
2889
|
}
|
|
2439
|
-
const refresh = this.performWatchedSessionRefresh(sessionId, watch);
|
|
2890
|
+
const refresh = this.performWatchedSessionRefresh(sessionId, watch, limit);
|
|
2440
2891
|
watch.refresh = refresh;
|
|
2441
2892
|
try {
|
|
2442
2893
|
return await refresh;
|
|
@@ -2446,7 +2897,7 @@ export class NodeConnector {
|
|
|
2446
2897
|
watch.refresh = undefined;
|
|
2447
2898
|
}
|
|
2448
2899
|
}
|
|
2449
|
-
async performWatchedSessionRefresh(sessionId, watch) {
|
|
2900
|
+
async performWatchedSessionRefresh(sessionId, watch, limit = 10) {
|
|
2450
2901
|
try {
|
|
2451
2902
|
const session = this.runtime.getAgentSession(sessionId) ??
|
|
2452
2903
|
(await this.resolveDirectNativeSession(sessionId));
|
|
@@ -2463,18 +2914,22 @@ export class NodeConnector {
|
|
|
2463
2914
|
watch.sessionSignature = sessionSignature;
|
|
2464
2915
|
this.emitWorkbenchEvent('session', { session: presentedSession });
|
|
2465
2916
|
}
|
|
2466
|
-
const page = await this.readConversationHistoryPage(session, { limit
|
|
2917
|
+
const page = await this.readConversationHistoryPage(session, { limit });
|
|
2467
2918
|
// A missing transcript is not a harmless empty update: continuing to
|
|
2468
2919
|
// renew an observer that has lost both official and native history would
|
|
2469
2920
|
// retain its timer forever. Treat it like every other read failure.
|
|
2470
2921
|
watch.failures = 0;
|
|
2471
|
-
|
|
2922
|
+
// `initial` may return a larger page than the background observer. The
|
|
2923
|
+
// observer's change signature remains fixed to the newest 10 Turns, so
|
|
2924
|
+
// switching back to its normal page size cannot manufacture an update.
|
|
2925
|
+
const observedPage = page.turns.length <= 10 ? page : { ...page, turns: page.turns.slice(0, 10) };
|
|
2926
|
+
const signature = JSON.stringify(observedPage.turns);
|
|
2472
2927
|
if (signature === watch.signature)
|
|
2473
2928
|
return page;
|
|
2474
2929
|
watch.signature = signature;
|
|
2475
2930
|
// 历史页按最新到最早排列,而 Web 会把单个实时 Turn 插到最新端。
|
|
2476
2931
|
// 因此逐帧推送必须反向,最终投影仍保持最新到最早。
|
|
2477
|
-
for (const turn of [...
|
|
2932
|
+
for (const turn of [...observedPage.turns].reverse())
|
|
2478
2933
|
this.emitWorkbenchEvent('conversation', { turn });
|
|
2479
2934
|
return page;
|
|
2480
2935
|
}
|
|
@@ -2679,6 +3134,8 @@ export class NodeConnector {
|
|
|
2679
3134
|
customTitle: metadata?.customTitle ?? null,
|
|
2680
3135
|
runnerTitle,
|
|
2681
3136
|
pinnedAt: metadata?.pinnedAt ?? null,
|
|
3137
|
+
pinOrder: metadata?.pinOrder ?? null,
|
|
3138
|
+
externalDiscovered: true,
|
|
2682
3139
|
titleSource: metadata?.customTitle === null || metadata?.customTitle === undefined
|
|
2683
3140
|
? runnerTitle === null
|
|
2684
3141
|
? 'AUTO'
|
|
@@ -2688,7 +3145,7 @@ export class NodeConnector {
|
|
|
2688
3145
|
createdAt: timestamp
|
|
2689
3146
|
};
|
|
2690
3147
|
}
|
|
2691
|
-
/**
|
|
3148
|
+
/** A pending Codex rename shields the UI from stale discovery until Runner confirms it. */
|
|
2692
3149
|
nativeSessionRunnerTitle(metadata, input) {
|
|
2693
3150
|
if (input.runner !== 'codex')
|
|
2694
3151
|
return input.title ?? null;
|
|
@@ -2707,24 +3164,17 @@ export class NodeConnector {
|
|
|
2707
3164
|
}
|
|
2708
3165
|
return cached ?? input.title;
|
|
2709
3166
|
}
|
|
2710
|
-
if (input.title !== cached) {
|
|
2711
|
-
this.database?.saveNativeSessionMetadata({
|
|
2712
|
-
runner: 'codex',
|
|
2713
|
-
externalSessionId: input.externalSessionId,
|
|
2714
|
-
workspaceId: input.workspace.id,
|
|
2715
|
-
runnerTitle: input.title,
|
|
2716
|
-
runnerTitlePending: false
|
|
2717
|
-
});
|
|
2718
|
-
}
|
|
2719
3167
|
return input.title;
|
|
2720
3168
|
}
|
|
2721
3169
|
/** Runner-specific title persistence: Codex owns its thread name; Claude metadata is Node-local. */
|
|
2722
3170
|
async renameRunnerSession(session, input) {
|
|
2723
3171
|
if (session.runner === 'codex') {
|
|
2724
|
-
if (session.externalSessionId === null
|
|
3172
|
+
if (session.externalSessionId === null)
|
|
2725
3173
|
return;
|
|
2726
|
-
|
|
2727
|
-
|
|
3174
|
+
if (input.title !== undefined) {
|
|
3175
|
+
await this.codexClient.start();
|
|
3176
|
+
await this.codexClient.setThreadName(session.externalSessionId, input.title ?? '');
|
|
3177
|
+
}
|
|
2728
3178
|
const database = this.database;
|
|
2729
3179
|
if (database === undefined)
|
|
2730
3180
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
@@ -2732,8 +3182,10 @@ export class NodeConnector {
|
|
|
2732
3182
|
runner: 'codex',
|
|
2733
3183
|
externalSessionId: session.externalSessionId,
|
|
2734
3184
|
workspaceId: session.workspaceId,
|
|
2735
|
-
runnerTitle: input.title,
|
|
2736
|
-
|
|
3185
|
+
...(input.title === undefined ? {} : { runnerTitle: input.title }),
|
|
3186
|
+
...(input.title === undefined
|
|
3187
|
+
? {}
|
|
3188
|
+
: { runnerTitlePending: input.title !== null && input.title.trim().length > 0 }),
|
|
2737
3189
|
...(input.pinned === undefined ? {} : { pinned: input.pinned })
|
|
2738
3190
|
});
|
|
2739
3191
|
return;
|
|
@@ -2912,6 +3364,8 @@ export class NodeConnector {
|
|
|
2912
3364
|
this.emitRunEvent(runId, 'run.running', { threadId, turnId }, 'RUNNING');
|
|
2913
3365
|
}
|
|
2914
3366
|
catch (error) {
|
|
3367
|
+
if (this.completeCancelledRun(runId, cwd))
|
|
3368
|
+
return;
|
|
2915
3369
|
this.emitRunEvent(runId, 'run.failed', { code: error instanceof Error ? error.message : 'CODEX_RUN_FAILED' }, 'FAILED');
|
|
2916
3370
|
this.codexRuns.delete(runId);
|
|
2917
3371
|
this.activeRuns.delete(runId);
|
|
@@ -3236,7 +3690,8 @@ export class NodeConnector {
|
|
|
3236
3690
|
if (run === undefined)
|
|
3237
3691
|
return false;
|
|
3238
3692
|
if (run.status === 'CANCELLING') {
|
|
3239
|
-
this.
|
|
3693
|
+
const status = this.interruptRequestedRuns.has(runId) ? 'INTERRUPTED' : 'CANCELLED';
|
|
3694
|
+
this.emitRunEvent(runId, 'run.completed', { status }, status);
|
|
3240
3695
|
}
|
|
3241
3696
|
else if (!isTerminalRunStatus(run.status)) {
|
|
3242
3697
|
return false;
|
|
@@ -3248,6 +3703,15 @@ export class NodeConnector {
|
|
|
3248
3703
|
return true;
|
|
3249
3704
|
}
|
|
3250
3705
|
presentSession(session) {
|
|
3706
|
+
if (session.externalSessionId !== null && this.database !== undefined) {
|
|
3707
|
+
const metadata = this.database.getNativeSessionMetadata(session.runner, session.externalSessionId, session.workspaceId);
|
|
3708
|
+
if (metadata !== undefined)
|
|
3709
|
+
session = {
|
|
3710
|
+
...session,
|
|
3711
|
+
pinnedAt: metadata.pinnedAt,
|
|
3712
|
+
pinOrder: metadata.pinOrder
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3251
3715
|
if (session.runner !== 'codex') {
|
|
3252
3716
|
const active = this.hasManagedActiveRun(session);
|
|
3253
3717
|
if (this.capabilities.claudeCode.available && session.nativeControl === 'MAR_MANAGED') {
|
|
@@ -3326,7 +3790,11 @@ export class NodeConnector {
|
|
|
3326
3790
|
: this.hasManagedActiveRun(session)
|
|
3327
3791
|
? 'MANAGED_ACTIVE'
|
|
3328
3792
|
: session.nativeControl === 'EXTERNAL'
|
|
3329
|
-
?
|
|
3793
|
+
? // Discovery deliberately uses thread/list only. Until a selected
|
|
3794
|
+
// session is read, treat activity as idle/unknown; takeover and
|
|
3795
|
+
// message handling still perform the authoritative read immediately
|
|
3796
|
+
// before resuming the external thread.
|
|
3797
|
+
(this.codexExternalActivity.get(session.id) ?? 'IDLE')
|
|
3330
3798
|
: 'IDLE';
|
|
3331
3799
|
return {
|
|
3332
3800
|
...session,
|
|
@@ -3639,7 +4107,7 @@ export class NodeConnector {
|
|
|
3639
4107
|
manifestVersion: 1,
|
|
3640
4108
|
revision,
|
|
3641
4109
|
observedAt: Date.now(),
|
|
3642
|
-
platform: this.capabilities.platform
|
|
4110
|
+
platform: this.capabilities.platform,
|
|
3643
4111
|
architecture: this.capabilities.architecture,
|
|
3644
4112
|
conversation: {
|
|
3645
4113
|
available: true,
|
|
@@ -3673,7 +4141,7 @@ export class NodeConnector {
|
|
|
3673
4141
|
available: true,
|
|
3674
4142
|
reasonCode: null,
|
|
3675
4143
|
supportsPty: this.capabilities.platform === 'linux',
|
|
3676
|
-
supportsConPty: this.capabilities.platform === '
|
|
4144
|
+
supportsConPty: this.capabilities.platform === 'windows',
|
|
3677
4145
|
supportsReplay: true,
|
|
3678
4146
|
supportsReadonlyAttach: true,
|
|
3679
4147
|
supportsTakeover: true,
|
|
@@ -4551,21 +5019,6 @@ function presentClaudeContextUsage(usage) {
|
|
|
4551
5019
|
percentage: usage.percentage
|
|
4552
5020
|
};
|
|
4553
5021
|
}
|
|
4554
|
-
async function mapWithConcurrency(values, concurrency, map) {
|
|
4555
|
-
const result = new Array(values.length);
|
|
4556
|
-
let next = 0;
|
|
4557
|
-
const worker = async () => {
|
|
4558
|
-
while (true) {
|
|
4559
|
-
const index = next;
|
|
4560
|
-
next += 1;
|
|
4561
|
-
if (index >= values.length)
|
|
4562
|
-
return;
|
|
4563
|
-
result[index] = await map(values[index]);
|
|
4564
|
-
}
|
|
4565
|
-
};
|
|
4566
|
-
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker));
|
|
4567
|
-
return result;
|
|
4568
|
-
}
|
|
4569
5022
|
function isNativeHistory(value) {
|
|
4570
5023
|
return (typeof value === 'object' &&
|
|
4571
5024
|
value !== null &&
|
|
@@ -4674,7 +5127,9 @@ function nativeUserClientMessageId(entry, createdAt, runtimeTurns) {
|
|
|
4674
5127
|
if (clientMessageId === null || payload.text !== entry.text)
|
|
4675
5128
|
continue;
|
|
4676
5129
|
const runtimeCreatedAt = user.startedAt ?? turn.startedAt;
|
|
4677
|
-
if (runtimeCreatedAt !== null &&
|
|
5130
|
+
if (runtimeCreatedAt !== null &&
|
|
5131
|
+
runtimeCreatedAt !== undefined &&
|
|
5132
|
+
Math.abs(createdAt - runtimeCreatedAt) > 60_000)
|
|
4678
5133
|
continue;
|
|
4679
5134
|
return clientMessageId;
|
|
4680
5135
|
}
|
|
@@ -5199,38 +5654,74 @@ function sessionPage(sessions, input) {
|
|
|
5199
5654
|
input.limit <= MAX_SESSION_PAGE_SIZE
|
|
5200
5655
|
? input.limit
|
|
5201
5656
|
: DEFAULT_SESSION_PAGE_SIZE;
|
|
5202
|
-
const cursor = typeof input.cursor === 'string' ?
|
|
5203
|
-
const ordered = [...sessions].sort(
|
|
5657
|
+
const cursor = typeof input.cursor === 'string' ? input.cursor : undefined;
|
|
5658
|
+
const ordered = [...sessions].sort(compareSessionPageEntries);
|
|
5659
|
+
const cursorEntry = cursor === undefined ? undefined : parseSessionPageCursor(cursor);
|
|
5204
5660
|
const afterCursor = cursor === undefined
|
|
5205
5661
|
? ordered
|
|
5206
|
-
:
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
const
|
|
5662
|
+
: cursorEntry === undefined
|
|
5663
|
+
? []
|
|
5664
|
+
: ordered.filter((session) => compareSessionPageEntries(session, cursorEntry) > 0);
|
|
5665
|
+
const pinned = ordered.filter((session) => session.pinOrder !== null && session.pinOrder !== undefined);
|
|
5666
|
+
const pinnedIndexes = new Map(pinned.map((session, index) => [session.id, index]));
|
|
5667
|
+
const page = afterCursor.slice(0, limit).map((session) => {
|
|
5668
|
+
const pinIndex = pinnedIndexes.get(session.id);
|
|
5669
|
+
if (pinIndex === undefined)
|
|
5670
|
+
return session;
|
|
5671
|
+
return {
|
|
5672
|
+
...session,
|
|
5673
|
+
canMovePinUp: pinIndex > 0,
|
|
5674
|
+
canMovePinDown: pinIndex < pinned.length - 1
|
|
5675
|
+
};
|
|
5676
|
+
});
|
|
5210
5677
|
const last = page.at(-1);
|
|
5211
5678
|
return {
|
|
5212
5679
|
sessions: page,
|
|
5213
5680
|
nextCursor: last !== undefined && afterCursor.length > page.length ? createSessionPageCursor(last) : null
|
|
5214
5681
|
};
|
|
5215
5682
|
}
|
|
5216
|
-
function
|
|
5217
|
-
return `${session.lastActivityAt}:${encodeURIComponent(session.id)}`;
|
|
5218
|
-
}
|
|
5219
|
-
function parseSessionPageCursor(value) {
|
|
5220
|
-
const separator = value.indexOf(':');
|
|
5221
|
-
if (separator <= 0 || separator === value.length - 1)
|
|
5222
|
-
return undefined;
|
|
5223
|
-
const lastActivityAt = Number(value.slice(0, separator));
|
|
5224
|
-
if (!Number.isFinite(lastActivityAt))
|
|
5225
|
-
return undefined;
|
|
5683
|
+
function parseSessionPageCursor(cursor) {
|
|
5226
5684
|
try {
|
|
5227
|
-
const
|
|
5228
|
-
|
|
5685
|
+
const value = JSON.parse(decodeURIComponent(cursor));
|
|
5686
|
+
if (!Array.isArray(value) || value.length !== 3)
|
|
5687
|
+
return undefined;
|
|
5688
|
+
const [pinOrder, lastActivityAt, id] = value;
|
|
5689
|
+
if ((pinOrder !== null && (typeof pinOrder !== 'number' || !Number.isFinite(pinOrder))) ||
|
|
5690
|
+
(lastActivityAt !== null &&
|
|
5691
|
+
(typeof lastActivityAt !== 'number' || !Number.isFinite(lastActivityAt))) ||
|
|
5692
|
+
typeof id !== 'string')
|
|
5693
|
+
return undefined;
|
|
5694
|
+
return {
|
|
5695
|
+
id,
|
|
5696
|
+
lastActivityAt: lastActivityAt ?? 0,
|
|
5697
|
+
pinOrder,
|
|
5698
|
+
externalDiscovered: lastActivityAt !== null
|
|
5699
|
+
};
|
|
5229
5700
|
}
|
|
5230
5701
|
catch {
|
|
5231
5702
|
return undefined;
|
|
5232
5703
|
}
|
|
5233
5704
|
}
|
|
5705
|
+
function createSessionPageCursor(session) {
|
|
5706
|
+
return encodeURIComponent(JSON.stringify([
|
|
5707
|
+
session.pinOrder ?? null,
|
|
5708
|
+
session.externalDiscovered === false ? null : session.lastActivityAt,
|
|
5709
|
+
session.id
|
|
5710
|
+
]));
|
|
5711
|
+
}
|
|
5712
|
+
function compareSessionPageEntries(left, right) {
|
|
5713
|
+
const leftPinned = left.pinOrder !== null && left.pinOrder !== undefined;
|
|
5714
|
+
const rightPinned = right.pinOrder !== null && right.pinOrder !== undefined;
|
|
5715
|
+
if (leftPinned !== rightPinned)
|
|
5716
|
+
return leftPinned ? -1 : 1;
|
|
5717
|
+
if (leftPinned && rightPinned)
|
|
5718
|
+
return (left.pinOrder - right.pinOrder || left.id.localeCompare(right.id));
|
|
5719
|
+
const leftFound = left.externalDiscovered !== false;
|
|
5720
|
+
const rightFound = right.externalDiscovered !== false;
|
|
5721
|
+
if (leftFound !== rightFound)
|
|
5722
|
+
return leftFound ? -1 : 1;
|
|
5723
|
+
return right.lastActivityAt - left.lastActivityAt || left.id.localeCompare(right.id, 'en');
|
|
5724
|
+
}
|
|
5234
5725
|
function compactRunnerText(value, limit) {
|
|
5235
5726
|
return value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 1))}…`;
|
|
5236
5727
|
}
|