@minionry/minion 0.7.36 → 0.7.39

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 (120) hide show
  1. package/bin/commands/minions.js +19 -0
  2. package/bin/minion.js +33 -4
  3. package/dist/server/cli/headless/claude-invoker-process.js +23 -7
  4. package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
  5. package/dist/server/cli/headless/haiku-assessments.js +2 -1
  6. package/dist/server/cli/headless/mcp-config.js +2 -1
  7. package/dist/server/engines/claude/claude-command.js +1 -0
  8. package/dist/server/engines/factory.js +2 -0
  9. package/dist/server/engines/hermes/HermesEngine.js +112 -219
  10. package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
  11. package/dist/server/index.js +10 -4
  12. package/dist/server/mcp/bouncer-integration.js +3 -2
  13. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
  14. package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
  15. package/dist/server/mcp/classifier/telemetry.js +0 -0
  16. package/dist/server/mcp/permission-channels.js +176 -0
  17. package/dist/server/mcp/security-analysis.js +9 -1
  18. package/dist/server/mcp/security-audit.js +22 -2
  19. package/dist/server/mcp/security-patterns.js +73 -1
  20. package/dist/server/mcp/server.js +80 -50
  21. package/dist/server/routes/internal.js +6 -1
  22. package/dist/server/routes/notifications.js +6 -18
  23. package/dist/server/server-setup.js +24 -48
  24. package/dist/server/services/analytics.js +4 -2
  25. package/dist/server/services/browser/host-mock.js +6 -1
  26. package/dist/server/services/browser/host.js +27 -3
  27. package/dist/server/services/browser/reasoning-agent.js +3 -1
  28. package/dist/server/services/chain/chain-engine.js +494 -0
  29. package/dist/server/services/chain/chain-store.js +542 -0
  30. package/dist/server/services/git/haiku.js +2 -2
  31. package/dist/server/services/plan/agents/check-injection.md +43 -2
  32. package/dist/server/services/plan/agents/review-code.md +62 -3
  33. package/dist/server/services/plan/agents/review-quality.md +52 -2
  34. package/dist/server/services/plan/board-export.js +4 -0
  35. package/dist/server/services/plan/composer-prompt.js +27 -8
  36. package/dist/server/services/plan/composer.js +10 -4
  37. package/dist/server/services/plan/config-installer.js +4 -83
  38. package/dist/server/services/plan/executor.js +155 -73
  39. package/dist/server/services/plan/issue-effort.js +9 -21
  40. package/dist/server/services/plan/issue-prompt-builder.js +72 -9
  41. package/dist/server/services/plan/parser-core.js +6 -1
  42. package/dist/server/services/plan/quality-delta.js +6 -24
  43. package/dist/server/services/plan/readiness-planner.js +12 -4
  44. package/dist/server/services/plan/record-file.js +52 -0
  45. package/dist/server/services/plan/review-approval.js +64 -0
  46. package/dist/server/services/plan/review-gate.js +186 -77
  47. package/dist/server/services/plan/review-outcome.js +146 -0
  48. package/dist/server/services/plan/review-report.js +28 -1
  49. package/dist/server/services/plan/review-target.js +57 -0
  50. package/dist/server/services/plan/state-reconciler.js +3 -3
  51. package/dist/server/services/plan/template-diff.js +2 -0
  52. package/dist/server/services/plan/template-instantiator.js +4 -0
  53. package/dist/server/services/plan/watcher.js +11 -1
  54. package/dist/server/services/platform-reconnect.js +7 -0
  55. package/dist/server/services/platform-token-lifecycle.js +2 -1
  56. package/dist/server/services/platform.js +293 -50
  57. package/dist/server/services/relay-http-client.js +107 -0
  58. package/dist/server/services/schedule/agent-schedule-store.js +69 -8
  59. package/dist/server/services/schedule/agent-scheduler.js +48 -7
  60. package/dist/server/services/schedule/daily-timing.js +52 -0
  61. package/dist/server/services/schedule/prompt-file.js +55 -0
  62. package/dist/server/services/schedule/run-bootstrap.js +20 -5
  63. package/dist/server/services/schedule/schedule-store.js +54 -4
  64. package/dist/server/services/schedule/schedule-wire.js +17 -0
  65. package/dist/server/services/schedule/scheduler.js +43 -9
  66. package/dist/server/services/sdk/agent-schedule.js +294 -0
  67. package/dist/server/services/sdk/agents.js +95 -13
  68. package/dist/server/services/sdk/app-tools.js +44 -7
  69. package/dist/server/services/sdk/atomic-write.js +75 -0
  70. package/dist/server/services/sdk/browser.js +5 -1
  71. package/dist/server/services/sdk/composer.js +2 -0
  72. package/dist/server/services/sdk/file-change-notify.js +60 -13
  73. package/dist/server/services/sdk/files-app.js +12 -7
  74. package/dist/server/services/sdk/files-search.js +14 -0
  75. package/dist/server/services/sdk/files-transfer.js +1 -1
  76. package/dist/server/services/sdk/files-watch.js +721 -0
  77. package/dist/server/services/sdk/files-workspace.js +37 -11
  78. package/dist/server/services/sdk/inference-claude.js +4 -2
  79. package/dist/server/services/sdk/inference.js +16 -2
  80. package/dist/server/services/sdk/pm-chain.js +0 -0
  81. package/dist/server/services/sdk/pm-schedule.js +72 -15
  82. package/dist/server/services/sdk/pm.js +22 -0
  83. package/dist/server/services/sdk/rate-limits.js +32 -0
  84. package/dist/server/services/sdk/registry.js +40 -2
  85. package/dist/server/services/sdk/terminal.js +1 -1
  86. package/dist/server/services/sentry.js +5 -1
  87. package/dist/server/services/terminal/pty-manager.js +26 -11
  88. package/dist/server/services/terminal/pty-utils.js +12 -1
  89. package/dist/server/services/terminal/terminal-modes.js +87 -0
  90. package/dist/server/services/timeline/assembler.js +2 -2
  91. package/dist/server/services/timeline/index.js +1 -1
  92. package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
  93. package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
  94. package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
  95. package/dist/server/services/websocket/browser-handlers.js +5 -2
  96. package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
  97. package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
  98. package/dist/server/services/websocket/file-transfer-http.js +12 -6
  99. package/dist/server/services/websocket/handler.js +185 -33
  100. package/dist/server/services/websocket/msg-id-tracker.js +94 -19
  101. package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
  102. package/dist/server/services/websocket/plan-handlers.js +10 -5
  103. package/dist/server/services/websocket/plan-helpers.js +23 -0
  104. package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
  105. package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
  106. package/dist/server/services/websocket/schedule-handlers.js +8 -2
  107. package/dist/server/services/websocket/sdk-agent-host.js +5 -1
  108. package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
  109. package/dist/server/services/websocket/sdk-browser-host.js +11 -4
  110. package/dist/server/services/websocket/sdk-handlers.js +6 -1
  111. package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
  112. package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
  113. package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
  114. package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
  115. package/dist/server/services/websocket/settings-handlers.js +6 -18
  116. package/dist/server/services/websocket/tab-broadcast.js +5 -1
  117. package/dist/server/services/websocket/terminal-handlers.js +51 -6
  118. package/dist/server/services/websocket/types.js +6 -5
  119. package/dist/server/services/websocket/viewer-interest.js +30 -0
  120. package/package.json +6 -6
@@ -11,6 +11,7 @@ export const APP_TOOL_MAX_RESULT_BYTES = 1024 * 1024;
11
11
  const registrations = new Map();
12
12
  const senders = new Map();
13
13
  const pending = new Map();
14
+ const detachedViewers = new Set();
14
15
  export class AppToolCallError extends Error {
15
16
  code;
16
17
  constructor(code, message) {
@@ -222,6 +223,13 @@ export function callAppTool(args) {
222
223
  const spec = registration.tools.get(args.tool);
223
224
  const timeoutMs = Math.min(args.timeoutMs ?? spec.timeoutMs ?? APP_TOOL_CALL_TIMEOUT_MS, APP_TOOL_CALL_TIMEOUT_MAX_MS);
224
225
  const callId = `tc-${randomUUID()}`;
226
+ const frame = {
227
+ op: 'call',
228
+ callId,
229
+ requestId: registration.requestId,
230
+ tool: args.tool,
231
+ ...(args.input !== undefined ? { input: args.input } : {}),
232
+ };
225
233
  return new Promise((resolve, reject) => {
226
234
  const timer = setTimeout(() => {
227
235
  const call = pending.get(callId);
@@ -238,19 +246,44 @@ export function callAppTool(args) {
238
246
  appId: args.appId,
239
247
  spaceKey: args.spaceKey,
240
248
  tool: args.tool,
249
+ frame,
241
250
  timer,
242
251
  settle: resolve,
243
252
  fail: reject,
244
253
  });
245
- sendToolCall(registration, {
246
- op: 'call',
247
- callId,
248
- requestId: registration.requestId,
249
- tool: args.tool,
250
- ...(args.input !== undefined ? { input: args.input } : {}),
251
- });
254
+ if (!isDetached(registration))
255
+ sendToolCall(registration, frame);
252
256
  });
253
257
  }
258
+ function isDetached(registration) {
259
+ return registration.viewerId !== undefined && detachedViewers.has(registration.viewerId);
260
+ }
261
+ export function holdAppToolCallsFor(viewerId) {
262
+ if (servesAnything(viewerId))
263
+ detachedViewers.add(viewerId);
264
+ }
265
+ function servesAnything(viewerId) {
266
+ for (const registration of registrations.values()) {
267
+ if (registration.viewerId === viewerId)
268
+ return true;
269
+ }
270
+ return false;
271
+ }
272
+ export function resumeAppToolCallsFor(viewerId) {
273
+ if (!detachedViewers.delete(viewerId))
274
+ return 0;
275
+ let sent = 0;
276
+ for (const call of [...pending.values()]) {
277
+ if (!pending.has(call.callId))
278
+ continue;
279
+ const registration = registrations.get(call.registrationId);
280
+ if (registration?.viewerId !== viewerId)
281
+ continue;
282
+ sendToolCall(registration, call.frame);
283
+ sent += 1;
284
+ }
285
+ return sent;
286
+ }
254
287
  export function cancelAppToolCall(callId, cause = 'cancelled by the host') {
255
288
  const call = pending.get(callId);
256
289
  if (!call)
@@ -267,6 +300,9 @@ export function retireAppToolRegistration(requestId, cause) {
267
300
  return false;
268
301
  registrations.delete(requestId);
269
302
  senders.delete(requestId);
303
+ if (registration.viewerId !== undefined && !servesAnything(registration.viewerId)) {
304
+ detachedViewers.delete(registration.viewerId);
305
+ }
270
306
  for (const call of [...pending.values()]) {
271
307
  if (call.registrationId !== requestId)
272
308
  continue;
@@ -300,4 +336,5 @@ export function resetAppTools() {
300
336
  pending.clear();
301
337
  registrations.clear();
302
338
  senders.clear();
339
+ detachedViewers.clear();
303
340
  }
@@ -0,0 +1,75 @@
1
+ // Copyright (c) 2025-present Minionry, Inc.
2
+ // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
+ import { closeSync, fchmodSync, fsyncSync, mkdirSync, openSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { SdkMethodError } from './registry.js';
6
+ export const MTIME_TOLERANCE_MS = 1;
7
+ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
8
+ export function parseExpectedModifiedAt(params, method) {
9
+ const value = params.expectedModifiedAt;
10
+ if (value === undefined)
11
+ return undefined;
12
+ if (typeof value !== 'string' || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) {
13
+ throw new SdkMethodError('BAD_REQUEST', `${method}: expectedModifiedAt must be an ISO-8601 timestamp with a zone, such as the modifiedAt files.readEntry returns`);
14
+ }
15
+ return value;
16
+ }
17
+ let tempCounter = 0;
18
+ function statOrUndefined(path) {
19
+ try {
20
+ return statSync(path);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function refuseUnlessUnmodified(existing, expected, displayPath) {
27
+ if (!existing) {
28
+ throw new SdkMethodError('CONFLICT', `'${displayPath}' no longer exists (expected it unmodified since ${expected}) — nothing was written`);
29
+ }
30
+ if (Math.abs(existing.mtimeMs - Date.parse(expected)) > MTIME_TOLERANCE_MS) {
31
+ throw new SdkMethodError('CONFLICT', `'${displayPath}' was modified at ${existing.mtime.toISOString()}, not ${expected} — nothing was written; files.readEntry returns its current content and modifiedAt`);
32
+ }
33
+ }
34
+ export function writeFileAtomic({ target, content, expectedModifiedAt, displayPath }) {
35
+ const existing = statOrUndefined(target);
36
+ if (expectedModifiedAt !== undefined)
37
+ refuseUnlessUnmodified(existing, expectedModifiedAt, displayPath);
38
+ const dir = dirname(target);
39
+ mkdirSync(dir, { recursive: true });
40
+ const temp = join(dir, `.${basename(target).slice(0, 64)}.${process.pid}.${++tempCounter}.tmp`);
41
+ let fd;
42
+ let created = false;
43
+ try {
44
+ fd = openSync(temp, 'wx');
45
+ created = true;
46
+ if (existing)
47
+ fchmodSync(fd, existing.mode & 0o7777);
48
+ writeFileSync(fd, content, 'utf-8');
49
+ fsyncSync(fd);
50
+ closeSync(fd);
51
+ fd = undefined;
52
+ renameSync(temp, target);
53
+ }
54
+ catch (error) {
55
+ if (fd !== undefined)
56
+ closeQuietly(fd);
57
+ if (created)
58
+ unlinkQuietly(temp);
59
+ throw error;
60
+ }
61
+ }
62
+ function closeQuietly(fd) {
63
+ try {
64
+ closeSync(fd);
65
+ }
66
+ catch {
67
+ }
68
+ }
69
+ function unlinkQuietly(path) {
70
+ try {
71
+ unlinkSync(path);
72
+ }
73
+ catch {
74
+ }
75
+ }
@@ -130,7 +130,11 @@ export async function browserOpenHandler(args) {
130
130
  const raw = paramsObject(args.params);
131
131
  const url = requireWebUrlParam(raw, 'url', args.method);
132
132
  const pinnedApp = raw.pinnedApp === true;
133
- return requireHost(args.method).open({ url, ...(pinnedApp ? { reuseOrigin: args.origin } : {}) });
133
+ return requireHost(args.method).open({
134
+ url,
135
+ ...(pinnedApp ? { reuseOrigin: args.origin } : {}),
136
+ opener: { appId: args.appId, origin: args.origin },
137
+ });
134
138
  }
135
139
  export async function browserCloseHandler(args) {
136
140
  const raw = paramsObject(args.params);
@@ -6,6 +6,7 @@ import { peekModelCatalog } from '../model-catalog.js';
6
6
  import { getEffortLevel, getEngine, getFastMode, getModel } from '../settings.js';
7
7
  import { collectAllSkills } from '../websocket/skill-handlers.js';
8
8
  import { rankAllSnippets, rankQuerySnippets } from '../websocket/snippet-handlers.js';
9
+ import { appRunEngines } from './agents.js';
9
10
  import { isAppOwnedProfile } from './app-endpoints.js';
10
11
  import { inferenceEngines } from './inference.js';
11
12
  import { SdkMethodError } from './registry.js';
@@ -125,5 +126,6 @@ export function composerSettingsHandler(_args) {
125
126
  modelsStatus: catalog.status,
126
127
  effortLevels: Object.fromEntries(ENGINE_IDS.map((engine) => [engine, [...ENGINE_EFFORT_LEVELS[engine]]])),
127
128
  inferenceEngines: inferenceEngines(),
129
+ agentRunEngines: appRunEngines(),
128
130
  };
129
131
  }
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2025-present Minionry, Inc.
2
2
  // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
3
  import { realpathSync } from 'node:fs';
4
- import { join, relative, sep } from 'node:path';
4
+ import { basename, dirname, join, relative, sep } from 'node:path';
5
5
  const listeners = new Set();
6
6
  export function registerFileChangeListener(spaceKey, root, cb) {
7
7
  const listener = { spaceKey, root, cb };
@@ -10,26 +10,73 @@ export function registerFileChangeListener(spaceKey, root, cb) {
10
10
  listeners.delete(listener);
11
11
  };
12
12
  }
13
- export function notifyFileChangedAbsolute(spaceKey, absolutePath, kind) {
13
+ function within(root, absolutePath) {
14
+ if (absolutePath === root)
15
+ return '';
16
+ return absolutePath.startsWith(root + sep) ? relative(root, absolutePath) : undefined;
17
+ }
18
+ function deliveryFor(root, source, kind, target, dir) {
19
+ const facts = dir ? { dir: true } : {};
20
+ const path = within(root, source);
21
+ if (kind === 'renamed') {
22
+ const newPath = target === undefined ? undefined : within(root, target);
23
+ if (path !== undefined && newPath !== undefined)
24
+ return { path, kind, newPath, ...facts };
25
+ if (path !== undefined)
26
+ return { path, kind: 'deleted', ...facts };
27
+ if (newPath !== undefined)
28
+ return { path: newPath, kind: 'created', ...facts };
29
+ }
30
+ else if (path !== undefined) {
31
+ return { path, kind, ...facts };
32
+ }
33
+ return undefined;
34
+ }
35
+ let observer;
36
+ export function setFileChangeObserver(next) {
37
+ observer = next;
38
+ }
39
+ function deliver(spaceKey, source, kind, target, dir) {
40
+ if (kind !== 'opened')
41
+ observer?.(spaceKey, source, target);
14
42
  for (const listener of listeners) {
15
43
  if (listener.spaceKey !== spaceKey)
16
44
  continue;
17
- if (absolutePath !== listener.root && !absolutePath.startsWith(listener.root + sep))
18
- continue;
19
- listener.cb({ path: relative(listener.root, absolutePath), kind });
45
+ const change = deliveryFor(listener.root, source, kind, target, dir);
46
+ if (change)
47
+ listener.cb(change);
20
48
  }
21
49
  }
22
- export function notifyFileChanged(spaceKey, workingDir, path, kind) {
50
+ export function notifyFileChangedAbsolute(spaceKey, absolutePath, kind, facts = {}) {
51
+ deliver(spaceKey, absolutePath, kind, facts.newPath, facts.dir === true);
52
+ }
53
+ function realpathLenient(absolutePath) {
54
+ const rest = [];
55
+ let current = absolutePath;
56
+ for (;;) {
57
+ try {
58
+ return join(realpathSync(current), ...rest);
59
+ }
60
+ catch {
61
+ const parent = dirname(current);
62
+ if (parent === current)
63
+ return undefined;
64
+ rest.unshift(basename(current));
65
+ current = parent;
66
+ }
67
+ }
68
+ }
69
+ export function notifyFileChanged(spaceKey, workingDir, path, kind, facts = {}) {
23
70
  if (listeners.size === 0)
24
71
  return;
25
- let absolute;
26
- try {
27
- absolute = realpathSync(path.startsWith('/') ? path : join(workingDir, path));
28
- }
29
- catch {
72
+ const toReal = (p) => realpathLenient(p.startsWith('/') ? p : join(workingDir, p));
73
+ const source = toReal(path);
74
+ if (source === undefined)
30
75
  return;
31
- }
32
- notifyFileChangedAbsolute(spaceKey, absolute, kind);
76
+ const target = facts.newPath === undefined ? undefined : toReal(facts.newPath);
77
+ if (kind === 'renamed' && target === undefined)
78
+ return;
79
+ deliver(spaceKey, source, kind, target, facts.dir === true);
33
80
  }
34
81
  export function resetFileChangeListeners() {
35
82
  listeners.clear();
@@ -1,12 +1,19 @@
1
1
  // Copyright (c) 2025-present Minionry, Inc.
2
2
  // SPDX-License-Identifier: LicenseRef-Minionry-Software
3
- import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
- import { dirname, isAbsolute } from 'node:path';
3
+ import { readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
4
+ import { isAbsolute } from 'node:path';
5
5
  import { createDirectory, renameFile } from '../files.js';
6
6
  import { readFileContent } from '../websocket/file-utils.js';
7
+ import { parseExpectedModifiedAt, writeFileAtomic } from './atomic-write.js';
7
8
  import { SdkMethodError } from './registry.js';
8
9
  import { provisionAppSandbox, resolveWithinRoot } from './sandbox.js';
9
10
  export const FILES_APP_MAX_WRITE_BYTES = 5 * 1024 * 1024;
11
+ export function readEntryResult(result, stat) {
12
+ const meta = { modifiedAt: stat.mtime.toISOString(), size: stat.size };
13
+ return result.isImage
14
+ ? { kind: 'binary', content: result.content, mimeType: result.mimeType ?? 'application/octet-stream', ...meta }
15
+ : { kind: 'text', content: result.content, ...meta };
16
+ }
10
17
  function paramsObject(params) {
11
18
  return typeof params === 'object' && params !== null && !Array.isArray(params)
12
19
  ? params
@@ -85,9 +92,7 @@ export function filesReadEntryHandler(args) {
85
92
  const result = readFileContent(resolved, dataDir);
86
93
  if (result.error)
87
94
  throw serviceError(result.error);
88
- return result.isImage
89
- ? { kind: 'binary', content: result.content, mimeType: result.mimeType ?? 'application/octet-stream' }
90
- : { kind: 'text', content: result.content };
95
+ return readEntryResult(result, stat);
91
96
  }
92
97
  export function filesWriteHandler(args) {
93
98
  const path = parsePathParam(args.params, args.method);
@@ -95,6 +100,7 @@ export function filesWriteHandler(args) {
95
100
  if (typeof raw.content !== 'string') {
96
101
  throw new SdkMethodError('BAD_REQUEST', `${args.method} requires params { path, content } with string content`);
97
102
  }
103
+ const expectedModifiedAt = parseExpectedModifiedAt(raw, args.method);
98
104
  const bytes = Buffer.byteLength(raw.content, 'utf-8');
99
105
  if (bytes > FILES_APP_MAX_WRITE_BYTES) {
100
106
  throw new SdkMethodError('BAD_REQUEST', `content is ${bytes} bytes — exceeds the ${FILES_APP_MAX_WRITE_BYTES}-byte per-file files.write cap`);
@@ -103,8 +109,7 @@ export function filesWriteHandler(args) {
103
109
  if (statOrUndefined(resolved)?.isDirectory()) {
104
110
  throw new SdkMethodError('BAD_REQUEST', `'${path}' is a directory — cannot write over it`);
105
111
  }
106
- mkdirSync(dirname(resolved), { recursive: true });
107
- writeFileSync(resolved, raw.content, 'utf-8');
112
+ writeFileAtomic({ target: resolved, content: raw.content, expectedModifiedAt, displayPath: path });
108
113
  return null;
109
114
  }
110
115
  export function filesListHandler(args) {
@@ -5,6 +5,7 @@ import { findDefinitions } from '../websocket/file-definition-handlers.js';
5
5
  import { streamRgSearch } from '../websocket/file-search-handlers.js';
6
6
  import { registerFileChangeListener } from './file-change-notify.js';
7
7
  import { appDataDir, guardAppDataPath } from './files-app.js';
8
+ import { acquireRootWatch } from './files-watch.js';
8
9
  import { guardWorkspacePath, parseWorkspacePath, resolveFilesReadScopes } from './files-workspace.js';
9
10
  import { getGrant } from './grant-store.js';
10
11
  import { listLiveSubscriptions, registerLiveSubscription } from './live-registry.js';
@@ -115,6 +116,18 @@ export function filesSubscribeChangesHandler(args) {
115
116
  return;
116
117
  args.emit('changed', change);
117
118
  });
119
+ const releaseWatch = parseWorkspacePath({ path }) === null
120
+ ? () => { }
121
+ : acquireRootWatch({
122
+ spaceKey: args.spaceKey,
123
+ appId: args.appId,
124
+ workingDir: args.workingDir,
125
+ realRoot: root,
126
+ onGap: (gap) => {
127
+ if (active)
128
+ args.emit('gap', gap);
129
+ },
130
+ });
118
131
  registerLiveSubscription({
119
132
  requestId: args.requestId,
120
133
  appId: args.appId,
@@ -125,6 +138,7 @@ export function filesSubscribeChangesHandler(args) {
125
138
  teardown: () => {
126
139
  active = false;
127
140
  unsubscribe();
141
+ releaseWatch();
128
142
  },
129
143
  });
130
144
  return { subscribed: true };
@@ -137,7 +137,7 @@ function finishUpload(args, uploadId, session) {
137
137
  writeFileSync(resolvedPath, Buffer.concat(session.chunks));
138
138
  uploads.delete(uploadId);
139
139
  if (session.guard.rooting === 'workspace') {
140
- notifyFileChangedAbsolute(session.spaceKey, resolvedPath, 'changed');
140
+ notifyFileChangedAbsolute(session.spaceKey, resolvedPath, stat ? 'changed' : 'created');
141
141
  }
142
142
  }
143
143
  export function filesUploadChunkHandler(args) {