@dotsetlabs/dotclaw 2.4.0 → 2.6.0

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/.env.example +9 -10
  2. package/README.md +8 -4
  3. package/config-examples/runtime.json +34 -8
  4. package/config-examples/tool-policy.json +12 -2
  5. package/container/agent-runner/package-lock.json +2 -2
  6. package/container/agent-runner/package.json +1 -1
  7. package/container/agent-runner/src/agent-config.ts +19 -3
  8. package/container/agent-runner/src/container-protocol.ts +11 -0
  9. package/container/agent-runner/src/context-overflow-recovery.ts +39 -0
  10. package/container/agent-runner/src/index.ts +603 -165
  11. package/container/agent-runner/src/openrouter-input.ts +159 -0
  12. package/container/agent-runner/src/system-prompt.ts +13 -3
  13. package/container/agent-runner/src/tool-loop-policy.ts +741 -0
  14. package/container/agent-runner/src/tools.ts +211 -8
  15. package/dist/agent-context.d.ts +1 -0
  16. package/dist/agent-context.d.ts.map +1 -1
  17. package/dist/agent-context.js +21 -9
  18. package/dist/agent-context.js.map +1 -1
  19. package/dist/agent-execution.d.ts +2 -0
  20. package/dist/agent-execution.d.ts.map +1 -1
  21. package/dist/agent-execution.js +164 -15
  22. package/dist/agent-execution.js.map +1 -1
  23. package/dist/agent-semaphore.d.ts +24 -1
  24. package/dist/agent-semaphore.d.ts.map +1 -1
  25. package/dist/agent-semaphore.js +109 -20
  26. package/dist/agent-semaphore.js.map +1 -1
  27. package/dist/cli.js +3 -11
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config.d.ts +2 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +2 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/container-protocol.d.ts +22 -0
  34. package/dist/container-protocol.d.ts.map +1 -1
  35. package/dist/container-protocol.js.map +1 -1
  36. package/dist/container-runner.d.ts +7 -0
  37. package/dist/container-runner.d.ts.map +1 -1
  38. package/dist/container-runner.js +417 -143
  39. package/dist/container-runner.js.map +1 -1
  40. package/dist/db.d.ts.map +1 -1
  41. package/dist/db.js +46 -12
  42. package/dist/db.js.map +1 -1
  43. package/dist/error-messages.d.ts.map +1 -1
  44. package/dist/error-messages.js +18 -4
  45. package/dist/error-messages.js.map +1 -1
  46. package/dist/failover-policy.d.ts +41 -0
  47. package/dist/failover-policy.d.ts.map +1 -0
  48. package/dist/failover-policy.js +261 -0
  49. package/dist/failover-policy.js.map +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/ipc-dispatcher.d.ts.map +1 -1
  53. package/dist/ipc-dispatcher.js +27 -43
  54. package/dist/ipc-dispatcher.js.map +1 -1
  55. package/dist/mcp-config.d.ts +22 -0
  56. package/dist/mcp-config.d.ts.map +1 -0
  57. package/dist/mcp-config.js +94 -0
  58. package/dist/mcp-config.js.map +1 -0
  59. package/dist/memory-backend.d.ts +27 -0
  60. package/dist/memory-backend.d.ts.map +1 -0
  61. package/dist/memory-backend.js +112 -0
  62. package/dist/memory-backend.js.map +1 -0
  63. package/dist/memory-recall.d.ts.map +1 -1
  64. package/dist/memory-recall.js +135 -22
  65. package/dist/memory-recall.js.map +1 -1
  66. package/dist/memory-store.d.ts +1 -0
  67. package/dist/memory-store.d.ts.map +1 -1
  68. package/dist/memory-store.js +55 -7
  69. package/dist/memory-store.js.map +1 -1
  70. package/dist/message-pipeline.d.ts +24 -0
  71. package/dist/message-pipeline.d.ts.map +1 -1
  72. package/dist/message-pipeline.js +131 -27
  73. package/dist/message-pipeline.js.map +1 -1
  74. package/dist/metrics.d.ts +1 -0
  75. package/dist/metrics.d.ts.map +1 -1
  76. package/dist/metrics.js +9 -0
  77. package/dist/metrics.js.map +1 -1
  78. package/dist/providers/discord/discord-provider.d.ts.map +1 -1
  79. package/dist/providers/discord/discord-provider.js +72 -4
  80. package/dist/providers/discord/discord-provider.js.map +1 -1
  81. package/dist/providers/telegram/telegram-provider.d.ts.map +1 -1
  82. package/dist/providers/telegram/telegram-provider.js +65 -3
  83. package/dist/providers/telegram/telegram-provider.js.map +1 -1
  84. package/dist/recall-policy.d.ts +12 -0
  85. package/dist/recall-policy.d.ts.map +1 -0
  86. package/dist/recall-policy.js +89 -0
  87. package/dist/recall-policy.js.map +1 -0
  88. package/dist/runtime-config.d.ts +33 -0
  89. package/dist/runtime-config.d.ts.map +1 -1
  90. package/dist/runtime-config.js +109 -9
  91. package/dist/runtime-config.js.map +1 -1
  92. package/dist/streaming.d.ts.map +1 -1
  93. package/dist/streaming.js +125 -33
  94. package/dist/streaming.js.map +1 -1
  95. package/dist/task-scheduler.d.ts.map +1 -1
  96. package/dist/task-scheduler.js +4 -2
  97. package/dist/task-scheduler.js.map +1 -1
  98. package/dist/tool-policy.d.ts.map +1 -1
  99. package/dist/tool-policy.js +26 -4
  100. package/dist/tool-policy.js.map +1 -1
  101. package/dist/trace-writer.d.ts +12 -0
  102. package/dist/trace-writer.d.ts.map +1 -1
  103. package/dist/trace-writer.js.map +1 -1
  104. package/dist/turn-hygiene.d.ts +14 -0
  105. package/dist/turn-hygiene.d.ts.map +1 -0
  106. package/dist/turn-hygiene.js +214 -0
  107. package/dist/turn-hygiene.js.map +1 -0
  108. package/dist/webhook.d.ts.map +1 -1
  109. package/dist/webhook.js +1 -0
  110. package/dist/webhook.js.map +1 -1
  111. package/package.json +15 -1
  112. package/scripts/benchmark-baseline.js +365 -0
  113. package/scripts/benchmark-harness.js +1413 -0
  114. package/scripts/benchmark-scenarios.js +301 -0
  115. package/scripts/canary-suite.js +123 -0
  116. package/scripts/generate-controlled-traces.js +230 -0
  117. package/scripts/release-slo-check.js +214 -0
  118. package/scripts/run-live-canary.js +339 -0
@@ -180,6 +180,22 @@ export type ToolPolicy = {
180
180
  default_max_per_run?: number;
181
181
  };
182
182
 
183
+ function applyPolicyToolFilter(tools: Tool[], policy?: ToolPolicy): Tool[] {
184
+ if (!policy) return tools;
185
+ const allowSet = Array.isArray(policy.allow)
186
+ ? new Set(policy.allow.map(name => String(name || '').trim().toLowerCase()).filter(Boolean))
187
+ : null;
188
+ const denySet = new Set((policy.deny || []).map(name => String(name || '').trim().toLowerCase()).filter(Boolean));
189
+ return tools.filter((entry) => {
190
+ const name = entry?.function?.name;
191
+ if (!name) return false;
192
+ const normalized = name.toLowerCase();
193
+ if (denySet.has(normalized)) return false;
194
+ if (allowSet && !allowSet.has(normalized)) return false;
195
+ return true;
196
+ });
197
+ }
198
+
183
199
  const TOOL_OUTPUT_PREVIEW_BYTES = 6000;
184
200
 
185
201
  function getAllowedRoots(isMain: boolean): string[] {
@@ -449,7 +465,188 @@ async function assertUrlAllowed(params: {
449
465
  }
450
466
  }
451
467
 
468
+ function parseLooseJson(value: string): unknown {
469
+ let parsed: unknown = value;
470
+ for (let i = 0; i < 2 && typeof parsed === 'string'; i += 1) {
471
+ parsed = JSON.parse(parsed);
472
+ }
473
+ return parsed;
474
+ }
475
+
476
+ function toPositiveInt(value: unknown): number | undefined {
477
+ if (typeof value === 'number' && Number.isFinite(value)) {
478
+ const rounded = Math.floor(value);
479
+ return rounded > 0 ? rounded : undefined;
480
+ }
481
+ if (typeof value === 'string') {
482
+ const parsed = Number(value.trim());
483
+ if (!Number.isFinite(parsed)) return undefined;
484
+ const rounded = Math.floor(parsed);
485
+ return rounded > 0 ? rounded : undefined;
486
+ }
487
+ return undefined;
488
+ }
489
+
490
+ function toNonNegativeInt(value: unknown): number | undefined {
491
+ if (typeof value === 'number' && Number.isFinite(value)) {
492
+ const rounded = Math.floor(value);
493
+ return rounded >= 0 ? rounded : undefined;
494
+ }
495
+ if (typeof value === 'string') {
496
+ const parsed = Number(value.trim());
497
+ if (!Number.isFinite(parsed)) return undefined;
498
+ const rounded = Math.floor(parsed);
499
+ return rounded >= 0 ? rounded : undefined;
500
+ }
501
+ return undefined;
502
+ }
503
+
504
+ function setAlias(record: Record<string, unknown>, canonical: string, aliases: string[]): void {
505
+ const existing = record[canonical];
506
+ if (typeof existing === 'string' && existing.trim()) return;
507
+ for (const alias of aliases) {
508
+ const value = record[alias];
509
+ if (typeof value === 'string' && value.trim()) {
510
+ record[canonical] = value;
511
+ return;
512
+ }
513
+ }
514
+ }
515
+
516
+ function sanitizeNumericField(
517
+ record: Record<string, unknown>,
518
+ field: string,
519
+ mode: 'positive' | 'nonnegative'
520
+ ): void {
521
+ if (!(field in record)) return;
522
+ const coerced = mode === 'positive'
523
+ ? toPositiveInt(record[field])
524
+ : toNonNegativeInt(record[field]);
525
+ if (coerced === undefined) {
526
+ delete record[field];
527
+ return;
528
+ }
529
+ record[field] = coerced;
530
+ }
531
+
532
+ function normalizeToolArgs(name: string, args: unknown): unknown {
533
+ const normalizedName = name.trim().toLowerCase();
534
+ let value = args;
535
+
536
+ if (typeof value === 'string') {
537
+ const trimmed = value.trim();
538
+ if (!trimmed) return value;
539
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"')) {
540
+ try {
541
+ value = parseLooseJson(trimmed);
542
+ } catch {
543
+ // Keep the original string if JSON is malformed/truncated.
544
+ }
545
+ }
546
+ if (typeof value === 'string') {
547
+ switch (normalizedName) {
548
+ case 'bash':
549
+ case 'process':
550
+ return { command: value };
551
+ case 'python':
552
+ return { code: value };
553
+ case 'webfetch':
554
+ return { url: value.trim() };
555
+ case 'websearch':
556
+ return { query: value };
557
+ case 'read':
558
+ return { path: value };
559
+ case 'glob':
560
+ return { pattern: value };
561
+ case 'grep':
562
+ return { pattern: value };
563
+ default:
564
+ return value;
565
+ }
566
+ }
567
+ }
568
+
569
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
570
+ return value;
571
+ }
572
+
573
+ const record = { ...(value as Record<string, unknown>) };
574
+ const pathAliases = ['file', 'filePath', 'filepath', 'file_path', 'targetPath', 'target_path'];
575
+
576
+ switch (normalizedName) {
577
+ case 'read':
578
+ case 'write':
579
+ case 'edit':
580
+ case 'analyzeimage':
581
+ setAlias(record, 'path', pathAliases);
582
+ break;
583
+ case 'bash':
584
+ case 'process':
585
+ setAlias(record, 'command', ['cmd', 'shell', 'script', 'input']);
586
+ break;
587
+ case 'python':
588
+ setAlias(record, 'code', ['script', 'input', 'content', 'text']);
589
+ break;
590
+ case 'glob':
591
+ setAlias(record, 'pattern', ['glob', 'path']);
592
+ break;
593
+ case 'grep':
594
+ setAlias(record, 'pattern', ['query', 'search', 'text']);
595
+ setAlias(record, 'path', pathAliases);
596
+ break;
597
+ case 'websearch':
598
+ setAlias(record, 'query', ['q', 'search', 'text', 'input']);
599
+ break;
600
+ case 'webfetch':
601
+ setAlias(record, 'url', ['uri', 'href', 'link']);
602
+ break;
603
+ case 'sendfile':
604
+ case 'sendphoto':
605
+ case 'sendvoice':
606
+ case 'sendaudio':
607
+ setAlias(record, 'path', pathAliases);
608
+ break;
609
+ default:
610
+ break;
611
+ }
612
+
613
+ if (normalizedName === 'write') {
614
+ setAlias(record, 'content', ['text', 'body', 'data', 'value']);
615
+ }
616
+
617
+ if (normalizedName === 'python' && typeof record.code !== 'string' && record.code !== undefined) {
618
+ record.code = String(record.code);
619
+ }
620
+ if ((normalizedName === 'bash' || normalizedName === 'process') && typeof record.command !== 'string' && record.command !== undefined) {
621
+ record.command = String(record.command);
622
+ }
623
+ if (normalizedName === 'write' && typeof record.content !== 'string' && record.content !== undefined) {
624
+ record.content = String(record.content);
625
+ }
626
+
627
+ const positiveFields = [
628
+ 'maxBytes', 'maxResults', 'timeoutMs', 'count', 'depth', 'duration', 'reply_to_message_id', 'message_id', 'limit'
629
+ ];
630
+ for (const field of positiveFields) {
631
+ sanitizeNumericField(record, field, 'positive');
632
+ }
633
+ sanitizeNumericField(record, 'offset', 'nonnegative');
634
+
635
+ return record;
636
+ }
637
+
452
638
  function sanitizeToolArgs(args: unknown): unknown {
639
+ if (typeof args === 'string') {
640
+ const trimmed = args.trim();
641
+ if ((trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"')) && trimmed.length <= 64_000) {
642
+ try {
643
+ return sanitizeToolArgs(parseLooseJson(trimmed));
644
+ } catch {
645
+ // Fall through to length-limited string handling.
646
+ }
647
+ }
648
+ return args.length > 512 ? `${args.slice(0, 512)}…` : args;
649
+ }
453
650
  if (!args || typeof args !== 'object') return args;
454
651
  const record = { ...(args as Record<string, unknown>) };
455
652
 
@@ -713,13 +910,14 @@ async function runCommand(command: string, timeoutMs: number, outputLimit: numbe
713
910
  }
714
911
 
715
912
  async function readFileSafe(filePath: string, maxBytes: number) {
913
+ const safeMaxBytes = Math.max(1, Math.floor(Number.isFinite(maxBytes) ? maxBytes : 1));
716
914
  const stat = fs.statSync(filePath);
717
- if (stat.size <= maxBytes) {
915
+ if (stat.size <= safeMaxBytes) {
718
916
  return { content: fs.readFileSync(filePath, 'utf-8'), truncated: false, size: stat.size };
719
917
  }
720
918
  const fd = fs.openSync(filePath, 'r');
721
- const buffer = Buffer.allocUnsafe(maxBytes);
722
- const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
919
+ const buffer = Buffer.allocUnsafe(safeMaxBytes);
920
+ const bytesRead = fs.readSync(fd, buffer, 0, safeMaxBytes, 0);
723
921
  fs.closeSync(fd);
724
922
  return { content: buffer.subarray(0, bytesRead).toString('utf-8'), truncated: true, size: stat.size };
725
923
  }
@@ -972,6 +1170,7 @@ export function createTools(
972
1170
  return async (args: TInput): Promise<TOutput> => {
973
1171
  const start = Date.now();
974
1172
  const normalizedName = name.toLowerCase();
1173
+ const normalizedArgs = normalizeToolArgs(name, args) as TInput;
975
1174
  try {
976
1175
  if (denyList.includes(normalizedName)) {
977
1176
  throw new Error(`Tool is disabled by policy: ${name}`);
@@ -986,7 +1185,7 @@ export function createTools(
986
1185
  }
987
1186
  usageCounts.set(name, currentCount + 1);
988
1187
 
989
- const rawResult = await execute(args);
1188
+ const rawResult = await execute(normalizedArgs);
990
1189
  const result = await maybeSummarizeToolResult(name, rawResult, runtime);
991
1190
  if (onToolResult) {
992
1191
  const preview = extractToolOutputText(result);
@@ -1015,7 +1214,7 @@ export function createTools(
1015
1214
  }
1016
1215
  onToolCall?.({
1017
1216
  name,
1018
- args: sanitizeToolArgs(args),
1217
+ args: sanitizeToolArgs(normalizedArgs),
1019
1218
  ok: true,
1020
1219
  duration_ms: Date.now() - start,
1021
1220
  output_bytes: outputBytes,
@@ -1025,7 +1224,7 @@ export function createTools(
1025
1224
  } catch (err) {
1026
1225
  onToolCall?.({
1027
1226
  name,
1028
- args: sanitizeToolArgs(args),
1227
+ args: sanitizeToolArgs(normalizedArgs),
1029
1228
  ok: false,
1030
1229
  duration_ms: Date.now() - start,
1031
1230
  error: err instanceof Error ? err.message : String(err)
@@ -1173,7 +1372,11 @@ export function createTools(
1173
1372
  }),
1174
1373
  execute: wrapExecute('Read', async ({ path: inputPath, maxBytes }: { path: string; maxBytes?: number }) => {
1175
1374
  const resolved = resolvePath(inputPath, isMain, true);
1176
- const { content, truncated, size } = await readFileSafe(resolved, Math.min(maxBytes || runtime.outputLimitBytes, runtime.outputLimitBytes));
1375
+ const requestedMaxBytes = Number.isFinite(maxBytes)
1376
+ ? Math.floor(maxBytes as number)
1377
+ : runtime.outputLimitBytes;
1378
+ const clampedMaxBytes = Math.max(1, Math.min(requestedMaxBytes, runtime.outputLimitBytes));
1379
+ const { content, truncated, size } = await readFileSafe(resolved, clampedMaxBytes);
1177
1380
  return { path: resolved, content, truncated, size };
1178
1381
  })
1179
1382
  });
@@ -2603,7 +2806,7 @@ export function createTools(
2603
2806
  }
2604
2807
  tools.push(analyzeImageTool as Tool);
2605
2808
 
2606
- return tools;
2809
+ return applyPolicyToolFilter(tools, policy);
2607
2810
  }
2608
2811
 
2609
2812
  /**
@@ -2,6 +2,7 @@ import { ToolPolicy } from './tool-policy.js';
2
2
  import { loadModelRegistry, getTokenEstimateConfig, getModelPricing, ModelCapabilities } from './model-registry.js';
3
3
  export type AgentContext = {
4
4
  memoryRecall: string[];
5
+ memoryRecallAttempted: boolean;
5
6
  userProfile: string | null;
6
7
  memoryStats: {
7
8
  total: number;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-context.d.ts","sourceRoot":"","sources":["../src/agent-context.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG3F,OAAO,EAAgB,iBAAiB,EAAE,sBAAsB,EAAE,eAAe,EAAwB,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxJ,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9G,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,CAAC;YAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC3H,aAAa,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IACpD,YAAY,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IACjD,aAAa,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;IACzD,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE;QACP,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH,CAAC;AA4BF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,UAAU,CAiB3F;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,YAAY,CAAC,CAgGxB"}
1
+ {"version":3,"file":"agent-context.d.ts","sourceRoot":"","sources":["../src/agent-context.ts"],"names":[],"mappings":"AAEA,OAAO,EAA+C,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG3F,OAAO,EAAgB,iBAAiB,EAAE,sBAAsB,EAAE,eAAe,EAAwB,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAIxJ,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC9G,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,CAAC;YAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC3H,aAAa,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IACpD,YAAY,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IACjD,aAAa,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;IACzD,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE;QACP,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH,CAAC;AA4BF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,UAAU,CAiB3F;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4GxB"}
@@ -1,11 +1,11 @@
1
- import { buildHybridMemoryRecall } from './memory-recall.js';
2
- import { buildUserProfile, getMemoryStats } from './memory-store.js';
1
+ import { resolveMemoryBackend } from './memory-backend.js';
3
2
  import { loadPersonalizedBehaviorConfig } from './personalization.js';
4
3
  import { getEffectiveToolPolicy, mergeToolPolicyDeny } from './tool-policy.js';
5
4
  import { applyToolBudgets } from './tool-budgets.js';
6
5
  import { getToolReliability } from './db.js';
7
6
  import { resolveModel, loadModelRegistry, getTokenEstimateConfig, getModelPricing, getModelCapabilities } from './model-registry.js';
8
7
  import { loadRuntimeConfig } from './runtime-config.js';
8
+ import { optimizeRecallQuery, resolveRecallBudget, shouldRunMemoryRecall } from './recall-policy.js';
9
9
  /**
10
10
  * Calculate dynamic memory token budget based on model capabilities
11
11
  * Reserves 15% of available context for memories (min 800, max 4000)
@@ -38,6 +38,7 @@ export function applyToolAllowOverride(policy, toolAllow) {
38
38
  export async function buildAgentContext(params) {
39
39
  const startedAt = Date.now();
40
40
  const runtime = loadRuntimeConfig();
41
+ const memoryBackend = await resolveMemoryBackend();
41
42
  // Use routing.model as the base — model.json per_user/per_group overrides take priority
42
43
  const defaultModel = runtime.host.routing.model || runtime.host.defaultModel;
43
44
  const modelRegistry = loadModelRegistry(defaultModel);
@@ -55,23 +56,33 @@ export async function buildAgentContext(params) {
55
56
  const dynamicMemoryBudget = calculateMemoryBudget(modelCapabilities, runtime.agent.context.maxOutputTokens, params.recallMaxTokens);
56
57
  let memoryRecall = [];
57
58
  let memoryRecallMs;
58
- if (params.recallEnabled !== false && params.recallMaxResults > 0 && params.recallMaxTokens > 0) {
59
+ const optimizedRecallQuery = optimizeRecallQuery(params.recallQuery, params.messageText || '');
60
+ const recallBudget = resolveRecallBudget({
61
+ query: optimizedRecallQuery,
62
+ maxResults: params.recallMaxResults,
63
+ maxTokens: params.recallMaxTokens
64
+ });
65
+ const recallEnabled = params.recallEnabled !== false
66
+ && recallBudget.maxResults > 0
67
+ && recallBudget.maxTokens > 0
68
+ && shouldRunMemoryRecall(optimizedRecallQuery);
69
+ if (recallEnabled) {
59
70
  const recallStart = Date.now();
60
- memoryRecall = await buildHybridMemoryRecall({
71
+ memoryRecall = await memoryBackend.buildRecall({
61
72
  groupFolder: params.groupFolder,
62
73
  userId: params.userId ?? null,
63
- query: params.recallQuery,
64
- maxResults: params.recallMaxResults,
65
- maxTokens: dynamicMemoryBudget,
74
+ query: optimizedRecallQuery,
75
+ maxResults: recallBudget.maxResults,
76
+ maxTokens: Math.min(dynamicMemoryBudget, recallBudget.maxTokens),
66
77
  minScore: runtime.host.memory.recall.minScore
67
78
  });
68
79
  memoryRecallMs = Date.now() - recallStart;
69
80
  }
70
- const userProfile = buildUserProfile({
81
+ const userProfile = memoryBackend.buildUserProfile({
71
82
  groupFolder: params.groupFolder,
72
83
  userId: params.userId ?? null
73
84
  });
74
- const memoryStats = getMemoryStats({
85
+ const memoryStats = memoryBackend.getStats({
75
86
  groupFolder: params.groupFolder,
76
87
  userId: params.userId ?? null
77
88
  });
@@ -101,6 +112,7 @@ export async function buildAgentContext(params) {
101
112
  }));
102
113
  return {
103
114
  memoryRecall,
115
+ memoryRecallAttempted: recallEnabled,
104
116
  userProfile,
105
117
  memoryStats,
106
118
  behaviorConfig,
@@ -1 +1 @@
1
- {"version":3,"file":"agent-context.js","sourceRoot":"","sources":["../src/agent-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-context.js","sourceRoot":"","sources":["../src/agent-context.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { AgentContext } from './agent-context.js';
2
+ import { AgentExecutionLane } from './agent-semaphore.js';
2
3
  import type { ContainerOutput } from './container-protocol.js';
3
4
  import type { RegisteredGroup } from './types.js';
4
5
  export type TraceBase = {
@@ -54,6 +55,7 @@ export declare function executeAgentRun(params: {
54
55
  maxToolSteps?: number;
55
56
  timeoutMs?: number;
56
57
  timezone?: string;
58
+ lane?: AgentExecutionLane;
57
59
  streamDir?: string;
58
60
  attachments?: Array<{
59
61
  type: 'photo' | 'document' | 'voice' | 'video' | 'audio';
@@ -1 +1 @@
1
- {"version":3,"file":"agent-execution.d.ts","sourceRoot":"","sources":["../src/agent-execution.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-execution.d.ts","sourceRoot":"","sources":["../src/agent-execution.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,9 @@ import { withGroupLock } from './locks.js';
8
8
  import { getModelPricing } from './model-registry.js';
9
9
  import { computeCostUSD } from './cost.js';
10
10
  import { writeTrace } from './trace-writer.js';
11
- import { recordLatency, recordTokenUsage, recordCost, recordMemoryRecall, recordMemoryUpsert, recordMemoryExtract, recordToolCall, recordError, recordStageLatency } from './metrics.js';
11
+ import { recordLatency, recordTokenUsage, recordCost, recordMemoryRecall, recordMemoryUpsert, recordMemoryExtract, recordToolCall, recordError, recordStageLatency, recordFailover } from './metrics.js';
12
+ import { loadRuntimeConfig } from './runtime-config.js';
13
+ import { buildFailoverEnvelope, chooseNextHostModelChain, downgradeReasoningEffort, reduceToolStepBudget, registerModelFailureCooldown } from './failover-policy.js';
12
14
  import { emitHook } from './hooks.js';
13
15
  import { logger } from './logger.js';
14
16
  export class AgentExecutionError extends Error {
@@ -46,7 +48,20 @@ function buildTaskSnapshot() {
46
48
  last_error: t.last_error ?? null
47
49
  }));
48
50
  }
51
+ function buildModelChain(primary, fallbacks) {
52
+ const deduped = [];
53
+ const seen = new Set();
54
+ for (const candidate of [primary, ...(fallbacks || [])]) {
55
+ const normalized = (candidate || '').trim();
56
+ if (!normalized || seen.has(normalized))
57
+ continue;
58
+ deduped.push(normalized);
59
+ seen.add(normalized);
60
+ }
61
+ return deduped;
62
+ }
49
63
  export async function executeAgentRun(params) {
64
+ const runStartedAt = Date.now();
50
65
  const group = params.group;
51
66
  const isMain = group.folder === MAIN_GROUP_FOLDER;
52
67
  const persistSession = params.persistSession !== false;
@@ -80,7 +95,65 @@ export async function executeAgentRun(params) {
80
95
  const resolvedMaxOutputTokens = outputCandidates.length > 0
81
96
  ? Math.min(...outputCandidates)
82
97
  : Infinity;
83
- const runContainer = () => runContainerAgent(group, {
98
+ const runtime = loadRuntimeConfig();
99
+ const hostFailover = runtime.host.routing.hostFailover;
100
+ const maxHostRetries = hostFailover.enabled ? Math.max(0, hostFailover.maxRetries) : 0;
101
+ const lane = params.lane || (params.isScheduledTask ? 'scheduled' : 'interactive');
102
+ const initialModelChain = buildModelChain(params.modelOverride || context.resolvedModel.model, params.modelFallbacks);
103
+ const initialChainSelection = chooseNextHostModelChain({
104
+ modelChain: initialModelChain,
105
+ attemptedPrimaryModels: new Set()
106
+ });
107
+ const initialPrimary = initialModelChain[0] || context.resolvedModel.model;
108
+ if (initialChainSelection && initialChainSelection.model !== initialPrimary) {
109
+ logger.info({
110
+ chatJid: params.chatJid,
111
+ groupFolder: group.folder,
112
+ skippedModel: initialPrimary,
113
+ selectedModel: initialChainSelection.model
114
+ }, 'Skipping primary model due to active host cooldown');
115
+ }
116
+ const attemptedPrimaryModels = new Set();
117
+ let activeModelChain = initialChainSelection
118
+ ? [initialChainSelection.model, ...initialChainSelection.fallbacks]
119
+ : [...initialModelChain];
120
+ let activeReasoningEffort = params.reasoningEffort;
121
+ let activeMaxToolSteps = params.maxToolSteps;
122
+ let hostAttempts = 0;
123
+ let hostRecovered = false;
124
+ let lastFailoverCategory;
125
+ let lastFailoverEnvelope;
126
+ const failoverEnvelopes = [];
127
+ const markFailoverExhaustedIfNeeded = () => {
128
+ if (hostAttempts > 1 && lastFailoverCategory) {
129
+ recordFailover('exhausted', lastFailoverCategory);
130
+ }
131
+ };
132
+ const planHostRetry = (hostAttempt, envelope) => {
133
+ const canRetry = hostAttempt < maxHostRetries && envelope.retryable;
134
+ if (!canRetry)
135
+ return null;
136
+ return chooseNextHostModelChain({
137
+ modelChain: initialModelChain,
138
+ attemptedPrimaryModels
139
+ });
140
+ };
141
+ const applyHostRetry = (hostAttempt, envelope, nextChain, message) => {
142
+ recordFailover('attempt', envelope.category);
143
+ activeModelChain = [nextChain.model, ...nextChain.fallbacks];
144
+ activeReasoningEffort = downgradeReasoningEffort(activeReasoningEffort);
145
+ activeMaxToolSteps = reduceToolStepBudget(activeMaxToolSteps);
146
+ logger.warn({
147
+ chatJid: params.chatJid,
148
+ groupFolder: group.folder,
149
+ attempt: hostAttempt + 1,
150
+ category: envelope.category,
151
+ source: envelope.source,
152
+ statusCode: envelope.statusCode,
153
+ retryModel: nextChain.model
154
+ }, message);
155
+ };
156
+ const runContainer = (modelOverride, modelFallbacks) => runContainerAgent(group, {
84
157
  prompt: params.prompt,
85
158
  sessionId: params.sessionId,
86
159
  groupFolder: group.folder,
@@ -91,15 +164,16 @@ export async function executeAgentRun(params) {
91
164
  userId: params.userId ?? undefined,
92
165
  userName: params.userName,
93
166
  memoryRecall: context.memoryRecall,
167
+ memoryRecallAttempted: context.memoryRecallAttempted,
94
168
  userProfile: context.userProfile,
95
169
  memoryStats: context.memoryStats,
96
170
  tokenEstimate: context.tokenEstimate,
97
171
  toolReliability: context.toolReliability,
98
172
  behaviorConfig: context.behaviorConfig,
99
173
  toolPolicy: context.toolPolicy,
100
- modelOverride: params.modelOverride || context.resolvedModel.model,
101
- modelFallbacks: params.modelFallbacks,
102
- reasoningEffort: params.reasoningEffort,
174
+ modelOverride,
175
+ modelFallbacks,
176
+ reasoningEffort: activeReasoningEffort,
103
177
  modelCapabilities: {
104
178
  context_length: context.modelCapabilities.context_length,
105
179
  max_completion_tokens: context.modelCapabilities.max_completion_tokens,
@@ -109,7 +183,7 @@ export async function executeAgentRun(params) {
109
183
  modelTemperature: context.resolvedModel.override?.temperature,
110
184
  timezone: params.timezone || TIMEZONE,
111
185
  hostPlatform: `${process.platform}/${process.arch}`,
112
- maxToolSteps: params.maxToolSteps,
186
+ maxToolSteps: activeMaxToolSteps,
113
187
  streamDir: params.streamDir,
114
188
  attachments: params.attachments
115
189
  }, { abortSignal: params.abortSignal, timeoutMs: params.timeoutMs });
@@ -121,17 +195,81 @@ export async function executeAgentRun(params) {
121
195
  model: params.modelOverride || context.resolvedModel.model,
122
196
  source: params.isScheduledTask ? 'scheduler' : 'message'
123
197
  });
124
- let output;
125
- try {
126
- const runner = () => (useGroupLock ? withGroupLock(group.folder, () => runContainer()) : runContainer());
127
- output = useSemaphore
128
- ? await runWithAgentSemaphore(runner)
129
- : await runner();
130
- }
131
- catch (err) {
132
- const message = err instanceof Error ? err.message : String(err);
198
+ let output = null;
199
+ let lastException = null;
200
+ for (let hostAttempt = 0; hostAttempt <= maxHostRetries; hostAttempt += 1) {
201
+ const nextPrimary = activeModelChain[0] || context.resolvedModel.model;
202
+ const nextFallbacks = activeModelChain.slice(1);
203
+ attemptedPrimaryModels.add(nextPrimary);
204
+ hostAttempts = hostAttempt + 1;
205
+ try {
206
+ const runner = () => (useGroupLock
207
+ ? withGroupLock(group.folder, () => runContainer(nextPrimary, nextFallbacks))
208
+ : runContainer(nextPrimary, nextFallbacks));
209
+ const attemptOutput = useSemaphore
210
+ ? await runWithAgentSemaphore(runner, { lane })
211
+ : await runner();
212
+ if (attemptOutput.status !== 'error') {
213
+ output = attemptOutput;
214
+ hostRecovered = hostAttempts > 1;
215
+ if (hostRecovered && lastFailoverCategory) {
216
+ recordFailover('recovered', lastFailoverCategory);
217
+ }
218
+ break;
219
+ }
220
+ const envelope = buildFailoverEnvelope({
221
+ error: attemptOutput.error || 'Unknown error',
222
+ source: 'container_output',
223
+ attempt: hostAttempts,
224
+ model: attemptOutput.model || nextPrimary
225
+ });
226
+ failoverEnvelopes.push(envelope);
227
+ lastFailoverEnvelope = envelope;
228
+ lastFailoverCategory = envelope.category;
229
+ registerModelFailureCooldown(envelope.model || nextPrimary, envelope.category, hostFailover);
230
+ const nextChain = planHostRetry(hostAttempt, envelope);
231
+ if (!nextChain) {
232
+ markFailoverExhaustedIfNeeded();
233
+ output = attemptOutput;
234
+ break;
235
+ }
236
+ applyHostRetry(hostAttempt, envelope, nextChain, 'Host-level failover retry');
237
+ continue;
238
+ }
239
+ catch (err) {
240
+ lastException = err;
241
+ const envelope = buildFailoverEnvelope({
242
+ error: err instanceof Error ? err.message : String(err),
243
+ source: 'runtime_exception',
244
+ attempt: hostAttempts,
245
+ model: nextPrimary
246
+ });
247
+ failoverEnvelopes.push(envelope);
248
+ lastFailoverEnvelope = envelope;
249
+ lastFailoverCategory = envelope.category;
250
+ registerModelFailureCooldown(nextPrimary, envelope.category, hostFailover);
251
+ const nextChain = planHostRetry(hostAttempt, envelope);
252
+ if (!nextChain) {
253
+ markFailoverExhaustedIfNeeded();
254
+ break;
255
+ }
256
+ applyHostRetry(hostAttempt, envelope, nextChain, 'Host-level failover retry after runtime error');
257
+ }
258
+ }
259
+ if (!output && lastException) {
260
+ const message = lastException instanceof Error ? lastException.message : String(lastException);
133
261
  throw new AgentExecutionError(message, context);
134
262
  }
263
+ if (!output) {
264
+ throw new AgentExecutionError('No output from agent run', context);
265
+ }
266
+ output.host_failover_attempts = hostAttempts;
267
+ output.host_failover_recovered = hostRecovered;
268
+ output.host_failover_category = lastFailoverCategory;
269
+ output.host_failover_source = lastFailoverEnvelope?.source;
270
+ output.host_failover_status_code = lastFailoverEnvelope?.statusCode;
271
+ output.host_failover_envelopes = failoverEnvelopes.length > 0 ? failoverEnvelopes : undefined;
272
+ output.latency_ms = Math.max(1, Date.now() - runStartedAt);
135
273
  void emitHook('agent:complete', {
136
274
  group_folder: group.folder,
137
275
  chat_jid: params.chatJid,
@@ -189,6 +327,17 @@ export function recordAgentTelemetry(params) {
189
327
  session_recall_count: output?.session_recall_count,
190
328
  memory_items_upserted: output?.memory_items_upserted,
191
329
  memory_items_extracted: output?.memory_items_extracted,
330
+ host_failover_attempts: output?.host_failover_attempts,
331
+ host_failover_recovered: output?.host_failover_recovered,
332
+ host_failover_category: output?.host_failover_category,
333
+ host_failover_source: output?.host_failover_source,
334
+ host_failover_status_code: output?.host_failover_status_code,
335
+ host_failover_envelopes: output?.host_failover_envelopes,
336
+ tool_retry_attempts: output?.tool_retry_attempts,
337
+ tool_outcome_verification_forced: output?.tool_outcome_verification_forced,
338
+ tool_loop_breaker_triggered: output?.tool_loop_breaker_triggered,
339
+ tool_loop_breaker_reason: output?.tool_loop_breaker_reason,
340
+ memory_extraction_error: output?.memory_extraction_error,
192
341
  timings: Object.keys(timingBundle).length > 0 ? timingBundle : undefined,
193
342
  error_code: params.errorMessage || (output?.status === 'error' ? output?.error : undefined)
194
343
  });