@myagentroam/node 0.11.36 → 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.
Files changed (142) hide show
  1. package/dist/application/execution-lifecycle.js +54 -0
  2. package/dist/application/node-lifecycle.js +104 -0
  3. package/dist/application/node-operation-admission.js +51 -0
  4. package/dist/application/node-operation-contracts.js +1 -0
  5. package/dist/application/runner-composition.js +22 -0
  6. package/dist/application/session-projection-composition.js +26 -0
  7. package/dist/application/workspace-application.js +61 -0
  8. package/dist/application/workspace-composition.js +21 -0
  9. package/dist/capabilities.js +2 -2
  10. package/dist/codex-app-server.js +1 -471
  11. package/dist/connection/authorization.js +26 -0
  12. package/dist/{service → connection}/direct-transfer-service.js +1 -1
  13. package/dist/connection/endpoints.js +32 -0
  14. package/dist/connector.js +144 -252
  15. package/dist/database.js +34 -14
  16. package/dist/git-askpass.js +1 -1
  17. package/dist/git-execution-helper.js +1 -1
  18. package/dist/hosting/hosted-conversation-file-identity.js +27 -0
  19. package/dist/hosting/hosted-conversation-file-operations.js +44 -0
  20. package/dist/hosting/hosted-conversation-node-service.js +974 -0
  21. package/dist/hosting/hosted-conversation-runs.js +599 -0
  22. package/dist/hosting/hosted-conversation-templates.js +280 -0
  23. package/dist/hosting/hosted-conversation-uploads.js +344 -0
  24. package/dist/hosting/hosted-conversation-watch.js +194 -0
  25. package/dist/hosting/runtime-contracts.js +1 -0
  26. package/dist/runner/abstract-runner.js +4 -13
  27. package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
  28. package/dist/runner/claude/conversation-parser.js +2 -1
  29. package/dist/runner/claude/managed-run-controller.js +6 -7
  30. package/dist/runner/claude/message-parsers.js +2 -1
  31. package/dist/runner/claude/native-transcript.js +2 -1
  32. package/dist/runner/claude/tool-normalizer.js +1 -1
  33. package/dist/runner/claude-code-runner.js +2 -2
  34. package/dist/runner/codex/codex-app-server.js +471 -0
  35. package/dist/runner/codex/conversation-parser.js +4 -2
  36. package/dist/runner/codex/managed-run-controller.js +7 -9
  37. package/dist/runner/codex/native-transcript.js +1 -1
  38. package/dist/runner/codex/tool-normalizer.js +1 -1
  39. package/dist/runner/codex-runner.js +9 -27
  40. package/dist/runner/contracts.js +1 -0
  41. package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
  42. package/dist/runner/mar-agent/history-projection.js +1 -1
  43. package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
  44. package/dist/runner/mar-agent/managed-run-controller.js +85 -61
  45. package/dist/runner/mar-agent-runner.js +1 -1
  46. package/dist/runner/normalization.js +42 -0
  47. package/dist/runner/opencode/conversation-parser.js +1 -1
  48. package/dist/runner/opencode/managed-run-controller.js +6 -7
  49. package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
  50. package/dist/runner/opencode/tool-normalizer.js +1 -1
  51. package/dist/runner/opencode-runner.js +4 -5
  52. package/dist/runner/runner-registry.js +4 -1
  53. package/dist/runtime-state.js +48 -372
  54. package/dist/service/computer-use-enablement-service.js +1 -1
  55. package/dist/service/continuous-run-coordinator.js +52 -7
  56. package/dist/service/conversation-history-service.js +4 -4
  57. package/dist/service/conversation-hosting-capability-service.js +1 -1
  58. package/dist/service/conversation-hosting-enablement-service.js +1 -1
  59. package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
  60. package/dist/service/conversation-hosting-maintenance-service.js +1 -1
  61. package/dist/service/conversation-observation.js +1 -1
  62. package/dist/service/conversation-segment-service.js +1 -1
  63. package/dist/service/external-session-resume-service.js +47 -27
  64. package/dist/service/fake-managed-run-service.js +3 -7
  65. package/dist/service/git-operation-service.js +1 -1
  66. package/dist/service/goal-continuation-coordinator.js +14 -7
  67. package/dist/service/mar-agent-model-transport-test-service.js +1 -1
  68. package/dist/service/native-session-watch-service.js +1 -1
  69. package/dist/service/node-control-channel.js +1 -5
  70. package/dist/service/node-request-service.js +1 -1
  71. package/dist/service/node-terminal-channel.js +2 -1
  72. package/dist/service/node-workspace-coordinator.js +3 -2
  73. package/dist/service/notification-mcp-bridge.js +6 -2
  74. package/dist/service/recent-run-projection-service.js +1 -1
  75. package/dist/service/restart-session-restorer.js +1 -1
  76. package/dist/service/run-event-service.js +49 -42
  77. package/dist/service/run-workbench-service.js +3 -3
  78. package/dist/service/runner-interaction-service.js +6 -5
  79. package/dist/service/runner-service.js +1 -1
  80. package/dist/service/session-recent-activity.js +2 -1
  81. package/dist/service/skill-directory-service.js +1 -1
  82. package/dist/service/skill-install-service.js +2 -2
  83. package/dist/service/terminal-service.js +1 -1
  84. package/dist/service/workbench-event-projection.js +3 -2
  85. package/dist/service/workbench-event-service.js +1 -1
  86. package/dist/service/workspace-service.js +3 -2
  87. package/dist/session/attachments.js +110 -0
  88. package/dist/session/contracts.js +1 -0
  89. package/dist/{util → session}/conversation-identity.js +1 -1
  90. package/dist/session/input-validation.js +15 -0
  91. package/dist/{service → session}/native-session-projection-service.js +40 -11
  92. package/dist/session/pagination.js +77 -0
  93. package/dist/{util → session}/runner-native-session-parsers.js +2 -1
  94. package/dist/session/runtime/approvals.js +90 -0
  95. package/dist/session/runtime/conversation-projection.js +310 -0
  96. package/dist/session/runtime/type-guards.js +19 -0
  97. package/dist/{service → session}/session-catalog-service.js +5 -4
  98. package/dist/{service → session}/session-command-service.js +3 -3
  99. package/dist/{service → session}/session-identity-service.js +1 -1
  100. package/dist/{service → session}/session-lifecycle-service.js +18 -7
  101. package/dist/{service → session}/session-message-service.js +6 -5
  102. package/dist/{service → session}/session-presentation-service.js +1 -1
  103. package/dist/{service → session}/session-query-service.js +2 -2
  104. package/dist/{service → session}/session-schedule-service.js +2 -2
  105. package/dist/util/records.js +3 -0
  106. package/dist/workspace/contracts.js +1 -0
  107. package/dist/workspace/file-index.js +534 -0
  108. package/dist/workspace/files.js +466 -0
  109. package/dist/workspace/git.js +991 -0
  110. package/dist/workspace/path-access.js +184 -0
  111. package/dist/workspace/utf8.js +12 -0
  112. package/dist/{service → workspace}/workspace-change-service.js +45 -21
  113. package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
  114. package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
  115. package/dist/{service → workspace}/workspace-file-service.js +16 -16
  116. package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
  117. package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
  118. package/dist/{service → workspace}/workspace-session-service.js +45 -40
  119. package/dist/{service → workspace}/workspace-upload-service.js +28 -28
  120. package/dist/{service → workspace}/workspace-watch-service.js +29 -28
  121. package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
  122. package/package.json +5 -5
  123. package/dist/service/hosted-conversation-node-service.js +0 -2252
  124. package/dist/service/run-domain-state.js +0 -4
  125. package/dist/service/session-domain-state.js +0 -9
  126. package/dist/service/workspace-domain-state.js +0 -41
  127. package/dist/util/node-operation-parsers.js +0 -320
  128. package/dist/workspace.js +0 -2168
  129. /package/dist/{connector → application}/node-connector-options.js +0 -0
  130. /package/dist/{util → connection}/node-operation-data.js +0 -0
  131. /package/dist/{connector → connection}/node-operation-router.js +0 -0
  132. /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
  133. /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
  134. /package/dist/{util → mcp}/secret-environment.js +0 -0
  135. /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
  136. /package/dist/{util → runner}/personal-instructions.js +0 -0
  137. /package/dist/{util → runner}/runner-error.js +0 -0
  138. /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
  139. /package/dist/{service → session}/session-context-service.js +0 -0
  140. /package/dist/{util → workspace}/git-credential-client.js +0 -0
  141. /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
  142. /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 '../workspace.js';
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
- state;
14
- constructor(state) {
15
- this.state = state;
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.state.changesVersions.get(workspaceId) ?? 0;
18
+ return this.changesVersions.get(workspaceId) ?? 0;
19
19
  }
20
20
  invalidate(workspaceId) {
21
- this.state.changesReads.delete(workspaceId);
22
- this.state.changesCache.delete(workspaceId);
23
- this.state.changesVersions.set(workspaceId, (this.state.changesVersions.get(workspaceId) ?? 0) + 1);
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.state.changesReads.clear();
27
- this.state.changesCache.clear();
28
- this.state.changesVersions.clear();
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.state.changesCache.get(workspace.id);
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.state.changesReads.get(workspace.id);
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.state.changesVersions.get(workspace.id) ?? 0;
41
- const request = this.state.changesLimiter
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.state.changesVersions.get(workspace.id) ?? 0) === version)
293
- this.state.changesCache.set(workspace.id, {
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.state.changesReads.set(workspace.id, request);
305
+ this.changesReads.set(workspace.id, request);
306
306
  try {
307
307
  return await request;
308
308
  }
309
309
  finally {
310
- if (this.state.changesReads.get(workspace.id) === request)
311
- this.state.changesReads.delete(workspace.id);
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 '../workspace.js';
12
- import { pathSpecifierResolver } from './code-navigation/navigation-resolution-registry.js';
13
- import { reparseFileReference } from './code-navigation/file-reference-parser.js';
14
- import { acquireBuild, buildIncrementalTags, ensurePrivateDirectory, manifestEntry, readManifest, removeInactiveGenerations, runCtags, sourceDelta, sourceFingerprint, tagsAvailable, tagsPath } from './code-navigation/ctags-index-storage.js';
15
- import { compareTagNames, firstTagOffset, parseTag, readTagLine } from './code-navigation/ctags-query.js';
16
- import { compareTargets, containsSymbol, sourceFileFingerprint, targetPreview, validatePathTarget, validateSymbolTarget } from './code-navigation/navigation-target-validator.js';
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 '../workspace.js';
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, WorkspaceFileIndex } from '../workspace.js';
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 './image-preview-service.js';
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(state, onInvalidated, indexLimits = {}, attachmentResource = () => undefined) {
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.state.fileIndexes.get(workspace.id);
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.state.fileIndexes.delete(workspace.id);
280
- this.state.fileIndexes.set(workspace.id, index);
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.state.fileIndexes.values()].reduce((total, index) => total + index.accountedBytes, 0);
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.state.fileIndexes) {
304
- if (this.state.fileIndexes.size <= INDEX_CACHE_LIMIT && bytes <= byteLimit)
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.state.fileIndexes.delete(id);
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.state.fileIndexes.get(workspaceId)?.dispose();
361
- this.state.fileIndexes.delete(workspaceId);
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.state.fileIndexes.values())
365
+ for (const index of this.fileIndexes.values())
366
366
  index.dispose();
367
- this.state.fileIndexes.clear();
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 '../workspace.js';
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, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
3
- import { assertQueueItemOwner, assertSessionControlOwner } from './run-ownership-policy.js';
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
- constructor(state, options) {
12
- this.state = state;
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.state.sessionWatchTimers.has(workspaceId) ||
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.state.sessionWatchTimers.get(workspaceId);
27
+ const timer = this.sessionWatchTimers.get(workspaceId);
23
28
  if (timer !== undefined)
24
29
  clearTimeout(timer);
25
- this.state.sessionWatchTimers.delete(workspaceId);
26
- this.state.sessionWatchSignatures.delete(workspaceId);
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.state.sessionCache.get(workspaceId);
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.state.sessionCacheReads.get(workspaceId);
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.state.sessionCache.get(workspaceId)?.sessions;
60
- this.state.sessionCache.set(workspaceId, {
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.state.sessionCache.get(workspaceId)?.sessions;
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.state.sessionCache.get(workspaceId)?.sessions;
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.state.sessionCache.get(workspaceId);
105
+ const partial = this.sessionCache.get(workspaceId);
101
106
  if (partial !== undefined)
102
- this.state.sessionCache.set(workspaceId, { ...partial, expiresAt: 0 });
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.state.sessionCacheReads.get(workspaceId) === read) {
109
- this.state.sessionCacheReads.delete(workspaceId);
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.state.sessionCacheReads.has(workspaceId) &&
119
- !this.state.sessionCache.has(workspaceId) &&
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.state.sessionCacheReads.set(workspaceId, discovery);
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.state.sessionWatchTimers.values())
138
+ for (const timer of this.sessionWatchTimers.values())
134
139
  clearTimeout(timer);
135
- this.state.sessionWatchTimers.clear();
136
- this.state.sessionWatchSignatures.clear();
137
- this.state.sessionWatchReads.clear();
140
+ this.sessionWatchTimers.clear();
141
+ this.sessionWatchSignatures.clear();
142
+ this.sessionWatchReads.clear();
138
143
  this.cacheGenerations.clear();
139
- this.state.sessionCache.clear();
140
- this.state.sessionCacheReads.clear();
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.state.sessionCache.get(workspaceId);
155
+ const cached = this.sessionCache.get(workspaceId);
151
156
  if (cached !== undefined)
152
- this.state.sessionCache.set(workspaceId, { ...cached, expiresAt: 0 });
153
- if (this.state.sessionCacheReads.has(workspaceId))
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.state.sessionCache.delete(workspaceId);
159
- this.state.sessionCacheReads.delete(workspaceId);
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.state.sessionWatchReads.get(workspaceId);
170
+ const current = this.sessionWatchReads.get(workspaceId);
166
171
  if (current !== undefined)
167
172
  return current;
168
- const refresh = this.performRefresh(workspaceId).finally(() => this.state.sessionWatchReads.delete(workspaceId));
169
- this.state.sessionWatchReads.set(workspaceId, refresh);
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.state.sessionWatchTimers.has(workspaceId))
179
+ if (!this.options.active(workspaceId) || this.sessionWatchTimers.has(workspaceId))
175
180
  return;
176
181
  const timer = setTimeout(() => {
177
- this.state.sessionWatchTimers.delete(workspaceId);
182
+ this.sessionWatchTimers.delete(workspaceId);
178
183
  void this.runWatchCycle(workspaceId);
179
184
  }, WATCH_INTERVAL_MS);
180
185
  timer.unref();
181
- this.state.sessionWatchTimers.set(workspaceId, timer);
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.state.sessionWatchSignatures.get(workspaceId);
191
- this.state.sessionWatchSignatures.set(workspaceId, signature);
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);