@0xmaxma/claude-gateway 2.0.5 → 2.0.6

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 (118) hide show
  1. package/dist/agent/runner.d.ts +14 -0
  2. package/dist/agent/runner.d.ts.map +1 -1
  3. package/dist/agent/runner.js +75 -71
  4. package/dist/agent/runner.js.map +1 -1
  5. package/dist/agent/session-compaction-scheduler.d.ts +22 -0
  6. package/dist/agent/session-compaction-scheduler.d.ts.map +1 -0
  7. package/dist/agent/session-compaction-scheduler.js +40 -0
  8. package/dist/agent/session-compaction-scheduler.js.map +1 -0
  9. package/dist/api/gateway-router.d.ts.map +1 -1
  10. package/dist/api/gateway-router.js +100 -10
  11. package/dist/api/gateway-router.js.map +1 -1
  12. package/dist/api/memory-activity.d.ts +18 -0
  13. package/dist/api/memory-activity.d.ts.map +1 -0
  14. package/dist/api/memory-activity.js +97 -0
  15. package/dist/api/memory-activity.js.map +1 -0
  16. package/dist/orchestration/agent-tool-schemas.js +1 -1
  17. package/dist/orchestration/agent-tool-schemas.js.map +1 -1
  18. package/dist/orchestration/bridge.js +1 -1
  19. package/dist/orchestration/bridge.js.map +1 -1
  20. package/dist/orchestration/compact-measurements.d.ts +21 -0
  21. package/dist/orchestration/compact-measurements.d.ts.map +1 -0
  22. package/dist/orchestration/compact-measurements.js +62 -0
  23. package/dist/orchestration/compact-measurements.js.map +1 -0
  24. package/dist/orchestration/config.d.ts +4 -0
  25. package/dist/orchestration/config.d.ts.map +1 -1
  26. package/dist/orchestration/config.js +1 -1
  27. package/dist/orchestration/config.js.map +1 -1
  28. package/dist/orchestration/context-delivery.d.ts +32 -0
  29. package/dist/orchestration/context-delivery.d.ts.map +1 -0
  30. package/dist/orchestration/context-delivery.js +101 -0
  31. package/dist/orchestration/context-delivery.js.map +1 -0
  32. package/dist/orchestration/conversation-intake.d.ts +10 -6
  33. package/dist/orchestration/conversation-intake.d.ts.map +1 -1
  34. package/dist/orchestration/conversation-intake.js +22 -8
  35. package/dist/orchestration/conversation-intake.js.map +1 -1
  36. package/dist/orchestration/dashboard-charts.d.ts +32 -0
  37. package/dist/orchestration/dashboard-charts.d.ts.map +1 -0
  38. package/dist/orchestration/dashboard-charts.js +77 -0
  39. package/dist/orchestration/dashboard-charts.js.map +1 -0
  40. package/dist/orchestration/dashboard-reader-worker.js +21 -3
  41. package/dist/orchestration/dashboard-reader-worker.js.map +1 -1
  42. package/dist/orchestration/dashboard-reader.d.ts +1 -1
  43. package/dist/orchestration/dashboard-reader.d.ts.map +1 -1
  44. package/dist/orchestration/dashboard-reader.js +2 -2
  45. package/dist/orchestration/dashboard-reader.js.map +1 -1
  46. package/dist/orchestration/decision-context.d.ts +1 -1
  47. package/dist/orchestration/decision-context.d.ts.map +1 -1
  48. package/dist/orchestration/decision-context.js +2 -2
  49. package/dist/orchestration/decision-context.js.map +1 -1
  50. package/dist/orchestration/mutation-recovery.d.ts +3 -2
  51. package/dist/orchestration/mutation-recovery.d.ts.map +1 -1
  52. package/dist/orchestration/mutation-recovery.js +8 -0
  53. package/dist/orchestration/mutation-recovery.js.map +1 -1
  54. package/dist/orchestration/native-compact.d.ts +4 -1
  55. package/dist/orchestration/native-compact.d.ts.map +1 -1
  56. package/dist/orchestration/native-compact.js +6 -2
  57. package/dist/orchestration/native-compact.js.map +1 -1
  58. package/dist/orchestration/process-turn.d.ts +1 -0
  59. package/dist/orchestration/process-turn.d.ts.map +1 -1
  60. package/dist/orchestration/process-turn.js +1 -1
  61. package/dist/orchestration/process-turn.js.map +1 -1
  62. package/dist/orchestration/runtime.d.ts +13 -1
  63. package/dist/orchestration/runtime.d.ts.map +1 -1
  64. package/dist/orchestration/runtime.js +161 -18
  65. package/dist/orchestration/runtime.js.map +1 -1
  66. package/dist/orchestration/session-compaction.d.ts +59 -0
  67. package/dist/orchestration/session-compaction.d.ts.map +1 -0
  68. package/dist/orchestration/session-compaction.js +212 -0
  69. package/dist/orchestration/session-compaction.js.map +1 -0
  70. package/dist/orchestration/tasks/driver.js +1 -1
  71. package/dist/orchestration/tasks/driver.js.map +1 -1
  72. package/dist/orchestration/token-ledger.d.ts +7 -0
  73. package/dist/orchestration/token-ledger.d.ts.map +1 -1
  74. package/dist/orchestration/token-ledger.js +1 -0
  75. package/dist/orchestration/token-ledger.js.map +1 -1
  76. package/dist/types.d.ts +3 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/ui/dashboard-charts.d.ts +3 -0
  79. package/dist/ui/dashboard-charts.d.ts.map +1 -0
  80. package/dist/ui/dashboard-charts.js +130 -0
  81. package/dist/ui/dashboard-charts.js.map +1 -0
  82. package/dist/ui/dashboard-client.d.ts.map +1 -1
  83. package/dist/ui/dashboard-client.js +10 -14
  84. package/dist/ui/dashboard-client.js.map +1 -1
  85. package/dist/ui/dashboard-controls.d.ts +4 -0
  86. package/dist/ui/dashboard-controls.d.ts.map +1 -0
  87. package/dist/ui/dashboard-controls.js +27 -0
  88. package/dist/ui/dashboard-controls.js.map +1 -0
  89. package/dist/ui/dashboard-presentation.d.ts +3 -0
  90. package/dist/ui/dashboard-presentation.d.ts.map +1 -1
  91. package/dist/ui/dashboard-presentation.js +11 -5
  92. package/dist/ui/dashboard-presentation.js.map +1 -1
  93. package/dist/ui/dashboard-range.d.ts +3 -3
  94. package/dist/ui/dashboard-range.d.ts.map +1 -1
  95. package/dist/ui/dashboard-range.js +21 -5
  96. package/dist/ui/dashboard-range.js.map +1 -1
  97. package/dist/ui/dashboard-theme.d.ts +3 -1
  98. package/dist/ui/dashboard-theme.d.ts.map +1 -1
  99. package/dist/ui/dashboard-theme.js +80 -3
  100. package/dist/ui/dashboard-theme.js.map +1 -1
  101. package/dist/ui/memory-activity.d.ts +4 -0
  102. package/dist/ui/memory-activity.d.ts.map +1 -0
  103. package/dist/ui/memory-activity.js +115 -0
  104. package/dist/ui/memory-activity.js.map +1 -0
  105. package/dist/ui/token-report-client.d.ts +1 -1
  106. package/dist/ui/token-report-client.d.ts.map +1 -1
  107. package/dist/ui/token-report-client.js +1 -1
  108. package/dist/ui/token-report.d.ts +1 -0
  109. package/dist/ui/token-report.d.ts.map +1 -1
  110. package/dist/ui/token-report.js +18 -15
  111. package/dist/ui/token-report.js.map +1 -1
  112. package/dist/ui/web-ui.d.ts +1 -1
  113. package/dist/ui/web-ui.d.ts.map +1 -1
  114. package/dist/ui/web-ui.js +15 -238
  115. package/dist/ui/web-ui.js.map +1 -1
  116. package/mcp/tools/telegram/receiver-server.ts +31 -17
  117. package/mcp/tools/telegram/session-browser.ts +10 -0
  118. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AgentOrchestrationRuntime = void 0;
4
+ const compact_measurements_1 = require("./compact-measurements");
5
+ const session_compaction_1 = require("./session-compaction");
6
+ const context_delivery_1 = require("./context-delivery");
4
7
  const native_compact_1 = require("./native-compact");
5
8
  const browser_voice_1 = require("./browser-voice");
6
9
  const mutation_recovery_1 = require("./mutation-recovery");
@@ -67,6 +70,9 @@ const capacity_1 = require("./capacity");
67
70
  const cleanup_1 = require("./tasks/cleanup");
68
71
  const task_files_1 = require("./task-files");
69
72
  const worker_shares_1 = require("./worker-shares");
73
+ /** Stable instructions are carried in the system prefix, not appended to every
74
+ * resumed user turn. Per-turn authorization flags remain explicit below it. */
75
+ const CONTEXT_DELIVERY_INSTRUCTIONS = 'Worker profiles: default-worker is the general-purpose worker for research, files, browser/API operations, services, calculations and code. In host mode it uses the Agent working environment; no Git or projectRoot is required. In container mode it stays inside the app container. Only explicitly configured isolated-worktree mode requires Git for default-worker; media-worker remains available for standalone scratch work in isolated modes. State the authorized working directory in task instructions; workers may change directories only within their execution boundary. Serialize conflicting edits to the same shared files; continue related work with continue_task_id. Task context is incremental within a resumed CLI conversation. Omission means unchanged, not deleted. On a fresh context only active/waiting tasks and current reports are bootstrapped; use task_status for other past work or full results. Receipt recovery is evidence, not authorization to replay a command. Previously supplied materials remain in the resumed context; preserve their references when assigning workers. Never infer that missing image bytes mean a missing attachment if its ref was already supplied.';
70
76
  class AgentOrchestrationRuntime {
71
77
  setBrowserVoice(sessionId, principalId, enabled) {
72
78
  this.authorizeSession(sessionId, principalId);
@@ -105,6 +111,7 @@ class AgentOrchestrationRuntime {
105
111
  this.sessionResponses = new Map();
106
112
  this.textListeners = new Map();
107
113
  this.intake = new conversation_intake_1.ConversationIntake(store);
114
+ this.contextDelivery = new context_delivery_1.ContextDelivery(store);
108
115
  this.cliSessions = new agent_cli_session_1.AgentCliSessions(store);
109
116
  this.store = store;
110
117
  this.history = history;
@@ -183,6 +190,7 @@ class AgentOrchestrationRuntime {
183
190
  bridge.recordRetrievals = (personalRetention.enabled && personalRetention.recordRetrievals) || (sharedRetention.enabled && sharedRetention.recordRetrievals);
184
191
  try {
185
192
  (0, recovery_1.recoverOrchestration)(store);
193
+ (0, session_compaction_1.recoverSessionCompaction)(store);
186
194
  // A prior gateway cannot prove that these processes stopped. Reserve
187
195
  // their global capacity conservatively as well as the per-agent slots.
188
196
  const recoveredReservations = new Map();
@@ -375,9 +383,18 @@ class AgentOrchestrationRuntime {
375
383
  process = await this.host.createAgentSession(sessionId, ticket.profile, model, { agentId: this.agent.id, agentSessionId: sessionId, source: conversation.source, accountId: String(conversation.account_id), chatId: String(conversation.chat_id), threadKey: String(conversation.thread_key), principalId: String(conversation.owner_principal_id) });
376
384
  if (active.stopping || this.closing)
377
385
  throw new types_2.OrchestrationError('INTERRUPTED');
378
- active.turn = (0, native_compact_1.startNativeCompact)(process);
379
- await active.turn.result;
386
+ this.contextDelivery.invalidateConversation(String(conversation.id));
387
+ const startedAt = Date.now(), compact = (0, native_compact_1.startNativeCompact)(process);
388
+ active.turn = compact;
389
+ await compact.result;
390
+ let measured = compact.measurements();
391
+ if (this.agent.type !== 'app-agent' && measured?.afterTokens == null) {
392
+ const storedMetrics = await (0, compact_measurements_1.readCompactMeasurements)((0, claude_settings_1.transcriptPath)(this.agent.workspace, cli.id), startedAt, Date.now());
393
+ if (storedMetrics)
394
+ measured = storedMetrics;
395
+ }
380
396
  this.store.transaction(() => this.store.appendEvent(String(conversation.id), 'session.context_compacted', { sessionId, cliSessionId: cli.id }));
397
+ return measured;
381
398
  }
382
399
  finally {
383
400
  revoke?.();
@@ -394,6 +411,27 @@ class AgentOrchestrationRuntime {
394
411
  void operation.finally(() => this.pending.delete(operation)).catch(() => { });
395
412
  return operation;
396
413
  }
414
+ compactionMaintenance(window) {
415
+ return this.sessionCompaction ?? (this.sessionCompaction = new session_compaction_1.SessionCompaction(this.store, this.agent.id, {
416
+ busy: id => this.active.has(id), stopping: () => this.closing || this.draining,
417
+ model: () => this.agent.claude.model, window, compact: (id, model) => this.compactSession(id, model),
418
+ }));
419
+ }
420
+ runSessionCompaction(config, window) {
421
+ const operation = this.compactionMaintenance(window).run(config);
422
+ this.pending.add(operation);
423
+ void operation.finally(() => this.pending.delete(operation)).catch(() => { });
424
+ return operation;
425
+ }
426
+ sessionContextWindow(sessionId) {
427
+ if (!this.store.get("SELECT name FROM sqlite_master WHERE name='token_turns'"))
428
+ return null;
429
+ const last = this.store.get(`SELECT MAX(COALESCE(d.ended_at,d.started_at)) at FROM conversation_decisions d
430
+ JOIN conversations c ON c.id=d.conversation_id WHERE c.agent_session_id=?`, sessionId)?.at;
431
+ if (last == null || Date.now() - Number(last) > 3600000)
432
+ return null;
433
+ return (0, token_ledger_1.latestAgentContextWindow)(this.store, sessionId);
434
+ }
397
435
  tokenReport(sessionId) {
398
436
  if (!this.ownsSession(sessionId))
399
437
  return undefined;
@@ -737,6 +775,7 @@ class AgentOrchestrationRuntime {
737
775
  let transcriptCheckpoint;
738
776
  let restoreContainerTranscript;
739
777
  let failedTurn = false;
778
+ let removeContextObserver;
740
779
  const questionReview = Boolean(input.ingressKey?.startsWith('question-review:'));
741
780
  let internalReview = false;
742
781
  let streamedDisplay = '';
@@ -816,12 +855,52 @@ class AgentOrchestrationRuntime {
816
855
  input = { ...input, metadata: replyMetadata, attachmentIds: [...new Set([...(input.attachmentIds ?? []), ...(replyMetadata?.repliedAttachmentIds ?? [])])] };
817
856
  const semantic = this.config.conversation.semanticIntake && !active.notification;
818
857
  const prepared = semantic ? this.intake.context(receipt.conversationId, input.scope.principalId, String(admitted.binding_id)) : undefined;
819
- const preparedInputs = prepared?.inputIds?.length ? this.store.all(`SELECT id,text,attachment_refs_json,ingress_json FROM conversation_inputs
820
- WHERE conversation_id=? AND principal_id=? AND id IN (SELECT value FROM json_each(?))`, receipt.conversationId, input.scope.principalId, JSON.stringify(prepared.inputIds)) : [];
858
+ const recoveryInputId = input.ingressKey?.startsWith('intake-recovery:') ? input.ingressKey.slice('intake-recovery:'.length) : undefined;
859
+ const preparedInputIds = [...new Set([...(prepared?.inputIds ?? []), ...(recoveryInputId ? [recoveryInputId] : [])])];
860
+ const preparedInputs = preparedInputIds.length ? this.store.all(`SELECT id,text,attachment_refs_json,ingress_json FROM conversation_inputs
861
+ 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)) : [];
821
862
  const preparedRefs = preparedInputs.flatMap(row => JSON.parse(String(row.attachment_refs_json)));
822
863
  if (preparedRefs.length)
823
864
  input = { ...input, attachmentIds: [...new Set([...(input.attachmentIds ?? []), ...preparedRefs])] };
824
- const visualInput = await (0, input_images_1.loadInputImages)((0, path_1.join)(this.agent.workspace, '../..'), this.agent.id, input.attachmentIds);
865
+ const cliSession = this.agent.type === 'app-agent'
866
+ ? await this.cliSessions.resolveContainer(sessionId, this.agent)
867
+ : this.cliSessions.resolve(sessionId, this.agent.workspace);
868
+ const contextPlan = this.contextDelivery.begin({ conversationId: receipt.conversationId,
869
+ principalId: input.scope.principalId, bindingId: String(admitted.binding_id),
870
+ cliSessionId: cliSession.id, resume: cliSession.resume });
871
+ const reusedImages = [];
872
+ const unreadImageRefs = input.attachmentIds?.filter(ref => {
873
+ const originalRef = contextPlan.imageReference(ref);
874
+ if (!originalRef)
875
+ return true;
876
+ reusedImages.push({ ref, originalRef });
877
+ return false;
878
+ });
879
+ const visualInput = await (0, input_images_1.loadInputImages)((0, path_1.join)(this.agent.workspace, '../..'), this.agent.id, unreadImageRefs);
880
+ // References are immutable ingress files. Also avoid sending identical image
881
+ // content twice when it arrived under different references.
882
+ const deliveredImages = visualInput.images.map((image, index) => ({ image, ref: visualInput.refs[index] }));
883
+ visualInput.images = [];
884
+ visualInput.refs = [];
885
+ for (const { image, ref } of deliveredImages) {
886
+ const originalRef = contextPlan.rememberImage(ref, (0, store_1.payloadHash)(image.source));
887
+ if (originalRef) {
888
+ reusedImages.push({ ref, originalRef });
889
+ continue;
890
+ }
891
+ visualInput.images.push(image);
892
+ visualInput.refs.push(ref);
893
+ }
894
+ // Immutable source IDs remain available to workers. Only the model delivery
895
+ // is incremental; full canonical text and attachment references are retained.
896
+ const freshPreparedInputs = preparedInputs.filter(row => row.id !== receipt.inputId && !contextPlan.includes('materials', String(row.id)));
897
+ for (const row of freshPreparedInputs)
898
+ contextPlan.mark('materials', String(row.id), true);
899
+ contextPlan.mark('materials', receipt.inputId, true);
900
+ const intakeValue = (value) => value ? { mode: value.mode, deferredDispatch: value.deferredDispatch,
901
+ preparation: value.preparation, inputIds: value.inputIds, task_id: value.task_id, resolution: value.resolution } : null;
902
+ const pendingState = intakeValue(prepared);
903
+ const pendingChanges = contextPlan.select('intake', [{ id: 'pending', value: pendingState }], row => row.id);
825
904
  if (!semantic && input.skill && input.modality !== 'live_voice' && !channelSpeech && !visualInput.images.length && !visualInput.unavailable.length) {
826
905
  const task = this.tasks.spawn({ ...capabilities, ...receipt, ...decision, principalId: input.scope.principalId,
827
906
  model: options.model ?? this.agent.claude.model, actionId: `skill:${receipt.inputId}` }, {
@@ -846,6 +925,15 @@ class AgentOrchestrationRuntime {
846
925
  const newerInputPending = () => !!this.store.get("SELECT id FROM conversation_inputs WHERE conversation_id=? AND principal_id=? AND binding_id=(SELECT binding_id FROM conversation_inputs WHERE id=?) AND status='accepted' AND input_seq>(SELECT input_seq FROM conversation_inputs WHERE id=?)", receipt.conversationId, input.scope.principalId, receipt.inputId, receipt.inputId);
847
926
  const intakeContext = { ...capabilities, ...receipt, ...decision, model: options.model ?? this.agent.claude.model, principalId: input.scope.principalId, actionId: `intake:${receipt.inputId}` };
848
927
  const deliverAcknowledgement = async (choice) => {
928
+ if (choice.mode === 'resolve') {
929
+ if (newerInputPending()) {
930
+ intakeDeferred = true;
931
+ return { deferred: true, reason: 'NEW_INPUT_PENDING' };
932
+ }
933
+ this.intake.choose(intakeContext, choice);
934
+ this.intake.consume(receipt.inputId, true);
935
+ return { resolved: true };
936
+ }
849
937
  if (acknowledgementId && (choice.mode !== intakeChoice?.mode || choice.task_id !== intakeChoice?.task_id))
850
938
  throw new types_2.OrchestrationError('INTAKE_ALREADY_ACKNOWLEDGED');
851
939
  if (acknowledgementReady)
@@ -853,6 +941,7 @@ class AgentOrchestrationRuntime {
853
941
  if (!acknowledgementId)
854
942
  intakeChoice = this.intake.choose(intakeContext, choice);
855
943
  if (choice.mode !== 'wait' && newerInputPending()) {
944
+ this.intake.deferDispatch(receipt.inputId);
856
945
  intakeDeferred = true;
857
946
  return { deferred: true, reason: 'NEW_INPUT_PENDING', instruction: 'New user input is already queued. End without another reply or task mutation; the next turn will receive these materials and the new instruction.' };
858
947
  }
@@ -926,8 +1015,8 @@ class AgentOrchestrationRuntime {
926
1015
  return (0, capabilities_1.readCapabilityPage)(await this.capabilityCatalog.snapshot(), this.host.skills?.(), args);
927
1016
  },
928
1017
  onIntake: semantic ? acknowledge : undefined,
929
- onMutationResult: semantic ? (actionId, committed, errorCode) => { taskActionResults.set(actionId, committed); const attempt = attemptedTaskActions.get(actionId); if (attempt)
930
- Object.assign(attempt, { committed, errorCode }); } : undefined,
1018
+ onMutationResult: (actionId, committed, errorCode) => { taskActionResults.set(actionId, committed); const attempt = attemptedTaskActions.get(actionId); if (attempt)
1019
+ Object.assign(attempt, { committed, errorCode }); },
931
1020
  beforeMutation: semantic ? async (tool, args, actionId) => {
932
1021
  if (actionId && !attemptedTaskActions.has(actionId))
933
1022
  attemptedTaskActions.set(actionId, { actionId, tool, args: JSON.parse(JSON.stringify(args)) });
@@ -938,17 +1027,23 @@ class AgentOrchestrationRuntime {
938
1027
  if (tool !== 'task_answer' && acknowledgementInFlight)
939
1028
  await acknowledgementInFlight;
940
1029
  if (intakeDeferred || newerInputPending()) {
1030
+ if (tool === 'task_spawn' || tool === 'task_update')
1031
+ this.intake.deferDispatch(receipt.inputId);
941
1032
  intakeDeferred = true;
942
1033
  throw new types_2.OrchestrationError('NEW_INPUT_PENDING');
943
1034
  }
944
1035
  if (tool === 'task_answer')
945
1036
  return;
946
- if (!intakeChoice || intakeChoice.mode === 'wait' || !acknowledgementReady)
1037
+ if (!intakeChoice || intakeChoice.mode === 'wait' || intakeChoice.mode === 'resolve' || !acknowledgementReady)
947
1038
  throw new types_2.OrchestrationError('ACKNOWLEDGEMENT_REQUIRED');
948
1039
  if (tool === 'task_spawn' && preparedInputs.length)
949
1040
  args.context_refs = [...new Set([...(Array.isArray(args.context_refs) ? args.context_refs : []), ...preparedRefs, ...preparedInputs.map(row => String(row.id))])];
950
- if (intakeChoice.mode === 'update' && (tool === 'task_spawn' || args.task_id !== intakeChoice.task_id))
1041
+ if (intakeChoice.mode === 'update' && (tool === 'task_spawn' || args.task_id !== intakeChoice.task_id)) {
1042
+ const attempt = attemptedTaskActions.get(actionId);
1043
+ if (attempt)
1044
+ attempt.intendedUpdateTaskId = intakeChoice.task_id;
951
1045
  throw new types_2.OrchestrationError('INTAKE_TASK_MISMATCH');
1046
+ }
952
1047
  } : undefined,
953
1048
  onTaskQueued: !semantic && speechEnabled && !active.notification ? spoken => {
954
1049
  if (taskSpeech)
@@ -997,8 +1092,9 @@ class AgentOrchestrationRuntime {
997
1092
  // those turns' byte-prefix diverge from every other turn in the same session, forcing a
998
1093
  // full cache-write every time such a turn interleaves with a differently shaped one.
999
1094
  // INTAKE_OVERLAY, the review directive and the speech directive below all live in the
1000
- // per-turn prompt instead that is new message content every turn regardless, so it was
1001
- // never part of the cached prefix and appending it there costs nothing extra.
1095
+ // per-turn prompt instead when their content changes. Stable intake/worker
1096
+ // instructions now live in the invariant system prefix below, rather than
1097
+ // accumulating another complete copy in each resumed user message.
1002
1098
  // The schema is resolved the other way round: ONE invariant union schema on every turn
1003
1099
  // shape, which is Anthropic's own remedy for mode switching (keep the tool set fixed,
1004
1100
  // convey the mode in message content). Verified against claude-code 2.1.274: --json-schema
@@ -1010,14 +1106,12 @@ class AgentOrchestrationRuntime {
1010
1106
  // produced, while speech and review turns fill the optional fields their per-turn overlay
1011
1107
  // asks for. This restores the structured-output guarantee without a per-turn tools diff.
1012
1108
  ticket.profile.responseSchema = response_schema_1.ORCHESTRATION_RESPONSE_SCHEMA;
1109
+ ticket.profile.overlay += `\n\n${CONTEXT_DELIVERY_INSTRUCTIONS}\n\nOnly when the current turn explicitly enables semantic intake, apply these rules:\n${conversation_intake_1.INTAKE_OVERLAY}`;
1013
1110
  // Continue the CLI session this agent session already has a transcript for. Each decision
1014
1111
  // turn is still its own process; resuming is what lets the next one reuse the previous
1015
1112
  // turn's cached prefix instead of paying a full cache write, and it replaces the flattened
1016
1113
  // history copy SessionProcess used to seed (see buildInitialPrompt). Container agents probe their transcript inside the validated container, never on the host.
1017
1114
  {
1018
- const cliSession = this.agent.type === 'app-agent'
1019
- ? await this.cliSessions.resolveContainer(sessionId, this.agent)
1020
- : this.cliSessions.resolve(sessionId, this.agent.workspace);
1021
1115
  ticket.profile.cliSession = { id: cliSession.id, resume: cliSession.resume };
1022
1116
  if (cliSession.resume && this.agent.type !== 'app-agent')
1023
1117
  transcriptCheckpoint = await (0, transcript_checkpoint_1.checkpointTranscript)((0, claude_settings_1.transcriptPath)(this.agent.workspace, cliSession.id));
@@ -1036,8 +1130,22 @@ class AgentOrchestrationRuntime {
1036
1130
  JOIN conversation_decisions d ON d.id=r.decision_id WHERE d.session_id=? AND r.state='failed'`, sessionId)
1037
1131
  .map(row => `response:${row.id}`);
1038
1132
  agentSession = await this.host.createAgentSession(sessionId, ticket.profile, options.model, input.scope);
1039
- const snapshots = this.tasks.context(receipt.conversationId, input.scope.principalId, decision.decisionId);
1040
- const committed = (0, decision_context_1.committedCommandContext)(this.store, receipt.conversationId);
1133
+ const reportingTasks = new Set(this.store.all("SELECT task_id FROM notifications WHERE decision_id=? AND status='assigned'", decision.decisionId).map(row => String(row.task_id)));
1134
+ const taskCandidates = this.tasks.context(receipt.conversationId, input.scope.principalId, decision.decisionId)
1135
+ .filter(task => !['completed', 'failed', 'cancelled'].includes(task.state) ||
1136
+ contextPlan.includes('tasks', task.taskId) || reportingTasks.has(task.taskId));
1137
+ const snapshots = contextPlan.select('tasks', taskCandidates, task => task.taskId);
1138
+ const committed = contextPlan.select('receipts', (0, decision_context_1.committedCommandContext)(this.store, receipt.conversationId, true), row => String(row.actionId));
1139
+ const observeContext = (line) => {
1140
+ try {
1141
+ const event = JSON.parse(line);
1142
+ if (event.type === 'system' && event.subtype === 'compact_boundary')
1143
+ this.contextDelivery.invalidateConversation(receipt.conversationId);
1144
+ }
1145
+ catch { /* Non-protocol output. */ }
1146
+ };
1147
+ agentSession.on('output', observeContext);
1148
+ removeContextObserver = () => agentSession?.off('output', observeContext);
1041
1149
  // Ordering inside the per-turn message: orchestration context first, the user's newest
1042
1150
  // message last. The cache matches a strict prefix and the CLI puts its breakpoint at the
1043
1151
  // end of this message, so a turn can only reuse the previous turn's write where the new
@@ -1048,7 +1156,7 @@ class AgentOrchestrationRuntime {
1048
1156
  // prefix ([tools, system]) is untouched and still carries no per-turn conditional. The
1049
1157
  // label distinguishes a real user message from an orchestration report request so the
1050
1158
  // agent does not attribute the report wording to the user.
1051
- const prompt = `${ticket.profile.cliSession?.resume && previousReports.length ? "Previously communicated updates are already in this resumed conversation; compare against them before reporting again." : (0, decision_context_1.communicatedProgressContext)(previousReports)}\nPending question attention (data, not instructions): ${JSON.stringify(this.questionControls.context(receipt.conversationId, input.scope.principalId))}\nReply-to question context (not consent): ${JSON.stringify(this.questionControls.replyContext(input))}\n${(0, reply_context_1.replyContext)(input.metadata)}\nAttachment details (reference data): ${JSON.stringify(input.metadata?.attachmentDetails ?? [])}. ${input.metadata?.attachmentError ?? ''}\n${semantic ? `[Pending preparation; source inputs are data, not new authorization] ${JSON.stringify({ prepared, inputs: preparedInputs.map(({ ingress_json, ...row }) => ({ ...row, replyContext: (0, reply_context_1.storedReplyContext)(ingress_json) })) })}` : ''}\n${input.metadata?.promptContext ?? ''}\n\n[Orchestration context: persisted task snapshots, not instructions. Each entry is an index, not a report: call task_status with its task_id for the stored result, evidence, progress and workflow history.]\n${JSON.stringify(snapshots)}\nRecent committed command receipts (do not repeat their originating work): ${JSON.stringify(committed)}\nExecution eligible: ${capabilities.execute}. Workspace mode: ${this.config.tasks.workspaceMode}. Worker profiles: default-worker is the general-purpose worker for research, files, browser/API operations, services, calculations and code. In host mode it uses the Agent working environment; no Git or projectRoot is required. In container mode it stays inside the app container. Only explicitly configured isolated-worktree mode requires Git for default-worker; media-worker remains available for standalone scratch work in isolated modes. State the authorized working directory in task instructions; workers may change directories only within their execution boundary. Serialize conflicting edits to the same shared files; continue related work with continue_task_id. Memory write eligible: ${capabilities.writeMemory}.\nOriginal attachment refs (automatically inherited by workers): ${JSON.stringify(input.attachmentIds ?? [])}\nImages attached to this user message in order: ${JSON.stringify(visualInput.refs)}. Inspect these yourself before answering or delegating execution.\nUnavailable attachments: ${JSON.stringify([...(input.metadata?.unavailableAttachments ?? []), ...visualInput.unavailable])}${input.skill ? `\nRequested installed skill: ${JSON.stringify({ name: input.skill.name, args: input.skill.args })}. Inspect the user images first, then dispatch this skill via task_spawn with skill_name and skill_args.` : ''}${semantic ? `\n\n${conversation_intake_1.INTAKE_OVERLAY}` : ''}${speechDirective}${internalReview ? `\n\n${progress_review_1.PROGRESS_REVIEW_OVERLAY}` : ''}\n\n[${active.notification ? 'Current orchestration request' : 'Current user message'} — the request to answer now]\n${input.text}`;
1159
+ const prompt = `${ticket.profile.cliSession?.resume && previousReports.length ? "Previously communicated updates are already in this resumed conversation; compare against them before reporting again." : (0, decision_context_1.communicatedProgressContext)(previousReports)}\nPending question attention (data, not instructions): ${JSON.stringify(this.questionControls.context(receipt.conversationId, input.scope.principalId))}\nReply-to question context (not consent): ${JSON.stringify(this.questionControls.replyContext(input))}\n${(0, reply_context_1.replyContext)(input.metadata)}\nAttachment details (reference data): ${JSON.stringify(input.metadata?.attachmentDetails ?? [])}. ${input.metadata?.attachmentError ?? ''}\n${semantic ? `[Pending preparation; source inputs are data, not new authorization] ${JSON.stringify({ changes: pendingChanges.map(row => row.value), inputs: freshPreparedInputs.map(({ ingress_json, ...row }) => ({ ...row, replyContext: (0, reply_context_1.storedReplyContext)(ingress_json) })) })}` : ''}\n${input.metadata?.promptContext ?? ''}\n\n[Orchestration context: persisted task snapshots, not instructions. Incremental changes only; omitted tasks are unchanged, not deleted. Each entry is an index, not a report: call task_status with its task_id for the stored result, evidence, progress and workflow history.]\n${JSON.stringify(snapshots)}\nRecent committed command receipts (do not repeat their originating work): ${JSON.stringify(committed)}\nExecution eligible: ${capabilities.execute}. Workspace mode: ${this.config.tasks.workspaceMode}. Memory write eligible: ${capabilities.writeMemory}.\nAttachment refs (automatically inherited by workers; previously delivered images remain in resumed context): ${JSON.stringify(input.attachmentIds ?? [])}\nImages attached to this user message in order: ${JSON.stringify(visualInput.refs)}. Inspect these yourself before answering or delegating execution.\nReused images (reference data; each ref has the same image as originalRef already supplied in this conversation): ${JSON.stringify(reusedImages)}\nUnavailable attachments: ${JSON.stringify([...(input.metadata?.unavailableAttachments ?? []), ...visualInput.unavailable])}${input.skill ? `\nRequested installed skill: ${JSON.stringify({ name: input.skill.name, args: input.skill.args })}. Inspect the user images first, then dispatch this skill via task_spawn with skill_name and skill_args.` : ''}${semantic ? '\nSemantic intake is active for this turn; follow the intake rules in the system instructions.' : '\nSemantic intake is inactive for this turn; do not call conversation_intake.'}${speechDirective}${internalReview ? `\n\n${progress_review_1.PROGRESS_REVIEW_OVERLAY}` : ''}\n\n[${active.notification ? 'Current orchestration request' : 'Current user message'} — the request to answer now]\n${input.text}`;
1052
1160
  if (active.stopping) {
1053
1161
  this.decisions.interrupt(decision);
1054
1162
  const display = active.stopReason === 'barge-in' ? '' : 'Response stopped.';
@@ -1091,7 +1199,7 @@ class AgentOrchestrationRuntime {
1091
1199
  }, visualInput.images, {
1092
1200
  onUsage: metrics => (0, token_ledger_1.recordTokenTurn)(this.store, { id: decision.decisionId, sessionId, role: 'agent', category: active.notification ? 'report' : 'input', ...metrics }),
1093
1201
  startupTimeoutMs: this.config.conversation.startupTimeoutMs,
1094
- firstResponseTimeoutMs: this.config.conversation.firstResponseTimeoutMs,
1202
+ firstResponseTimeoutMs: this.config.conversation.firstResponseTimeoutMs, compactionTimeoutMs: this.config.conversation.compactionTimeoutMs,
1095
1203
  idleTimeoutMs: this.config.conversation.idleTimeoutMs,
1096
1204
  }, chunk => {
1097
1205
  // StructuredOutput tool arguments are a separate JSON stream from commentary.
@@ -1103,6 +1211,22 @@ class AgentOrchestrationRuntime {
1103
1211
  });
1104
1212
  active.turn = turn;
1105
1213
  const response = await turn.result;
1214
+ if (!response.interrupted) {
1215
+ // A completed CLI turn has consumed its tool results. Failed/interrupted
1216
+ // turns never advance this checkpoint, so committed receipts can recover.
1217
+ for (const row of this.store.all("SELECT action_id,receipt_json FROM task_commands WHERE decision_id=?", decision.decisionId)) {
1218
+ if (!taskActionResults.get(String(row.action_id)))
1219
+ continue;
1220
+ const task = JSON.parse(String(row.receipt_json));
1221
+ // The worker may already have progressed beyond the returned receipt.
1222
+ // Only acknowledge the version actually returned to this tool call.
1223
+ if (typeof task?.taskId === 'string' && typeof task.stateVersion === 'number')
1224
+ contextPlan.mark('tasks', task.taskId, (0, service_1.taskIndexEntry)(task));
1225
+ }
1226
+ if (intakeChoice)
1227
+ contextPlan.mark('intake', 'pending', { id: 'pending', value: intakeValue(intakeChoice) });
1228
+ contextPlan.commit();
1229
+ }
1106
1230
  // Single conversion point from the raw turn text to the user-facing surfaces; every
1107
1231
  // downstream consumer (channels, web, dashboard, history, token accounting) reads the
1108
1232
  // result of this boundary, so the union schema stays invisible to them. Every turn is
@@ -1183,6 +1307,24 @@ class AgentOrchestrationRuntime {
1183
1307
  }
1184
1308
  if (semantic && intakeChoice?.mode !== 'wait' && (intakeChoice || display.trim()) && !intakeDeferred && !newerInputPending() && !response.interrupted)
1185
1309
  this.intake.consume(receipt.inputId);
1310
+ const pendingDispatch = semantic && this.intake.context(receipt.conversationId, input.scope.principalId, String(admitted.binding_id));
1311
+ if (pendingDispatch?.deferredDispatch && pendingDispatch.mode !== 'wait' && capabilities.execute &&
1312
+ !input.ingressKey?.startsWith('intake-recovery:') && !intakeDeferred && !newerInputPending() &&
1313
+ !response.interrupted && !active.stopping) {
1314
+ // One bounded reconciliation turn after a direct reply, never an automatic
1315
+ // replay of the rejected command. It sees current instructions and uses
1316
+ // the same principal, binding and execution permissions as this turn.
1317
+ try {
1318
+ this.store.acceptInput({ scope: input.scope, storeUserMessage: false,
1319
+ ingressKey: `intake-recovery:${receipt.inputId}`, capabilities, model: options.model,
1320
+ text: 'Reconcile the pending deferred dispatch with the latest user instructions. Earlier NEW_INPUT_PENDING was temporary. If still authorized, acknowledge and commit the appropriate task command now. If cancelled, replaced, or already satisfied, use conversation_intake mode=resolve with a concrete resolution. Do not merely repeat a promise or the earlier rejection. If a new decision is necessary, ask a specific question and preserve the pending work.' + '\nLatest user input ID: ' + receipt.inputId }, this.config.conversation.maxPendingInputs);
1321
+ }
1322
+ catch (error) {
1323
+ if (!(error instanceof types_2.OrchestrationError) || error.code !== 'QUEUE_FULL')
1324
+ throw error;
1325
+ // Keep the durable pending dispatch for the next input when admission is full.
1326
+ }
1327
+ }
1186
1328
  return silent ? acknowledgement : display || acknowledgement;
1187
1329
  }
1188
1330
  catch (error) {
@@ -1239,6 +1381,7 @@ class AgentOrchestrationRuntime {
1239
1381
  throw error;
1240
1382
  }
1241
1383
  finally {
1384
+ removeContextObserver?.();
1242
1385
  revoke?.();
1243
1386
  if (agentSession)
1244
1387
  await this.host.releaseAgentSession(sessionId, agentSession);