@botcord/daemon 0.2.75 → 0.2.77

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 (62) hide show
  1. package/dist/cloud-auth.d.ts +47 -0
  2. package/dist/cloud-auth.js +51 -0
  3. package/dist/cloud-daemon.d.ts +43 -0
  4. package/dist/cloud-daemon.js +252 -0
  5. package/dist/cloud-mode.d.ts +45 -0
  6. package/dist/cloud-mode.js +55 -0
  7. package/dist/cloud-settle.d.ts +81 -0
  8. package/dist/cloud-settle.js +100 -0
  9. package/dist/daemon-singleton.d.ts +26 -0
  10. package/dist/daemon-singleton.js +91 -0
  11. package/dist/daemon.d.ts +1 -1
  12. package/dist/daemon.js +15 -6
  13. package/dist/doctor.d.ts +4 -1
  14. package/dist/doctor.js +15 -4
  15. package/dist/gateway/channels/botcord.d.ts +1 -1
  16. package/dist/gateway/channels/botcord.js +280 -52
  17. package/dist/gateway/dispatcher.d.ts +34 -1
  18. package/dist/gateway/dispatcher.js +277 -20
  19. package/dist/gateway/gateway.d.ts +9 -1
  20. package/dist/gateway/gateway.js +4 -1
  21. package/dist/gateway/runtime-errors.d.ts +6 -0
  22. package/dist/gateway/runtime-errors.js +14 -0
  23. package/dist/gateway/runtimes/claude-code.d.ts +8 -0
  24. package/dist/gateway/runtimes/claude-code.js +92 -4
  25. package/dist/gateway/runtimes/deepseek-tui.js +19 -5
  26. package/dist/gateway/transcript.d.ts +1 -1
  27. package/dist/gateway/types.d.ts +33 -0
  28. package/dist/index.js +71 -80
  29. package/dist/provision.d.ts +2 -0
  30. package/dist/provision.js +39 -1
  31. package/dist/status-render.js +17 -0
  32. package/package.json +2 -2
  33. package/src/__tests__/cloud-auth.test.ts +42 -0
  34. package/src/__tests__/cloud-daemon.test.ts +237 -0
  35. package/src/__tests__/cloud-mode.test.ts +65 -0
  36. package/src/__tests__/cloud-settle.test.ts +287 -0
  37. package/src/__tests__/daemon-singleton.test.ts +89 -0
  38. package/src/__tests__/doctor.test.ts +34 -0
  39. package/src/__tests__/runtime-discovery.test.ts +90 -0
  40. package/src/__tests__/status-render.test.ts +34 -0
  41. package/src/cloud-auth.ts +78 -0
  42. package/src/cloud-daemon.ts +338 -0
  43. package/src/cloud-mode.ts +70 -0
  44. package/src/cloud-settle.ts +182 -0
  45. package/src/daemon-singleton.ts +122 -0
  46. package/src/daemon.ts +18 -5
  47. package/src/doctor.ts +18 -5
  48. package/src/gateway/__tests__/botcord-channel.test.ts +98 -0
  49. package/src/gateway/__tests__/claude-code-adapter.test.ts +101 -1
  50. package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +19 -0
  51. package/src/gateway/__tests__/dispatcher.test.ts +120 -0
  52. package/src/gateway/channels/botcord.ts +299 -43
  53. package/src/gateway/dispatcher.ts +354 -21
  54. package/src/gateway/gateway.ts +16 -1
  55. package/src/gateway/runtime-errors.ts +15 -0
  56. package/src/gateway/runtimes/claude-code.ts +98 -2
  57. package/src/gateway/runtimes/deepseek-tui.ts +23 -5
  58. package/src/gateway/transcript.ts +1 -1
  59. package/src/gateway/types.ts +34 -0
  60. package/src/index.ts +83 -74
  61. package/src/provision.ts +45 -1
  62. package/src/status-render.ts +24 -0
@@ -57,7 +57,12 @@ function defaultClientFactory(input) {
57
57
  function isOwnerTrust(msg) {
58
58
  if (msg.room_id?.startsWith(OWNER_CHAT_PREFIX))
59
59
  return true;
60
- if (msg.source_type === "dashboard_user_chat")
60
+ const sourceType = msg.source_type;
61
+ if (sourceType === "dashboard_user_chat")
62
+ return true;
63
+ // Cloud Agent run tasks are Hub-issued on the user's behalf, same
64
+ // trust posture as owner chat.
65
+ if (sourceType === "cloud_agent_run")
61
66
  return true;
62
67
  return false;
63
68
  }
@@ -89,11 +94,17 @@ function normalizeInbox(msg, options) {
89
94
  return null;
90
95
  // `message` is the normal conversational envelope; `contact_request` is
91
96
  // a lightweight inbound asking the agent to notify its owner (the
92
- // composer appends the notify-owner hint). All other envelope types
93
- // (notification, system, contact_added/removed, …) are still filtered
94
- // out here they belong in a separate push-notification path that
95
- // daemon does not yet implement.
96
- if (env.type !== "message" && env.type !== "contact_request")
97
+ // composer appends the notify-owner hint); `cloud_run` carries a
98
+ // Cloud Agent run task with embedded run_id + budget (the cloud
99
+ // daemon's runtime adapter reads them from `raw.envelope.payload.cloud_run`
100
+ // and reports usage back via /internal/cloud-agents/.../settle when the
101
+ // run completes). All other envelope types (notification, system,
102
+ // contact_added/removed, …) are still filtered out — they belong in
103
+ // a separate push-notification path that daemon does not yet implement.
104
+ const envType = env.type;
105
+ if (envType !== "message" &&
106
+ envType !== "contact_request" &&
107
+ envType !== "cloud_run")
97
108
  return null;
98
109
  if (!msg.room_id)
99
110
  return null;
@@ -104,7 +115,8 @@ function normalizeInbox(msg, options) {
104
115
  const text = ownerTrust ? rawText : sanitizeUntrustedContent(rawText);
105
116
  const isDm = msg.room_id.startsWith(DM_ROOM_PREFIX);
106
117
  const isOwnerChat = msg.room_id.startsWith(OWNER_CHAT_PREFIX);
107
- const senderKind = ownerTrust || msg.source_type === "dashboard_human_room" ? "user" : "agent";
118
+ const sourceType = msg.source_type;
119
+ const senderKind = ownerTrust || sourceType === "dashboard_human_room" ? "user" : "agent";
108
120
  const senderName = msg.source_user_name ?? undefined;
109
121
  const threadId = msg.topic_id ?? msg.topic ?? null;
110
122
  const streamable = isOwnerChat;
@@ -348,6 +360,7 @@ export function createBotCordChannel(options) {
348
360
  let ws = null;
349
361
  let reconnectTimer = null;
350
362
  let keepaliveTimer = null;
363
+ let pollTimer = null;
351
364
  let reconnectAttempt = 0;
352
365
  let connectionSeq = 0;
353
366
  let consecutiveAuthFailures = 0;
@@ -371,6 +384,10 @@ export function createBotCordChannel(options) {
371
384
  clearInterval(keepaliveTimer);
372
385
  keepaliveTimer = null;
373
386
  }
387
+ if (pollTimer) {
388
+ clearInterval(pollTimer);
389
+ pollTimer = null;
390
+ }
374
391
  }
375
392
  function markStatus(patch) {
376
393
  statusSnapshot = { ...statusSnapshot, ...patch };
@@ -574,6 +591,17 @@ export function createBotCordChannel(options) {
574
591
  });
575
592
  log.info("botcord ws authenticated", { agentId: msg.agent_id });
576
593
  void fireInbox("ws_auth_ok");
594
+ const pollIntervalMs = options.pollIntervalMs ?? 30_000;
595
+ if (pollTimer)
596
+ clearInterval(pollTimer);
597
+ if (pollIntervalMs > 0) {
598
+ pollTimer = setInterval(() => {
599
+ if (ws === socket && socket.readyState === WebSocket.OPEN) {
600
+ void fireInbox("poll_interval");
601
+ }
602
+ }, pollIntervalMs);
603
+ pollTimer.unref?.();
604
+ }
577
605
  if (keepaliveTimer)
578
606
  clearInterval(keepaliveTimer);
579
607
  keepaliveTimer = setInterval(() => {
@@ -835,55 +863,29 @@ function normalizeBlockForHub(block, seq) {
835
863
  return { kind: "assistant", seq, payload: { text } };
836
864
  }
837
865
  if (kind === "tool_use") {
838
- // Claude Code: assistant message w/ content[].type === "tool_use" {id,name,input}
839
- // Codex: item.started for command_execution, file_change, mcp_tool_call, web_search
840
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
841
- const tu = contents.find((c) => c?.type === "tool_use");
842
- if (tu) {
843
- payload.name = typeof tu.name === "string" ? tu.name : "tool";
844
- if (tu.input && typeof tu.input === "object")
845
- payload.params = tu.input;
846
- if (typeof tu.id === "string")
847
- payload.id = tu.id;
848
- }
849
- else if (raw?.item && typeof raw.item === "object") {
850
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
851
- const params = codexToolParams(raw.item);
852
- if (Object.keys(params).length > 0)
853
- payload.params = params;
854
- if (typeof raw.item.id === "string")
855
- payload.id = raw.item.id;
856
- if (typeof raw.item.status === "string")
857
- payload.status = raw.item.status;
866
+ // Claude Code, Codex, DeepSeek TUI, Kimi, and ACP all expose tool calls
867
+ // with slightly different field names. Preserve the real invocation input
868
+ // so the dashboard can show more than a bare "tool" label.
869
+ const call = extractToolCall(raw);
870
+ if (call) {
871
+ payload.name = call.name;
872
+ if (call.params !== undefined && !isEmptyRecord(call.params))
873
+ payload.params = call.params;
874
+ if (call.id)
875
+ payload.id = call.id;
876
+ if (call.status)
877
+ payload.status = call.status;
858
878
  }
859
879
  return { kind: "tool_call", seq, payload };
860
880
  }
861
881
  if (kind === "tool_result") {
862
- // Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
863
- // Codex: item.completed for command_execution, file_change, mcp_tool_call, web_search
864
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
865
- const tr = contents.find((c) => c?.type === "tool_result");
866
- if (tr) {
867
- let resultStr = "";
868
- if (typeof tr.content === "string") {
869
- resultStr = tr.content;
870
- }
871
- else if (Array.isArray(tr.content)) {
872
- resultStr = tr.content
873
- .map((c) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
874
- .join("\n");
875
- }
876
- payload.result = resultStr;
877
- if (typeof tr.tool_use_id === "string")
878
- payload.tool_use_id = tr.tool_use_id;
879
- }
880
- else if (raw?.item && typeof raw.item === "object") {
881
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
882
- if (typeof raw.item.id === "string")
883
- payload.tool_use_id = raw.item.id;
884
- const result = codexToolResult(raw.item);
885
- if (result)
886
- payload.result = result;
882
+ const result = extractToolResult(raw);
883
+ if (result) {
884
+ if (result.name)
885
+ payload.name = result.name;
886
+ payload.result = result.result;
887
+ if (result.id)
888
+ payload.tool_use_id = result.id;
887
889
  }
888
890
  return { kind: "tool_result", seq, payload };
889
891
  }
@@ -912,6 +914,15 @@ function normalizeBlockForHub(block, seq) {
912
914
  return { kind: "thinking", seq, payload };
913
915
  }
914
916
  // "other" — e.g. Claude Code `type:"result"` end-of-turn summary.
917
+ if (isTerminalRuntimeBlock(raw)) {
918
+ payload.terminal = true;
919
+ payload.details = formatBlockDetails(raw);
920
+ const event = typeof raw?.event === "string" ? raw.event : undefined;
921
+ const embedded = typeof raw?.payload?.event === "string" ? raw.payload.event : undefined;
922
+ if (event || embedded)
923
+ payload.event = event ?? embedded;
924
+ return { kind: "other", seq, payload };
925
+ }
915
926
  if (raw?.type === "result") {
916
927
  if (typeof raw.result === "string")
917
928
  payload.text = raw.result;
@@ -922,6 +933,177 @@ function normalizeBlockForHub(block, seq) {
922
933
  }
923
934
  return { kind: "other", seq, payload };
924
935
  }
936
+ function isTerminalRuntimeBlock(raw) {
937
+ const event = typeof raw?.event === "string" ? raw.event : undefined;
938
+ const embedded = typeof raw?.payload?.event === "string" ? raw.payload.event : undefined;
939
+ const terminal = event ?? embedded;
940
+ return (terminal === "turn.completed" ||
941
+ terminal === "turn.finished" ||
942
+ terminal === "turn.done" ||
943
+ terminal === "done");
944
+ }
945
+ function extractToolCall(raw) {
946
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
947
+ const tu = contents.find((c) => c?.type === "tool_use");
948
+ if (tu) {
949
+ return {
950
+ name: stringField(tu, "name") ?? "tool",
951
+ params: parseMaybeJson(tu.input ?? tu.arguments),
952
+ id: stringField(tu, "id"),
953
+ };
954
+ }
955
+ const deepseek = extractDeepseekToolCall(raw);
956
+ if (deepseek)
957
+ return deepseek;
958
+ const item = raw?.item;
959
+ if (item && typeof item === "object") {
960
+ const params = codexToolParams(item);
961
+ return {
962
+ name: stringField(item, "type") ?? stringField(item, "name") ?? "tool",
963
+ params,
964
+ id: stringField(item, "id"),
965
+ status: stringField(item, "status"),
966
+ };
967
+ }
968
+ const toolCalls = Array.isArray(raw?.tool_calls) ? raw.tool_calls : [];
969
+ const toolCall = toolCalls.find((t) => t && typeof t === "object");
970
+ if (toolCall) {
971
+ const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
972
+ return {
973
+ name: stringField(fn, "name") ?? stringField(toolCall, "name") ?? "tool",
974
+ params: parseMaybeJson(fn?.arguments ?? toolCall.arguments ?? toolCall.input ?? toolCall.rawInput),
975
+ id: stringField(toolCall, "id"),
976
+ };
977
+ }
978
+ const update = raw?.params?.update ?? raw?.update;
979
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
980
+ if (acpTool && typeof acpTool === "object") {
981
+ return {
982
+ name: stringField(acpTool, "name") ?? stringField(update, "name") ?? "tool",
983
+ params: parseMaybeJson(acpTool.rawInput ??
984
+ acpTool.raw_input ??
985
+ acpTool.input ??
986
+ acpTool.arguments ??
987
+ acpTool.args ??
988
+ acpTool.params) ?? acpTool,
989
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
990
+ };
991
+ }
992
+ return null;
993
+ }
994
+ function extractToolResult(raw) {
995
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
996
+ const tr = contents.find((c) => c?.type === "tool_result");
997
+ if (tr) {
998
+ return {
999
+ result: stringifyToolResult(tr.content),
1000
+ id: stringField(tr, "tool_use_id"),
1001
+ };
1002
+ }
1003
+ const deepseek = extractDeepseekToolResult(raw);
1004
+ if (deepseek)
1005
+ return deepseek;
1006
+ const item = raw?.item;
1007
+ if (item && typeof item === "object") {
1008
+ const result = codexToolResult(item);
1009
+ return {
1010
+ name: stringField(item, "type") ?? stringField(item, "name"),
1011
+ result: result || stringifyToolResult(item),
1012
+ id: stringField(item, "id"),
1013
+ };
1014
+ }
1015
+ if (raw?.role === "tool") {
1016
+ return {
1017
+ result: stringifyToolResult(raw.content),
1018
+ id: stringField(raw, "tool_call_id"),
1019
+ };
1020
+ }
1021
+ const update = raw?.params?.update ?? raw?.update;
1022
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
1023
+ if (acpTool && typeof acpTool === "object") {
1024
+ const result = acpTool.output ??
1025
+ acpTool.result ??
1026
+ acpTool.content ??
1027
+ acpTool.error ??
1028
+ update.content ??
1029
+ update;
1030
+ return {
1031
+ name: stringField(acpTool, "name") ?? stringField(update, "name"),
1032
+ result: stringifyToolResult(result),
1033
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
1034
+ };
1035
+ }
1036
+ return null;
1037
+ }
1038
+ function extractDeepseekToolCall(raw) {
1039
+ const payload = raw?.payload;
1040
+ if (!payload || typeof payload !== "object")
1041
+ return null;
1042
+ if (raw?.event === "tool.started") {
1043
+ const tool = payload.tool && typeof payload.tool === "object" ? payload.tool : undefined;
1044
+ return {
1045
+ name: stringField(payload, "name") ?? stringField(tool, "name") ?? "tool",
1046
+ params: parseMaybeJson(payload.input ?? payload.arguments ?? payload.params ?? tool?.input ?? tool?.rawInput),
1047
+ id: stringField(payload, "id") ?? stringField(tool, "id"),
1048
+ status: stringField(payload, "status") ?? stringField(tool, "status"),
1049
+ };
1050
+ }
1051
+ if (payload.event === "item.started") {
1052
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1053
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1054
+ const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
1055
+ return {
1056
+ name: stringField(tool, "name") ??
1057
+ stringField(inner, "name") ??
1058
+ stringField(item, "name") ??
1059
+ stringField(item, "type") ??
1060
+ "tool",
1061
+ params: parseMaybeJson(tool?.input ??
1062
+ tool?.rawInput ??
1063
+ tool?.arguments ??
1064
+ inner.input ??
1065
+ item?.input ??
1066
+ item?.arguments) ?? tool ?? item,
1067
+ id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
1068
+ status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
1069
+ };
1070
+ }
1071
+ return null;
1072
+ }
1073
+ function extractDeepseekToolResult(raw) {
1074
+ const payload = raw?.payload;
1075
+ if (!payload || typeof payload !== "object")
1076
+ return null;
1077
+ if (raw?.event === "tool.completed") {
1078
+ const result = payload.output ?? payload.result ?? payload.content ?? payload.error ?? payload;
1079
+ return {
1080
+ name: stringField(payload, "name"),
1081
+ result: stringifyToolResult(result),
1082
+ id: stringField(payload, "id"),
1083
+ };
1084
+ }
1085
+ if (payload.event === "item.completed" || payload.event === "item.failed") {
1086
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1087
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1088
+ const result = item?.output ??
1089
+ item?.result ??
1090
+ item?.content ??
1091
+ item?.detail ??
1092
+ item?.summary ??
1093
+ item?.error ??
1094
+ inner.output ??
1095
+ inner.result ??
1096
+ inner.error ??
1097
+ item ??
1098
+ inner;
1099
+ return {
1100
+ name: stringField(item, "name") ?? stringField(item, "type") ?? stringField(inner, "name"),
1101
+ result: stringifyToolResult(result),
1102
+ id: stringField(item, "id") ?? stringField(inner, "id"),
1103
+ };
1104
+ }
1105
+ return null;
1106
+ }
925
1107
  function formatBlockDetails(raw) {
926
1108
  if (!raw || typeof raw !== "object")
927
1109
  return "";
@@ -991,6 +1173,52 @@ function codexToolResult(item) {
991
1173
  }
992
1174
  return parts.join("\n");
993
1175
  }
1176
+ function stringifyToolResult(value) {
1177
+ if (value == null)
1178
+ return "";
1179
+ if (typeof value === "string")
1180
+ return value;
1181
+ if (Array.isArray(value)) {
1182
+ return value
1183
+ .map((c) => {
1184
+ if (typeof c === "string")
1185
+ return c;
1186
+ if (typeof c?.text === "string")
1187
+ return c.text;
1188
+ return stringifyToolResult(c);
1189
+ })
1190
+ .filter(Boolean)
1191
+ .join("\n");
1192
+ }
1193
+ try {
1194
+ return JSON.stringify(value, null, 2);
1195
+ }
1196
+ catch {
1197
+ return String(value);
1198
+ }
1199
+ }
1200
+ function parseMaybeJson(value) {
1201
+ if (typeof value !== "string")
1202
+ return value;
1203
+ const trimmed = value.trim();
1204
+ if (!trimmed)
1205
+ return value;
1206
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
1207
+ return value;
1208
+ try {
1209
+ return JSON.parse(trimmed);
1210
+ }
1211
+ catch {
1212
+ return value;
1213
+ }
1214
+ }
1215
+ function isEmptyRecord(value) {
1216
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1217
+ }
1218
+ function stringField(obj, key) {
1219
+ const value = obj?.[key];
1220
+ return typeof value === "string" && value.length > 0 ? value : undefined;
1221
+ }
994
1222
  function extractContentText(content) {
995
1223
  if (!content)
996
1224
  return "";
@@ -1,7 +1,7 @@
1
1
  import type { GatewayLogger } from "./log.js";
2
2
  import { type SessionStore } from "./session-store.js";
3
3
  import { type TranscriptWriter } from "./transcript.js";
4
- import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
4
+ import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, RuntimeRunResult, RuntimeCircuitBreakerSnapshot, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
5
5
  /** Factory signature for building a runtime adapter at turn dispatch time. */
6
6
  export type RuntimeFactory = (runtimeId: string, extraArgs?: string[]) => RuntimeAdapter;
7
7
  /** Constructor options for `Dispatcher`. */
@@ -12,6 +12,8 @@ export interface DispatcherOptions {
12
12
  sessionStore: SessionStore;
13
13
  log: GatewayLogger;
14
14
  turnTimeoutMs?: number;
15
+ runtimeAuthFailureThreshold?: number;
16
+ runtimeAuthFailureCooldownMs?: number;
15
17
  /**
16
18
  * Live reference to the Gateway's managed-route map. Dispatcher reads
17
19
  * `values()` on every `resolveRoute` call so hot-add/remove take effect
@@ -50,6 +52,24 @@ export interface DispatcherOptions {
50
52
  * and suppressed so observer failures never break the turn.
51
53
  */
52
54
  onOutbound?: OutboundObserver;
55
+ onRuntimeCircuitBreakerChange?: () => void;
56
+ /**
57
+ * Optional observer fired exactly once per turn after ``runtime.run``
58
+ * resolves (or throws / times out). Receives the inbound message, the
59
+ * raw runtime result (may be undefined on throw), the elapsed wall
60
+ * time in milliseconds, and any thrown error. The cloud daemon hooks
61
+ * this to settle ``cloud_run`` envelopes against the Hub's usage
62
+ * ledger; local daemons leave it unset.
63
+ *
64
+ * Errors thrown by the observer are logged and swallowed — settle
65
+ * failures must never break the agent reply path.
66
+ */
67
+ onTurnComplete?: (event: {
68
+ message: GatewayInboundMessage;
69
+ result?: RuntimeRunResult;
70
+ wallTimeMs: number;
71
+ error?: unknown;
72
+ }) => Promise<void> | void;
53
73
  /**
54
74
  * Optional attention gate (PR3, design §4.2). Resolved AFTER `onInbound`
55
75
  * runs and BEFORE the runtime turn enqueues, so working memory / activity
@@ -92,10 +112,14 @@ export declare class Dispatcher {
92
112
  private readonly sessionStore;
93
113
  private readonly log;
94
114
  private readonly turnTimeoutMs;
115
+ private readonly runtimeAuthFailureThreshold;
116
+ private readonly runtimeAuthFailureCooldownMs;
95
117
  private readonly buildSystemContext?;
96
118
  private readonly buildMemoryContext?;
97
119
  private readonly onInbound?;
98
120
  private readonly onOutbound?;
121
+ private readonly onTurnComplete?;
122
+ private readonly onRuntimeCircuitBreakerChange?;
99
123
  private readonly composeUserTurn?;
100
124
  private readonly managedRoutes?;
101
125
  private readonly attentionGate?;
@@ -103,6 +127,7 @@ export declare class Dispatcher {
103
127
  private readonly transcript;
104
128
  private readonly queues;
105
129
  private readonly deferredMultimodal;
130
+ private readonly runtimeAuthFailures;
106
131
  /**
107
132
  * Last `/hub/typing` ping timestamp per (accountId, conversationId).
108
133
  * Used to debounce cancel-previous bursts so we don't trip Hub's 20/min
@@ -114,10 +139,18 @@ export declare class Dispatcher {
114
139
  handle(envelope: GatewayInboundEnvelope): Promise<void>;
115
140
  /** Snapshot of currently running turns keyed by queue key. */
116
141
  turns(): Record<string, TurnStatusSnapshot>;
142
+ runtimeCircuitBreakers(): Record<string, RuntimeCircuitBreakerSnapshot>;
117
143
  private safeAck;
118
144
  private getQueue;
119
145
  private deferMultimodal;
120
146
  private takeDeferredMultimodal;
147
+ private runtimeAuthBreakerKey;
148
+ private openRuntimeAuthBreaker;
149
+ private pruneExpiredRuntimeAuthBreakers;
150
+ private recordRuntimeAuthFailure;
151
+ private clearRuntimeAuthFailures;
152
+ private notifyRuntimeCircuitBreakerChange;
153
+ private skipRuntimeForAuthBreaker;
121
154
  private runCancelPrevious;
122
155
  /**
123
156
  * Serial mode with coalesce-on-drain semantics: