@myagentroam/node 0.11.35 → 0.11.37
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/application/execution-lifecycle.js +54 -0
- package/dist/application/node-lifecycle.js +104 -0
- package/dist/application/node-operation-admission.js +51 -0
- package/dist/application/node-operation-contracts.js +1 -0
- package/dist/application/runner-composition.js +22 -0
- package/dist/application/session-projection-composition.js +26 -0
- package/dist/application/workspace-application.js +61 -0
- package/dist/application/workspace-composition.js +21 -0
- package/dist/capabilities.js +2 -2
- package/dist/codex-app-server.js +1 -471
- package/dist/connection/authorization.js +26 -0
- package/dist/{service → connection}/direct-transfer-service.js +1 -1
- package/dist/connection/endpoints.js +32 -0
- package/dist/connector.js +144 -252
- package/dist/database.js +34 -14
- package/dist/git-askpass.js +1 -1
- package/dist/git-execution-helper.js +1 -1
- package/dist/hosting/hosted-conversation-file-identity.js +27 -0
- package/dist/hosting/hosted-conversation-file-operations.js +44 -0
- package/dist/hosting/hosted-conversation-node-service.js +974 -0
- package/dist/hosting/hosted-conversation-runs.js +599 -0
- package/dist/hosting/hosted-conversation-templates.js +280 -0
- package/dist/hosting/hosted-conversation-uploads.js +344 -0
- package/dist/hosting/hosted-conversation-watch.js +194 -0
- package/dist/hosting/runtime-contracts.js +1 -0
- package/dist/marctl-guidance.js +1 -1
- package/dist/runner/abstract-runner.js +4 -13
- package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
- package/dist/runner/claude/conversation-parser.js +2 -1
- package/dist/runner/claude/managed-run-controller.js +6 -7
- package/dist/runner/claude/message-parsers.js +2 -1
- package/dist/runner/claude/native-transcript.js +2 -1
- package/dist/runner/claude/tool-normalizer.js +1 -1
- package/dist/runner/claude-code-runner.js +2 -2
- package/dist/runner/codex/codex-app-server.js +471 -0
- package/dist/runner/codex/conversation-parser.js +4 -2
- package/dist/runner/codex/managed-run-controller.js +7 -9
- package/dist/runner/codex/native-transcript.js +1 -1
- package/dist/runner/codex/tool-normalizer.js +1 -1
- package/dist/runner/codex-runner.js +9 -27
- package/dist/runner/contracts.js +1 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
- package/dist/runner/mar-agent/history-projection.js +1 -1
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
- package/dist/runner/mar-agent/managed-run-controller.js +85 -61
- package/dist/runner/mar-agent-runner.js +1 -1
- package/dist/runner/normalization.js +42 -0
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.js +6 -7
- package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
- package/dist/runner/opencode/tool-normalizer.js +1 -1
- package/dist/runner/opencode-runner.js +4 -5
- package/dist/runner/runner-registry.js +4 -1
- package/dist/runtime-state.js +48 -372
- package/dist/service/computer-use-enablement-service.js +1 -1
- package/dist/service/continuous-run-coordinator.js +52 -7
- package/dist/service/conversation-history-service.js +4 -4
- package/dist/service/conversation-hosting-capability-service.js +1 -1
- package/dist/service/conversation-hosting-enablement-service.js +1 -1
- package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
- package/dist/service/conversation-hosting-maintenance-service.js +1 -1
- package/dist/service/conversation-observation.js +1 -1
- package/dist/service/conversation-segment-service.js +1 -1
- package/dist/service/external-session-resume-service.js +47 -27
- package/dist/service/fake-managed-run-service.js +3 -7
- package/dist/service/git-operation-service.js +1 -1
- package/dist/service/goal-continuation-coordinator.js +14 -7
- package/dist/service/mar-agent-model-transport-test-service.js +1 -1
- package/dist/service/native-session-watch-service.js +1 -1
- package/dist/service/node-control-channel.js +1 -5
- package/dist/service/node-request-service.js +1 -1
- package/dist/service/node-terminal-channel.js +2 -1
- package/dist/service/node-workspace-coordinator.js +3 -2
- package/dist/service/notification-mcp-bridge.js +6 -2
- package/dist/service/recent-run-projection-service.js +1 -1
- package/dist/service/restart-session-restorer.js +1 -1
- package/dist/service/run-event-service.js +49 -42
- package/dist/service/run-workbench-service.js +3 -3
- package/dist/service/runner-interaction-service.js +6 -5
- package/dist/service/runner-service.js +1 -1
- package/dist/service/session-recent-activity.js +2 -1
- package/dist/service/skill-directory-service.js +1 -1
- package/dist/service/skill-install-service.js +2 -2
- package/dist/service/terminal-service.js +1 -1
- package/dist/service/workbench-event-projection.js +3 -2
- package/dist/service/workbench-event-service.js +1 -1
- package/dist/service/workspace-service.js +3 -2
- package/dist/session/attachments.js +110 -0
- package/dist/session/contracts.js +1 -0
- package/dist/{util → session}/conversation-identity.js +1 -1
- package/dist/session/input-validation.js +15 -0
- package/dist/{service → session}/native-session-projection-service.js +40 -11
- package/dist/session/pagination.js +77 -0
- package/dist/{util → session}/runner-native-session-parsers.js +2 -1
- package/dist/session/runtime/approvals.js +90 -0
- package/dist/session/runtime/conversation-projection.js +310 -0
- package/dist/session/runtime/type-guards.js +19 -0
- package/dist/{service → session}/session-catalog-service.js +5 -4
- package/dist/{service → session}/session-command-service.js +3 -3
- package/dist/{service → session}/session-identity-service.js +1 -1
- package/dist/{service → session}/session-lifecycle-service.js +18 -7
- package/dist/{service → session}/session-message-service.js +6 -5
- package/dist/{service → session}/session-presentation-service.js +1 -1
- package/dist/{service → session}/session-query-service.js +2 -2
- package/dist/{service → session}/session-schedule-service.js +2 -2
- package/dist/util/records.js +3 -0
- package/dist/workspace/contracts.js +1 -0
- package/dist/workspace/file-index.js +534 -0
- package/dist/workspace/files.js +466 -0
- package/dist/workspace/git.js +991 -0
- package/dist/workspace/path-access.js +184 -0
- package/dist/workspace/utf8.js +12 -0
- package/dist/{service → workspace}/workspace-change-service.js +45 -21
- package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
- package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
- package/dist/{service → workspace}/workspace-file-service.js +16 -16
- package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
- package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
- package/dist/{service → workspace}/workspace-session-service.js +45 -40
- package/dist/{service → workspace}/workspace-upload-service.js +28 -28
- package/dist/{service → workspace}/workspace-watch-service.js +29 -28
- package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
- package/package.json +4 -4
- package/dist/service/hosted-conversation-node-service.js +0 -2252
- package/dist/service/run-domain-state.js +0 -4
- package/dist/service/session-domain-state.js +0 -9
- package/dist/service/workspace-domain-state.js +0 -41
- package/dist/util/node-operation-parsers.js +0 -320
- package/dist/workspace.js +0 -2168
- /package/dist/{connector → application}/node-connector-options.js +0 -0
- /package/dist/{util → connection}/node-operation-data.js +0 -0
- /package/dist/{connector → connection}/node-operation-router.js +0 -0
- /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
- /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
- /package/dist/{util → mcp}/secret-environment.js +0 -0
- /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
- /package/dist/{util → runner}/personal-instructions.js +0 -0
- /package/dist/{util → runner}/runner-error.js +0 -0
- /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
- /package/dist/{service → session}/session-context-service.js +0 -0
- /package/dist/{util → workspace}/git-credential-client.js +0 -0
- /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
- /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { lstat, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { platform as hostPlatform } from 'node:os';
|
|
3
|
+
import { isAbsolute, posix, relative, resolve, sep, win32 } from 'node:path';
|
|
4
|
+
export async function resolveNewWorkspaceEntry(root, requestedPath) {
|
|
5
|
+
const normalized = requestedPath.replaceAll('\\', '/');
|
|
6
|
+
const segments = normalized.split('/');
|
|
7
|
+
const name = segments.at(-1);
|
|
8
|
+
if (requestedPath.includes('\0') ||
|
|
9
|
+
normalized.startsWith('/') ||
|
|
10
|
+
isAbsolute(requestedPath) ||
|
|
11
|
+
win32.isAbsolute(normalized) ||
|
|
12
|
+
name === undefined ||
|
|
13
|
+
name.length === 0 ||
|
|
14
|
+
segments.some(invalidWorkspaceMutationSegment))
|
|
15
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
16
|
+
const parentPath = segments.length === 1 ? '.' : segments.slice(0, -1).join('/');
|
|
17
|
+
await assertWorkspaceMutationPathHasNoSymlinks(root, parentPath);
|
|
18
|
+
const parent = await resolveWorkspaceEntryFromCanonicalRoot(root, parentPath, true);
|
|
19
|
+
const target = resolve(parent, name);
|
|
20
|
+
try {
|
|
21
|
+
await lstat(target);
|
|
22
|
+
throw new Error('FILE_ALREADY_EXISTS');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'ENOENT')
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
export async function assertWorkspaceMutationPathHasNoSymlinks(root, requestedPath) {
|
|
31
|
+
const normalized = requestedPath.replaceAll('\\', '/');
|
|
32
|
+
if (normalized === '.')
|
|
33
|
+
return;
|
|
34
|
+
const segments = normalized.split('/');
|
|
35
|
+
if (requestedPath.includes('\0') ||
|
|
36
|
+
normalized.startsWith('/') ||
|
|
37
|
+
isAbsolute(requestedPath) ||
|
|
38
|
+
win32.isAbsolute(normalized) ||
|
|
39
|
+
segments.some(invalidWorkspaceMutationSegment))
|
|
40
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
41
|
+
let candidate = root;
|
|
42
|
+
for (const segment of segments) {
|
|
43
|
+
candidate = resolve(candidate, segment);
|
|
44
|
+
const metadata = await lstat(candidate).catch((error) => {
|
|
45
|
+
if (error.code === 'ENOENT')
|
|
46
|
+
throw new Error('FILE_NOT_FOUND');
|
|
47
|
+
throw error;
|
|
48
|
+
});
|
|
49
|
+
if (metadata.isSymbolicLink())
|
|
50
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function invalidWorkspaceMutationSegment(segment) {
|
|
54
|
+
if (segment.length === 0 || segment === '.' || segment === '..' || segment === '.git')
|
|
55
|
+
return true;
|
|
56
|
+
if (hostPlatform() !== 'win32')
|
|
57
|
+
return false;
|
|
58
|
+
const normalized = segment.replace(/[. ]+$/u, '').toUpperCase();
|
|
59
|
+
return (/[<>:"|?*]/u.test(segment) ||
|
|
60
|
+
/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/u.test(normalized));
|
|
61
|
+
}
|
|
62
|
+
export async function resolveRegisteredWorkspaceRoot(workspaceRoot) {
|
|
63
|
+
const root = await realpath(workspaceRoot);
|
|
64
|
+
if (normalizeWorkspacePath(root) !== normalizeWorkspacePath(workspaceRoot))
|
|
65
|
+
throw Object.assign(new Error('WORKSPACE_NOT_ALLOWED'), { code: 'WORKSPACE_NOT_ALLOWED' });
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
68
|
+
export class SessionExecutionDirectoryError extends Error {
|
|
69
|
+
publicMessage;
|
|
70
|
+
code = 'SESSION_EXECUTION_DIRECTORY_INVALID';
|
|
71
|
+
publicParameters;
|
|
72
|
+
constructor(reason, publicMessage, options) {
|
|
73
|
+
super(publicMessage, options);
|
|
74
|
+
this.publicMessage = publicMessage;
|
|
75
|
+
this.publicParameters = { reason };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Bind ordinary Session execution to one registered Workspace, not all allowed roots. */
|
|
79
|
+
export async function resolveWorkspaceExecutionDirectory(workspaceRoot, cwd) {
|
|
80
|
+
if (!isAbsolute(cwd) || (process.platform === 'win32' && win32.parse(cwd).root.length <= 1))
|
|
81
|
+
throw new SessionExecutionDirectoryError('ABSOLUTE_PATH_REQUIRED', 'The execution directory must be an absolute path on this Node.');
|
|
82
|
+
try {
|
|
83
|
+
const root = await resolveRegisteredWorkspaceRoot(workspaceRoot);
|
|
84
|
+
const directory = await realpath(cwd);
|
|
85
|
+
if (!isWithinAllowedRoot(directory, [root]))
|
|
86
|
+
throw new SessionExecutionDirectoryError('OUTSIDE_WORKSPACE', 'The execution directory is outside the authorized Workspace.');
|
|
87
|
+
if (!(await stat(directory)).isDirectory())
|
|
88
|
+
throw new SessionExecutionDirectoryError('NOT_DIRECTORY', 'The execution directory is not a directory.');
|
|
89
|
+
return directory;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof SessionExecutionDirectoryError)
|
|
93
|
+
throw error;
|
|
94
|
+
const code = error !== null && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
95
|
+
const [reason, message] = code === 'WORKSPACE_NOT_ALLOWED'
|
|
96
|
+
? [
|
|
97
|
+
'WORKSPACE_ROOT_CHANGED',
|
|
98
|
+
'The registered Workspace root no longer identifies the same directory.'
|
|
99
|
+
]
|
|
100
|
+
: code === 'ENOENT' || code === 'ENOTDIR'
|
|
101
|
+
? ['NOT_FOUND', 'The execution directory or Workspace root is unavailable.']
|
|
102
|
+
: code === 'EACCES' || code === 'EPERM'
|
|
103
|
+
? [
|
|
104
|
+
'ACCESS_DENIED',
|
|
105
|
+
'The Node account cannot access the execution directory or Workspace root.'
|
|
106
|
+
]
|
|
107
|
+
: ['IO_ERROR', 'The execution directory could not be verified.'];
|
|
108
|
+
throw new SessionExecutionDirectoryError(reason, message, { cause: error });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
|
|
112
|
+
const root = await resolveRegisteredWorkspaceRoot(workspaceRoot);
|
|
113
|
+
return resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory);
|
|
114
|
+
}
|
|
115
|
+
export async function resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory) {
|
|
116
|
+
const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
|
|
117
|
+
if (directory
|
|
118
|
+
? !entry.metadata.isDirectory()
|
|
119
|
+
: !entry.metadata.isFile() && !entry.metadata.isDirectory())
|
|
120
|
+
throw new Error(directory ? 'FILE_NOT_DIRECTORY' : 'FILE_NOT_REGULAR');
|
|
121
|
+
return entry.path;
|
|
122
|
+
}
|
|
123
|
+
export async function resolveWorkspaceEntryMetadata(root, requestedPath) {
|
|
124
|
+
if (requestedPath.includes('\0') ||
|
|
125
|
+
requestedPath.length === 0 ||
|
|
126
|
+
requestedPath.startsWith('/') ||
|
|
127
|
+
requestedPath.startsWith('\\'))
|
|
128
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
129
|
+
const normalized = requestedPath.replaceAll('\\', '/');
|
|
130
|
+
if (normalized.split('/').some((segment) => segment === '..' || segment === '.git'))
|
|
131
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
132
|
+
let candidate;
|
|
133
|
+
try {
|
|
134
|
+
candidate = await realpath(resolve(root, normalized));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error.code === 'ENOENT')
|
|
138
|
+
throw new Error('FILE_NOT_FOUND');
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const pathToCandidate = relative(root, candidate);
|
|
142
|
+
if (isAbsolute(pathToCandidate) ||
|
|
143
|
+
pathToCandidate.startsWith('..') ||
|
|
144
|
+
pathToCandidate === '..' ||
|
|
145
|
+
pathToCandidate.startsWith(`..${sep}`))
|
|
146
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
147
|
+
const metadata = await lstat(candidate);
|
|
148
|
+
return { path: candidate, metadata };
|
|
149
|
+
}
|
|
150
|
+
export function normalizeWorkspacePath(input, platform = hostPlatform()) {
|
|
151
|
+
if (input.length === 0 || input.includes('\u0000')) {
|
|
152
|
+
throw new Error('WORKSPACE_PATH_INVALID');
|
|
153
|
+
}
|
|
154
|
+
if (platform === 'win32') {
|
|
155
|
+
// Device paths bypass normal Win32 resolution and must never be accepted.
|
|
156
|
+
if (/^\\\\[?.]/.test(input)) {
|
|
157
|
+
throw new Error('WORKSPACE_DEVICE_PATH_REJECTED');
|
|
158
|
+
}
|
|
159
|
+
const normalized = win32.normalize(input.replaceAll('/', '\\'));
|
|
160
|
+
if (!win32.isAbsolute(normalized)) {
|
|
161
|
+
throw new Error('WORKSPACE_PATH_NOT_ABSOLUTE');
|
|
162
|
+
}
|
|
163
|
+
// A drive root must remain absolute instead of becoming drive-relative.
|
|
164
|
+
const root = win32.parse(normalized).root;
|
|
165
|
+
return (normalized === root ? root : normalized.replace(/\\+$/, '')).toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
if (platform !== 'linux' && platform !== 'darwin') {
|
|
168
|
+
throw new Error('WORKSPACE_PLATFORM_UNSUPPORTED');
|
|
169
|
+
}
|
|
170
|
+
const normalized = posix.normalize(input);
|
|
171
|
+
if (!posix.isAbsolute(normalized)) {
|
|
172
|
+
throw new Error('WORKSPACE_PATH_NOT_ABSOLUTE');
|
|
173
|
+
}
|
|
174
|
+
return normalized === '/' ? normalized : normalized.replace(/\/+$/, '');
|
|
175
|
+
}
|
|
176
|
+
export function isWithinAllowedRoot(target, roots, platform = hostPlatform()) {
|
|
177
|
+
const normalizedTarget = normalizeWorkspacePath(target, platform);
|
|
178
|
+
const pathApi = platform === 'win32' ? win32 : posix;
|
|
179
|
+
return roots.some((root) => {
|
|
180
|
+
const normalizedRoot = normalizeWorkspacePath(root, platform);
|
|
181
|
+
const relative = pathApi.relative(normalizedRoot, normalizedTarget);
|
|
182
|
+
return relative === '' || (!relative.startsWith('..') && !pathApi.isAbsolute(relative));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isValidUtf8(source) {
|
|
2
|
+
try {
|
|
3
|
+
new TextDecoder('utf-8', { fatal: true }).decode(source);
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function isUtf8ContinuationByte(value) {
|
|
11
|
+
return (value & 0b1100_0000) === 0b1000_0000;
|
|
12
|
+
}
|
|
@@ -2,7 +2,7 @@ import { lstat } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, posix, resolve } from 'node:path';
|
|
3
3
|
import { publicErrorMessage } from '@myagentroam/protocol';
|
|
4
4
|
import { nodeLog } from '../operational.js';
|
|
5
|
-
import { GitCommandError, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository, resolveWorkspaceGitRoot } from '
|
|
5
|
+
import { GitCommandError, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository, resolveWorkspaceGitRoot } from './git.js';
|
|
6
6
|
const CACHE_TTL_MS = 5_000;
|
|
7
7
|
const REPOSITORY_READ_CONCURRENCY = 4;
|
|
8
8
|
const WORKTREE_READ_CONCURRENCY = 4;
|
|
@@ -10,35 +10,35 @@ const CHANGE_VISIBILITY_CONCURRENCY = 16;
|
|
|
10
10
|
const SUBMODULE_LIMIT = 128;
|
|
11
11
|
const CHANGE_RESULT_LIMIT = 1000;
|
|
12
12
|
export class WorkspaceChangeService {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
changesReads = new Map();
|
|
14
|
+
changesCache = new Map();
|
|
15
|
+
changesVersions = new Map();
|
|
16
|
+
changesLimiter = new AsyncLimiter(3);
|
|
17
17
|
revision(workspaceId) {
|
|
18
|
-
return this.
|
|
18
|
+
return this.changesVersions.get(workspaceId) ?? 0;
|
|
19
19
|
}
|
|
20
20
|
invalidate(workspaceId) {
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
21
|
+
this.changesReads.delete(workspaceId);
|
|
22
|
+
this.changesCache.delete(workspaceId);
|
|
23
|
+
this.changesVersions.set(workspaceId, (this.changesVersions.get(workspaceId) ?? 0) + 1);
|
|
24
24
|
}
|
|
25
25
|
clear() {
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
26
|
+
this.changesReads.clear();
|
|
27
|
+
this.changesCache.clear();
|
|
28
|
+
this.changesVersions.clear();
|
|
29
29
|
}
|
|
30
30
|
async current(workspace) {
|
|
31
31
|
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
32
32
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
33
|
-
const cached = this.
|
|
33
|
+
const cached = this.changesCache.get(workspace.id);
|
|
34
34
|
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
35
35
|
return cached.value;
|
|
36
|
-
const existing = this.
|
|
36
|
+
const existing = this.changesReads.get(workspace.id);
|
|
37
37
|
if (existing !== undefined)
|
|
38
38
|
return existing;
|
|
39
39
|
// Invalidation may arrive during this shared read; its old result must not repopulate the cache.
|
|
40
|
-
const version = this.
|
|
41
|
-
const request = this.
|
|
40
|
+
const version = this.changesVersions.get(workspace.id) ?? 0;
|
|
41
|
+
const request = this.changesLimiter
|
|
42
42
|
.run(async () => {
|
|
43
43
|
const repositoryRoot = await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true);
|
|
44
44
|
const startedAt = performance.now();
|
|
@@ -289,8 +289,8 @@ export class WorkspaceChangeService {
|
|
|
289
289
|
discoveryErrors: errors.length,
|
|
290
290
|
durationMs: Math.round(performance.now() - startedAt)
|
|
291
291
|
});
|
|
292
|
-
if ((this.
|
|
293
|
-
this.
|
|
292
|
+
if ((this.changesVersions.get(workspace.id) ?? 0) === version)
|
|
293
|
+
this.changesCache.set(workspace.id, {
|
|
294
294
|
value,
|
|
295
295
|
expiresAt: Date.now() + CACHE_TTL_MS
|
|
296
296
|
});
|
|
@@ -302,13 +302,13 @@ export class WorkspaceChangeService {
|
|
|
302
302
|
throw new Error('GIT_NOT_FOUND');
|
|
303
303
|
throw error;
|
|
304
304
|
});
|
|
305
|
-
this.
|
|
305
|
+
this.changesReads.set(workspace.id, request);
|
|
306
306
|
try {
|
|
307
307
|
return await request;
|
|
308
308
|
}
|
|
309
309
|
finally {
|
|
310
|
-
if (this.
|
|
311
|
-
this.
|
|
310
|
+
if (this.changesReads.get(workspace.id) === request)
|
|
311
|
+
this.changesReads.delete(workspace.id);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
}
|
|
@@ -324,3 +324,27 @@ async function mapWithConcurrency(values, concurrency, project) {
|
|
|
324
324
|
}));
|
|
325
325
|
return output;
|
|
326
326
|
}
|
|
327
|
+
class AsyncLimiter {
|
|
328
|
+
limit;
|
|
329
|
+
active = 0;
|
|
330
|
+
waiters = [];
|
|
331
|
+
constructor(limit) {
|
|
332
|
+
this.limit = limit;
|
|
333
|
+
}
|
|
334
|
+
async run(task) {
|
|
335
|
+
if (this.active < this.limit)
|
|
336
|
+
this.active += 1;
|
|
337
|
+
else
|
|
338
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
339
|
+
try {
|
|
340
|
+
return await task();
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
const next = this.waiters.shift();
|
|
344
|
+
if (next === undefined)
|
|
345
|
+
this.active -= 1;
|
|
346
|
+
else
|
|
347
|
+
next();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -8,12 +8,12 @@ import { supportsSymbolNavigationPath } from '@myagentroam/code-navigation/symbo
|
|
|
8
8
|
import { workspaceFileReferenceSchema } from '@myagentroam/protocol';
|
|
9
9
|
import { bundledCtagsPath, BUNDLED_CTAGS_VERSION, repairBundledCtagsExecutable } from '../ctags-binary.js';
|
|
10
10
|
import { nodeLog } from '../operational.js';
|
|
11
|
-
import { normalizeWorkspacePath } from '
|
|
12
|
-
import { pathSpecifierResolver } from '
|
|
13
|
-
import { reparseFileReference } from '
|
|
14
|
-
import { acquireBuild, buildIncrementalTags, ensurePrivateDirectory, manifestEntry, readManifest, removeInactiveGenerations, runCtags, sourceDelta, sourceFingerprint, tagsAvailable, tagsPath } from '
|
|
15
|
-
import { compareTagNames, firstTagOffset, parseTag, readTagLine } from '
|
|
16
|
-
import { compareTargets, containsSymbol, sourceFileFingerprint, targetPreview, validatePathTarget, validateSymbolTarget } from '
|
|
11
|
+
import { normalizeWorkspacePath } from './path-access.js';
|
|
12
|
+
import { pathSpecifierResolver } from '../service/code-navigation/navigation-resolution-registry.js';
|
|
13
|
+
import { reparseFileReference } from '../service/code-navigation/file-reference-parser.js';
|
|
14
|
+
import { acquireBuild, buildIncrementalTags, ensurePrivateDirectory, manifestEntry, readManifest, removeInactiveGenerations, runCtags, sourceDelta, sourceFingerprint, tagsAvailable, tagsPath } from '../service/code-navigation/ctags-index-storage.js';
|
|
15
|
+
import { compareTagNames, firstTagOffset, parseTag, readTagLine } from '../service/code-navigation/ctags-query.js';
|
|
16
|
+
import { compareTargets, containsSymbol, sourceFileFingerprint, targetPreview, validatePathTarget, validateSymbolTarget } from '../service/code-navigation/navigation-target-validator.js';
|
|
17
17
|
const INDEX_SCHEMA_VERSION = 1;
|
|
18
18
|
const INDEX_OPTIONS_VERSION = 2;
|
|
19
19
|
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { resolveWorkspaceFileManagerTarget } from '
|
|
3
|
+
import { resolveWorkspaceFileManagerTarget } from './files.js';
|
|
4
4
|
export function fileManagerInvocation(platform, target) {
|
|
5
5
|
if (platform === 'win32')
|
|
6
6
|
return {
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
+
import { WorkspaceFileIndex } from './file-index.js';
|
|
1
2
|
import { basename, dirname, isAbsolute, relative, resolve, win32 } from 'node:path';
|
|
2
3
|
import { lstat, realpath } from 'node:fs/promises';
|
|
3
4
|
import { imagePreviewOptionsSchema, isComposerAttachmentResourcePath } from '@myagentroam/protocol';
|
|
4
|
-
import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAbsoluteFileContentRange, readWorkspaceFileContentRange, readAbsoluteTextFile, readWorkspaceTextFile, writeWorkspaceTextFile
|
|
5
|
+
import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAbsoluteFileContentRange, readWorkspaceFileContentRange, readAbsoluteTextFile, readWorkspaceTextFile, writeWorkspaceTextFile } from './files.js';
|
|
5
6
|
import { revealWorkspaceFileInFileManager } from './workspace-file-manager-service.js';
|
|
6
|
-
import { imagePreviews, MAX_IMAGE_PREVIEW_SOURCE_BYTES } from '
|
|
7
|
+
import { imagePreviews, MAX_IMAGE_PREVIEW_SOURCE_BYTES } from '../service/image-preview-service.js';
|
|
7
8
|
const INDEX_CACHE_LIMIT = 32;
|
|
8
9
|
const INDEX_CACHE_BYTE_LIMIT = 128 * 1024 * 1024;
|
|
9
10
|
const INDEX_QUEUE_LIMIT = 16;
|
|
10
11
|
const INDEX_QUEUE_TIMEOUT_MS = 120_000;
|
|
11
12
|
export class WorkspaceFileService {
|
|
12
|
-
state;
|
|
13
13
|
onInvalidated;
|
|
14
14
|
indexLimits;
|
|
15
15
|
attachmentResource;
|
|
16
|
+
fileIndexes = new Map();
|
|
16
17
|
mutationTails = new Map();
|
|
17
18
|
indexQueue = [];
|
|
18
19
|
indexRunning = false;
|
|
19
|
-
constructor(
|
|
20
|
-
this.state = state;
|
|
20
|
+
constructor(onInvalidated, indexLimits = {}, attachmentResource = () => undefined) {
|
|
21
21
|
this.onInvalidated = onInvalidated;
|
|
22
22
|
this.indexLimits = indexLimits;
|
|
23
23
|
this.attachmentResource = attachmentResource;
|
|
@@ -266,7 +266,7 @@ export class WorkspaceFileService {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
index(workspace) {
|
|
269
|
-
let index = this.
|
|
269
|
+
let index = this.fileIndexes.get(workspace.id);
|
|
270
270
|
if (index === undefined) {
|
|
271
271
|
index = new WorkspaceFileIndex(workspace.path, {
|
|
272
272
|
...(this.indexLimits.indexByteLimit === undefined
|
|
@@ -276,8 +276,8 @@ export class WorkspaceFileService {
|
|
|
276
276
|
onRebuilt: () => this.pruneIndexes(workspace.id)
|
|
277
277
|
});
|
|
278
278
|
}
|
|
279
|
-
this.
|
|
280
|
-
this.
|
|
279
|
+
this.fileIndexes.delete(workspace.id);
|
|
280
|
+
this.fileIndexes.set(workspace.id, index);
|
|
281
281
|
this.pruneIndexes(workspace.id);
|
|
282
282
|
return index;
|
|
283
283
|
}
|
|
@@ -298,16 +298,16 @@ export class WorkspaceFileService {
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
pruneIndexes(protectedId) {
|
|
301
|
-
let bytes = [...this.
|
|
301
|
+
let bytes = [...this.fileIndexes.values()].reduce((total, index) => total + index.accountedBytes, 0);
|
|
302
302
|
const byteLimit = this.indexLimits.cacheByteLimit ?? INDEX_CACHE_BYTE_LIMIT;
|
|
303
|
-
for (const [id, index] of this.
|
|
304
|
-
if (this.
|
|
303
|
+
for (const [id, index] of this.fileIndexes) {
|
|
304
|
+
if (this.fileIndexes.size <= INDEX_CACHE_LIMIT && bytes <= byteLimit)
|
|
305
305
|
break;
|
|
306
306
|
if (id === protectedId)
|
|
307
307
|
continue;
|
|
308
308
|
bytes -= index.accountedBytes;
|
|
309
309
|
index.dispose();
|
|
310
|
-
this.
|
|
310
|
+
this.fileIndexes.delete(id);
|
|
311
311
|
}
|
|
312
312
|
this.prunePreparations();
|
|
313
313
|
}
|
|
@@ -357,14 +357,14 @@ export class WorkspaceFileService {
|
|
|
357
357
|
}
|
|
358
358
|
dispose(workspaceId) {
|
|
359
359
|
if (workspaceId !== undefined) {
|
|
360
|
-
this.
|
|
361
|
-
this.
|
|
360
|
+
this.fileIndexes.get(workspaceId)?.dispose();
|
|
361
|
+
this.fileIndexes.delete(workspaceId);
|
|
362
362
|
this.prunePreparations();
|
|
363
363
|
return;
|
|
364
364
|
}
|
|
365
|
-
for (const index of this.
|
|
365
|
+
for (const index of this.fileIndexes.values())
|
|
366
366
|
index.dispose();
|
|
367
|
-
this.
|
|
367
|
+
this.fileIndexes.clear();
|
|
368
368
|
this.prunePreparations();
|
|
369
369
|
}
|
|
370
370
|
}
|
|
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
import { lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { GitCommandError, resolveWorkspaceGitRoot } from '
|
|
6
|
+
import { GitCommandError, resolveWorkspaceGitRoot } from './git.js';
|
|
7
7
|
const MAR_TEMP_DIRECTORY = '/.mar/';
|
|
8
8
|
const BLOCK_BEGIN = '# BEGIN MyAgentRoam';
|
|
9
9
|
const BLOCK_END = '# END MyAgentRoam';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { steerComposerMessage } from '@myagentroam/protocol';
|
|
2
|
-
import { isPlainRecord
|
|
3
|
-
import {
|
|
2
|
+
import { isPlainRecord } from '../util/records.js';
|
|
3
|
+
import { withNonImageAttachmentPrompt } from '../session/attachments.js';
|
|
4
|
+
import { assertQueueItemOwner, assertSessionControlOwner } from '../service/run-ownership-policy.js';
|
|
4
5
|
import { nodeLog } from '../operational.js';
|
|
5
6
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
6
7
|
export class WorkspaceQueueWorkbenchService {
|
|
@@ -2,38 +2,43 @@ import { nodeLog } from '../operational.js';
|
|
|
2
2
|
const WATCH_INTERVAL_MS = 30_000;
|
|
3
3
|
const CACHE_TTL_MS = 15_000;
|
|
4
4
|
export class WorkspaceSessionService {
|
|
5
|
-
state;
|
|
6
5
|
options;
|
|
7
6
|
cacheGenerations = new Map();
|
|
8
7
|
initialReads = new Map();
|
|
9
8
|
incompleteDiscoveries = new Set();
|
|
10
9
|
staleDiscoveries = new Set();
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
sessionWatchTimers = new Map();
|
|
11
|
+
sessionWatchSignatures = new Map();
|
|
12
|
+
sessionWatchReads = new Map();
|
|
13
|
+
sessionCache = new Map();
|
|
14
|
+
sessionCacheReads = new Map();
|
|
15
|
+
constructor(options) {
|
|
13
16
|
this.options = options;
|
|
14
17
|
}
|
|
18
|
+
cachedSnapshot(workspaceId) {
|
|
19
|
+
return this.sessionCache.get(workspaceId)?.sessions ?? [];
|
|
20
|
+
}
|
|
15
21
|
ensureWatch(workspaceId) {
|
|
16
|
-
if (this.
|
|
17
|
-
this.state.sessionWatchReads.has(workspaceId))
|
|
22
|
+
if (this.sessionWatchTimers.has(workspaceId) || this.sessionWatchReads.has(workspaceId))
|
|
18
23
|
return;
|
|
19
24
|
void this.runWatchCycle(workspaceId);
|
|
20
25
|
}
|
|
21
26
|
stopWatch(workspaceId) {
|
|
22
|
-
const timer = this.
|
|
27
|
+
const timer = this.sessionWatchTimers.get(workspaceId);
|
|
23
28
|
if (timer !== undefined)
|
|
24
29
|
clearTimeout(timer);
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
30
|
+
this.sessionWatchTimers.delete(workspaceId);
|
|
31
|
+
this.sessionWatchSignatures.delete(workspaceId);
|
|
27
32
|
}
|
|
28
33
|
discoveryPending(workspaceId) {
|
|
29
34
|
// Partial cold batches still await native identity confirmation for the remaining Runners.
|
|
30
35
|
return this.incompleteDiscoveries.has(workspaceId);
|
|
31
36
|
}
|
|
32
37
|
async cached(workspaceId) {
|
|
33
|
-
const cached = this.
|
|
38
|
+
const cached = this.sessionCache.get(workspaceId);
|
|
34
39
|
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
35
40
|
return cached.sessions;
|
|
36
|
-
let discovery = this.
|
|
41
|
+
let discovery = this.sessionCacheReads.get(workspaceId);
|
|
37
42
|
if (discovery === undefined) {
|
|
38
43
|
// A retired read must not regain ownership when its Workspace state is reclaimed.
|
|
39
44
|
const generation = Symbol();
|
|
@@ -56,8 +61,8 @@ export class WorkspaceSessionService {
|
|
|
56
61
|
const publish = (sessions, notify = true) => {
|
|
57
62
|
if (this.cacheGenerations.get(workspaceId) !== generation)
|
|
58
63
|
return;
|
|
59
|
-
const previous = this.
|
|
60
|
-
this.
|
|
64
|
+
const previous = this.sessionCache.get(workspaceId)?.sessions;
|
|
65
|
+
this.sessionCache.set(workspaceId, {
|
|
61
66
|
sessions,
|
|
62
67
|
expiresAt: this.staleDiscoveries.has(workspaceId) ? 0 : Date.now() + CACHE_TTL_MS
|
|
63
68
|
});
|
|
@@ -70,7 +75,7 @@ export class WorkspaceSessionService {
|
|
|
70
75
|
.discover(workspaceId, (runner, sessions) => {
|
|
71
76
|
if (!acceptingBatches || this.cacheGenerations.get(workspaceId) !== generation)
|
|
72
77
|
return;
|
|
73
|
-
const previous = this.
|
|
78
|
+
const previous = this.sessionCache.get(workspaceId)?.sessions;
|
|
74
79
|
const merged = [
|
|
75
80
|
...(previous ?? []).filter((session) => session.runner !== runner),
|
|
76
81
|
...sessions
|
|
@@ -85,7 +90,7 @@ export class WorkspaceSessionService {
|
|
|
85
90
|
.then((sessions) => {
|
|
86
91
|
if (this.cacheGenerations.get(workspaceId) === generation)
|
|
87
92
|
this.incompleteDiscoveries.delete(workspaceId);
|
|
88
|
-
const previous = this.
|
|
93
|
+
const previous = this.sessionCache.get(workspaceId)?.sessions;
|
|
89
94
|
notifyCompletion =
|
|
90
95
|
batchPublished ||
|
|
91
96
|
(previous !== undefined && sessionSignature(previous) !== sessionSignature(sessions));
|
|
@@ -97,16 +102,16 @@ export class WorkspaceSessionService {
|
|
|
97
102
|
rejectInitial(error);
|
|
98
103
|
if (this.cacheGenerations.get(workspaceId) === generation) {
|
|
99
104
|
nodeLog('native.session.discovery.failed', { workspaceId });
|
|
100
|
-
const partial = this.
|
|
105
|
+
const partial = this.sessionCache.get(workspaceId);
|
|
101
106
|
if (partial !== undefined)
|
|
102
|
-
this.
|
|
107
|
+
this.sessionCache.set(workspaceId, { ...partial, expiresAt: 0 });
|
|
103
108
|
}
|
|
104
109
|
throw error;
|
|
105
110
|
})
|
|
106
111
|
.finally(() => {
|
|
107
112
|
acceptingBatches = false;
|
|
108
|
-
if (this.
|
|
109
|
-
this.
|
|
113
|
+
if (this.sessionCacheReads.get(workspaceId) === read) {
|
|
114
|
+
this.sessionCacheReads.delete(workspaceId);
|
|
110
115
|
// Events during discovery require a trailing refresh, even if its result is unchanged.
|
|
111
116
|
if (this.staleDiscoveries.delete(workspaceId))
|
|
112
117
|
notifyCompletion = true;
|
|
@@ -115,13 +120,13 @@ export class WorkspaceSessionService {
|
|
|
115
120
|
this.initialReads.delete(workspaceId);
|
|
116
121
|
if (notifyCompletion && this.cacheGenerations.get(workspaceId) === generation)
|
|
117
122
|
this.options.invalidate(workspaceId);
|
|
118
|
-
if (!this.
|
|
119
|
-
!this.
|
|
123
|
+
if (!this.sessionCacheReads.has(workspaceId) &&
|
|
124
|
+
!this.sessionCache.has(workspaceId) &&
|
|
120
125
|
this.cacheGenerations.get(workspaceId) === generation)
|
|
121
126
|
this.cacheGenerations.delete(workspaceId);
|
|
122
127
|
});
|
|
123
128
|
discovery = read;
|
|
124
|
-
this.
|
|
129
|
+
this.sessionCacheReads.set(workspaceId, discovery);
|
|
125
130
|
// Later Runner failures cannot become unhandled after the first batch was returned.
|
|
126
131
|
void discovery.catch(() => undefined);
|
|
127
132
|
}
|
|
@@ -130,14 +135,14 @@ export class WorkspaceSessionService {
|
|
|
130
135
|
return this.initialReads.get(workspaceId) ?? discovery;
|
|
131
136
|
}
|
|
132
137
|
clear() {
|
|
133
|
-
for (const timer of this.
|
|
138
|
+
for (const timer of this.sessionWatchTimers.values())
|
|
134
139
|
clearTimeout(timer);
|
|
135
|
-
this.
|
|
136
|
-
this.
|
|
137
|
-
this.
|
|
140
|
+
this.sessionWatchTimers.clear();
|
|
141
|
+
this.sessionWatchSignatures.clear();
|
|
142
|
+
this.sessionWatchReads.clear();
|
|
138
143
|
this.cacheGenerations.clear();
|
|
139
|
-
this.
|
|
140
|
-
this.
|
|
144
|
+
this.sessionCache.clear();
|
|
145
|
+
this.sessionCacheReads.clear();
|
|
141
146
|
this.initialReads.clear();
|
|
142
147
|
this.incompleteDiscoveries.clear();
|
|
143
148
|
this.staleDiscoveries.clear();
|
|
@@ -147,38 +152,38 @@ export class WorkspaceSessionService {
|
|
|
147
152
|
this.stopWatch(workspaceId);
|
|
148
153
|
}
|
|
149
154
|
markCacheStale(workspaceId) {
|
|
150
|
-
const cached = this.
|
|
155
|
+
const cached = this.sessionCache.get(workspaceId);
|
|
151
156
|
if (cached !== undefined)
|
|
152
|
-
this.
|
|
153
|
-
if (this.
|
|
157
|
+
this.sessionCache.set(workspaceId, { ...cached, expiresAt: 0 });
|
|
158
|
+
if (this.sessionCacheReads.has(workspaceId))
|
|
154
159
|
this.staleDiscoveries.add(workspaceId);
|
|
155
160
|
}
|
|
156
161
|
invalidateCache(workspaceId) {
|
|
157
162
|
this.cacheGenerations.delete(workspaceId);
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
163
|
+
this.sessionCache.delete(workspaceId);
|
|
164
|
+
this.sessionCacheReads.delete(workspaceId);
|
|
160
165
|
this.initialReads.delete(workspaceId);
|
|
161
166
|
this.incompleteDiscoveries.delete(workspaceId);
|
|
162
167
|
this.staleDiscoveries.delete(workspaceId);
|
|
163
168
|
}
|
|
164
169
|
async refreshWatch(workspaceId) {
|
|
165
|
-
const current = this.
|
|
170
|
+
const current = this.sessionWatchReads.get(workspaceId);
|
|
166
171
|
if (current !== undefined)
|
|
167
172
|
return current;
|
|
168
|
-
const refresh = this.performRefresh(workspaceId).finally(() => this.
|
|
169
|
-
this.
|
|
173
|
+
const refresh = this.performRefresh(workspaceId).finally(() => this.sessionWatchReads.delete(workspaceId));
|
|
174
|
+
this.sessionWatchReads.set(workspaceId, refresh);
|
|
170
175
|
return refresh;
|
|
171
176
|
}
|
|
172
177
|
async runWatchCycle(workspaceId) {
|
|
173
178
|
await this.refreshWatch(workspaceId);
|
|
174
|
-
if (!this.options.active(workspaceId) || this.
|
|
179
|
+
if (!this.options.active(workspaceId) || this.sessionWatchTimers.has(workspaceId))
|
|
175
180
|
return;
|
|
176
181
|
const timer = setTimeout(() => {
|
|
177
|
-
this.
|
|
182
|
+
this.sessionWatchTimers.delete(workspaceId);
|
|
178
183
|
void this.runWatchCycle(workspaceId);
|
|
179
184
|
}, WATCH_INTERVAL_MS);
|
|
180
185
|
timer.unref();
|
|
181
|
-
this.
|
|
186
|
+
this.sessionWatchTimers.set(workspaceId, timer);
|
|
182
187
|
}
|
|
183
188
|
async performRefresh(workspaceId) {
|
|
184
189
|
if (!this.options.active(workspaceId))
|
|
@@ -187,8 +192,8 @@ export class WorkspaceSessionService {
|
|
|
187
192
|
const signature = JSON.stringify(await this.options.listPage(workspaceId));
|
|
188
193
|
if (!this.options.active(workspaceId))
|
|
189
194
|
return this.stopWatch(workspaceId);
|
|
190
|
-
const previous = this.
|
|
191
|
-
this.
|
|
195
|
+
const previous = this.sessionWatchSignatures.get(workspaceId);
|
|
196
|
+
this.sessionWatchSignatures.set(workspaceId, signature);
|
|
192
197
|
if (previous === undefined || previous === signature)
|
|
193
198
|
return;
|
|
194
199
|
this.options.invalidate(workspaceId);
|