@0xmaxma/claude-gateway 2.0.12 → 2.0.14

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 (67) hide show
  1. package/config.template.json +1 -1
  2. package/dist/agent/runner.d.ts.map +1 -1
  3. package/dist/agent/runner.js +2 -25
  4. package/dist/agent/runner.js.map +1 -1
  5. package/dist/build-provenance.json +4 -4
  6. package/dist/orchestration/bridge.d.ts +3 -0
  7. package/dist/orchestration/bridge.d.ts.map +1 -1
  8. package/dist/orchestration/bridge.js +22 -11
  9. package/dist/orchestration/bridge.js.map +1 -1
  10. package/dist/orchestration/cli-skills.d.ts +5 -0
  11. package/dist/orchestration/cli-skills.d.ts.map +1 -1
  12. package/dist/orchestration/cli-skills.js +63 -0
  13. package/dist/orchestration/cli-skills.js.map +1 -1
  14. package/dist/orchestration/config.d.ts +44 -0
  15. package/dist/orchestration/config.d.ts.map +1 -1
  16. package/dist/orchestration/config.js +18 -5
  17. package/dist/orchestration/config.js.map +1 -1
  18. package/dist/orchestration/conversation-intake.d.ts +1 -1
  19. package/dist/orchestration/conversation-intake.d.ts.map +1 -1
  20. package/dist/orchestration/conversation-intake.js +6 -5
  21. package/dist/orchestration/conversation-intake.js.map +1 -1
  22. package/dist/orchestration/delivery.d.ts +1 -0
  23. package/dist/orchestration/delivery.d.ts.map +1 -1
  24. package/dist/orchestration/delivery.js +1 -1
  25. package/dist/orchestration/delivery.js.map +1 -1
  26. package/dist/orchestration/file-delivery.d.ts.map +1 -1
  27. package/dist/orchestration/file-delivery.js +47 -3
  28. package/dist/orchestration/file-delivery.js.map +1 -1
  29. package/dist/orchestration/input-batch.d.ts +16 -0
  30. package/dist/orchestration/input-batch.d.ts.map +1 -0
  31. package/dist/orchestration/input-batch.js +59 -0
  32. package/dist/orchestration/input-batch.js.map +1 -0
  33. package/dist/orchestration/narrate.d.ts +42 -0
  34. package/dist/orchestration/narrate.d.ts.map +1 -0
  35. package/dist/orchestration/narrate.js +197 -0
  36. package/dist/orchestration/narrate.js.map +1 -0
  37. package/dist/orchestration/process-turn.js +1 -1
  38. package/dist/orchestration/process-turn.js.map +1 -1
  39. package/dist/orchestration/runtime.d.ts +2 -0
  40. package/dist/orchestration/runtime.d.ts.map +1 -1
  41. package/dist/orchestration/runtime.js +84 -16
  42. package/dist/orchestration/runtime.js.map +1 -1
  43. package/dist/orchestration/skills.d.ts +6 -0
  44. package/dist/orchestration/skills.d.ts.map +1 -1
  45. package/dist/orchestration/skills.js +9 -2
  46. package/dist/orchestration/skills.js.map +1 -1
  47. package/dist/orchestration/task-files.d.ts +10 -1
  48. package/dist/orchestration/task-files.d.ts.map +1 -1
  49. package/dist/orchestration/task-files.js +46 -8
  50. package/dist/orchestration/task-files.js.map +1 -1
  51. package/dist/session/runtime-profile.d.ts.map +1 -1
  52. package/dist/session/runtime-profile.js +2 -1
  53. package/dist/session/runtime-profile.js.map +1 -1
  54. package/dist/voice/line-audio.d.ts +10 -1
  55. package/dist/voice/line-audio.d.ts.map +1 -1
  56. package/dist/voice/line-audio.js +19 -6
  57. package/dist/voice/line-audio.js.map +1 -1
  58. package/dist/voice/narrate-split.d.ts +17 -0
  59. package/dist/voice/narrate-split.d.ts.map +1 -0
  60. package/dist/voice/narrate-split.js +77 -0
  61. package/dist/voice/narrate-split.js.map +1 -0
  62. package/mcp/lazy-tools.test.ts +2 -1
  63. package/mcp/modules.ts +2 -0
  64. package/mcp/server.ts +2 -1
  65. package/mcp/tools/narrate/module.ts +42 -0
  66. package/mcp/tools/tasks/module.ts +7 -2
  67. package/package.json +1 -1
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AgentOrchestrationRuntime = exports.GATEWAY_TASK_INSTRUCTIONS = void 0;
4
4
  exports.readRequestDirective = readRequestDirective;
5
5
  exports.agentHandoffDirective = agentHandoffDirective;
6
+ const input_batch_1 = require("./input-batch");
6
7
  const computer_prompt_1 = require("./computer-prompt");
7
8
  const control_notification_1 = require("./control-notification");
8
9
  const computer_choice_ids_1 = require("../automation/computer-choice-ids");
@@ -91,6 +92,7 @@ const bounded_queue_1 = require("./bounded-queue");
91
92
  const capacity_1 = require("./capacity");
92
93
  const cleanup_1 = require("./tasks/cleanup");
93
94
  const task_files_1 = require("./task-files");
95
+ const narrate_1 = require("./narrate");
94
96
  const worker_shares_1 = require("./worker-shares");
95
97
  const provider_admission_1 = require("./provider-admission");
96
98
  const provider_scope_1 = require("./provider-scope");
@@ -322,6 +324,10 @@ class AgentOrchestrationRuntime {
322
324
  progress: (task, report) => store.transaction(() => { const current = store.task(task.taskId); if (!current || current.activeAttemptId !== task.activeAttemptId || !['starting', 'running', 'interrupting'].includes(current.state))
323
325
  return; current.computerReport = report; current.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Computer Use: ${report.phase} · ${report.steps} actions · ${report.evaluations ?? 0} evaluations.${current.queuedCommands?.length ? ` ${current.queuedCommands.length} queued command${current.queuedCommands.length === 1 ? '' : 's'} waiting.` : ''}` }; store.saveTask(current, current.stateVersion); }) }));
324
326
  const bridge = new bridge_1.TaskBridge(tasks, files, (0, worker_shares_1.workerShares)(files, agent, gateway), host.skills ? () => host.skills() : undefined, agent.type === 'app-agent' ? { agent, spool: (0, path_1.join)(root, 'container-files') } : undefined, (0, worker_crons_1.workerCrons)(files, agent, gateway), gatewayAdapters);
327
+ const voiceConfig = () => (0, config_2.resolveOrchestrationConfig)(agent.orchestration, agent.voice ?? { enabled: false }).voice;
328
+ const narrateVoices = new telegram_voices_1.TelegramVoices(store, () => voiceConfig().tts);
329
+ bridge.narrateCall = (0, narrate_1.workerNarrate)({ files, config: () => voiceConfig().narrate, voiceEnabled: () => voiceConfig().enabled,
330
+ settings: chat => narrateVoices.settings((0, store_1.channelVoiceKey)(chat.source, chat.chat_id, chat.thread_key ?? '')) });
325
331
  bridge.computerEnabled = computerAllowed;
326
332
  bridge.jevEnabled = () => (0, jev_gateway_1.jevAllowed)(gateway, agent);
327
333
  bridge.browserEnabled = () => Boolean(gateway.gateway.jev?.browser) && (0, jev_gateway_1.jevAllowed)(gateway, agent) && gateway.gateway.jev?.features?.browserTasks?.enabled === true;
@@ -1124,12 +1130,18 @@ class AgentOrchestrationRuntime {
1124
1130
  mailboxCursor = Number(row.mailbox_row);
1125
1131
  if (this.active.size >= this.config.conversation.maxActiveSessions)
1126
1132
  break;
1127
- const input = JSON.parse(String(row.ingress_json));
1128
- if (!input.scope || !input.capabilities || this.active.has(input.scope.agentSessionId))
1133
+ // Batch members share the row's conversation and session, so admission is
1134
+ // decided before scanning later rows for the batch.
1135
+ const head = JSON.parse(String(row.ingress_json));
1136
+ if (!head.scope || !head.capabilities || this.active.has(head.scope.agentSessionId))
1129
1137
  continue;
1130
- if (visitedSessions.has(input.scope.agentSessionId))
1138
+ if (visitedSessions.has(head.scope.agentSessionId))
1139
+ continue;
1140
+ visitedSessions.add(head.scope.agentSessionId);
1141
+ const batch = (0, input_batch_1.inputBatch)(this.store, row, this.batchDebounceMs(), this.config.conversation.inputMaxWaitMs);
1142
+ const input = batch.input;
1143
+ if (Date.now() < batch.readyAt)
1131
1144
  continue;
1132
- visitedSessions.add(input.scope.agentSessionId);
1133
1145
  const scope = (0, provider_scope_1.resolveProviderScope)(this.agent, this.gateway, input.model, 'agent');
1134
1146
  const waiting = this.providerAdmission.inspect(scope, this.config.providerAdmission);
1135
1147
  if (waiting) {
@@ -1140,7 +1152,7 @@ class AgentOrchestrationRuntime {
1140
1152
  }
1141
1153
  // A persisted input retains the authenticated scope and model from ingress.
1142
1154
  // Recovery can repeat inference, but committed tool receipts remain fenced.
1143
- const result = this.send({ ...input, acceptedInputId: String(row.id) }, input.capabilities, { timeoutMs: this.config.conversation.maxDecisionDurationMs, model: input.model, onTool: event => this.inputTools.get(String(row.id))?.(event), onText: text => {
1155
+ const result = this.send({ ...input, acceptedInputId: String(batch.last.id) }, input.capabilities, { inputIds: batch.inputIds, timeoutMs: this.config.conversation.maxDecisionDurationMs, model: input.model, onTool: event => this.inputTools.get(String(row.id))?.(event), onText: text => {
1144
1156
  const responseId = this.responseIdForInput(String(row.id));
1145
1157
  if (input.modality !== 'live_voice' && responseId) {
1146
1158
  try {
@@ -1149,13 +1161,26 @@ class AgentOrchestrationRuntime {
1149
1161
  catch { /* slow audio cannot stall inference */ }
1150
1162
  }
1151
1163
  } });
1152
- void result.then(text => this.deferred.get(String(row.id))?.resolve(text), error => this.deferred.get(String(row.id))?.reject(error))
1153
- .finally(() => this.deferred.delete(String(row.id)));
1164
+ void result.then(text => {
1165
+ // Superseded, uncommitted inputs remain durable and their callers wait
1166
+ // for the replacement decision rather than receiving an empty answer.
1167
+ for (const id of batch.inputIds)
1168
+ if (this.store.get('SELECT status FROM conversation_inputs WHERE id=?', id)?.status === 'handled') {
1169
+ this.deferred.get(id)?.resolve(text);
1170
+ this.deferred.delete(id);
1171
+ }
1172
+ }, error => { for (const id of batch.inputIds) {
1173
+ this.deferred.get(id)?.reject(error);
1174
+ this.deferred.delete(id);
1175
+ } });
1154
1176
  }
1155
1177
  if (this.active.size >= this.config.conversation.maxActiveSessions)
1156
1178
  break mailbox;
1157
1179
  }
1158
1180
  }
1181
+ batchDebounceMs() {
1182
+ return this.config.conversation.semanticIntake ? this.config.conversation.inputDebounceMs : 0;
1183
+ }
1159
1184
  async run(input, capabilities, options) {
1160
1185
  const sessionId = input.scope.agentSessionId;
1161
1186
  const channelTts = this.telegramVoices.settings((0, store_1.channelVoiceKey)(input.scope.source, input.scope.chatId, input.scope.threadKey));
@@ -1205,7 +1230,7 @@ class AgentOrchestrationRuntime {
1205
1230
  this.store.run("UPDATE conversations SET status='draining' WHERE id=?", receipt.conversationId);
1206
1231
  this.seenSessions.add(sessionId);
1207
1232
  this.questionControls.tick();
1208
- const decision = this.decisions.begin(receipt.conversationId, input.scope.principalId, [receipt.inputId], input.requestId);
1233
+ const decision = this.decisions.begin(receipt.conversationId, input.scope.principalId, options.inputIds ?? [receipt.inputId], input.requestId);
1209
1234
  active.decision = decision;
1210
1235
  if (questionReview) {
1211
1236
  // Notifications may arrive after this internal input was queued. A
@@ -1272,9 +1297,9 @@ class AgentOrchestrationRuntime {
1272
1297
  const semantic = this.config.conversation.semanticIntake && !active.notification && !liveControl;
1273
1298
  const prepared = semantic ? this.intake.context(receipt.conversationId, input.scope.principalId, String(admitted.binding_id)) : undefined;
1274
1299
  const recoveryInputId = input.ingressKey?.startsWith('intake-recovery:') ? input.ingressKey.slice('intake-recovery:'.length) : undefined;
1275
- const preparedInputIds = [...new Set([...(prepared?.inputIds ?? []), ...(recoveryInputId ? [recoveryInputId] : [])])];
1300
+ const preparedInputIds = [...new Set([...(prepared?.inputIds ?? []), ...(!recoveryInputId && (prepared?.inputIds?.length || decision.inputIds.length > 1) ? decision.inputIds : []), ...(recoveryInputId ? [recoveryInputId] : [])])];
1276
1301
  const preparedInputs = preparedInputIds.length ? this.store.all(`SELECT id,text,attachment_refs_json,ingress_json FROM conversation_inputs
1277
- WHERE conversation_id=? AND principal_id=? AND binding_id=? AND id IN (SELECT value FROM json_each(?))`, receipt.conversationId, input.scope.principalId, admitted.binding_id, JSON.stringify(preparedInputIds)) : [];
1302
+ WHERE conversation_id=? AND principal_id=? AND binding_id=? AND id IN (SELECT value FROM json_each(?)) ORDER BY input_seq`, receipt.conversationId, input.scope.principalId, admitted.binding_id, JSON.stringify(preparedInputIds)) : [];
1278
1303
  const preparedRefs = preparedInputs.flatMap(row => JSON.parse(String(row.attachment_refs_json)));
1279
1304
  if (preparedRefs.length)
1280
1305
  input = { ...input, attachmentIds: [...new Set([...(input.attachmentIds ?? []), ...preparedRefs])] };
@@ -1309,14 +1334,18 @@ class AgentOrchestrationRuntime {
1309
1334
  }
1310
1335
  // Immutable source IDs remain available to workers. Only the model delivery
1311
1336
  // is incremental; full canonical text and attachment references are retained.
1312
- const freshPreparedInputs = preparedInputs.filter(row => row.id !== receipt.inputId && !contextPlan.includes('materials', String(row.id)));
1337
+ // Unresolved execution is an obligation, not optional cached context. Replay
1338
+ // it on follow-up/recovery even when this CLI has seen it before.
1339
+ const replayPending = Boolean(prepared?.deferredDispatch || recoveryInputId);
1340
+ const freshPreparedInputs = preparedInputs.filter(row => row.id !== receipt.inputId && (replayPending || !contextPlan.includes('materials', String(row.id))));
1313
1341
  for (const row of freshPreparedInputs)
1314
1342
  contextPlan.mark('materials', String(row.id), true);
1315
1343
  contextPlan.mark('materials', receipt.inputId, true);
1316
1344
  const intakeValue = (value) => value ? { mode: value.mode, deferredDispatch: value.deferredDispatch,
1317
1345
  preparation: value.preparation, inputIds: value.inputIds, task_id: value.task_id, resolution: value.resolution } : null;
1318
1346
  const pendingState = intakeValue(prepared);
1319
- const pendingChanges = contextPlan.select('intake', [{ id: 'pending', value: pendingState }], row => row.id);
1347
+ const pendingDelta = contextPlan.select('intake', [{ id: 'pending', value: pendingState }], row => row.id);
1348
+ const pendingChanges = replayPending ? [{ id: 'pending', value: pendingState }] : pendingDelta;
1320
1349
  if (!semantic && input.skill && input.modality !== 'live_voice' && !channelSpeech && !visualInput.images.length && !visualInput.unavailable.length) {
1321
1350
  const task = this.tasks.spawn({ ...capabilities, ...receipt, ...decision, principalId: input.scope.principalId,
1322
1351
  model: options.model ?? this.agent.claude.model, actionId: `skill:${receipt.inputId}` }, {
@@ -1464,7 +1493,7 @@ class AgentOrchestrationRuntime {
1464
1493
  return;
1465
1494
  if (!intakeChoice || intakeChoice.mode === 'wait' || intakeChoice.mode === 'resolve' || !acknowledgementReady)
1466
1495
  throw new types_2.OrchestrationError('ACKNOWLEDGEMENT_REQUIRED');
1467
- if (tool === 'task_spawn' && preparedInputs.length)
1496
+ if (tool === 'task_spawn' && !args.gateway_target && preparedInputs.length)
1468
1497
  args.context_refs = [...new Set([...(Array.isArray(args.context_refs) ? args.context_refs : []), ...preparedRefs, ...preparedInputs.map(row => String(row.id))])];
1469
1498
  if (intakeChoice.mode === 'update' && (tool === 'task_spawn' || args.task_id !== intakeChoice.task_id)) {
1470
1499
  const attempt = attemptedTaskActions.get(actionId);
@@ -1658,14 +1687,18 @@ class AgentOrchestrationRuntime {
1658
1687
  await this.flushHistory();
1659
1688
  return display;
1660
1689
  }
1690
+ let unsafeToReplay = false;
1661
1691
  agentSession.on('output', (0, tool_activity_1.toolActivity)(event => {
1692
+ if (event.type === 'tool_use' && !/^(StructuredOutput|mcp__gateway__(conversation_intake|capabilities_list|memory_get|memory_search|task_status))$/.test(event.name))
1693
+ unsafeToReplay = true;
1662
1694
  this.store.transaction(() => this.store.appendEvent(receipt.conversationId, internalReview ? 'progress.review.tool' : 'tool.activity', { ...event, responseId: decision.responseId, role: 'agent' }));
1663
1695
  if (!internalReview)
1664
1696
  options.onTool?.(event);
1665
1697
  }));
1698
+ const bufferChat = semantic && this.config.conversation.inputDebounceMs > 0 && (0, input_batch_1.batchableInput)(input);
1666
1699
  let rawDisplay = '', structuredStarted = false;
1667
1700
  const displayChunk = (chunk) => {
1668
- if (internalReview || questionReview)
1701
+ if (internalReview || questionReview || bufferChat || recoveryInputId)
1669
1702
  return; // Buffer until the notify/silence decision is final.
1670
1703
  if (semantic && (intakeChoice?.mode === 'wait' || intakeDeferred || acknowledgementId))
1671
1704
  return;
@@ -1721,6 +1754,28 @@ class AgentOrchestrationRuntime {
1721
1754
  }
1722
1755
  catch { /* Provider bookkeeping cannot invalidate a completed inference. */ }
1723
1756
  }
1757
+ const committedAction = this.store.get('SELECT action_id FROM task_commands WHERE decision_id=? LIMIT 1', decision.decisionId);
1758
+ const supersededCount = Number(this.store.get("SELECT count(*) n FROM conversation_events WHERE conversation_id=? AND type='response.superseded' AND json_extract(payload_json,'$.payload.inputId')=?", receipt.conversationId, decision.inputIds[0])?.n ?? 0);
1759
+ // Supersede only when the batcher would now absorb newer input into this
1760
+ // batch; an incompatible follow-up gets its own turn without re-inference.
1761
+ const batchGrows = () => {
1762
+ const first = this.store.get('SELECT * FROM conversation_inputs WHERE id=?', decision.inputIds[0]);
1763
+ return !!first && (0, input_batch_1.inputBatch)(this.store, first, this.batchDebounceMs(), this.config.conversation.inputMaxWaitMs, decision.inputIds)
1764
+ .inputIds.some(id => !decision.inputIds.includes(id));
1765
+ };
1766
+ if (bufferChat && newerInputPending() && batchGrows() && !response.interrupted && !active.stopping && !acknowledgementId && !committedAction && !unsafeToReplay && supersededCount < 2) {
1767
+ this.store.compose(() => {
1768
+ this.decisions.finish(decision, '', 'interrupted', undefined, false);
1769
+ for (const id of decision.inputIds)
1770
+ this.store.run("UPDATE conversation_inputs SET status='accepted' WHERE id=?", id);
1771
+ this.store.appendEvent(receipt.conversationId, 'response.superseded', { responseId: decision.responseId, inputId: decision.inputIds[0], inputIds: decision.inputIds });
1772
+ });
1773
+ // No user-visible answer or task action was committed. Re-read canonical
1774
+ // inputs with the newer messages; do not checkpoint discarded context.
1775
+ contextPlan.invalidate();
1776
+ await this.flushHistory();
1777
+ return '';
1778
+ }
1724
1779
  if (!response.interrupted) {
1725
1780
  // A completed CLI turn has consumed its tool results. Failed/interrupted
1726
1781
  // turns never advance this checkpoint, so committed receipts can recover.
@@ -1806,7 +1861,20 @@ class AgentOrchestrationRuntime {
1806
1861
  : 'The requested task was not started or updated. Please try again.';
1807
1862
  surfaces.spoken = '';
1808
1863
  }
1809
- const intakeSilent = semantic && !uncommittedDispatch && (intakeChoice?.mode === 'wait' || intakeDeferred || (acknowledgementId && this.store.get("SELECT action_id FROM task_commands WHERE decision_id=? AND command_type IN ('spawn','update','answer') LIMIT 1", decision.decisionId)));
1864
+ const dispatchCommitted = this.store.get("SELECT action_id FROM task_commands WHERE decision_id=? AND command_type IN ('spawn','update','answer') LIMIT 1", decision.decisionId);
1865
+ if (semantic && !response.interrupted && !active.stopping && intakeChoice && ['ready', 'update'].includes(intakeChoice.mode) && !dispatchCommitted)
1866
+ this.intake.deferDispatch(receipt.inputId);
1867
+ const unresolved = semantic && this.intake.context(receipt.conversationId, input.scope.principalId, String(admitted.binding_id));
1868
+ const recoveryFailed = Boolean(recoveryInputId && unresolved?.deferredDispatch && unresolved.mode !== 'wait' && !response.interrupted && !active.stopping && !newerInputPending());
1869
+ if (recoveryFailed) {
1870
+ const thai = /[\u0E00-\u0E7F]/.test(input.text + String(unresolved && unresolved.preparation) + preparedInputs.map(row => row.text).join('') + response.text);
1871
+ surfaces.display = thai
1872
+ ? 'ระบบยังจัดการคำขอค้างไม่สำเร็จ: agent จบการกู้คืนโดยไม่ได้ยืนยันว่างานถูกส่งต่อหรือปิดเรื่องแล้ว คำขอเดิมยังถูกเก็บไว้และยังไม่ถือว่าเสร็จค่ะ'
1873
+ : 'The agent ended recovery without confirming that the pending request was dispatched or resolved. The request is still retained and is not complete.';
1874
+ surfaces.spoken = '';
1875
+ this.store.transaction(() => this.store.appendEvent(receipt.conversationId, 'response.dispatch_unresolved', { responseId: decision.responseId, inputId: receipt.inputId }));
1876
+ }
1877
+ const intakeSilent = !recoveryFailed && semantic && !uncommittedDispatch && (intakeChoice?.mode === 'wait' || intakeDeferred || (acknowledgementId && unresolved?.deferredDispatch && !dispatchCommitted) || (acknowledgementId && this.store.get("SELECT action_id FROM task_commands WHERE decision_id=? AND command_type IN ('spawn','update','answer') LIMIT 1", decision.decisionId)));
1810
1878
  if (intakeSilent) {
1811
1879
  // A receipt/preparation turn has not reported older task results. Keep
1812
1880
  // their notifications (and attachments) available to the next report.
@@ -1826,7 +1894,7 @@ class AgentOrchestrationRuntime {
1826
1894
  for (const image of endScreenshots) {
1827
1895
  (0, computer_end_screenshot_1.attachComputerEndScreenshot)(this.store, (0, path_1.join)(this.agent.workspace, '../..'), { ...image, responseId: decision.responseId, principalId: input.scope.principalId, conversationId: receipt.conversationId });
1828
1896
  }
1829
- this.decisions.finish(decision, display, response.interrupted ? 'interrupted' : 'completed', channelSpeech && !silent ? taskSpeech || surfaces.spoken : undefined, !active.stopping && !silent);
1897
+ this.decisions.finish(decision, display, response.interrupted ? 'interrupted' : recoveryFailed ? 'failed' : 'completed', channelSpeech && !silent ? taskSpeech || surfaces.spoken : undefined, !active.stopping && !silent);
1830
1898
  if (active.notification && !response.interrupted) {
1831
1899
  const batch = this.store.get('SELECT COUNT(*)-COUNT(DISTINCT task_id) n FROM notifications WHERE decision_id=?', decision.decisionId);
1832
1900
  try {