@agdf/mcp-server 0.20.1 → 0.30.0

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/src/worker.js CHANGED
@@ -30,21 +30,30 @@ export function createWorkerDispatchExecutor({
30
30
  // Reserve up to one second instead of beginning shutdown at the deadline.
31
31
  const terminationReserveMs = Math.min(1_000, Math.max(1, Math.floor(timeoutMs / 4)));
32
32
  let active = null;
33
+ let closed = false;
34
+ const waiting = [];
35
+ const rejectWaiting = error => { for (const job of waiting.splice(0)) { job.cleanup(); job.reject(error); } };
36
+ const drain = () => {
37
+ if (active || closed || !waiting.length) return;
38
+ const job = waiting.shift(); job.cleanup();
39
+ execute(job.argumentsValue, { ...job.options, deadline: job.deadline }).then(job.resolve, job.reject).finally(drain);
40
+ };
33
41
  const event = (phase, started) => {
34
42
  try { onEvent({ phase, elapsed_ms: performance.now() - started }); } catch {}
35
43
  };
36
- return Object.freeze({
37
- async execute(argumentsValue, { signal, toolName } = {}) {
44
+ const execute = async (argumentsValue, { signal, toolName, trusted, deadline: queuedDeadline } = {}) => {
38
45
  if (active) throw new DispatchExecutionError("dispatch_busy", 'not_dispatched');
39
46
  if (signal?.aborted) throw new DispatchExecutionError("dispatch_cancelled", 'not_dispatched');
40
47
  return new Promise((resolve, reject) => {
41
48
  let settled = false, exited = false, completion, workerPhase = 'worker_start';
42
49
  let confirmExit;
43
50
  const actualExit = new Promise(resolve => { confirmExit = resolve; });
44
- const started = performance.now(), deadline = started + timeoutMs;
51
+ const started = performance.now(), deadline = queuedDeadline ?? started + timeoutMs;
52
+ const budget = deadline - started;
53
+ if (budget <= 0) { reject(new DispatchExecutionError('dispatch_timeout', 'not_dispatched')); return; }
45
54
  let worker;
46
55
  try { worker = new WorkerClass(WORKER_URL, {
47
- workerData: { surface, expectedVersion, commandGroups, argumentsValue, toolName: toolName ?? null },
56
+ workerData: { surface, expectedVersion, commandGroups, argumentsValue, toolName: toolName ?? null, materialGrants: trusted?.materialGrants, inputContext: trusted?.inputContext },
48
57
  }); } catch { reject(new DispatchExecutionError('dispatch_worker_failed', 'not_dispatched')); return; }
49
58
  event('worker_start', started);
50
59
  const current = { worker, timer: null, cleanupSignal: null, cancel: () => finish(reject, new DispatchExecutionError("dispatch_cancelled")) };
@@ -62,7 +71,7 @@ export function createWorkerDispatchExecutor({
62
71
  try { onEvent({ phase: 'locks_released', elapsed_ms: performance.now() - started, count: released }); } catch {}
63
72
  };
64
73
  // Called only after actual exit or confirmed termination: the thread no longer runs.
65
- const release = () => { releaseHeldLocks(); if (active === current) active = null; };
74
+ const release = () => { releaseHeldLocks(); if (active === current) { active = null; queueMicrotask(drain); } };
66
75
  const finish = (callback, value) => {
67
76
  if (settled) return completion;
68
77
  settled = true;
@@ -87,6 +96,8 @@ export function createWorkerDispatchExecutor({
87
96
  } else {
88
97
  // A late actual exit may release capacity; rejection alone never proves termination.
89
98
  event('termination_unconfirmed', started);
99
+ current.unconfirmed = true;
100
+ rejectWaiting(new DispatchExecutionError('dispatch_busy', 'not_dispatched'));
90
101
  reject(new DispatchExecutionError('dispatch_worker_termination_unconfirmed', 'unknown', undefined, {
91
102
  ...(callback === reject && value instanceof DispatchExecutionError ? { cause_code: value.code } : {}),
92
103
  worker_phase: workerPhase,
@@ -100,7 +111,8 @@ export function createWorkerDispatchExecutor({
100
111
  current.cleanupSignal = () => signal.removeEventListener("abort", cancel);
101
112
  signal.addEventListener("abort", cancel, { once: true });
102
113
  }
103
- current.timer = setTimeout(() => finish(reject, new DispatchExecutionError("dispatch_timeout")), timeoutMs - terminationReserveMs);
114
+ current.timer = setTimeout(() => finish(reject, new DispatchExecutionError("dispatch_timeout")),
115
+ budget - Math.min(terminationReserveMs, Math.max(1, Math.floor(budget / 4))));
104
116
  worker.on("message", message => {
105
117
  // Lock messages also arrive after settlement while the worker drains.
106
118
  if (message?.type === 'lock') {
@@ -127,8 +139,42 @@ export function createWorkerDispatchExecutor({
127
139
  // Cover an abort racing listener installation.
128
140
  if (signal?.aborted) cancel();
129
141
  });
142
+ };
143
+ return Object.freeze({
144
+ execute(argumentsValue, options = {}) {
145
+ if (closed) return Promise.reject(new DispatchExecutionError(active ? 'dispatch_busy' : 'dispatch_cancelled', 'not_dispatched'));
146
+ if (options.signal?.aborted) return Promise.reject(new DispatchExecutionError('dispatch_cancelled', 'not_dispatched'));
147
+ const deadline = performance.now() + timeoutMs;
148
+ if (!active && !waiting.length) return execute(argumentsValue, { ...options, deadline });
149
+ // Only immutable Inspect requests wait. Commands still report contention;
150
+ // no write is replayed, parallelized or admitted after unknown termination.
151
+ if (options.toolName !== 'agdf_inspect' || active?.unconfirmed)
152
+ return Promise.reject(new DispatchExecutionError('dispatch_busy', 'not_dispatched'));
153
+ if (waiting.length >= 8) return Promise.reject(new DispatchExecutionError('resource_limit', 'not_dispatched'));
154
+ return new Promise((resolve, reject) => {
155
+ const job = { argumentsValue, options, deadline, resolve, reject };
156
+ const remove = error => {
157
+ const index = waiting.indexOf(job);
158
+ if (index < 0) return;
159
+ waiting.splice(index, 1); job.cleanup(); reject(error);
160
+ };
161
+ const abort = () => remove(new DispatchExecutionError('dispatch_cancelled', 'not_dispatched'));
162
+ const timer = setTimeout(() => remove(new DispatchExecutionError('dispatch_timeout', 'not_dispatched')), timeoutMs);
163
+ job.cleanup = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', abort); };
164
+ waiting.push(job); options.signal?.addEventListener('abort', abort, { once: true });
165
+ if (options.signal?.aborted) abort();
166
+ });
130
167
  },
131
- async close() { await active?.cancel(); },
168
+ async close() { closed = true; rejectWaiting(new DispatchExecutionError('dispatch_cancelled', 'not_dispatched')); await active?.cancel(); },
132
169
  get active() { return Boolean(active); },
133
170
  });
134
171
  }
172
+
173
+ export function createAgdfWorkerExecutor(runtime, options = {}) {
174
+ return createWorkerDispatchExecutor({
175
+ surface: runtime.trustedContext.surface,
176
+ expectedVersion: runtime.trustedContext.expectedVersion,
177
+ commandGroups: runtime.trustedContext.commandGroups,
178
+ ...options,
179
+ });
180
+ }