@0xmaxma/claude-gateway 2.0.13 → 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.
@@ -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");
@@ -1129,12 +1130,18 @@ class AgentOrchestrationRuntime {
1129
1130
  mailboxCursor = Number(row.mailbox_row);
1130
1131
  if (this.active.size >= this.config.conversation.maxActiveSessions)
1131
1132
  break;
1132
- const input = JSON.parse(String(row.ingress_json));
1133
- 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))
1134
1137
  continue;
1135
- 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)
1136
1144
  continue;
1137
- visitedSessions.add(input.scope.agentSessionId);
1138
1145
  const scope = (0, provider_scope_1.resolveProviderScope)(this.agent, this.gateway, input.model, 'agent');
1139
1146
  const waiting = this.providerAdmission.inspect(scope, this.config.providerAdmission);
1140
1147
  if (waiting) {
@@ -1145,7 +1152,7 @@ class AgentOrchestrationRuntime {
1145
1152
  }
1146
1153
  // A persisted input retains the authenticated scope and model from ingress.
1147
1154
  // Recovery can repeat inference, but committed tool receipts remain fenced.
1148
- 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 => {
1149
1156
  const responseId = this.responseIdForInput(String(row.id));
1150
1157
  if (input.modality !== 'live_voice' && responseId) {
1151
1158
  try {
@@ -1154,13 +1161,26 @@ class AgentOrchestrationRuntime {
1154
1161
  catch { /* slow audio cannot stall inference */ }
1155
1162
  }
1156
1163
  } });
1157
- void result.then(text => this.deferred.get(String(row.id))?.resolve(text), error => this.deferred.get(String(row.id))?.reject(error))
1158
- .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
+ } });
1159
1176
  }
1160
1177
  if (this.active.size >= this.config.conversation.maxActiveSessions)
1161
1178
  break mailbox;
1162
1179
  }
1163
1180
  }
1181
+ batchDebounceMs() {
1182
+ return this.config.conversation.semanticIntake ? this.config.conversation.inputDebounceMs : 0;
1183
+ }
1164
1184
  async run(input, capabilities, options) {
1165
1185
  const sessionId = input.scope.agentSessionId;
1166
1186
  const channelTts = this.telegramVoices.settings((0, store_1.channelVoiceKey)(input.scope.source, input.scope.chatId, input.scope.threadKey));
@@ -1210,7 +1230,7 @@ class AgentOrchestrationRuntime {
1210
1230
  this.store.run("UPDATE conversations SET status='draining' WHERE id=?", receipt.conversationId);
1211
1231
  this.seenSessions.add(sessionId);
1212
1232
  this.questionControls.tick();
1213
- 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);
1214
1234
  active.decision = decision;
1215
1235
  if (questionReview) {
1216
1236
  // Notifications may arrive after this internal input was queued. A
@@ -1277,9 +1297,9 @@ class AgentOrchestrationRuntime {
1277
1297
  const semantic = this.config.conversation.semanticIntake && !active.notification && !liveControl;
1278
1298
  const prepared = semantic ? this.intake.context(receipt.conversationId, input.scope.principalId, String(admitted.binding_id)) : undefined;
1279
1299
  const recoveryInputId = input.ingressKey?.startsWith('intake-recovery:') ? input.ingressKey.slice('intake-recovery:'.length) : undefined;
1280
- 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] : [])])];
1281
1301
  const preparedInputs = preparedInputIds.length ? this.store.all(`SELECT id,text,attachment_refs_json,ingress_json FROM conversation_inputs
1282
- 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)) : [];
1283
1303
  const preparedRefs = preparedInputs.flatMap(row => JSON.parse(String(row.attachment_refs_json)));
1284
1304
  if (preparedRefs.length)
1285
1305
  input = { ...input, attachmentIds: [...new Set([...(input.attachmentIds ?? []), ...preparedRefs])] };
@@ -1314,14 +1334,18 @@ class AgentOrchestrationRuntime {
1314
1334
  }
1315
1335
  // Immutable source IDs remain available to workers. Only the model delivery
1316
1336
  // is incremental; full canonical text and attachment references are retained.
1317
- 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))));
1318
1341
  for (const row of freshPreparedInputs)
1319
1342
  contextPlan.mark('materials', String(row.id), true);
1320
1343
  contextPlan.mark('materials', receipt.inputId, true);
1321
1344
  const intakeValue = (value) => value ? { mode: value.mode, deferredDispatch: value.deferredDispatch,
1322
1345
  preparation: value.preparation, inputIds: value.inputIds, task_id: value.task_id, resolution: value.resolution } : null;
1323
1346
  const pendingState = intakeValue(prepared);
1324
- 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;
1325
1349
  if (!semantic && input.skill && input.modality !== 'live_voice' && !channelSpeech && !visualInput.images.length && !visualInput.unavailable.length) {
1326
1350
  const task = this.tasks.spawn({ ...capabilities, ...receipt, ...decision, principalId: input.scope.principalId,
1327
1351
  model: options.model ?? this.agent.claude.model, actionId: `skill:${receipt.inputId}` }, {
@@ -1469,7 +1493,7 @@ class AgentOrchestrationRuntime {
1469
1493
  return;
1470
1494
  if (!intakeChoice || intakeChoice.mode === 'wait' || intakeChoice.mode === 'resolve' || !acknowledgementReady)
1471
1495
  throw new types_2.OrchestrationError('ACKNOWLEDGEMENT_REQUIRED');
1472
- if (tool === 'task_spawn' && preparedInputs.length)
1496
+ if (tool === 'task_spawn' && !args.gateway_target && preparedInputs.length)
1473
1497
  args.context_refs = [...new Set([...(Array.isArray(args.context_refs) ? args.context_refs : []), ...preparedRefs, ...preparedInputs.map(row => String(row.id))])];
1474
1498
  if (intakeChoice.mode === 'update' && (tool === 'task_spawn' || args.task_id !== intakeChoice.task_id)) {
1475
1499
  const attempt = attemptedTaskActions.get(actionId);
@@ -1663,14 +1687,18 @@ class AgentOrchestrationRuntime {
1663
1687
  await this.flushHistory();
1664
1688
  return display;
1665
1689
  }
1690
+ let unsafeToReplay = false;
1666
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;
1667
1694
  this.store.transaction(() => this.store.appendEvent(receipt.conversationId, internalReview ? 'progress.review.tool' : 'tool.activity', { ...event, responseId: decision.responseId, role: 'agent' }));
1668
1695
  if (!internalReview)
1669
1696
  options.onTool?.(event);
1670
1697
  }));
1698
+ const bufferChat = semantic && this.config.conversation.inputDebounceMs > 0 && (0, input_batch_1.batchableInput)(input);
1671
1699
  let rawDisplay = '', structuredStarted = false;
1672
1700
  const displayChunk = (chunk) => {
1673
- if (internalReview || questionReview)
1701
+ if (internalReview || questionReview || bufferChat || recoveryInputId)
1674
1702
  return; // Buffer until the notify/silence decision is final.
1675
1703
  if (semantic && (intakeChoice?.mode === 'wait' || intakeDeferred || acknowledgementId))
1676
1704
  return;
@@ -1726,6 +1754,28 @@ class AgentOrchestrationRuntime {
1726
1754
  }
1727
1755
  catch { /* Provider bookkeeping cannot invalidate a completed inference. */ }
1728
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
+ }
1729
1779
  if (!response.interrupted) {
1730
1780
  // A completed CLI turn has consumed its tool results. Failed/interrupted
1731
1781
  // turns never advance this checkpoint, so committed receipts can recover.
@@ -1811,7 +1861,20 @@ class AgentOrchestrationRuntime {
1811
1861
  : 'The requested task was not started or updated. Please try again.';
1812
1862
  surfaces.spoken = '';
1813
1863
  }
1814
- 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)));
1815
1878
  if (intakeSilent) {
1816
1879
  // A receipt/preparation turn has not reported older task results. Keep
1817
1880
  // their notifications (and attachments) available to the next report.
@@ -1831,7 +1894,7 @@ class AgentOrchestrationRuntime {
1831
1894
  for (const image of endScreenshots) {
1832
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 });
1833
1896
  }
1834
- 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);
1835
1898
  if (active.notification && !response.interrupted) {
1836
1899
  const batch = this.store.get('SELECT COUNT(*)-COUNT(DISTINCT task_id) n FROM notifications WHERE decision_id=?', decision.decisionId);
1837
1900
  try {