maka-agent 0.2.0-dev.76.20261009 → 0.2.0-dev.78.20261010

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 (78) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +3 -3
  3. package/dist/pi-tui-pickers.js +5 -2
  4. package/dist/pi-tui-runner.js +211 -33
  5. package/dist/runtime-host-session-driver.js +91 -13
  6. package/dist/session-catalog-limits.js +22 -0
  7. package/dist/session-driver.js +10 -0
  8. package/dist/tui-copy-catalog.js +15 -0
  9. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  10. package/node_modules/@maka/core/dist/bot-chat-settings.js +3 -0
  11. package/node_modules/@maka/core/dist/bot-events.js +1 -8
  12. package/node_modules/@maka/core/dist/local-memory.js +38 -1
  13. package/node_modules/@maka/core/dist/scheduled-task.js +14 -15
  14. package/node_modules/@maka/core/dist/session.js +3 -0
  15. package/node_modules/@maka/core/dist/text-sanitize.js +8 -3
  16. package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +115 -17
  17. package/node_modules/@maka/runtime/dist/agent-run.js +11 -0
  18. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +11 -0
  19. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +13 -5
  20. package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +29 -1
  21. package/node_modules/@maka/runtime/dist/computer-use-observation-text.js +22 -0
  22. package/node_modules/@maka/runtime/dist/conversation-copy-agent-output.js +227 -0
  23. package/node_modules/@maka/runtime/dist/conversation-copy.js +176 -46
  24. package/node_modules/@maka/runtime/dist/file-stable-write.js +15 -3
  25. package/node_modules/@maka/runtime/dist/filesystem-worker/resource-resolver.js +3 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +20 -1
  27. package/node_modules/@maka/runtime/dist/network/proxy-test.js +28 -1
  28. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +13 -2
  29. package/node_modules/@maka/runtime/dist/owned-child-process.js +209 -0
  30. package/node_modules/@maka/runtime/dist/owned-process-main.js +183 -0
  31. package/node_modules/@maka/runtime/dist/pipe-process-driver.js +19 -25
  32. package/node_modules/@maka/runtime/dist/runtime-kernel.js +216 -8
  33. package/node_modules/@maka/runtime/dist/sandbox-boundary-tool.js +12 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +156 -17
  35. package/node_modules/@maka/runtime/dist/shell-exec.js +18 -20
  36. package/node_modules/@maka/runtime/dist/shell-run-manager.js +2 -1
  37. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +279 -8
  38. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +209 -37
  39. package/node_modules/@maka/runtime/dist/tool-availability.js +55 -18
  40. package/node_modules/@maka/runtime/dist/tool-call-snapshot.js +142 -0
  41. package/node_modules/@maka/runtime/dist/tool-runtime.js +27 -84
  42. package/node_modules/@maka/runtime/dist/workers/{filesystem-worker.js → filesystem-worker.mjs} +12 -1
  43. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +30 -6
  44. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  45. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +44 -13
  46. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -0
  47. package/node_modules/@maka/runtime-host/dist/client/wsl-environment.js +4 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  49. package/node_modules/@maka/runtime-host/dist/protocol/message.js +1 -13
  50. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +46 -9
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -13
  52. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +1 -13
  53. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +4 -7
  54. package/node_modules/@maka/runtime-host/dist/server/archive-retention-coordinator.js +81 -105
  55. package/node_modules/@maka/runtime-host/dist/server/candidate.js +47 -40
  56. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +87 -67
  57. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +5 -1
  58. package/node_modules/@maka/runtime-host/dist/server/host-maintenance-guards.js +95 -0
  59. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +18 -8
  60. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +32 -3
  61. package/node_modules/@maka/runtime-host/dist/server/project-catalog-coordinator.js +2 -0
  62. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +109 -8
  63. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +20 -3
  64. package/node_modules/@maka/runtime-host/dist/server/session-background-activity.js +71 -0
  65. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +8 -4
  66. package/node_modules/@maka/runtime-host/dist/server/session-interaction-activity.js +78 -0
  67. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +56 -0
  68. package/node_modules/@maka/runtime-host/dist/server/session-revision-graph-references.js +16 -2
  69. package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +2 -0
  70. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +19 -24
  71. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +74 -19
  72. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +1 -1
  73. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +6 -99
  74. package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +14 -126
  75. package/node_modules/@maka/storage/dist/context-offload-store.js +8 -3
  76. package/node_modules/@maka/storage/dist/git-entry.js +142 -5
  77. package/node_modules/@maka/storage/dist/session-bundle-policy.js +54 -19
  78. package/package.json +2 -2
@@ -61,12 +61,14 @@ export function createDefaultBotChannel(provider) {
61
61
  }
62
62
  export function createDefaultBotChatSettings() {
63
63
  return {
64
+ messageHandling: 'task',
64
65
  channels: Object.fromEntries(BOT_PROVIDERS.map((provider) => [provider, createDefaultBotChannel(provider)])),
65
66
  };
66
67
  }
67
68
  export function mergeBotChatSettings(current, patch) {
68
69
  return {
69
70
  ...current,
71
+ ...(patch?.messageHandling ? { messageHandling: patch.messageHandling } : {}),
70
72
  channels: {
71
73
  ...current.channels,
72
74
  ...Object.fromEntries(Object.entries(patch?.channels ?? {}).map(([provider, channelPatch]) => {
@@ -93,6 +95,7 @@ export function mergeBotChatSettings(current, patch) {
93
95
  }
94
96
  export function normalizeBotChatSettings(settings, rawSettings) {
95
97
  return {
98
+ messageHandling: rawSettings?.messageHandling === 'workhub' ? 'workhub' : 'task',
96
99
  channels: Object.fromEntries(BOT_PROVIDERS.map((provider) => {
97
100
  const rawChannel = rawSettings?.channels?.[provider];
98
101
  return [provider, normalizeBotChannel(provider, settings.channels[provider], rawChannel)];
@@ -130,14 +130,7 @@ export function plaintextHelpReply() {
130
130
  ].join('\n');
131
131
  }
132
132
  export function formatBotMessageForSession(message) {
133
- return `[${botDisplayLabel(message.platform)}:${sanitizeBotUserName(message.userName)}] ${message.text.trim()}`;
134
- }
135
- function sanitizeBotUserName(value) {
136
- return (value
137
- .replace(/[\u0000-\u001F\u007F-\u009F]+/g, ' ')
138
- .replace(/[\p{Cf}]+/gu, ' ')
139
- .replace(/\s+/g, ' ')
140
- .trim() || 'unknown');
133
+ return `[${botDisplayLabel(message.platform)}] ${message.text.trim()}`;
141
134
  }
142
135
  /**
143
136
  * Non-error `BotStatus.reason` values. These states have their own UI surface,
@@ -318,8 +318,11 @@ export function findLocalMemoryEntryDraftRange(input, entryId) {
318
318
  end: lineStarts[endLineIndex] ?? input.length,
319
319
  };
320
320
  };
321
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
321
322
  for (let index = 0; index < lines.length; index += 1) {
322
323
  const line = lines[index] ?? '';
324
+ if (isCodeFenceLine(line))
325
+ continue;
323
326
  const heading = /^##\s+(.+?)\s*$/.exec(line);
324
327
  if (heading) {
325
328
  const matched = matchCurrent(index);
@@ -421,7 +424,12 @@ function parseLocalMemoryMarkdownRaw(input) {
421
424
  }
422
425
  current = null;
423
426
  };
427
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
424
428
  for (const line of lines) {
429
+ if (isCodeFenceLine(line)) {
430
+ current?.body.push(line);
431
+ continue;
432
+ }
425
433
  const heading = /^##\s+(.+?)\s*$/.exec(line);
426
434
  if (heading) {
427
435
  flush();
@@ -455,8 +463,29 @@ function stripPromptContent(entry) {
455
463
  const { promptContent: _promptContent, ...preview } = entry;
456
464
  return preview;
457
465
  }
466
+ // All entry readers and editors share fence state so code examples cannot split entries.
467
+ function createMemoryCodeFenceTracker() {
468
+ let fence;
469
+ return (line) => {
470
+ const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
471
+ if (fence) {
472
+ if (match &&
473
+ match[1][0] === fence.marker &&
474
+ match[1].length >= fence.length &&
475
+ match[2].trim() === '') {
476
+ fence = undefined;
477
+ }
478
+ return true;
479
+ }
480
+ if (!match || (match[1][0] === '`' && match[2].includes('`')))
481
+ return false;
482
+ fence = { marker: match[1][0], length: match[1].length };
483
+ return true;
484
+ };
485
+ }
458
486
  function parseMetaComment(line) {
459
- const match = /^<!--\s*maka-memory:\s*(.*?)\s*-->$/.exec(line.trim());
487
+ // Four-space or tab indentation denotes a code example, not entry identity or state.
488
+ const match = /^ {0,3}<!--\s*maka-memory:\s*(.*?)\s*-->[ \t]*$/.exec(line);
460
489
  if (!match)
461
490
  return null;
462
491
  const meta = {};
@@ -482,8 +511,11 @@ function findLocalMemoryEntrySection(input, entryId) {
482
511
  const proposalId = current.meta?.proposalId;
483
512
  return id === entryId || proposalId === entryId ? { id, ...current } : null;
484
513
  };
514
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
485
515
  for (let index = 0; index < lines.length; index += 1) {
486
516
  const line = lines[index] ?? '';
517
+ if (isCodeFenceLine(line))
518
+ continue;
487
519
  const heading = /^##\s+(.+?)\s*$/.exec(line);
488
520
  if (heading) {
489
521
  const matched = matchCurrent();
@@ -534,8 +566,13 @@ function findLocalMemoryEntryFullSection(input, entryId) {
534
566
  },
535
567
  };
536
568
  };
569
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
537
570
  for (let index = 0; index < lines.length; index += 1) {
538
571
  const line = lines[index] ?? '';
572
+ if (isCodeFenceLine(line)) {
573
+ current?.body.push(line);
574
+ continue;
575
+ }
539
576
  const heading = /^##\s+(.+?)\s*$/.exec(line);
540
577
  if (heading) {
541
578
  const matched = matchCurrent(index);
@@ -493,23 +493,22 @@ function asFiniteNumber(value) {
493
493
  return value;
494
494
  return null;
495
495
  }
496
+ /**
497
+ * Calendar recurrence uses Date's local-time rules: shift gaps forward and take
498
+ * the first repeated time. Rebuild from the anchor so gap shifts do not carry
499
+ * into later cycles; cron instead skips nonexistent local times.
500
+ */
496
501
  function nextCalendarFireAt(schedule, after) {
497
502
  const anchor = new Date(schedule.anchorAt);
498
- if (schedule.recurrence === 'daily') {
499
- const next = new Date(after);
500
- next.setHours(anchor.getHours(), anchor.getMinutes(), anchor.getSeconds(), anchor.getMilliseconds());
501
- if (next.getTime() <= after)
502
- next.setDate(next.getDate() + 1);
503
- return next.getTime();
504
- }
505
- if (schedule.recurrence === 'weekly') {
506
- const next = new Date(after);
507
- next.setHours(anchor.getHours(), anchor.getMinutes(), anchor.getSeconds(), anchor.getMilliseconds());
508
- const dayDelta = (anchor.getDay() - next.getDay() + 7) % 7;
509
- next.setDate(next.getDate() + dayDelta);
510
- if (next.getTime() <= after)
511
- next.setDate(next.getDate() + 7);
512
- return next.getTime();
503
+ if (schedule.recurrence === 'daily' || schedule.recurrence === 'weekly') {
504
+ const base = new Date(after);
505
+ const dayOffset = schedule.recurrence === 'daily' ? 0 : (anchor.getDay() - base.getDay() + 7) % 7;
506
+ const periodDays = schedule.recurrence === 'daily' ? 1 : 7;
507
+ // Build each occurrence from the anchor: a DST gap may normalize today's
508
+ // wall time, but that adjusted hour must not carry into another date.
509
+ const occurrence = (offset) => new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset, anchor.getHours(), anchor.getMinutes(), anchor.getSeconds(), anchor.getMilliseconds()).getTime();
510
+ const next = occurrence(dayOffset);
511
+ return next > after ? next : occurrence(dayOffset + periodDays);
513
512
  }
514
513
  const afterDate = new Date(after);
515
514
  for (let offset = 0; offset <= 480; offset += 1) {
@@ -467,6 +467,7 @@ const WORKHUB_DELEGATION_ASSIGNED_MESSAGE_SHAPE = defineObjectShape()([
467
467
  'attachments',
468
468
  'targetAttachments',
469
469
  'returnResults',
470
+ 'executionPermissionMode',
470
471
  'create',
471
472
  'steered',
472
473
  'replacesActionId',
@@ -797,6 +798,8 @@ function isWorkHubCoordinationMessage(message) {
797
798
  typeof message.targetSessionName === 'string' &&
798
799
  message.targetSessionName.trim().length > 0 &&
799
800
  (message.returnResults === undefined || message.returnResults === true) &&
801
+ (message.executionPermissionMode === undefined ||
802
+ isPermissionMode(message.executionPermissionMode)) &&
800
803
  (message.steered === undefined || message.steered === true) &&
801
804
  ((message.schemaVersion === WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION &&
802
805
  message.replacesActionId === undefined &&
@@ -84,6 +84,11 @@ const ZERO_WIDTH_REGEX = /[\u200B-\u200D\u2060-\u2064\uFEFF]/g;
84
84
  */
85
85
  export function sanitizeUnicodeText(text, opts) {
86
86
  const suffix = opts.truncatedSuffix ?? '…';
87
+ // A negative cap is clamped to zero: without it the loop never breaks (the
88
+ // whole input is expanded) and the slice's negative end keeps nearly the
89
+ // whole string — the opposite of the "hard cap" this option promises. Same
90
+ // boundary contract as truncateUtf16Safe's `maxUnits <= 0` guard.
91
+ const maxCodePoints = Math.max(0, opts.maxCodePoints);
87
92
  const cleaned = text
88
93
  .normalize('NFC')
89
94
  .replace(CONTROL_CHARS_REGEX, ' ')
@@ -95,12 +100,12 @@ export function sanitizeUnicodeText(text, opts) {
95
100
  for (const point of cleaned) {
96
101
  points.push(point);
97
102
  // One extra point detects truncation without expanding the entire input.
98
- if (opts.maxCodePoints >= 0 && points.length > opts.maxCodePoints)
103
+ if (points.length > maxCodePoints)
99
104
  break;
100
105
  }
101
- if (points.length <= opts.maxCodePoints)
106
+ if (points.length <= maxCodePoints)
102
107
  return cleaned;
103
- return points.slice(0, opts.maxCodePoints).join('') + suffix;
108
+ return points.slice(0, maxCodePoints).join('') + suffix;
104
109
  }
105
110
  /**
106
111
  * Truncate to at most `maxUnits` UTF-16 code units without ending on an
@@ -83,6 +83,10 @@ export class AgentGraphSupervisorWakeCoordinator {
83
83
  #tasks = new Set();
84
84
  #tasksBySession = new Map();
85
85
  #pendingWakeIds = new Set();
86
+ #sessionAttention = new Map();
87
+ #parkedWakeIds = new Map();
88
+ #activityVersions = new Map();
89
+ #publishedActivity = new Map();
86
90
  #abortController = new AbortController();
87
91
  #sessionAbortControllers = new Map();
88
92
  #sessionWakeSuppressions = new Map();
@@ -95,18 +99,45 @@ export class AgentGraphSupervisorWakeCoordinator {
95
99
  throw new Error('Agent graph supervisor wake attempts must be a positive safe integer');
96
100
  }
97
101
  }
102
+ /** Includes queued checkpoint delivery before a supervisor Turn is admitted. */
103
+ readSessionActivity(rootSessionId) {
104
+ if (this.#closed || this.#sessionWakesSuppressed(rootSessionId))
105
+ return 'idle';
106
+ const attention = this.#sessionAttention.get(rootSessionId);
107
+ if (attention === 'waiting_for_user')
108
+ return attention;
109
+ if (this.#tasksBySession.has(rootSessionId))
110
+ return 'running';
111
+ return attention ?? 'idle';
112
+ }
113
+ #publishSessionActivity(rootSessionId) {
114
+ const activity = this.readSessionActivity(rootSessionId);
115
+ if ((this.#publishedActivity.get(rootSessionId) ?? 'idle') === activity)
116
+ return;
117
+ if (activity === 'idle')
118
+ this.#publishedActivity.delete(rootSessionId);
119
+ else
120
+ this.#publishedActivity.set(rootSessionId, activity);
121
+ try {
122
+ this.#input.onSessionActivityChanged?.(rootSessionId);
123
+ }
124
+ catch {
125
+ // A catalog observer never controls wake delivery.
126
+ }
127
+ }
98
128
  notify(rootSessionId, result) {
99
129
  if (this.#closed ||
100
130
  this.#sessionWakesSuppressed(rootSessionId) ||
101
131
  (!this.#input.shouldWake && (!result || !isAgentGraphSupervisorMilestone(result)))) {
102
132
  return undefined;
103
133
  }
104
- return this.#runTracked(rootSessionId, async (abortSignal) => {
134
+ return this.#runTracked(rootSessionId, async (abortSignal, setAttention) => {
105
135
  try {
106
- await this.#wake(rootSessionId, abortSignal, result);
136
+ await this.#wake(rootSessionId, abortSignal, setAttention, result);
107
137
  }
108
138
  catch (error) {
109
139
  if (!this.#closed && !isAbortError(error)) {
140
+ setAttention('blocked');
110
141
  await notifyError(this.#input.onError, rootSessionId, error);
111
142
  }
112
143
  }
@@ -122,12 +153,15 @@ export class AgentGraphSupervisorWakeCoordinator {
122
153
  notifyPermissionResponse(rootSessionId) {
123
154
  if (this.#closed || this.#sessionWakesSuppressed(rootSessionId))
124
155
  return undefined;
125
- return this.#runTracked(rootSessionId, async (abortSignal) => {
156
+ this.#sessionAttention.delete(rootSessionId);
157
+ this.#parkedWakeIds.delete(rootSessionId);
158
+ return this.#runTracked(rootSessionId, async (abortSignal, setAttention) => {
126
159
  try {
127
- await this.#settlePermissionResponse(rootSessionId, abortSignal);
160
+ await this.#settlePermissionResponse(rootSessionId, abortSignal, setAttention);
128
161
  }
129
162
  catch (error) {
130
163
  if (!this.#closed && !isAbortError(error)) {
164
+ setAttention('blocked');
131
165
  await notifyError(this.#input.onError, rootSessionId, error);
132
166
  }
133
167
  }
@@ -174,6 +208,11 @@ export class AgentGraphSupervisorWakeCoordinator {
174
208
  return this.#input.activityRegistry.whenIdle(rootSessionId) !== undefined;
175
209
  }
176
210
  async retireSessions(rootSessionIds) {
211
+ for (const rootSessionId of rootSessionIds) {
212
+ this.#sessionAttention.delete(rootSessionId);
213
+ this.#parkedWakeIds.delete(rootSessionId);
214
+ this.#publishSessionActivity(rootSessionId);
215
+ }
177
216
  return this.#input.wakeStore.supersedeAgentGraphSupervisorWakes({
178
217
  rootSessionIds,
179
218
  reason: 'session_retired',
@@ -184,19 +223,30 @@ export class AgentGraphSupervisorWakeCoordinator {
184
223
  return;
185
224
  this.#closed = true;
186
225
  this.#abortController.abort();
226
+ for (const sessionId of [...this.#publishedActivity.keys()])
227
+ this.#publishSessionActivity(sessionId);
187
228
  }
188
229
  async close() {
189
230
  this.beginDrain();
190
231
  await this.waitForIdle();
191
232
  this.#sessionAbortControllers.clear();
192
233
  this.#tasksBySession.clear();
234
+ this.#sessionAttention.clear();
235
+ this.#parkedWakeIds.clear();
236
+ this.#activityVersions.clear();
193
237
  }
194
- async #wake(rootSessionId, abortSignal, result) {
238
+ async #wake(rootSessionId, abortSignal, setAttention, result) {
195
239
  abortSignal.throwIfAborted();
196
240
  if (!(await this.#isSessionDeliverable(rootSessionId)))
197
241
  return;
198
242
  const snapshot = await this.#input.readSnapshot(rootSessionId);
199
243
  abortSignal.throwIfAborted();
244
+ const parkedWakeId = this.#parkedWakeIds.get(rootSessionId);
245
+ if (parkedWakeId &&
246
+ (snapshot.closed || parkedWakeId !== `${snapshot.graphId}:${snapshot.snapshotVersion}`)) {
247
+ this.#parkedWakeIds.delete(rootSessionId);
248
+ setAttention('idle');
249
+ }
200
250
  const wakeDecision = await this.#input.shouldWake?.(rootSessionId, result, snapshot);
201
251
  abortSignal.throwIfAborted();
202
252
  if (wakeDecision === false ||
@@ -217,12 +267,19 @@ export class AgentGraphSupervisorWakeCoordinator {
217
267
  snapshotVersion: snapshot.snapshotVersion,
218
268
  rootSessionId,
219
269
  });
220
- if (this.#closed ||
221
- claimed.wake.status === 'delivered' ||
222
- claimed.wake.status === 'exhausted')
270
+ if (this.#closed || claimed.wake.status === 'exhausted')
271
+ return;
272
+ if (claimed.wake.status === 'delivered') {
273
+ this.#parkedWakeIds.delete(rootSessionId);
274
+ setAttention('idle');
223
275
  return;
276
+ }
277
+ if (claimed.wake.status === 'waiting_permission') {
278
+ this.#parkedWakeIds.set(rootSessionId, wakeId);
279
+ setAttention('waiting_for_user');
280
+ }
224
281
  abortSignal.throwIfAborted();
225
- await this.#deliverWake(claimed.wake, snapshot, abortSignal, result);
282
+ await this.#deliverWake(claimed.wake, snapshot, abortSignal, setAttention, result);
226
283
  }
227
284
  finally {
228
285
  this.#pendingWakeIds.delete(wakeId);
@@ -232,12 +289,13 @@ export class AgentGraphSupervisorWakeCoordinator {
232
289
  if (this.#closed || this.#pendingWakeIds.has(wake.wakeId))
233
290
  return;
234
291
  this.#pendingWakeIds.add(wake.wakeId);
235
- void this.#runTracked(wake.rootSessionId, async (abortSignal) => {
292
+ void this.#runTracked(wake.rootSessionId, async (abortSignal, setAttention) => {
236
293
  try {
237
- await this.#resumeWake(wake, abortSignal);
294
+ await this.#resumeWake(wake, abortSignal, setAttention);
238
295
  }
239
296
  catch (error) {
240
297
  if (!this.#closed && !isAbortError(error)) {
298
+ setAttention('blocked');
241
299
  await notifyError(this.#input.onError, wake.rootSessionId, error);
242
300
  }
243
301
  }
@@ -247,19 +305,39 @@ export class AgentGraphSupervisorWakeCoordinator {
247
305
  });
248
306
  }
249
307
  #runTracked(rootSessionId, operation) {
308
+ // Duplicate checkpoint notifications share the in-flight generation. They
309
+ // cannot invalidate the outcome of the attempt already holding the wake.
310
+ const version = (this.#activityVersions.get(rootSessionId) ?? 0) +
311
+ (this.#tasksBySession.has(rootSessionId) ? 0 : 1);
312
+ this.#activityVersions.set(rootSessionId, version);
313
+ if (this.#sessionAttention.get(rootSessionId) !== 'waiting_for_user') {
314
+ this.#sessionAttention.delete(rootSessionId);
315
+ }
316
+ const setAttention = (activity) => {
317
+ if (this.#closed ||
318
+ this.#sessionWakesSuppressed(rootSessionId) ||
319
+ this.#activityVersions.get(rootSessionId) !== version)
320
+ return;
321
+ if (activity === 'idle')
322
+ this.#sessionAttention.delete(rootSessionId);
323
+ else
324
+ this.#sessionAttention.set(rootSessionId, activity);
325
+ this.#publishSessionActivity(rootSessionId);
326
+ };
250
327
  const residency = this.#input.acquireResidency?.(rootSessionId);
251
328
  const abortSignal = this.#sessionAbortSignal(rootSessionId);
252
329
  const task = Promise.resolve()
253
- .then(() => operation(abortSignal))
330
+ .then(() => operation(abortSignal, setAttention))
254
331
  .finally(() => residency?.release());
255
332
  this.#tasks.add(task);
256
333
  const sessionTasks = this.#tasksBySession.get(rootSessionId) ?? new Set();
257
334
  sessionTasks.add(task);
258
335
  this.#tasksBySession.set(rootSessionId, sessionTasks);
336
+ this.#publishSessionActivity(rootSessionId);
259
337
  void task.then(() => this.#forgetTask(rootSessionId, task), () => this.#forgetTask(rootSessionId, task));
260
338
  return task;
261
339
  }
262
- async #resumeWake(wake, abortSignal) {
340
+ async #resumeWake(wake, abortSignal, setAttention) {
263
341
  abortSignal.throwIfAborted();
264
342
  if (!(await this.#isSessionDeliverable(wake.rootSessionId))) {
265
343
  await this.#supersedeSession(wake.rootSessionId, 'session_unavailable');
@@ -278,9 +356,9 @@ export class AgentGraphSupervisorWakeCoordinator {
278
356
  return;
279
357
  }
280
358
  abortSignal.throwIfAborted();
281
- await this.#deliverWake(wake, snapshot, abortSignal);
359
+ await this.#deliverWake(wake, snapshot, abortSignal, setAttention);
282
360
  }
283
- async #deliverWake(wake, snapshot, abortSignal, result) {
361
+ async #deliverWake(wake, snapshot, abortSignal, setAttention, result) {
284
362
  if (wake.status === 'exhausted')
285
363
  return;
286
364
  if (wake.attemptCount >= this.#maxDeliveryAttempts) {
@@ -323,6 +401,8 @@ export class AgentGraphSupervisorWakeCoordinator {
323
401
  }
324
402
  return;
325
403
  }
404
+ this.#parkedWakeIds.delete(wake.rootSessionId);
405
+ setAttention('idle');
326
406
  if (this.#sessionWakesSuppressed(wake.rootSessionId))
327
407
  return;
328
408
  if (this.#closed) {
@@ -351,6 +431,8 @@ export class AgentGraphSupervisorWakeCoordinator {
351
431
  attemptId,
352
432
  status: 'delivered',
353
433
  });
434
+ this.#parkedWakeIds.delete(wake.rootSessionId);
435
+ setAttention('idle');
354
436
  return;
355
437
  }
356
438
  if (outcome.kind === 'superseded') {
@@ -370,6 +452,8 @@ export class AgentGraphSupervisorWakeCoordinator {
370
452
  attemptId,
371
453
  status: 'waiting_permission',
372
454
  });
455
+ this.#parkedWakeIds.set(wake.rootSessionId, wake.wakeId);
456
+ setAttention('waiting_for_user');
373
457
  return;
374
458
  }
375
459
  lastFailure = wakeOutcomeFailure(outcome);
@@ -501,6 +585,9 @@ export class AgentGraphSupervisorWakeCoordinator {
501
585
  `${snapshot.graphId}:${snapshot.snapshotVersion}` === wake.wakeId);
502
586
  }
503
587
  #supersedeSession(rootSessionId, reason) {
588
+ this.#sessionAttention.delete(rootSessionId);
589
+ this.#parkedWakeIds.delete(rootSessionId);
590
+ this.#publishSessionActivity(rootSessionId);
504
591
  return this.#input.wakeStore.supersedeAgentGraphSupervisorWakes({
505
592
  rootSessionIds: [rootSessionId],
506
593
  reason,
@@ -527,7 +614,7 @@ export class AgentGraphSupervisorWakeCoordinator {
527
614
  : `host_restart:${runStatus}`);
528
615
  return 1;
529
616
  }
530
- async #settlePermissionResponse(rootSessionId, abortSignal) {
617
+ async #settlePermissionResponse(rootSessionId, abortSignal, setAttention) {
531
618
  const activity = await this.#input.activityRegistry.acquire(rootSessionId, abortSignal);
532
619
  const retryable = [];
533
620
  try {
@@ -548,6 +635,8 @@ export class AgentGraphSupervisorWakeCoordinator {
548
635
  attemptId,
549
636
  status: 'delivered',
550
637
  });
638
+ this.#parkedWakeIds.delete(rootSessionId);
639
+ setAttention('idle');
551
640
  continue;
552
641
  }
553
642
  retryable.push(await this.#markRetryable(wake.graphId, wake.wakeId, attemptId, `permission_waiter_lost:${runStatus}`));
@@ -562,6 +651,10 @@ export class AgentGraphSupervisorWakeCoordinator {
562
651
  #beginSessionWakeSuppression(rootSessionId) {
563
652
  const count = this.#sessionWakeSuppressions.get(rootSessionId) ?? 0;
564
653
  this.#sessionWakeSuppressions.set(rootSessionId, count + 1);
654
+ this.#activityVersions.set(rootSessionId, (this.#activityVersions.get(rootSessionId) ?? 0) + 1);
655
+ this.#sessionAttention.delete(rootSessionId);
656
+ this.#parkedWakeIds.delete(rootSessionId);
657
+ this.#publishSessionActivity(rootSessionId);
565
658
  if (count === 0) {
566
659
  const controller = this.#sessionAbortControllers.get(rootSessionId) ?? new AbortController();
567
660
  this.#sessionAbortControllers.set(rootSessionId, controller);
@@ -577,6 +670,7 @@ export class AgentGraphSupervisorWakeCoordinator {
577
670
  return;
578
671
  }
579
672
  this.#sessionWakeSuppressions.delete(rootSessionId);
673
+ this.#publishSessionActivity(rootSessionId);
580
674
  if (!this.#tasksBySession.has(rootSessionId)) {
581
675
  this.#sessionAbortControllers.delete(rootSessionId);
582
676
  }
@@ -601,11 +695,15 @@ export class AgentGraphSupervisorWakeCoordinator {
601
695
  this.#tasks.delete(task);
602
696
  const sessionTasks = this.#tasksBySession.get(rootSessionId);
603
697
  sessionTasks?.delete(task);
604
- if (sessionTasks?.size === 0)
698
+ if (sessionTasks?.size === 0) {
605
699
  this.#tasksBySession.delete(rootSessionId);
700
+ if (!this.#sessionAttention.has(rootSessionId))
701
+ this.#activityVersions.delete(rootSessionId);
702
+ }
606
703
  if (!this.#sessionWakesSuppressed(rootSessionId) && !this.#tasksBySession.has(rootSessionId)) {
607
704
  this.#sessionAbortControllers.delete(rootSessionId);
608
705
  }
706
+ this.#publishSessionActivity(rootSessionId);
609
707
  }
610
708
  }
611
709
  export function isAgentGraphSupervisorMilestone(result) {
@@ -328,6 +328,17 @@ export class AgentRun {
328
328
  const runStore = this.input.runStore;
329
329
  if (!this.input.runtimeEventStore || !runStore)
330
330
  return;
331
+ // A stream may have attempted finalization after an earlier stop failed.
332
+ // Join that write, but let this explicit retry reuse its reserved fact if
333
+ // the write failed; a rejected promise is not durable terminal evidence.
334
+ const previousWrite = this.terminalClaim.write;
335
+ try {
336
+ await previousWrite;
337
+ }
338
+ catch {
339
+ if (this.terminalClaim.write === previousWrite)
340
+ this.terminalClaim.write = undefined;
341
+ }
331
342
  await this.flushRuntimePartialBuffer(true);
332
343
  // The claim only fences writers inside this Run. Another owner — a Host
333
344
  // recovery, a resumed continuation — may have sealed the ledger already,
@@ -90,6 +90,16 @@ export class AiSdkBackend {
90
90
  * read back as absent.
91
91
  */
92
92
  activeTurns = new Set();
93
+ /**
94
+ * Deferred-tool activations for this Session backend. `tool_search` only
95
+ * adds names; Turn end and compaction do not clear them. Clearing at Turn
96
+ * end would change the provider tool list and miss the prompt cache on the
97
+ * next Turn. Compaction already invalidates that cache, so clearing there
98
+ * would change the prefix a second time. Concurrent Runs on this backend
99
+ * share the map, so a search in one Run is visible on a sibling Run's next
100
+ * step. A rebuilt backend starts empty.
101
+ */
102
+ sessionActiveTools = new Map();
93
103
  compaction;
94
104
  turnSessionState = {};
95
105
  constructor(input) {
@@ -254,6 +264,7 @@ export class AiSdkBackend {
254
264
  providerTelemetry: this.providerTelemetry,
255
265
  compaction: this.compaction,
256
266
  snapshotToolAvailability: () => this.snapshotToolAvailability(),
267
+ sessionActiveTools: this.sessionActiveTools,
257
268
  codeCellAdmission: this.codeCellAdmission,
258
269
  resolvedProviderOptions: this.resolvedProviderOptions,
259
270
  session: this.turnSessionState,
@@ -37,7 +37,7 @@ import { contextDiagnosticsCompactionOf, } from './context-diagnostics.js';
37
37
  import { hasBlockingReplayDiagnostics } from './ai-sdk-compaction.js';
38
38
  import { portableApplyPatchTool } from './apply-patch-profile.js';
39
39
  import { RunTrace } from './run-trace.js';
40
- import { REQUEST_SANDBOX_BOUNDARY_TOOL_NAME, SANDBOX_BOUNDARY_DENIED_FOR_TURN, SANDBOX_BOUNDARY_FINALIZATION_PROMPT, } from './sandbox-boundary-tool.js';
40
+ import { REQUEST_SANDBOX_BOUNDARY_TOOL_NAME, SANDBOX_BOUNDARY_DENIED_FOR_TURN, SANDBOX_BOUNDARY_FINALIZATION_PROMPT, requiredSandboxBoundaryToolNames, } from './sandbox-boundary-tool.js';
41
41
  import { buildRuntimeEventModelReplayPlan, appendPriorUnknownToolResponses, buildSteeringEnvelope, collectToolActivityTurnIds, compatibleProviderReasoningReplayEventIds, formatTextWithInlineRefs, inspectPriorUnknownToolOutcomes, steeringMessagesMissingFromBase, steeringModelMessage, } from './model-history.js';
42
42
  import { toolSchemaCharsForDiagnostics, requestCompositionToolSchemas, stableHash, toolCatalogHash, } from './request-shape.js';
43
43
  import { toolAvailabilityHash } from './tool-availability.js';
@@ -374,13 +374,15 @@ function providerRetryDelayMs(failedAttempt, retryAfterMs) {
374
374
  *
375
375
  * Each turn owns its ToolRuntime for the same reason: gating, the loop gate,
376
376
  * the subagent and child-run limiters, durable attempts, and step admission are
377
- * all per-turn facts.
377
+ * all per-turn facts. Deferred-tool activation is the exception: it lives on
378
+ * the Session backend so later Turns — and concurrent Runs on the same
379
+ * backend — keep the same provider tool list.
378
380
  */
379
381
  export class AiSdkTurn {
380
382
  deps;
381
383
  request;
382
384
  abortController = new AbortController();
383
- activeTools = new Map();
385
+ activeTools;
384
386
  aborted = false;
385
387
  loopStopRequested = false;
386
388
  loopStopReason;
@@ -412,6 +414,7 @@ export class AiSdkTurn {
412
414
  request.orchestration ??
413
415
  resolveEffectiveOrchestration(deps.backend.header.orchestrationMode, undefined);
414
416
  this.toolRuntime = deps.createToolRuntime(this);
417
+ this.activeTools = deps.sessionActiveTools;
415
418
  }
416
419
  async *run() {
417
420
  yield* this.runWithinScope(this.request);
@@ -805,7 +808,7 @@ export class AiSdkTurn {
805
808
  }
806
809
  // --- Build the provider-visible schema set. Tool execution stays in Runtime. ---
807
810
  // Each logical step freezes its own scoped catalog and search projection.
808
- // Mutable activation belongs to this turn and follows contribution identity.
811
+ // Mutable activation is the Session backend map and follows contribution identity.
809
812
  const requiredOrchestrationTools = this.orchestration.mode === 'swarm'
810
813
  ? new Set([
811
814
  'agent_list',
@@ -834,7 +837,11 @@ export class AiSdkTurn {
834
837
  if (toolMode === 'code_mode' && snapshot.hostTools.some((tool) => tool.name === 'exec')) {
835
838
  throw new Error('Tool name "exec" is reserved for Code Mode.');
836
839
  }
837
- const basePlan = snapshot.runtime.prepare(this.activeTools, requiredOrchestrationTools);
840
+ const requiredTools = new Set([
841
+ ...requiredOrchestrationTools,
842
+ ...requiredSandboxBoundaryToolNames(snapshot.hostTools),
843
+ ]);
844
+ const basePlan = snapshot.runtime.prepare(this.activeTools, requiredTools);
838
845
  const nestedTools = nestableToolSnapshot(basePlan.providerTools, basePlan.activeTools);
839
846
  const plan = projectToolModePlan(basePlan, toolMode, codeModeExecTool);
840
847
  const modelTools = {};
@@ -1231,6 +1238,7 @@ export class AiSdkTurn {
1231
1238
  result = await this.deps.modelAdapter.startStream({
1232
1239
  model,
1233
1240
  messages: dispatchMessages,
1241
+ historyMessageCount: attemptMessages.length,
1234
1242
  tools: modelTools,
1235
1243
  activeTools: activeToolsForRequest,
1236
1244
  onStreamActivity: () => requestWatchdog?.markActivity(),
@@ -22,6 +22,11 @@ import { buildProxyDispatcher } from '../network/proxy-dispatcher.js';
22
22
  import { resolveActiveProxy } from '../network/active-proxy-state.js';
23
23
  import { FETCH_PROXY_SNAPSHOT } from '../network/scoped-fetch-transport.js';
24
24
  const DEFAULT_TIMEOUT_MS = 15_000;
25
+ // Bounded wait for dispatcher teardown in the error path: a graceful close
26
+ // that never settles must not delay the rejection the caller is owed. The
27
+ // success path keeps its fire-and-forget close, which owns the streaming
28
+ // body lifecycle and is not bounded here.
29
+ const DISPATCHER_CLOSE_GRACE_MS = 1_000;
25
30
  export async function proxiedFetch(input, init = {}) {
26
31
  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
27
32
  const proxy = resolveActiveProxy();
@@ -70,8 +75,31 @@ export async function proxiedFetch(input, init = {}) {
70
75
  catch (error) {
71
76
  if (timer)
72
77
  clearTimeout(timer);
78
+ // Mirror the shared transport teardown ordering: begin dispatcher
79
+ // teardown before aborting so the graceful close can retire live
80
+ // CONNECT tunnels, with abort still cancelling pending connects. Keep
81
+ // the wait bounded so a stalled close cannot hold the rejection.
82
+ const teardown = disposeDispatcher(timedOut);
73
83
  controller.abort(error);
74
- await disposeDispatcher(timedOut);
84
+ let graceTimer;
85
+ const graceExpired = await Promise.race([
86
+ teardown.then(() => false),
87
+ new Promise((resolve) => {
88
+ graceTimer = setTimeout(() => resolve(true), DISPATCHER_CLOSE_GRACE_MS);
89
+ }),
90
+ ]);
91
+ if (graceTimer)
92
+ clearTimeout(graceTimer);
93
+ if (graceExpired) {
94
+ // The graceful close never settled within the grace, so destroy the
95
+ // dispatcher instead of leaking it. Fire-and-forget with the error
96
+ // swallowed: teardown must never replace the rejection the caller is
97
+ // owed.
98
+ const disposable = dispatcher;
99
+ if (typeof disposable.destroy === 'function') {
100
+ void Promise.resolve(disposable.destroy.call(dispatcher, new Error('Dispatcher close grace expired'))).catch(() => { });
101
+ }
102
+ }
75
103
  throw error;
76
104
  }
77
105
  // Headers are in: return the original Fetch Response now. close() is