agentbnb 8.2.3 → 8.3.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 (62) hide show
  1. package/dist/{card-EX2EYGCZ.js → card-BN643ZOY.js} +6 -2
  2. package/dist/card-T2XJZA5A.js +92 -0
  3. package/dist/{chunk-3LWBH7P3.js → chunk-4NFJ3VYZ.js} +20 -1
  4. package/dist/chunk-5AIYALBX.js +857 -0
  5. package/dist/chunk-6QMDJVMS.js +238 -0
  6. package/dist/{chunk-LKLKYXLV.js → chunk-74LZDEDT.js} +6 -4
  7. package/dist/{chunk-GKVTD4EZ.js → chunk-77KGEDH4.js} +1 -1
  8. package/dist/{chunk-QCGIG7WW.js → chunk-7IQE34QK.js} +14 -7
  9. package/dist/{chunk-QHZGOG3O.js → chunk-D242QZCR.js} +168 -41
  10. package/dist/chunk-EE3V3DXK.js +60 -0
  11. package/dist/{chunk-RYISHSHB.js → chunk-F3KIEVJ2.js} +207 -265
  12. package/dist/{chunk-XBGVQMQJ.js → chunk-FELGHDCA.js} +16 -39
  13. package/dist/{chunk-EJKW57ZV.js → chunk-GIEJVKZZ.js} +1 -1
  14. package/dist/{chunk-WVY2W7AA.js → chunk-I7KWA7OB.js} +20 -0
  15. package/dist/{chunk-4IPJJRTP.js → chunk-IGQNP3ZO.js} +5 -2
  16. package/dist/chunk-NQANA6WH.js +797 -0
  17. package/dist/{chunk-Z4MCGKTL.js → chunk-NX27AFPA.js} +15 -2
  18. package/dist/{chunk-Z2GEFFDO.js → chunk-O4Q7BRG6.js} +2 -2
  19. package/dist/{chunk-SSK653A6.js → chunk-PQIP7EXY.js} +6 -0
  20. package/dist/{chunk-EG6RS4JC.js → chunk-QFPXZITP.js} +20 -65
  21. package/dist/chunk-R4F4XII4.js +264 -0
  22. package/dist/{chunk-DYQOFGGI.js → chunk-RVBW2QXU.js} +178 -49
  23. package/dist/{chunk-CQFBNTGT.js → chunk-S7DZHKCG.js} +25 -12
  24. package/dist/chunk-U6LP4KWN.js +238 -0
  25. package/dist/{chunk-MWOXW7JQ.js → chunk-VJ7XBEY6.js} +24 -16
  26. package/dist/chunk-WTHMHNKC.js +129 -0
  27. package/dist/{chunk-OCSU2S6W.js → chunk-WX3GZVFG.js} +2 -1
  28. package/dist/{chunk-CKOOVZOI.js → chunk-YKMBFQC2.js} +37 -5
  29. package/dist/{chunk-S3V6R3EN.js → chunk-ZU2TP7CN.js} +70 -27
  30. package/dist/cli/index.js +203 -237
  31. package/dist/client-OKJJ3UP2.js +19 -0
  32. package/dist/client-UQBGCIPA.js +20 -0
  33. package/dist/conduct-4JDMWBQD.js +22 -0
  34. package/dist/{conduct-AZFLNUX3.js → conduct-VYYBCPHA.js} +14 -13
  35. package/dist/{conductor-mode-WKB42PYM.js → conductor-mode-OPGQJFLA.js} +12 -8
  36. package/dist/{conductor-mode-PLTB6MS3.js → conductor-mode-SBDCRIX6.js} +16 -11
  37. package/dist/execute-FZLQGIXB.js +14 -0
  38. package/dist/execute-TEZPQ5WP.js +15 -0
  39. package/dist/index.d.ts +172 -11
  40. package/dist/index.js +1529 -433
  41. package/dist/{process-guard-GH5LRNWO.js → process-guard-TNSUNHSR.js} +1 -1
  42. package/dist/{publish-capability-QDR2QIZ2.js → publish-capability-HVYILTPR.js} +4 -3
  43. package/dist/{reliability-metrics-QG7WC5QK.js → reliability-metrics-G7LPUYJD.js} +3 -1
  44. package/dist/reliability-metrics-RRUKJ4ME.js +20 -0
  45. package/dist/{request-OERS5BE7.js → request-KJNKR27T.js} +76 -71
  46. package/dist/{serve-skill-E6EJQYAK.js → serve-skill-GC6NIQ5T.js} +10 -11
  47. package/dist/{server-46VEG2W7.js → server-YV3XPTX5.js} +11 -10
  48. package/dist/{service-coordinator-KMSA6BST.js → service-coordinator-RY5AKUZS.js} +420 -171
  49. package/dist/{skill-config-FETXPNVP.js → skill-config-5O2VR546.js} +1 -1
  50. package/dist/skills/agentbnb/bootstrap.js +528 -253
  51. package/dist/websocket-client-3U27WJUU.js +7 -0
  52. package/dist/{websocket-client-4Z5P54RU.js → websocket-client-SNDF3B6N.js} +1 -1
  53. package/package.json +1 -1
  54. package/dist/chunk-MCED4GDW.js +0 -1572
  55. package/dist/chunk-NWIQJ2CL.js +0 -108
  56. package/dist/chunk-TUCEDQGM.js +0 -44
  57. package/dist/chunk-WNXXLCV5.js +0 -32
  58. package/dist/client-XOLP5IUZ.js +0 -12
  59. package/dist/conduct-VPUYTNEA.js +0 -21
  60. package/dist/execute-NNDCXTN4.js +0 -13
  61. package/dist/execute-RIRHTIBU.js +0 -16
  62. package/dist/websocket-client-QOVARTRN.js +0 -7
package/dist/index.js CHANGED
@@ -1,8 +1,3 @@
1
- import {
2
- RelayClient,
3
- RelayMessageSchema
4
- } from "./chunk-3LWBH7P3.js";
5
-
6
1
  // src/types/index.ts
7
2
  import { z } from "zod";
8
3
  var IOSchemaSchema = z.object({
@@ -40,6 +35,16 @@ var CapabilityCardSchema = z.object({
40
35
  schedule: z.string().optional()
41
36
  // cron expression
42
37
  }),
38
+ /**
39
+ * Provider-estimated typical execution duration in milliseconds.
40
+ * Used by requesters to derive default client-side timeouts.
41
+ */
42
+ expected_duration_ms: z.number().positive().optional(),
43
+ /**
44
+ * Provider hard timeout in milliseconds.
45
+ * Used as a fallback timeout hint when expected_duration_ms is unavailable.
46
+ */
47
+ hard_timeout_ms: z.number().positive().optional(),
43
48
  powered_by: z.array(PoweredBySchema).optional(),
44
49
  /**
45
50
  * Private per-card metadata. Stripped from all API and CLI responses —
@@ -93,6 +98,16 @@ var SkillSchema = z.object({
93
98
  credits_per_minute: z.number().nonnegative().optional(),
94
99
  free_tier: z.number().nonnegative().optional()
95
100
  }),
101
+ /**
102
+ * Provider-estimated typical execution duration in milliseconds.
103
+ * Used by requesters to derive default client-side timeouts.
104
+ */
105
+ expected_duration_ms: z.number().positive().optional(),
106
+ /**
107
+ * Provider hard timeout in milliseconds.
108
+ * Used as a fallback timeout hint when expected_duration_ms is unavailable.
109
+ */
110
+ hard_timeout_ms: z.number().positive().optional(),
96
111
  /** Per-skill online flag — overrides card-level availability for this skill. */
97
112
  availability: z.object({ online: z.boolean() }).optional(),
98
113
  powered_by: z.array(PoweredBySchema).optional(),
@@ -343,6 +358,61 @@ function initEvolutionTable(db) {
343
358
  `);
344
359
  }
345
360
 
361
+ // src/identity/agent-identity.ts
362
+ var AGENTS_SCHEMA = `
363
+ CREATE TABLE IF NOT EXISTS agents (
364
+ agent_id TEXT PRIMARY KEY,
365
+ display_name TEXT NOT NULL,
366
+ public_key TEXT NOT NULL,
367
+ operator_id TEXT,
368
+ server_id TEXT,
369
+ legacy_owner TEXT,
370
+ created_at TEXT NOT NULL,
371
+ updated_at TEXT NOT NULL
372
+ );
373
+
374
+ CREATE INDEX IF NOT EXISTS idx_agents_operator ON agents(operator_id);
375
+ CREATE INDEX IF NOT EXISTS idx_agents_legacy_owner ON agents(legacy_owner);
376
+ `;
377
+ function ensureAgentsTable(db) {
378
+ db.exec(AGENTS_SCHEMA);
379
+ }
380
+ function lookupAgent(db, agentId) {
381
+ return db.prepare("SELECT * FROM agents WHERE agent_id = ?").get(agentId) ?? null;
382
+ }
383
+ function lookupAgentByOwner(db, owner) {
384
+ return db.prepare("SELECT * FROM agents WHERE legacy_owner = ?").get(owner) ?? null;
385
+ }
386
+ function resolveCanonicalIdentity(db, identifier) {
387
+ ensureAgentsTable(db);
388
+ if (/^[a-f0-9]{16}$/.test(identifier)) {
389
+ const byAgentId = lookupAgent(db, identifier);
390
+ if (byAgentId) {
391
+ return {
392
+ agent_id: byAgentId.agent_id,
393
+ legacy_owner: byAgentId.legacy_owner,
394
+ resolved: true,
395
+ source: "agent_id"
396
+ };
397
+ }
398
+ }
399
+ const byOwner = lookupAgentByOwner(db, identifier);
400
+ if (byOwner) {
401
+ return {
402
+ agent_id: byOwner.agent_id,
403
+ legacy_owner: byOwner.legacy_owner,
404
+ resolved: true,
405
+ source: "legacy_owner"
406
+ };
407
+ }
408
+ return {
409
+ agent_id: identifier,
410
+ legacy_owner: null,
411
+ resolved: false,
412
+ source: "unresolved"
413
+ };
414
+ }
415
+
346
416
  // src/registry/store.ts
347
417
  var V2_FTS_TRIGGERS = `
348
418
  DROP TRIGGER IF EXISTS cards_ai;
@@ -562,6 +632,7 @@ function openDatabase(path = ":memory:") {
562
632
  ON capability_cards(owner);
563
633
  `);
564
634
  createRequestLogTable(db);
635
+ ensureAgentsTable(db);
565
636
  initFeedbackTable(db);
566
637
  initEvolutionTable(db);
567
638
  runMigrations(db);
@@ -667,7 +738,11 @@ function rebuildCardsFts(db) {
667
738
  }
668
739
  function insertCard(db, card) {
669
740
  const now = (/* @__PURE__ */ new Date()).toISOString();
670
- const withTimestamps = { ...card, created_at: card.created_at ?? now, updated_at: now };
741
+ const withTimestamps = attachCanonicalAgentId(db, {
742
+ ...card,
743
+ created_at: card.created_at ?? now,
744
+ updated_at: now
745
+ });
671
746
  const parsed = CapabilityCardSchema.safeParse(withTimestamps);
672
747
  if (!parsed.success) {
673
748
  throw new AgentBnBError(
@@ -693,6 +768,19 @@ function getCard(db, id) {
693
768
  if (!row) return null;
694
769
  return JSON.parse(row.data);
695
770
  }
771
+ function attachCanonicalAgentId(db, card) {
772
+ const resolved = resolveCanonicalIdentity(db, card.owner);
773
+ if (!resolved.resolved) {
774
+ return card;
775
+ }
776
+ if (card.agent_id === resolved.agent_id) {
777
+ return card;
778
+ }
779
+ return {
780
+ ...card,
781
+ agent_id: resolved.agent_id
782
+ };
783
+ }
696
784
  function updateReputation(db, cardId, success, latencyMs) {
697
785
  const existing = getCard(db, cardId);
698
786
  if (!existing) return;
@@ -717,16 +805,35 @@ function updateReputation(db, cardId, success, latencyMs) {
717
805
  stmt.run(JSON.stringify(updatedCard), now, cardId);
718
806
  }
719
807
  function listCards(db, owner) {
720
- let stmt;
721
- let rows;
722
- if (owner !== void 0) {
723
- stmt = db.prepare("SELECT data FROM capability_cards WHERE owner = ?");
724
- rows = stmt.all(owner);
725
- } else {
726
- stmt = db.prepare("SELECT data FROM capability_cards");
727
- rows = stmt.all();
808
+ if (owner === void 0) {
809
+ const rows = db.prepare("SELECT data FROM capability_cards").all();
810
+ return rows.map((row) => JSON.parse(row.data));
811
+ }
812
+ const filtered = /* @__PURE__ */ new Map();
813
+ const resolved = resolveCanonicalIdentity(db, owner);
814
+ const ownerAliases = /* @__PURE__ */ new Set([owner]);
815
+ if (resolved.legacy_owner) {
816
+ ownerAliases.add(resolved.legacy_owner);
817
+ }
818
+ for (const alias of ownerAliases) {
819
+ const aliasRows = db.prepare("SELECT data FROM capability_cards WHERE owner = ?").all(alias);
820
+ for (const row of aliasRows) {
821
+ const parsed = JSON.parse(row.data);
822
+ filtered.set(parsed.id, parsed);
823
+ }
728
824
  }
729
- return rows.map((row) => JSON.parse(row.data));
825
+ const agentIdentifiers = /* @__PURE__ */ new Set([owner]);
826
+ if (resolved.resolved) {
827
+ agentIdentifiers.add(resolved.agent_id);
828
+ }
829
+ for (const agentIdentifier of agentIdentifiers) {
830
+ const agentRows = db.prepare("SELECT data FROM capability_cards WHERE json_extract(data, '$.agent_id') = ?").all(agentIdentifier);
831
+ for (const row of agentRows) {
832
+ const parsed = JSON.parse(row.data);
833
+ filtered.set(parsed.id, parsed);
834
+ }
835
+ }
836
+ return [...filtered.values()];
730
837
  }
731
838
  function getCardsByCapabilityType(db, capabilityType) {
732
839
  const rows = db.prepare(
@@ -915,6 +1022,122 @@ function applyReputationFilter(db, cards, minReputation) {
915
1022
  import Database2 from "better-sqlite3";
916
1023
  import { randomUUID as randomUUID3 } from "crypto";
917
1024
 
1025
+ // src/credit/owner-normalization.ts
1026
+ var RESERVED_OWNERS = /* @__PURE__ */ new Set(["platform_treasury"]);
1027
+ function tableExists(db, table) {
1028
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
1029
+ return row !== void 0;
1030
+ }
1031
+ function mergeBalanceRows(db, oldOwner, newOwner, now) {
1032
+ if (!tableExists(db, "credit_balances")) return;
1033
+ const oldRow = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(oldOwner);
1034
+ if (!oldRow) return;
1035
+ const newRow = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(newOwner);
1036
+ if (newRow) {
1037
+ db.prepare(
1038
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
1039
+ ).run(oldRow.balance, now, newOwner);
1040
+ db.prepare("DELETE FROM credit_balances WHERE owner = ?").run(oldOwner);
1041
+ return;
1042
+ }
1043
+ db.prepare("UPDATE credit_balances SET owner = ?, updated_at = ? WHERE owner = ?").run(
1044
+ newOwner,
1045
+ now,
1046
+ oldOwner
1047
+ );
1048
+ }
1049
+ function mergeProviderRegistryRows(db, oldOwner, newOwner) {
1050
+ if (!tableExists(db, "provider_registry")) return;
1051
+ const oldRow = db.prepare("SELECT provider_number FROM provider_registry WHERE owner = ?").get(oldOwner);
1052
+ if (!oldRow) return;
1053
+ const newRow = db.prepare("SELECT provider_number FROM provider_registry WHERE owner = ?").get(newOwner);
1054
+ if (newRow) {
1055
+ db.prepare("DELETE FROM provider_registry WHERE owner = ?").run(oldOwner);
1056
+ return;
1057
+ }
1058
+ db.prepare("UPDATE provider_registry SET owner = ? WHERE owner = ?").run(newOwner, oldOwner);
1059
+ }
1060
+ function mergeReliabilityRows(db, oldOwner, newOwner, now) {
1061
+ if (!tableExists(db, "provider_reliability_metrics")) return;
1062
+ const oldRow = db.prepare("SELECT * FROM provider_reliability_metrics WHERE owner = ?").get(oldOwner);
1063
+ if (!oldRow) return;
1064
+ const newRow = db.prepare("SELECT * FROM provider_reliability_metrics WHERE owner = ?").get(newOwner);
1065
+ if (!newRow) {
1066
+ db.prepare(
1067
+ "UPDATE provider_reliability_metrics SET owner = ?, updated_at = ? WHERE owner = ?"
1068
+ ).run(newOwner, now, oldOwner);
1069
+ return;
1070
+ }
1071
+ const cycleStartCandidates = [oldRow.cycle_start, newRow.cycle_start].filter(Boolean);
1072
+ const mergedCycleStart = cycleStartCandidates.length > 0 ? cycleStartCandidates.slice().sort((left, right) => left.localeCompare(right))[0] : now;
1073
+ db.prepare(
1074
+ `UPDATE provider_reliability_metrics
1075
+ SET current_streak = ?,
1076
+ longest_streak = ?,
1077
+ total_hires = ?,
1078
+ repeat_hires = ?,
1079
+ feedback_count = ?,
1080
+ feedback_sum = ?,
1081
+ availability_checks = ?,
1082
+ availability_hits = ?,
1083
+ cycle_start = ?,
1084
+ updated_at = ?
1085
+ WHERE owner = ?`
1086
+ ).run(
1087
+ Math.max(oldRow.current_streak, newRow.current_streak),
1088
+ Math.max(oldRow.longest_streak, newRow.longest_streak),
1089
+ oldRow.total_hires + newRow.total_hires,
1090
+ oldRow.repeat_hires + newRow.repeat_hires,
1091
+ oldRow.feedback_count + newRow.feedback_count,
1092
+ oldRow.feedback_sum + newRow.feedback_sum,
1093
+ oldRow.availability_checks + newRow.availability_checks,
1094
+ oldRow.availability_hits + newRow.availability_hits,
1095
+ mergedCycleStart,
1096
+ now,
1097
+ newOwner
1098
+ );
1099
+ db.prepare("DELETE FROM provider_reliability_metrics WHERE owner = ?").run(oldOwner);
1100
+ }
1101
+ function migrateCreditOwnerData(db, oldOwner, newOwner) {
1102
+ if (!oldOwner || !newOwner || oldOwner === newOwner) return;
1103
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1104
+ db.transaction(() => {
1105
+ mergeBalanceRows(db, oldOwner, newOwner, now);
1106
+ if (tableExists(db, "credit_transactions")) {
1107
+ db.prepare("UPDATE credit_transactions SET owner = ? WHERE owner = ?").run(newOwner, oldOwner);
1108
+ }
1109
+ if (tableExists(db, "credit_escrow")) {
1110
+ db.prepare("UPDATE credit_escrow SET owner = ? WHERE owner = ?").run(newOwner, oldOwner);
1111
+ }
1112
+ mergeProviderRegistryRows(db, oldOwner, newOwner);
1113
+ if (tableExists(db, "demand_vouchers")) {
1114
+ db.prepare("UPDATE demand_vouchers SET owner = ? WHERE owner = ?").run(newOwner, oldOwner);
1115
+ }
1116
+ mergeReliabilityRows(db, oldOwner, newOwner, now);
1117
+ })();
1118
+ }
1119
+ function canonicalizeCreditOwner(db, owner) {
1120
+ if (!owner || RESERVED_OWNERS.has(owner)) {
1121
+ return owner;
1122
+ }
1123
+ ensureAgentsTable(db);
1124
+ const resolved = resolveCanonicalIdentity(db, owner);
1125
+ if (!resolved.resolved) {
1126
+ return owner;
1127
+ }
1128
+ const aliases = /* @__PURE__ */ new Set();
1129
+ if (owner !== resolved.agent_id) {
1130
+ aliases.add(owner);
1131
+ }
1132
+ if (resolved.legacy_owner && resolved.legacy_owner !== resolved.agent_id) {
1133
+ aliases.add(resolved.legacy_owner);
1134
+ }
1135
+ for (const alias of aliases) {
1136
+ migrateCreditOwnerData(db, alias, resolved.agent_id);
1137
+ }
1138
+ return resolved.agent_id;
1139
+ }
1140
+
918
1141
  // src/credit/reliability-metrics.ts
919
1142
  var RELIABILITY_METRICS_SCHEMA = `
920
1143
  CREATE TABLE IF NOT EXISTS provider_reliability_metrics (
@@ -935,6 +1158,7 @@ function ensureReliabilityTable(db) {
935
1158
  db.exec(RELIABILITY_METRICS_SCHEMA);
936
1159
  }
937
1160
  function ensureRow(db, owner) {
1161
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
938
1162
  const now = (/* @__PURE__ */ new Date()).toISOString();
939
1163
  db.prepare(
940
1164
  `INSERT OR IGNORE INTO provider_reliability_metrics
@@ -942,18 +1166,20 @@ function ensureRow(db, owner) {
942
1166
  feedback_count, feedback_sum, availability_checks, availability_hits,
943
1167
  cycle_start, updated_at)
944
1168
  VALUES (?, 0, 0, 0, 0, 0, 0, 0, 0, ?, ?)`
945
- ).run(owner, now, now);
1169
+ ).run(canonicalOwner, now, now);
946
1170
  }
947
1171
  function recordSuccessfulHire(db, providerOwner, consumerOwner) {
1172
+ const canonicalProviderOwner = canonicalizeCreditOwner(db, providerOwner);
1173
+ const canonicalConsumerOwner = canonicalizeCreditOwner(db, consumerOwner);
948
1174
  const now = (/* @__PURE__ */ new Date()).toISOString();
949
- ensureRow(db, providerOwner);
1175
+ ensureRow(db, canonicalProviderOwner);
950
1176
  const isRepeat = db.prepare(
951
1177
  `SELECT COUNT(*) as cnt FROM credit_transactions
952
1178
  WHERE owner = ? AND reason = 'settlement'
953
1179
  AND reference_id IN (
954
1180
  SELECT id FROM credit_escrow WHERE owner = ?
955
1181
  )`
956
- ).get(providerOwner, consumerOwner);
1182
+ ).get(canonicalProviderOwner, canonicalConsumerOwner);
957
1183
  const repeatIncrement = (isRepeat?.cnt ?? 0) > 0 ? 1 : 0;
958
1184
  db.prepare(
959
1185
  `UPDATE provider_reliability_metrics
@@ -963,27 +1189,7 @@ function recordSuccessfulHire(db, providerOwner, consumerOwner) {
963
1189
  repeat_hires = repeat_hires + ?,
964
1190
  updated_at = ?
965
1191
  WHERE owner = ?`
966
- ).run(repeatIncrement, now, providerOwner);
967
- }
968
-
969
- // src/identity/agent-identity.ts
970
- var AGENTS_SCHEMA = `
971
- CREATE TABLE IF NOT EXISTS agents (
972
- agent_id TEXT PRIMARY KEY,
973
- display_name TEXT NOT NULL,
974
- public_key TEXT NOT NULL,
975
- operator_id TEXT,
976
- server_id TEXT,
977
- legacy_owner TEXT,
978
- created_at TEXT NOT NULL,
979
- updated_at TEXT NOT NULL
980
- );
981
-
982
- CREATE INDEX IF NOT EXISTS idx_agents_operator ON agents(operator_id);
983
- CREATE INDEX IF NOT EXISTS idx_agents_legacy_owner ON agents(legacy_owner);
984
- `;
985
- function ensureAgentsTable(db) {
986
- db.exec(AGENTS_SCHEMA);
1192
+ ).run(repeatIncrement, now, canonicalProviderOwner);
987
1193
  }
988
1194
 
989
1195
  // src/credit/ledger.ts
@@ -1046,19 +1252,22 @@ function openCreditDb(path = ":memory:") {
1046
1252
  return db;
1047
1253
  }
1048
1254
  function getBalance(db, owner) {
1049
- const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
1255
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
1256
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(canonicalOwner);
1050
1257
  return row?.balance ?? 0;
1051
1258
  }
1052
1259
  function registerProvider(db, owner) {
1260
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
1053
1261
  const now = (/* @__PURE__ */ new Date()).toISOString();
1054
1262
  const maxRow = db.prepare("SELECT MAX(provider_number) as maxNum FROM provider_registry").get();
1055
1263
  const nextNum = (maxRow?.maxNum ?? 0) + 1;
1056
- db.prepare("INSERT OR IGNORE INTO provider_registry (owner, provider_number, registered_at) VALUES (?, ?, ?)").run(owner, nextNum, now);
1057
- const row = db.prepare("SELECT provider_number FROM provider_registry WHERE owner = ?").get(owner);
1264
+ db.prepare("INSERT OR IGNORE INTO provider_registry (owner, provider_number, registered_at) VALUES (?, ?, ?)").run(canonicalOwner, nextNum, now);
1265
+ const row = db.prepare("SELECT provider_number FROM provider_registry WHERE owner = ?").get(canonicalOwner);
1058
1266
  return row.provider_number;
1059
1267
  }
1060
1268
  function getProviderNumber(db, owner) {
1061
- const row = db.prepare("SELECT provider_number FROM provider_registry WHERE owner = ?").get(owner);
1269
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
1270
+ const row = db.prepare("SELECT provider_number FROM provider_registry WHERE owner = ?").get(canonicalOwner);
1062
1271
  return row?.provider_number ?? null;
1063
1272
  }
1064
1273
  function getProviderBonus(providerNumber) {
@@ -1067,10 +1276,11 @@ function getProviderBonus(providerNumber) {
1067
1276
  return 1;
1068
1277
  }
1069
1278
  function getActiveVoucher(db, owner) {
1279
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
1070
1280
  const now = (/* @__PURE__ */ new Date()).toISOString();
1071
1281
  const row = db.prepare(
1072
1282
  "SELECT id, remaining, expires_at FROM demand_vouchers WHERE owner = ? AND is_active = 1 AND remaining > 0 AND expires_at > ? ORDER BY created_at ASC LIMIT 1"
1073
- ).get(owner, now);
1283
+ ).get(canonicalOwner, now);
1074
1284
  return row ?? null;
1075
1285
  }
1076
1286
  function consumeVoucher(db, voucherId, amount) {
@@ -1079,6 +1289,7 @@ function consumeVoucher(db, voucherId, amount) {
1079
1289
  ).run(amount, voucherId, amount);
1080
1290
  }
1081
1291
  function recordEarning(db, owner, amount, _cardId, receiptNonce) {
1292
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
1082
1293
  const now = (/* @__PURE__ */ new Date()).toISOString();
1083
1294
  db.transaction(() => {
1084
1295
  const existing = db.prepare(
@@ -1087,13 +1298,13 @@ function recordEarning(db, owner, amount, _cardId, receiptNonce) {
1087
1298
  if (existing) return;
1088
1299
  db.prepare(
1089
1300
  "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
1090
- ).run(owner, now);
1301
+ ).run(canonicalOwner, now);
1091
1302
  db.prepare(
1092
1303
  "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
1093
- ).run(amount, now, owner);
1304
+ ).run(amount, now, canonicalOwner);
1094
1305
  db.prepare(
1095
1306
  "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1096
- ).run(randomUUID3(), owner, amount, "remote_earning", receiptNonce, now);
1307
+ ).run(randomUUID3(), canonicalOwner, amount, "remote_earning", receiptNonce, now);
1097
1308
  })();
1098
1309
  }
1099
1310
 
@@ -1107,59 +1318,116 @@ import { randomUUID as randomUUID5 } from "crypto";
1107
1318
  // src/credit/escrow.ts
1108
1319
  import { randomUUID as randomUUID4 } from "crypto";
1109
1320
  var NETWORK_FEE_RATE = 0.05;
1321
+ var FINALIZABLE_ESCROW_STATUSES = /* @__PURE__ */ new Set([
1322
+ "held",
1323
+ "started",
1324
+ "progressing",
1325
+ "abandoned"
1326
+ ]);
1327
+ var TERMINAL_ESCROW_STATUSES = /* @__PURE__ */ new Set(["settled", "released"]);
1328
+ function getEscrowForMutation(db, escrowId) {
1329
+ const escrow = db.prepare("SELECT id, owner, amount, status, funding_source FROM credit_escrow WHERE id = ?").get(escrowId);
1330
+ if (!escrow) {
1331
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
1332
+ }
1333
+ return {
1334
+ ...escrow,
1335
+ owner: canonicalizeCreditOwner(db, escrow.owner)
1336
+ };
1337
+ }
1338
+ function updateEscrowStatus(db, escrowId, fromStatuses, toStatus) {
1339
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1340
+ const transition = db.transaction(() => {
1341
+ const escrow = getEscrowForMutation(db, escrowId);
1342
+ const current = escrow.status;
1343
+ if (!fromStatuses.includes(current)) {
1344
+ throw new AgentBnBError(
1345
+ `Invalid escrow transition for ${escrowId}: ${current} -> ${toStatus}`,
1346
+ "ESCROW_INVALID_TRANSITION"
1347
+ );
1348
+ }
1349
+ if (current === toStatus) return;
1350
+ const settledAt = TERMINAL_ESCROW_STATUSES.has(toStatus) ? now : null;
1351
+ db.prepare("UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?").run(
1352
+ toStatus,
1353
+ settledAt,
1354
+ escrowId
1355
+ );
1356
+ });
1357
+ transition();
1358
+ }
1359
+ function assertEscrowCanFinalize(escrow) {
1360
+ const status = escrow.status;
1361
+ if (FINALIZABLE_ESCROW_STATUSES.has(status)) {
1362
+ return;
1363
+ }
1364
+ if (TERMINAL_ESCROW_STATUSES.has(status)) {
1365
+ throw new AgentBnBError(
1366
+ `Escrow ${escrow.id} is already ${status}`,
1367
+ "ESCROW_ALREADY_SETTLED"
1368
+ );
1369
+ }
1370
+ throw new AgentBnBError(
1371
+ `Escrow ${escrow.id} has invalid lifecycle status: ${escrow.status}`,
1372
+ "ESCROW_INVALID_TRANSITION"
1373
+ );
1374
+ }
1110
1375
  function holdEscrow(db, owner, amount, cardId) {
1376
+ const canonicalOwner = canonicalizeCreditOwner(db, owner);
1111
1377
  const escrowId = randomUUID4();
1112
1378
  const now = (/* @__PURE__ */ new Date()).toISOString();
1113
1379
  const hold = db.transaction(() => {
1114
- const voucher = getActiveVoucher(db, owner);
1380
+ const voucher = getActiveVoucher(db, canonicalOwner);
1115
1381
  if (voucher && voucher.remaining >= amount) {
1116
1382
  consumeVoucher(db, voucher.id, amount);
1117
1383
  db.prepare(
1118
1384
  "INSERT INTO credit_escrow (id, owner, amount, card_id, status, created_at, funding_source) VALUES (?, ?, ?, ?, ?, ?, ?)"
1119
- ).run(escrowId, owner, amount, cardId, "held", now, "voucher");
1385
+ ).run(escrowId, canonicalOwner, amount, cardId, "held", now, "voucher");
1120
1386
  db.prepare(
1121
1387
  "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1122
- ).run(randomUUID4(), owner, -amount, "voucher_hold", escrowId, now);
1388
+ ).run(randomUUID4(), canonicalOwner, -amount, "voucher_hold", escrowId, now);
1123
1389
  } else {
1124
- const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
1390
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(canonicalOwner);
1125
1391
  if (!row || row.balance < amount) {
1126
1392
  throw new AgentBnBError("Insufficient credits", "INSUFFICIENT_CREDITS");
1127
1393
  }
1128
1394
  db.prepare(
1129
1395
  "UPDATE credit_balances SET balance = balance - ?, updated_at = ? WHERE owner = ? AND balance >= ?"
1130
- ).run(amount, now, owner, amount);
1396
+ ).run(amount, now, canonicalOwner, amount);
1131
1397
  db.prepare(
1132
1398
  "INSERT INTO credit_escrow (id, owner, amount, card_id, status, created_at, funding_source) VALUES (?, ?, ?, ?, ?, ?, ?)"
1133
- ).run(escrowId, owner, amount, cardId, "held", now, "balance");
1399
+ ).run(escrowId, canonicalOwner, amount, cardId, "held", now, "balance");
1134
1400
  db.prepare(
1135
1401
  "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1136
- ).run(randomUUID4(), owner, -amount, "escrow_hold", escrowId, now);
1402
+ ).run(randomUUID4(), canonicalOwner, -amount, "escrow_hold", escrowId, now);
1137
1403
  }
1138
1404
  });
1139
1405
  hold();
1140
1406
  return escrowId;
1141
1407
  }
1408
+ function markEscrowStarted(db, escrowId) {
1409
+ updateEscrowStatus(db, escrowId, ["held", "started"], "started");
1410
+ }
1411
+ function markEscrowProgressing(db, escrowId) {
1412
+ updateEscrowStatus(db, escrowId, ["held", "started", "progressing"], "progressing");
1413
+ }
1414
+ function markEscrowAbandoned(db, escrowId) {
1415
+ updateEscrowStatus(db, escrowId, ["started", "progressing", "abandoned"], "abandoned");
1416
+ }
1142
1417
  function settleEscrow(db, escrowId, recipientOwner) {
1418
+ const canonicalRecipientOwner = canonicalizeCreditOwner(db, recipientOwner);
1143
1419
  const now = (/* @__PURE__ */ new Date()).toISOString();
1144
1420
  const settle = db.transaction(() => {
1145
- const escrow = db.prepare("SELECT id, owner, amount, status, funding_source FROM credit_escrow WHERE id = ?").get(escrowId);
1146
- if (!escrow) {
1147
- throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
1148
- }
1149
- if (escrow.status !== "held") {
1150
- throw new AgentBnBError(
1151
- `Escrow ${escrowId} is already ${escrow.status}`,
1152
- "ESCROW_ALREADY_SETTLED"
1153
- );
1154
- }
1421
+ const escrow = getEscrowForMutation(db, escrowId);
1422
+ assertEscrowCanFinalize(escrow);
1155
1423
  const feeAmount = Math.floor(escrow.amount * NETWORK_FEE_RATE);
1156
1424
  const providerAmount = escrow.amount - feeAmount;
1157
1425
  db.prepare(
1158
1426
  "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
1159
- ).run(recipientOwner, now);
1427
+ ).run(canonicalRecipientOwner, now);
1160
1428
  db.prepare(
1161
1429
  "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
1162
- ).run(providerAmount, now, recipientOwner);
1430
+ ).run(providerAmount, now, canonicalRecipientOwner);
1163
1431
  if (feeAmount > 0) {
1164
1432
  db.prepare(
1165
1433
  "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
@@ -1176,10 +1444,10 @@ function settleEscrow(db, escrowId, recipientOwner) {
1176
1444
  ).run("settled", now, escrowId);
1177
1445
  db.prepare(
1178
1446
  "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1179
- ).run(randomUUID4(), recipientOwner, providerAmount, "settlement", escrowId, now);
1180
- let providerNum = getProviderNumber(db, recipientOwner);
1447
+ ).run(randomUUID4(), canonicalRecipientOwner, providerAmount, "settlement", escrowId, now);
1448
+ let providerNum = getProviderNumber(db, canonicalRecipientOwner);
1181
1449
  if (providerNum === null) {
1182
- providerNum = registerProvider(db, recipientOwner);
1450
+ providerNum = registerProvider(db, canonicalRecipientOwner);
1183
1451
  }
1184
1452
  const bonus = getProviderBonus(providerNum);
1185
1453
  if (bonus > 1) {
@@ -1190,14 +1458,14 @@ function settleEscrow(db, escrowId, recipientOwner) {
1190
1458
  ).run("platform_treasury", now);
1191
1459
  db.prepare(
1192
1460
  "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
1193
- ).run(bonusAmount, now, recipientOwner);
1461
+ ).run(bonusAmount, now, canonicalRecipientOwner);
1194
1462
  db.prepare(
1195
1463
  "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
1196
- ).run(randomUUID4(), recipientOwner, bonusAmount, "provider_bonus", escrowId, now);
1464
+ ).run(randomUUID4(), canonicalRecipientOwner, bonusAmount, "provider_bonus", escrowId, now);
1197
1465
  }
1198
1466
  }
1199
1467
  try {
1200
- recordSuccessfulHire(db, recipientOwner, escrow.owner);
1468
+ recordSuccessfulHire(db, canonicalRecipientOwner, escrow.owner);
1201
1469
  } catch {
1202
1470
  }
1203
1471
  });
@@ -1206,16 +1474,8 @@ function settleEscrow(db, escrowId, recipientOwner) {
1206
1474
  function releaseEscrow(db, escrowId) {
1207
1475
  const now = (/* @__PURE__ */ new Date()).toISOString();
1208
1476
  const release = db.transaction(() => {
1209
- const escrow = db.prepare("SELECT id, owner, amount, status, funding_source FROM credit_escrow WHERE id = ?").get(escrowId);
1210
- if (!escrow) {
1211
- throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
1212
- }
1213
- if (escrow.status !== "held") {
1214
- throw new AgentBnBError(
1215
- `Escrow ${escrowId} is already ${escrow.status}`,
1216
- "ESCROW_ALREADY_SETTLED"
1217
- );
1218
- }
1477
+ const escrow = getEscrowForMutation(db, escrowId);
1478
+ assertEscrowCanFinalize(escrow);
1219
1479
  db.prepare(
1220
1480
  "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
1221
1481
  ).run(escrow.amount, now, escrow.owner);
@@ -1231,16 +1491,8 @@ function releaseEscrow(db, escrowId) {
1231
1491
  function confirmEscrowDebit(db, escrowId) {
1232
1492
  const now = (/* @__PURE__ */ new Date()).toISOString();
1233
1493
  const confirm = db.transaction(() => {
1234
- const escrow = db.prepare("SELECT id, owner, amount, status, funding_source FROM credit_escrow WHERE id = ?").get(escrowId);
1235
- if (!escrow) {
1236
- throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
1237
- }
1238
- if (escrow.status !== "held") {
1239
- throw new AgentBnBError(
1240
- `Escrow ${escrowId} is already ${escrow.status}`,
1241
- "ESCROW_ALREADY_SETTLED"
1242
- );
1243
- }
1494
+ const escrow = getEscrowForMutation(db, escrowId);
1495
+ assertEscrowCanFinalize(escrow);
1244
1496
  db.prepare(
1245
1497
  "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
1246
1498
  ).run("settled", now, escrowId);
@@ -1251,111 +1503,21 @@ function confirmEscrowDebit(db, escrowId) {
1251
1503
  confirm();
1252
1504
  }
1253
1505
 
1254
- // src/credit/signing.ts
1255
- import { generateKeyPairSync, sign, verify, createPublicKey, createPrivateKey } from "crypto";
1256
- import { writeFileSync, readFileSync, existsSync, chmodSync } from "fs";
1257
- import { join } from "path";
1258
- function generateKeyPair() {
1259
- const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
1260
- publicKeyEncoding: { type: "spki", format: "der" },
1261
- privateKeyEncoding: { type: "pkcs8", format: "der" }
1262
- });
1263
- return {
1264
- publicKey: Buffer.from(publicKey),
1265
- privateKey: Buffer.from(privateKey)
1266
- };
1267
- }
1268
- function saveKeyPair(configDir, keys) {
1269
- const privatePath = join(configDir, "private.key");
1270
- const publicPath = join(configDir, "public.key");
1271
- writeFileSync(privatePath, keys.privateKey);
1272
- chmodSync(privatePath, 384);
1273
- writeFileSync(publicPath, keys.publicKey);
1274
- }
1275
- function loadKeyPair(configDir) {
1276
- const privatePath = join(configDir, "private.key");
1277
- const publicPath = join(configDir, "public.key");
1278
- if (!existsSync(privatePath) || !existsSync(publicPath)) {
1279
- throw new AgentBnBError("Keypair not found. Run `agentbnb init` to generate one.", "KEYPAIR_NOT_FOUND");
1280
- }
1281
- return {
1282
- publicKey: readFileSync(publicPath),
1283
- privateKey: readFileSync(privatePath)
1284
- };
1285
- }
1286
- function canonicalJson(data) {
1287
- return JSON.stringify(sortForCanonicalJson(data));
1288
- }
1289
- function sortForCanonicalJson(value) {
1290
- if (Array.isArray(value)) {
1291
- return value.map((item) => sortForCanonicalJson(item));
1292
- }
1293
- if (value !== null && typeof value === "object") {
1294
- const proto = Object.getPrototypeOf(value);
1295
- if (proto === Object.prototype || proto === null) {
1296
- const input = value;
1297
- const output = {};
1298
- const sortedKeys = Object.keys(input).sort();
1299
- for (const key of sortedKeys) {
1300
- output[key] = sortForCanonicalJson(input[key]);
1301
- }
1302
- return output;
1303
- }
1304
- }
1305
- return value;
1306
- }
1307
- function signEscrowReceipt(data, privateKey) {
1308
- const message = Buffer.from(canonicalJson(data), "utf-8");
1309
- const keyObject = createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
1310
- const signature = sign(null, message, keyObject);
1311
- return signature.toString("base64url");
1312
- }
1313
- function verifyEscrowReceipt(data, signature, publicKey) {
1314
- try {
1315
- const message = Buffer.from(canonicalJson(data), "utf-8");
1316
- const keyObject = createPublicKey({ key: publicKey, format: "der", type: "spki" });
1317
- const sigBuffer = Buffer.from(signature, "base64url");
1318
- return verify(null, message, keyObject, sigBuffer);
1319
- } catch {
1320
- return false;
1321
- }
1322
- }
1323
-
1324
- // src/credit/settlement.ts
1325
- function settleProviderEarning(providerDb, providerOwner, receipt) {
1326
- const feeAmount = Math.floor(receipt.amount * NETWORK_FEE_RATE);
1327
- const providerAmount = receipt.amount - feeAmount;
1328
- recordEarning(
1329
- providerDb,
1330
- providerOwner,
1331
- providerAmount,
1332
- receipt.card_id,
1333
- receipt.nonce
1334
- );
1335
- return { settled: true };
1336
- }
1337
- function settleRequesterEscrow(requesterDb, escrowId) {
1338
- confirmEscrowDebit(requesterDb, escrowId);
1339
- }
1340
- function releaseRequesterEscrow(requesterDb, escrowId) {
1341
- releaseEscrow(requesterDb, escrowId);
1342
- }
1343
-
1344
1506
  // src/cli/config.ts
1345
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2 } from "fs";
1507
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1346
1508
  import { homedir } from "os";
1347
- import { join as join2 } from "path";
1509
+ import { join } from "path";
1348
1510
  function getConfigDir() {
1349
- return process.env["AGENTBNB_DIR"] ?? join2(homedir(), ".agentbnb");
1511
+ return process.env["AGENTBNB_DIR"] ?? join(homedir(), ".agentbnb");
1350
1512
  }
1351
1513
  function getConfigPath() {
1352
- return join2(getConfigDir(), "config.json");
1514
+ return join(getConfigDir(), "config.json");
1353
1515
  }
1354
1516
  function loadConfig() {
1355
1517
  const configPath = getConfigPath();
1356
- if (!existsSync2(configPath)) return null;
1518
+ if (!existsSync(configPath)) return null;
1357
1519
  try {
1358
- const raw = readFileSync2(configPath, "utf-8");
1520
+ const raw = readFileSync(configPath, "utf-8");
1359
1521
  return JSON.parse(raw);
1360
1522
  } catch {
1361
1523
  return null;
@@ -1508,23 +1670,17 @@ async function executeCapabilityRequest(opts) {
1508
1670
  cardName = card.name;
1509
1671
  }
1510
1672
  let escrowId = null;
1511
- let isRemoteEscrow = false;
1512
1673
  if (relayAuthorized) {
1513
1674
  } else if (receipt) {
1514
- const { signature, ...receiptData2 } = receipt;
1515
- const publicKeyBuf = Buffer.from(receipt.requester_public_key, "hex");
1516
- const valid = verifyEscrowReceipt(receiptData2, signature, publicKeyBuf);
1517
- if (!valid) {
1518
- return { success: false, error: { code: -32603, message: "Invalid escrow receipt signature" } };
1519
- }
1520
- if (receipt.amount < creditsNeeded) {
1521
- return { success: false, error: { code: -32603, message: "Insufficient escrow amount" } };
1522
- }
1523
- const receiptAge = Date.now() - new Date(receipt.timestamp).getTime();
1524
- if (receiptAge > 5 * 60 * 1e3) {
1525
- return { success: false, error: { code: -32603, message: "Escrow receipt expired" } };
1675
+ if (creditsNeeded > 0) {
1676
+ return {
1677
+ success: false,
1678
+ error: {
1679
+ code: -32603,
1680
+ message: "Direct HTTP paid remote settlement is disabled. Route paid requests through relay."
1681
+ }
1682
+ };
1526
1683
  }
1527
- isRemoteEscrow = true;
1528
1684
  } else {
1529
1685
  try {
1530
1686
  const balance = getBalance(creditDb, requester);
@@ -1538,9 +1694,8 @@ async function executeCapabilityRequest(opts) {
1538
1694
  }
1539
1695
  }
1540
1696
  const startMs = Date.now();
1541
- const receiptData = isRemoteEscrow ? { receipt_released: true } : void 0;
1542
1697
  const handleFailure = (status, latencyMs, message, failureReason = "bad_execution") => {
1543
- if (!isRemoteEscrow && escrowId) releaseEscrow(creditDb, escrowId);
1698
+ if (escrowId) releaseEscrow(creditDb, escrowId);
1544
1699
  if (failureReason === "bad_execution" || failureReason === "auth_error") {
1545
1700
  updateReputation(registryDb, cardId, false, latencyMs);
1546
1701
  }
@@ -1559,15 +1714,10 @@ async function executeCapabilityRequest(opts) {
1559
1714
  });
1560
1715
  } catch {
1561
1716
  }
1562
- return {
1563
- success: false,
1564
- error: { code: -32603, message, ...receiptData ? { data: receiptData } : {} }
1565
- };
1717
+ return { success: false, error: { code: -32603, message } };
1566
1718
  };
1567
1719
  const handleSuccess = (result, latencyMs) => {
1568
- if (isRemoteEscrow && receipt) {
1569
- settleProviderEarning(creditDb, card.owner, receipt);
1570
- } else if (escrowId) {
1720
+ if (escrowId) {
1571
1721
  settleEscrow(creditDb, escrowId, card.owner);
1572
1722
  }
1573
1723
  updateReputation(registryDb, cardId, true, latencyMs);
@@ -1595,12 +1745,7 @@ async function executeCapabilityRequest(opts) {
1595
1745
  latencyMs
1596
1746
  }).catch(() => {
1597
1747
  });
1598
- const successResult = isRemoteEscrow ? {
1599
- ...typeof result === "object" && result !== null ? result : { data: result },
1600
- receipt_settled: true,
1601
- receipt_nonce: receipt.nonce
1602
- } : result;
1603
- return { success: true, result: successResult };
1748
+ return { success: true, result };
1604
1749
  };
1605
1750
  if (skillExecutor) {
1606
1751
  let targetSkillId = resolvedSkillId ?? skillId;
@@ -1628,33 +1773,103 @@ async function executeCapabilityRequest(opts) {
1628
1773
  return handleFailure("failure", Date.now() - startMs, message, "bad_execution");
1629
1774
  }
1630
1775
  }
1631
- if (!handlerUrl) {
1632
- return handleFailure("failure", Date.now() - startMs, "No skill executor or handler URL configured", "bad_execution");
1633
- }
1634
- const controller = new AbortController();
1635
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1776
+ if (!handlerUrl) {
1777
+ return handleFailure("failure", Date.now() - startMs, "No skill executor or handler URL configured", "bad_execution");
1778
+ }
1779
+ const controller = new AbortController();
1780
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1781
+ try {
1782
+ const response = await fetch(handlerUrl, {
1783
+ method: "POST",
1784
+ headers: { "Content-Type": "application/json" },
1785
+ body: JSON.stringify({ card_id: cardId, skill_id: resolvedSkillId, params }),
1786
+ signal: controller.signal
1787
+ });
1788
+ clearTimeout(timer);
1789
+ if (!response.ok) {
1790
+ return handleFailure("failure", Date.now() - startMs, `Handler returned ${response.status}`, "bad_execution");
1791
+ }
1792
+ const result = await response.json();
1793
+ return handleSuccess(result, Date.now() - startMs);
1794
+ } catch (err) {
1795
+ clearTimeout(timer);
1796
+ const isTimeout = err instanceof Error && err.name === "AbortError";
1797
+ return handleFailure(
1798
+ isTimeout ? "timeout" : "failure",
1799
+ Date.now() - startMs,
1800
+ isTimeout ? "Execution timeout" : "Handler error",
1801
+ isTimeout ? "timeout" : "bad_execution"
1802
+ );
1803
+ }
1804
+ }
1805
+
1806
+ // src/credit/signing.ts
1807
+ import { generateKeyPairSync, sign, verify, createPublicKey, createPrivateKey } from "crypto";
1808
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2, chmodSync } from "fs";
1809
+ import { join as join2 } from "path";
1810
+ function generateKeyPair() {
1811
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
1812
+ publicKeyEncoding: { type: "spki", format: "der" },
1813
+ privateKeyEncoding: { type: "pkcs8", format: "der" }
1814
+ });
1815
+ return {
1816
+ publicKey: Buffer.from(publicKey),
1817
+ privateKey: Buffer.from(privateKey)
1818
+ };
1819
+ }
1820
+ function saveKeyPair(configDir, keys) {
1821
+ const privatePath = join2(configDir, "private.key");
1822
+ const publicPath = join2(configDir, "public.key");
1823
+ writeFileSync2(privatePath, keys.privateKey);
1824
+ chmodSync(privatePath, 384);
1825
+ writeFileSync2(publicPath, keys.publicKey);
1826
+ }
1827
+ function loadKeyPair(configDir) {
1828
+ const privatePath = join2(configDir, "private.key");
1829
+ const publicPath = join2(configDir, "public.key");
1830
+ if (!existsSync2(privatePath) || !existsSync2(publicPath)) {
1831
+ throw new AgentBnBError("Keypair not found. Run `agentbnb init` to generate one.", "KEYPAIR_NOT_FOUND");
1832
+ }
1833
+ return {
1834
+ publicKey: readFileSync2(publicPath),
1835
+ privateKey: readFileSync2(privatePath)
1836
+ };
1837
+ }
1838
+ function canonicalJson(data) {
1839
+ return JSON.stringify(sortForCanonicalJson(data));
1840
+ }
1841
+ function sortForCanonicalJson(value) {
1842
+ if (Array.isArray(value)) {
1843
+ return value.map((item) => sortForCanonicalJson(item));
1844
+ }
1845
+ if (value !== null && typeof value === "object") {
1846
+ const proto = Object.getPrototypeOf(value);
1847
+ if (proto === Object.prototype || proto === null) {
1848
+ const input = value;
1849
+ const output = {};
1850
+ const sortedKeys = Object.keys(input).sort();
1851
+ for (const key of sortedKeys) {
1852
+ output[key] = sortForCanonicalJson(input[key]);
1853
+ }
1854
+ return output;
1855
+ }
1856
+ }
1857
+ return value;
1858
+ }
1859
+ function signEscrowReceipt(data, privateKey) {
1860
+ const message = Buffer.from(canonicalJson(data), "utf-8");
1861
+ const keyObject = createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
1862
+ const signature = sign(null, message, keyObject);
1863
+ return signature.toString("base64url");
1864
+ }
1865
+ function verifyEscrowReceipt(data, signature, publicKey) {
1636
1866
  try {
1637
- const response = await fetch(handlerUrl, {
1638
- method: "POST",
1639
- headers: { "Content-Type": "application/json" },
1640
- body: JSON.stringify({ card_id: cardId, skill_id: resolvedSkillId, params }),
1641
- signal: controller.signal
1642
- });
1643
- clearTimeout(timer);
1644
- if (!response.ok) {
1645
- return handleFailure("failure", Date.now() - startMs, `Handler returned ${response.status}`, "bad_execution");
1646
- }
1647
- const result = await response.json();
1648
- return handleSuccess(result, Date.now() - startMs);
1649
- } catch (err) {
1650
- clearTimeout(timer);
1651
- const isTimeout = err instanceof Error && err.name === "AbortError";
1652
- return handleFailure(
1653
- isTimeout ? "timeout" : "failure",
1654
- Date.now() - startMs,
1655
- isTimeout ? "Execution timeout" : "Handler error",
1656
- isTimeout ? "timeout" : "bad_execution"
1657
- );
1867
+ const message = Buffer.from(canonicalJson(data), "utf-8");
1868
+ const keyObject = createPublicKey({ key: publicKey, format: "der", type: "spki" });
1869
+ const sigBuffer = Buffer.from(signature, "base64url");
1870
+ return verify(null, message, keyObject, sigBuffer);
1871
+ } catch {
1872
+ return false;
1658
1873
  }
1659
1874
  }
1660
1875
 
@@ -1853,6 +2068,9 @@ function createGatewayServer(opts) {
1853
2068
  }
1854
2069
 
1855
2070
  // src/skills/executor.ts
2071
+ function buildTimeoutError(skillId, timeoutMs) {
2072
+ return `Skill "${skillId}" timed out after ${timeoutMs}ms`;
2073
+ }
1856
2074
  var SkillExecutor = class {
1857
2075
  skillMap;
1858
2076
  modeMap;
@@ -1911,7 +2129,20 @@ var SkillExecutor = class {
1911
2129
  this.concurrencyGuard.acquire(skillId);
1912
2130
  }
1913
2131
  try {
1914
- const modeResult = await mode.execute(config, params, onProgress);
2132
+ const configuredTimeoutMs = typeof config.timeout_ms === "number" ? config.timeout_ms : void 0;
2133
+ const modeExecution = mode.execute(config, params, onProgress);
2134
+ const modeResult = configuredTimeoutMs === void 0 ? await modeExecution : await new Promise((resolve, reject) => {
2135
+ const timeout = setTimeout(() => {
2136
+ reject(new Error(buildTimeoutError(skillId, configuredTimeoutMs)));
2137
+ }, configuredTimeoutMs);
2138
+ modeExecution.then((value) => {
2139
+ clearTimeout(timeout);
2140
+ resolve(value);
2141
+ }).catch((err) => {
2142
+ clearTimeout(timeout);
2143
+ reject(err);
2144
+ });
2145
+ });
1915
2146
  return {
1916
2147
  ...modeResult,
1917
2148
  latency_ms: Date.now() - startTime
@@ -1982,7 +2213,8 @@ var CapabilityDeclarationSchema = {
1982
2213
  description: z2.string().optional(),
1983
2214
  capability_types: z2.array(z2.string()).optional(),
1984
2215
  requires_capabilities: z2.array(z2.string()).optional(),
1985
- visibility: z2.enum(["public", "private"]).optional()
2216
+ visibility: z2.enum(["public", "private"]).optional(),
2217
+ expected_duration_ms: z2.number().positive().optional()
1986
2218
  };
1987
2219
  var ApiSkillConfigSchema = z2.object({
1988
2220
  id: z2.string().min(1),
@@ -2315,6 +2547,15 @@ function interpolateValue(value, context) {
2315
2547
 
2316
2548
  // src/skills/pipeline-executor.ts
2317
2549
  var execFileAsync = promisify(execFile);
2550
+ function isTimedOutCommandError(err) {
2551
+ if (!(err instanceof Error)) {
2552
+ return false;
2553
+ }
2554
+ const timeoutCode = err.code;
2555
+ const signal = err.signal;
2556
+ const killed = err.killed;
2557
+ return timeoutCode === "ETIMEDOUT" || err.message.includes("timed out") || killed === true && typeof signal === "string";
2558
+ }
2318
2559
  function shellEscape(value) {
2319
2560
  return "'" + value.replace(/'/g, "'\\''") + "'";
2320
2561
  }
@@ -2366,6 +2607,8 @@ var PipelineExecutor = class {
2366
2607
  async execute(config, params, onProgress) {
2367
2608
  const pipelineConfig = config;
2368
2609
  const steps = pipelineConfig.steps ?? [];
2610
+ const pipelineTimeoutMs = pipelineConfig.timeout_ms;
2611
+ const deadline = typeof pipelineTimeoutMs === "number" ? Date.now() + pipelineTimeoutMs : void 0;
2369
2612
  if (steps.length === 0) {
2370
2613
  return { success: true, result: null };
2371
2614
  }
@@ -2387,11 +2630,41 @@ var PipelineExecutor = class {
2387
2630
  context
2388
2631
  );
2389
2632
  let stepResult;
2633
+ const runWithRemainingDeadline = async (operation) => {
2634
+ if (deadline === void 0) {
2635
+ return operation();
2636
+ }
2637
+ const remainingMs = deadline - Date.now();
2638
+ if (remainingMs <= 0) {
2639
+ throw new Error(`pipeline timed out after ${pipelineTimeoutMs}ms`);
2640
+ }
2641
+ return new Promise((resolve, reject) => {
2642
+ const timeout = setTimeout(() => {
2643
+ reject(new Error(`pipeline timed out after ${pipelineTimeoutMs}ms`));
2644
+ }, remainingMs);
2645
+ operation().then((value) => {
2646
+ clearTimeout(timeout);
2647
+ resolve(value);
2648
+ }).catch((err) => {
2649
+ clearTimeout(timeout);
2650
+ reject(err);
2651
+ });
2652
+ });
2653
+ };
2390
2654
  if ("skill_id" in step && step.skill_id) {
2391
- const subResult = await this.skillExecutor.execute(
2392
- step.skill_id,
2393
- resolvedInputs
2394
- );
2655
+ let subResult;
2656
+ try {
2657
+ subResult = await runWithRemainingDeadline(() => this.skillExecutor.execute(
2658
+ step.skill_id,
2659
+ resolvedInputs
2660
+ ));
2661
+ } catch (err) {
2662
+ const message = err instanceof Error ? err.message : String(err);
2663
+ return {
2664
+ success: false,
2665
+ error: `Step ${i} failed: ${message}`
2666
+ };
2667
+ }
2395
2668
  if (!subResult.success) {
2396
2669
  return {
2397
2670
  success: false,
@@ -2405,10 +2678,16 @@ var PipelineExecutor = class {
2405
2678
  context
2406
2679
  );
2407
2680
  try {
2408
- const { stdout } = await execFileAsync("/bin/sh", ["-c", interpolatedCommand], { timeout: 3e4 });
2681
+ const remainingMs = deadline === void 0 ? void 0 : deadline - Date.now();
2682
+ if (remainingMs !== void 0 && remainingMs <= 0) {
2683
+ throw new Error(`pipeline timed out after ${pipelineTimeoutMs}ms`);
2684
+ }
2685
+ const { stdout } = await execFileAsync("/bin/sh", ["-c", interpolatedCommand], {
2686
+ timeout: remainingMs !== void 0 ? remainingMs : 3e4
2687
+ });
2409
2688
  stepResult = stdout.trim();
2410
2689
  } catch (err) {
2411
- const message = err instanceof Error ? err.message : String(err);
2690
+ const message = pipelineTimeoutMs !== void 0 && isTimedOutCommandError(err) ? `pipeline timed out after ${pipelineTimeoutMs}ms` : err instanceof Error ? err.message : String(err);
2412
2691
  return {
2413
2692
  success: false,
2414
2693
  error: `Step ${i} failed: ${message}`
@@ -2495,17 +2774,43 @@ function executeProcess(config, payload) {
2495
2774
  error: `Invalid agent name: "${config.agent_name}" (only alphanumeric, hyphens, underscores, dots allowed)`
2496
2775
  };
2497
2776
  }
2498
- const inputJson = JSON.stringify(payload);
2777
+ const skillId = config.id;
2778
+ const message = `[AgentBnB Rental Request]
2779
+ You are executing the "${skillId}" skill for an AgentBnB network rental.
2780
+ Read your skills/${skillId}/SKILL.md for detailed instructions.
2781
+
2782
+ Input parameters:
2783
+ ${JSON.stringify(payload.params ?? {}, null, 2)}
2784
+
2785
+ IMPORTANT: Return ONLY a JSON object as your response.
2786
+ Do NOT include explanations, markdown formatting, or code blocks.
2787
+ The JSON should contain the output fields specified in your SKILL.md.
2788
+ If you cannot complete the task, return: {"error": "reason"}`;
2499
2789
  try {
2500
- const stdout = execFileSync("openclaw", ["run", config.agent_name, "--input", inputJson], {
2790
+ const stdout = execFileSync("openclaw", [
2791
+ "agent",
2792
+ "--agent",
2793
+ config.agent_name,
2794
+ "--message",
2795
+ message,
2796
+ "--json",
2797
+ "--local"
2798
+ ], {
2501
2799
  timeout: timeoutMs
2502
2800
  });
2503
2801
  const text = stdout.toString().trim();
2504
- const result = JSON.parse(text);
2505
- return { success: true, result };
2802
+ try {
2803
+ const parsed = JSON.parse(text);
2804
+ return { success: true, result: parsed };
2805
+ } catch {
2806
+ return {
2807
+ success: false,
2808
+ error: `OpenClaw process channel returned invalid JSON: ${text}`
2809
+ };
2810
+ }
2506
2811
  } catch (err) {
2507
- const message = err instanceof Error ? err.message : String(err);
2508
- return { success: false, error: message };
2812
+ const message2 = err instanceof Error ? err.message : String(err);
2813
+ return { success: false, error: message2 };
2509
2814
  }
2510
2815
  }
2511
2816
  async function executeTelegram(config, payload) {
@@ -3077,18 +3382,29 @@ function decompose(task, _availableCapabilities) {
3077
3382
  return [];
3078
3383
  }
3079
3384
 
3080
- // src/autonomy/auto-request.ts
3081
- import { randomUUID as randomUUID11 } from "crypto";
3082
-
3083
3385
  // src/gateway/client.ts
3084
3386
  import { randomUUID as randomUUID8 } from "crypto";
3085
3387
  import { Agent } from "undici";
3388
+ var REQUEST_TIMEOUT_FALLBACK_MS = 3e5;
3389
+ var REQUEST_TIMEOUT_GRACE_MS = 3e4;
3390
+ var REQUEST_TIMEOUT_EXPECTED_MULTIPLIER = 1.5;
3086
3391
  var gatewayAgent = new Agent({
3087
3392
  keepAliveTimeout: 3e4,
3088
3393
  keepAliveMaxTimeout: 6e4,
3089
3394
  connections: 10,
3090
3395
  pipelining: 1
3091
3396
  });
3397
+ function deriveRequestTimeoutMs(hint) {
3398
+ const expectedDurationMs = hint?.expected_duration_ms;
3399
+ if (typeof expectedDurationMs === "number" && expectedDurationMs > 0) {
3400
+ return Math.ceil(expectedDurationMs * REQUEST_TIMEOUT_EXPECTED_MULTIPLIER) + REQUEST_TIMEOUT_GRACE_MS;
3401
+ }
3402
+ const hardTimeoutMs = hint?.hard_timeout_ms;
3403
+ if (typeof hardTimeoutMs === "number" && hardTimeoutMs > 0) {
3404
+ return Math.ceil(hardTimeoutMs) + REQUEST_TIMEOUT_GRACE_MS;
3405
+ }
3406
+ return REQUEST_TIMEOUT_FALLBACK_MS;
3407
+ }
3092
3408
  function buildGatewayAuthHeaders(payload, token, identity) {
3093
3409
  const headers = { "Content-Type": "application/json" };
3094
3410
  if (identity) {
@@ -3103,7 +3419,17 @@ function buildGatewayAuthHeaders(payload, token, identity) {
3103
3419
  return headers;
3104
3420
  }
3105
3421
  async function requestCapability(opts) {
3106
- const { gatewayUrl, token, cardId, params = {}, timeoutMs = 3e5, escrowReceipt, identity } = opts;
3422
+ const {
3423
+ gatewayUrl,
3424
+ token,
3425
+ cardId,
3426
+ params = {},
3427
+ timeoutMs: timeoutOverrideMs,
3428
+ timeoutHint,
3429
+ escrowReceipt,
3430
+ identity
3431
+ } = opts;
3432
+ const timeoutMs = timeoutOverrideMs ?? deriveRequestTimeoutMs(timeoutHint);
3107
3433
  const id = randomUUID8();
3108
3434
  const payload = {
3109
3435
  jsonrpc: "2.0",
@@ -3155,82 +3481,640 @@ async function requestCapabilityBatch(gatewayUrl, token, items, opts = {}) {
3155
3481
  params: item.params,
3156
3482
  escrowReceipt: item.escrowReceipt,
3157
3483
  timeoutMs: opts.timeoutMs,
3484
+ timeoutHint: opts.timeoutHint,
3158
3485
  identity: opts.identity
3159
3486
  });
3160
3487
  return /* @__PURE__ */ new Map([[item.id, result]]);
3161
3488
  }
3162
- const { timeoutMs = 3e5, identity } = opts;
3163
- const batchPayload = items.map((item) => ({
3164
- jsonrpc: "2.0",
3165
- id: item.id,
3166
- method: "capability.execute",
3167
- params: {
3168
- card_id: item.cardId,
3169
- ...item.params,
3170
- ...item.escrowReceipt ? { escrow_receipt: item.escrowReceipt } : {}
3489
+ const { timeoutMs: timeoutOverrideMs, timeoutHint, identity } = opts;
3490
+ const timeoutMs = timeoutOverrideMs ?? deriveRequestTimeoutMs(timeoutHint);
3491
+ const batchPayload = items.map((item) => ({
3492
+ jsonrpc: "2.0",
3493
+ id: item.id,
3494
+ method: "capability.execute",
3495
+ params: {
3496
+ card_id: item.cardId,
3497
+ ...item.params,
3498
+ ...item.escrowReceipt ? { escrow_receipt: item.escrowReceipt } : {}
3499
+ }
3500
+ }));
3501
+ const headers = buildGatewayAuthHeaders(batchPayload, token, identity);
3502
+ const controller = new AbortController();
3503
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3504
+ let response;
3505
+ try {
3506
+ response = await fetch(`${gatewayUrl}/rpc`, {
3507
+ method: "POST",
3508
+ headers,
3509
+ body: JSON.stringify(batchPayload),
3510
+ signal: controller.signal,
3511
+ dispatcher: gatewayAgent
3512
+ });
3513
+ } catch (err) {
3514
+ clearTimeout(timer);
3515
+ const isTimeout = err instanceof Error && err.name === "AbortError";
3516
+ throw new AgentBnBError(
3517
+ isTimeout ? "Batch request timed out" : `Network error: ${String(err)}`,
3518
+ isTimeout ? "TIMEOUT" : "NETWORK_ERROR"
3519
+ );
3520
+ } finally {
3521
+ clearTimeout(timer);
3522
+ }
3523
+ const body = await response.json();
3524
+ const results = /* @__PURE__ */ new Map();
3525
+ for (const resp of body) {
3526
+ if (resp.error) {
3527
+ results.set(resp.id, new AgentBnBError(resp.error.message, `RPC_ERROR_${resp.error.code}`));
3528
+ } else {
3529
+ results.set(resp.id, resp.result);
3530
+ }
3531
+ }
3532
+ return results;
3533
+ }
3534
+ async function requestViaRelay(relay, opts) {
3535
+ const timeoutMs = opts.timeoutMs ?? deriveRequestTimeoutMs(opts.timeoutHint);
3536
+ try {
3537
+ return await relay.request({
3538
+ targetOwner: opts.targetOwner,
3539
+ targetAgentId: opts.targetAgentId,
3540
+ cardId: opts.cardId,
3541
+ skillId: opts.skillId,
3542
+ params: opts.params ?? {},
3543
+ requester: opts.requester,
3544
+ escrowReceipt: opts.escrowReceipt,
3545
+ timeoutMs
3546
+ });
3547
+ } catch (err) {
3548
+ const message = err instanceof Error ? err.message : String(err);
3549
+ if (message.includes("timeout")) {
3550
+ throw new AgentBnBError(message, "TIMEOUT");
3551
+ }
3552
+ if (message.includes("offline")) {
3553
+ throw new AgentBnBError(message, "AGENT_OFFLINE");
3554
+ }
3555
+ throw new AgentBnBError(message, "RELAY_ERROR");
3556
+ }
3557
+ }
3558
+
3559
+ // src/gateway/relay-dispatch.ts
3560
+ import { randomUUID as randomUUID10 } from "crypto";
3561
+
3562
+ // src/relay/websocket-client.ts
3563
+ import WebSocket from "ws";
3564
+ import { randomUUID as randomUUID9 } from "crypto";
3565
+
3566
+ // src/relay/types.ts
3567
+ import { z as z3 } from "zod";
3568
+ var RegisterMessageSchema = z3.object({
3569
+ type: z3.literal("register"),
3570
+ owner: z3.string().min(1),
3571
+ /** V8: Cryptographic agent identity. When present, used as the canonical key. */
3572
+ agent_id: z3.string().optional(),
3573
+ /** V8 Phase 3: Server identifier for multi-agent delegation. */
3574
+ server_id: z3.string().optional(),
3575
+ token: z3.string().min(1),
3576
+ card: z3.record(z3.unknown()),
3577
+ // CapabilityCard (validated separately)
3578
+ cards: z3.array(z3.record(z3.unknown())).optional(),
3579
+ // Additional cards (e.g., conductor card)
3580
+ /** V8 Phase 3: Additional agents served by this server (multi-agent registration). */
3581
+ agents: z3.array(z3.object({
3582
+ agent_id: z3.string().min(1),
3583
+ display_name: z3.string().min(1),
3584
+ cards: z3.array(z3.record(z3.unknown())),
3585
+ delegation_token: z3.record(z3.unknown()).optional()
3586
+ })).optional()
3587
+ });
3588
+ var RegisteredMessageSchema = z3.object({
3589
+ type: z3.literal("registered"),
3590
+ agent_id: z3.string()
3591
+ });
3592
+ var RelayRequestMessageSchema = z3.object({
3593
+ type: z3.literal("relay_request"),
3594
+ id: z3.string().uuid(),
3595
+ target_owner: z3.string().min(1),
3596
+ /** V8: Target agent's cryptographic identity. Preferred over target_owner. */
3597
+ target_agent_id: z3.string().optional(),
3598
+ card_id: z3.string(),
3599
+ skill_id: z3.string().optional(),
3600
+ params: z3.record(z3.unknown()).default({}),
3601
+ requester: z3.string().optional(),
3602
+ escrow_receipt: z3.record(z3.unknown()).optional()
3603
+ });
3604
+ var IncomingRequestMessageSchema = z3.object({
3605
+ type: z3.literal("incoming_request"),
3606
+ id: z3.string().uuid(),
3607
+ from_owner: z3.string().min(1),
3608
+ card_id: z3.string(),
3609
+ skill_id: z3.string().optional(),
3610
+ params: z3.record(z3.unknown()).default({}),
3611
+ requester: z3.string().optional(),
3612
+ escrow_receipt: z3.record(z3.unknown()).optional()
3613
+ });
3614
+ var RelayResponseMessageSchema = z3.object({
3615
+ type: z3.literal("relay_response"),
3616
+ id: z3.string().uuid(),
3617
+ result: z3.unknown().optional(),
3618
+ error: z3.object({
3619
+ code: z3.number(),
3620
+ message: z3.string()
3621
+ }).optional()
3622
+ });
3623
+ var ResponseMessageSchema = z3.object({
3624
+ type: z3.literal("response"),
3625
+ id: z3.string().uuid(),
3626
+ result: z3.unknown().optional(),
3627
+ error: z3.object({
3628
+ code: z3.number(),
3629
+ message: z3.string()
3630
+ }).optional()
3631
+ });
3632
+ var ErrorMessageSchema = z3.object({
3633
+ type: z3.literal("error"),
3634
+ code: z3.string(),
3635
+ message: z3.string(),
3636
+ request_id: z3.string().optional()
3637
+ });
3638
+ var RelayProgressMessageSchema = z3.object({
3639
+ type: z3.literal("relay_progress"),
3640
+ id: z3.string().uuid(),
3641
+ // request ID this progress relates to
3642
+ progress: z3.number().min(0).max(100).optional(),
3643
+ // optional percentage
3644
+ message: z3.string().optional()
3645
+ // optional status message
3646
+ });
3647
+ var RelayStartedMessageSchema = z3.object({
3648
+ type: z3.literal("relay_started"),
3649
+ id: z3.string().uuid(),
3650
+ message: z3.string().optional()
3651
+ });
3652
+ var HeartbeatMessageSchema = z3.object({
3653
+ type: z3.literal("heartbeat"),
3654
+ owner: z3.string().min(1),
3655
+ capacity: z3.object({
3656
+ current_load: z3.number(),
3657
+ max_concurrent: z3.number(),
3658
+ queue_depth: z3.number()
3659
+ }),
3660
+ self_summary: z3.object({
3661
+ capabilities: z3.array(z3.string()),
3662
+ success_rate: z3.number(),
3663
+ credit_balance: z3.number(),
3664
+ total_completed: z3.number(),
3665
+ provider_number: z3.number(),
3666
+ reliability: z3.object({
3667
+ current_streak: z3.number(),
3668
+ repeat_hire_rate: z3.number(),
3669
+ avg_feedback: z3.number()
3670
+ })
3671
+ })
3672
+ });
3673
+ var EscrowHoldMessageSchema = z3.object({
3674
+ type: z3.literal("escrow_hold"),
3675
+ consumer_agent_id: z3.string().min(1),
3676
+ provider_agent_id: z3.string().min(1),
3677
+ skill_id: z3.string().min(1),
3678
+ amount: z3.number().positive(),
3679
+ request_id: z3.string().uuid(),
3680
+ signature: z3.string().optional(),
3681
+ public_key: z3.string().optional()
3682
+ });
3683
+ var EscrowHoldConfirmedMessageSchema = z3.object({
3684
+ type: z3.literal("escrow_hold_confirmed"),
3685
+ request_id: z3.string(),
3686
+ escrow_id: z3.string(),
3687
+ hold_amount: z3.number(),
3688
+ consumer_remaining: z3.number()
3689
+ });
3690
+ var EscrowSettleMessageSchema = z3.object({
3691
+ type: z3.literal("escrow_settle"),
3692
+ escrow_id: z3.string().min(1),
3693
+ request_id: z3.string().uuid(),
3694
+ success: z3.boolean(),
3695
+ failure_reason: z3.enum(["bad_execution", "overload", "timeout", "auth_error", "not_found"]).optional(),
3696
+ result_hash: z3.string().optional(),
3697
+ signature: z3.string().optional(),
3698
+ public_key: z3.string().optional(),
3699
+ consumer_agent_id: z3.string().optional()
3700
+ });
3701
+ var EscrowSettledMessageSchema = z3.object({
3702
+ type: z3.literal("escrow_settled"),
3703
+ escrow_id: z3.string(),
3704
+ request_id: z3.string(),
3705
+ provider_earned: z3.number(),
3706
+ network_fee: z3.number(),
3707
+ consumer_remaining: z3.number(),
3708
+ provider_balance: z3.number()
3709
+ });
3710
+ var BalanceSyncMessageSchema = z3.object({
3711
+ type: z3.literal("balance_sync"),
3712
+ agent_id: z3.string().min(1)
3713
+ });
3714
+ var BalanceSyncResponseMessageSchema = z3.object({
3715
+ type: z3.literal("balance_sync_response"),
3716
+ agent_id: z3.string(),
3717
+ balance: z3.number()
3718
+ });
3719
+ var RelayMessageSchema = z3.discriminatedUnion("type", [
3720
+ RegisterMessageSchema,
3721
+ RegisteredMessageSchema,
3722
+ RelayRequestMessageSchema,
3723
+ IncomingRequestMessageSchema,
3724
+ RelayResponseMessageSchema,
3725
+ ResponseMessageSchema,
3726
+ ErrorMessageSchema,
3727
+ RelayProgressMessageSchema,
3728
+ RelayStartedMessageSchema,
3729
+ HeartbeatMessageSchema,
3730
+ EscrowHoldMessageSchema,
3731
+ EscrowHoldConfirmedMessageSchema,
3732
+ EscrowSettleMessageSchema,
3733
+ EscrowSettledMessageSchema,
3734
+ BalanceSyncMessageSchema,
3735
+ BalanceSyncResponseMessageSchema
3736
+ ]);
3737
+
3738
+ // src/relay/websocket-client.ts
3739
+ var RelayClient = class {
3740
+ ws = null;
3741
+ opts;
3742
+ pendingRequests = /* @__PURE__ */ new Map();
3743
+ reconnectAttempts = 0;
3744
+ reconnectTimer = null;
3745
+ intentionalClose = false;
3746
+ registered = false;
3747
+ pongTimeout = null;
3748
+ pingInterval = null;
3749
+ constructor(opts) {
3750
+ this.opts = opts;
3751
+ }
3752
+ /**
3753
+ * Connect to the registry relay and register.
3754
+ * Resolves when registration is acknowledged.
3755
+ */
3756
+ async connect() {
3757
+ return new Promise((resolve, reject) => {
3758
+ this.intentionalClose = false;
3759
+ this.registered = false;
3760
+ const wsUrl = this.buildWsUrl();
3761
+ this.ws = new WebSocket(wsUrl);
3762
+ let resolved = false;
3763
+ this.ws.on("open", () => {
3764
+ this.reconnectAttempts = 0;
3765
+ this.startPingInterval();
3766
+ this.send({
3767
+ type: "register",
3768
+ owner: this.opts.owner,
3769
+ ...this.opts.agent_id ? { agent_id: this.opts.agent_id } : {},
3770
+ ...this.opts.server_id ? { server_id: this.opts.server_id } : {},
3771
+ token: this.opts.token,
3772
+ card: this.opts.card,
3773
+ ...this.opts.cards && this.opts.cards.length > 0 ? { cards: this.opts.cards } : {},
3774
+ ...this.opts.agents && this.opts.agents.length > 0 ? { agents: this.opts.agents } : {}
3775
+ });
3776
+ });
3777
+ this.ws.on("message", (raw) => {
3778
+ this.handleMessage(raw, (err) => {
3779
+ if (!resolved) {
3780
+ resolved = true;
3781
+ if (err) reject(err);
3782
+ else resolve();
3783
+ }
3784
+ });
3785
+ });
3786
+ this.ws.on("close", () => {
3787
+ this.cleanup();
3788
+ if (!this.intentionalClose) {
3789
+ if (!resolved) {
3790
+ resolved = true;
3791
+ reject(new Error("WebSocket closed before registration"));
3792
+ }
3793
+ this.scheduleReconnect();
3794
+ }
3795
+ });
3796
+ this.ws.on("error", (err) => {
3797
+ if (!resolved) {
3798
+ resolved = true;
3799
+ reject(err);
3800
+ }
3801
+ });
3802
+ setTimeout(() => {
3803
+ if (!resolved) {
3804
+ resolved = true;
3805
+ reject(new Error("Connection timeout"));
3806
+ this.ws?.close();
3807
+ }
3808
+ }, 1e4);
3809
+ });
3810
+ }
3811
+ /**
3812
+ * Disconnect from the registry relay.
3813
+ */
3814
+ disconnect() {
3815
+ this.intentionalClose = true;
3816
+ this.cleanup();
3817
+ if (this.ws) {
3818
+ try {
3819
+ this.ws.close(1e3, "Client disconnect");
3820
+ } catch {
3821
+ }
3822
+ this.ws = null;
3823
+ }
3824
+ for (const [id, pending] of this.pendingRequests) {
3825
+ clearTimeout(pending.timeout);
3826
+ pending.reject(new Error("Client disconnected"));
3827
+ this.pendingRequests.delete(id);
3828
+ }
3829
+ }
3830
+ /**
3831
+ * Send a relay request to another agent via the registry.
3832
+ * @returns The result from the target agent.
3833
+ */
3834
+ async request(opts) {
3835
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.registered) {
3836
+ throw new Error("Not connected to registry relay");
3837
+ }
3838
+ const id = randomUUID9();
3839
+ const timeoutMs = opts.timeoutMs ?? 3e5;
3840
+ return new Promise((resolve, reject) => {
3841
+ const timeout = setTimeout(() => {
3842
+ this.pendingRequests.delete(id);
3843
+ reject(new Error("Relay request timeout"));
3844
+ }, timeoutMs);
3845
+ this.pendingRequests.set(id, { resolve, reject, timeout, timeoutMs, onProgress: opts.onProgress });
3846
+ this.send({
3847
+ type: "relay_request",
3848
+ id,
3849
+ target_owner: opts.targetOwner,
3850
+ ...opts.targetAgentId ? { target_agent_id: opts.targetAgentId } : {},
3851
+ card_id: opts.cardId,
3852
+ skill_id: opts.skillId,
3853
+ params: opts.params,
3854
+ requester: opts.requester ?? this.opts.owner,
3855
+ escrow_receipt: opts.escrowReceipt
3856
+ });
3857
+ });
3858
+ }
3859
+ /**
3860
+ * Send a relay_started message to acknowledge that provider has started work.
3861
+ *
3862
+ * @param requestId - The relay request ID being processed.
3863
+ * @param message - Optional status message.
3864
+ */
3865
+ sendStarted(requestId, message) {
3866
+ this.send({
3867
+ type: "relay_started",
3868
+ id: requestId,
3869
+ ...message ? { message } : {}
3870
+ });
3871
+ }
3872
+ /**
3873
+ * Send a relay_progress message to the relay server for a given request.
3874
+ * Used by the onRequest handler to forward SkillExecutor progress updates
3875
+ * to the requesting agent while work is in-flight.
3876
+ *
3877
+ * @param requestId - The relay request ID to associate progress with.
3878
+ * @param info - Progress details (step, total, message).
3879
+ */
3880
+ sendProgress(requestId, info) {
3881
+ this.send({
3882
+ type: "relay_progress",
3883
+ id: requestId,
3884
+ progress: Math.round(info.step / info.total * 100),
3885
+ message: info.message
3886
+ });
3887
+ }
3888
+ /** Whether the client is connected and registered */
3889
+ get isConnected() {
3890
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.registered;
3891
+ }
3892
+ // ── Private methods ─────────────────────────────────────────────────────────
3893
+ buildWsUrl() {
3894
+ let url = this.opts.registryUrl;
3895
+ if (url.startsWith("http://")) {
3896
+ url = "ws://" + url.slice(7);
3897
+ } else if (url.startsWith("https://")) {
3898
+ url = "wss://" + url.slice(8);
3899
+ } else if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
3900
+ url = "wss://" + url;
3901
+ }
3902
+ if (!url.endsWith("/ws")) {
3903
+ url = url.replace(/\/$/, "") + "/ws";
3904
+ }
3905
+ return url;
3906
+ }
3907
+ handleMessage(raw, onRegistered) {
3908
+ let data;
3909
+ try {
3910
+ data = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf-8"));
3911
+ } catch {
3912
+ return;
3913
+ }
3914
+ const parsed = RelayMessageSchema.safeParse(data);
3915
+ if (!parsed.success) return;
3916
+ const msg = parsed.data;
3917
+ switch (msg.type) {
3918
+ case "registered":
3919
+ this.registered = true;
3920
+ if (!this.opts.silent) {
3921
+ console.log(` \u2713 Registered with registry (agent_id: ${msg.agent_id})`);
3922
+ }
3923
+ onRegistered?.();
3924
+ break;
3925
+ case "incoming_request":
3926
+ this.handleIncomingRequest(msg);
3927
+ break;
3928
+ case "response":
3929
+ this.handleResponse(msg);
3930
+ break;
3931
+ case "error":
3932
+ this.handleError(msg);
3933
+ break;
3934
+ case "relay_progress":
3935
+ this.handleProgress(msg);
3936
+ break;
3937
+ default:
3938
+ break;
3939
+ }
3940
+ }
3941
+ async handleIncomingRequest(msg) {
3942
+ try {
3943
+ const result = await this.opts.onRequest(msg);
3944
+ this.send({
3945
+ type: "relay_response",
3946
+ id: msg.id,
3947
+ result: result.result,
3948
+ error: result.error
3949
+ });
3950
+ } catch (err) {
3951
+ this.send({
3952
+ type: "relay_response",
3953
+ id: msg.id,
3954
+ error: {
3955
+ code: -32603,
3956
+ message: err instanceof Error ? err.message : "Internal error"
3957
+ }
3958
+ });
3959
+ }
3960
+ }
3961
+ handleResponse(msg) {
3962
+ const pending = this.pendingRequests.get(msg.id);
3963
+ if (!pending) return;
3964
+ clearTimeout(pending.timeout);
3965
+ this.pendingRequests.delete(msg.id);
3966
+ if (msg.error) {
3967
+ pending.reject(new Error(msg.error.message));
3968
+ } else {
3969
+ pending.resolve(msg.result);
3970
+ }
3971
+ }
3972
+ handleError(msg) {
3973
+ if (msg.request_id) {
3974
+ const pending = this.pendingRequests.get(msg.request_id);
3975
+ if (pending) {
3976
+ clearTimeout(pending.timeout);
3977
+ this.pendingRequests.delete(msg.request_id);
3978
+ pending.reject(new Error(`${msg.code}: ${msg.message}`));
3979
+ }
3980
+ }
3981
+ }
3982
+ handleProgress(msg) {
3983
+ const pending = this.pendingRequests.get(msg.id);
3984
+ if (!pending) return;
3985
+ clearTimeout(pending.timeout);
3986
+ const newTimeout = setTimeout(() => {
3987
+ this.pendingRequests.delete(msg.id);
3988
+ pending.reject(new Error("Relay request timeout"));
3989
+ }, pending.timeoutMs);
3990
+ pending.timeout = newTimeout;
3991
+ if (pending.onProgress) {
3992
+ pending.onProgress({ id: msg.id, progress: msg.progress, message: msg.message });
3171
3993
  }
3172
- }));
3173
- const headers = buildGatewayAuthHeaders(batchPayload, token, identity);
3174
- const controller = new AbortController();
3175
- const timer = setTimeout(() => controller.abort(), timeoutMs);
3176
- let response;
3177
- try {
3178
- response = await fetch(`${gatewayUrl}/rpc`, {
3179
- method: "POST",
3180
- headers,
3181
- body: JSON.stringify(batchPayload),
3182
- signal: controller.signal,
3183
- dispatcher: gatewayAgent
3994
+ }
3995
+ send(msg) {
3996
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
3997
+ this.ws.send(JSON.stringify(msg));
3998
+ }
3999
+ }
4000
+ startPingInterval() {
4001
+ this.stopPingInterval();
4002
+ this.pingInterval = setInterval(() => {
4003
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
4004
+ this.ws.ping();
4005
+ this.pongTimeout = setTimeout(() => {
4006
+ if (!this.opts.silent) {
4007
+ console.log(" \u26A0 Registry pong timeout, reconnecting...");
4008
+ }
4009
+ this.ws?.terminate();
4010
+ }, 15e3);
4011
+ }
4012
+ }, 3e4);
4013
+ this.ws?.on("pong", () => {
4014
+ if (this.pongTimeout) {
4015
+ clearTimeout(this.pongTimeout);
4016
+ this.pongTimeout = null;
4017
+ }
3184
4018
  });
3185
- } catch (err) {
3186
- clearTimeout(timer);
3187
- const isTimeout = err instanceof Error && err.name === "AbortError";
3188
- throw new AgentBnBError(
3189
- isTimeout ? "Batch request timed out" : `Network error: ${String(err)}`,
3190
- isTimeout ? "TIMEOUT" : "NETWORK_ERROR"
3191
- );
3192
- } finally {
3193
- clearTimeout(timer);
3194
4019
  }
3195
- const body = await response.json();
3196
- const results = /* @__PURE__ */ new Map();
3197
- for (const resp of body) {
3198
- if (resp.error) {
3199
- results.set(resp.id, new AgentBnBError(resp.error.message, `RPC_ERROR_${resp.error.code}`));
3200
- } else {
3201
- results.set(resp.id, resp.result);
4020
+ stopPingInterval() {
4021
+ if (this.pingInterval) {
4022
+ clearInterval(this.pingInterval);
4023
+ this.pingInterval = null;
4024
+ }
4025
+ if (this.pongTimeout) {
4026
+ clearTimeout(this.pongTimeout);
4027
+ this.pongTimeout = null;
3202
4028
  }
3203
4029
  }
3204
- return results;
3205
- }
3206
- async function requestViaRelay(relay, opts) {
4030
+ cleanup() {
4031
+ this.stopPingInterval();
4032
+ this.registered = false;
4033
+ }
4034
+ scheduleReconnect() {
4035
+ if (this.intentionalClose) return;
4036
+ if (this.reconnectTimer) return;
4037
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
4038
+ this.reconnectAttempts++;
4039
+ if (!this.opts.silent) {
4040
+ console.log(` \u21BB Reconnecting to registry in ${delay / 1e3}s...`);
4041
+ }
4042
+ this.reconnectTimer = setTimeout(async () => {
4043
+ this.reconnectTimer = null;
4044
+ try {
4045
+ await this.connect();
4046
+ if (!this.opts.silent) {
4047
+ console.log(" \u2713 Reconnected to registry");
4048
+ }
4049
+ } catch {
4050
+ }
4051
+ }, delay);
4052
+ }
4053
+ };
4054
+
4055
+ // src/gateway/relay-dispatch.ts
4056
+ async function requestViaTemporaryRelay(opts) {
4057
+ const {
4058
+ registryUrl,
4059
+ owner,
4060
+ token,
4061
+ targetOwner,
4062
+ targetAgentId,
4063
+ cardId,
4064
+ skillId,
4065
+ params,
4066
+ timeoutMs = 3e5
4067
+ } = opts;
4068
+ const requesterId = `${owner}:req:${randomUUID10()}`;
4069
+ const relay = new RelayClient({
4070
+ registryUrl,
4071
+ owner: requesterId,
4072
+ token,
4073
+ card: {
4074
+ spec_version: "1.0",
4075
+ id: randomUUID10(),
4076
+ owner: requesterId,
4077
+ name: requesterId,
4078
+ description: "Temporary relay requester",
4079
+ level: 1,
4080
+ inputs: [],
4081
+ outputs: [],
4082
+ pricing: { credits_per_call: 0 },
4083
+ availability: { online: false }
4084
+ },
4085
+ onRequest: async () => ({
4086
+ error: { code: -32601, message: "Temporary relay requester does not serve capabilities" }
4087
+ }),
4088
+ silent: true
4089
+ });
3207
4090
  try {
3208
- return await relay.request({
3209
- targetOwner: opts.targetOwner,
3210
- cardId: opts.cardId,
3211
- skillId: opts.skillId,
3212
- params: opts.params ?? {},
3213
- requester: opts.requester,
3214
- escrowReceipt: opts.escrowReceipt,
3215
- timeoutMs: opts.timeoutMs
3216
- });
4091
+ await relay.connect();
3217
4092
  } catch (err) {
4093
+ relay.disconnect();
3218
4094
  const message = err instanceof Error ? err.message : String(err);
3219
- if (message.includes("timeout")) {
3220
- throw new AgentBnBError(message, "TIMEOUT");
3221
- }
3222
- if (message.includes("offline")) {
3223
- throw new AgentBnBError(message, "AGENT_OFFLINE");
3224
- }
3225
- throw new AgentBnBError(message, "RELAY_ERROR");
4095
+ throw new AgentBnBError(`Relay connection failed: ${message}`, "RELAY_UNAVAILABLE");
4096
+ }
4097
+ try {
4098
+ return await requestViaRelay(relay, {
4099
+ targetOwner,
4100
+ targetAgentId,
4101
+ cardId,
4102
+ skillId,
4103
+ params,
4104
+ requester: owner,
4105
+ // actual owner for credit tracking on relay server
4106
+ timeoutMs
4107
+ });
4108
+ } finally {
4109
+ relay.disconnect();
3226
4110
  }
3227
4111
  }
3228
4112
 
3229
4113
  // src/autonomy/tiers.ts
3230
- import { randomUUID as randomUUID9 } from "crypto";
4114
+ import { randomUUID as randomUUID11 } from "crypto";
3231
4115
 
3232
4116
  // src/autonomy/pending-requests.ts
3233
- import { randomUUID as randomUUID10 } from "crypto";
4117
+ import { randomUUID as randomUUID12 } from "crypto";
3234
4118
 
3235
4119
  // src/cli/peers.ts
3236
4120
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
@@ -3247,8 +4131,17 @@ function minMaxNormalize(values) {
3247
4131
  }
3248
4132
  return values.map((v) => (v - min) / (max - min));
3249
4133
  }
3250
- function scorePeers(candidates, selfOwner) {
3251
- const eligible = candidates.filter((c) => c.card.owner !== selfOwner);
4134
+ function scorePeers(candidates, selfOwner, registryDb) {
4135
+ const selfIdentity = registryDb ? resolveCanonicalIdentity(registryDb, selfOwner) : null;
4136
+ const eligible = candidates.filter((c) => {
4137
+ if (c.card.owner === selfOwner) return false;
4138
+ if (!registryDb || !selfIdentity?.resolved) return true;
4139
+ if (typeof c.card.agent_id === "string" && c.card.agent_id.length > 0) {
4140
+ return c.card.agent_id !== selfIdentity.agent_id;
4141
+ }
4142
+ const peerIdentity = resolveCanonicalIdentity(registryDb, c.card.owner);
4143
+ return !peerIdentity.resolved || peerIdentity.agent_id !== selfIdentity.agent_id;
4144
+ });
3252
4145
  if (eligible.length === 0) return [];
3253
4146
  const successRates = eligible.map((c) => {
3254
4147
  if (c.skillMetadata?.success_rate !== void 0) {
@@ -3473,7 +4366,7 @@ function buildConductorCard(owner) {
3473
4366
  return CapabilityCardV2Schema.parse(card);
3474
4367
  }
3475
4368
  function registerConductorCard(db) {
3476
- const card = buildConductorCard();
4369
+ const card = attachCanonicalAgentId(db, buildConductorCard());
3477
4370
  const now = (/* @__PURE__ */ new Date()).toISOString();
3478
4371
  const existing = db.prepare("SELECT id FROM capability_cards WHERE id = ?").get(card.id);
3479
4372
  if (existing) {
@@ -3574,6 +4467,7 @@ async function executeSingleTask(pt, gatewayToken, timeoutMs, requesterOwner, re
3574
4467
  async function orchestrate(opts) {
3575
4468
  const { subtasks, matches, gatewayToken, resolveAgentUrl, timeoutMs = 3e5, maxBudget, relayClient, requesterOwner } = opts;
3576
4469
  const startTime = Date.now();
4470
+ const hardDeadline = startTime + timeoutMs;
3577
4471
  const teamMemberMap = /* @__PURE__ */ new Map();
3578
4472
  if (opts.team) {
3579
4473
  for (const member of opts.team.matched) {
@@ -3595,6 +4489,12 @@ async function orchestrate(opts) {
3595
4489
  const waves = computeWaves(subtasks);
3596
4490
  const subtaskMap = new Map(subtasks.map((s) => [s.id, s]));
3597
4491
  for (const wave of waves) {
4492
+ const remainingWaveMs = hardDeadline - Date.now();
4493
+ if (remainingWaveMs <= 0) {
4494
+ errors.push(`Orchestration timed out after ${timeoutMs}ms`);
4495
+ break;
4496
+ }
4497
+ const waveTimeoutMs = Math.max(1, Math.min(timeoutMs, remainingWaveMs));
3598
4498
  if (maxBudget !== void 0 && totalCredits >= maxBudget) {
3599
4499
  errors.push(`Budget exceeded: spent ${totalCredits} cr, max ${maxBudget} cr`);
3600
4500
  break;
@@ -3668,7 +4568,7 @@ async function orchestrate(opts) {
3668
4568
  gatewayUrl,
3669
4569
  gatewayToken,
3670
4570
  items.map(({ _pt, ...item }) => item),
3671
- { timeoutMs }
4571
+ { timeoutMs: waveTimeoutMs }
3672
4572
  );
3673
4573
  return items.map((item) => {
3674
4574
  const res = batchResults.get(item.id);
@@ -3692,7 +4592,7 @@ async function orchestrate(opts) {
3692
4592
  } catch (batchErr) {
3693
4593
  return Promise.all(group.map(async (pt) => {
3694
4594
  try {
3695
- const res = await executeSingleTask(pt, gatewayToken, timeoutMs, requesterOwner, relayClient, resolveAgentUrl);
4595
+ const res = await executeSingleTask(pt, gatewayToken, waveTimeoutMs, requesterOwner, relayClient, resolveAgentUrl);
3696
4596
  return { status: "fulfilled", value: res };
3697
4597
  } catch (err) {
3698
4598
  return { status: "rejected", reason: err };
@@ -3706,7 +4606,7 @@ async function orchestrate(opts) {
3706
4606
  batchPromises.push(
3707
4607
  (async () => {
3708
4608
  try {
3709
- const res = await executeSingleTask(pt, gatewayToken, timeoutMs, requesterOwner, relayClient, resolveAgentUrl);
4609
+ const res = await executeSingleTask(pt, gatewayToken, waveTimeoutMs, requesterOwner, relayClient, resolveAgentUrl);
3710
4610
  return [{ status: "fulfilled", value: res }];
3711
4611
  } catch (err) {
3712
4612
  return [{ status: "rejected", reason: err }];
@@ -3719,7 +4619,7 @@ async function orchestrate(opts) {
3719
4619
  batchPromises.push(
3720
4620
  (async () => {
3721
4621
  try {
3722
- const res = await executeSingleTask(pt, gatewayToken, timeoutMs, requesterOwner, relayClient, resolveAgentUrl);
4622
+ const res = await executeSingleTask(pt, gatewayToken, waveTimeoutMs, requesterOwner, relayClient, resolveAgentUrl);
3723
4623
  return [{ status: "fulfilled", value: res }];
3724
4624
  } catch (err) {
3725
4625
  return [{ status: "rejected", reason: err }];
@@ -3796,7 +4696,7 @@ var BudgetManager = class {
3796
4696
  };
3797
4697
 
3798
4698
  // src/conductor/team-formation.ts
3799
- import { randomUUID as randomUUID12 } from "crypto";
4699
+ import { randomUUID as randomUUID13 } from "crypto";
3800
4700
  function selectByStrategy(matches, strategy) {
3801
4701
  if (matches.length === 0) return void 0;
3802
4702
  if (strategy === "balanced") {
@@ -3813,7 +4713,7 @@ function selectByStrategy(matches, strategy) {
3813
4713
  }
3814
4714
  async function formTeam(opts) {
3815
4715
  const { subtasks, strategy, db, conductorOwner, registryUrl } = opts;
3816
- const team_id = randomUUID12();
4716
+ const team_id = randomUUID13();
3817
4717
  if (subtasks.length === 0) {
3818
4718
  return { team_id, strategy, matched: [], unrouted: [] };
3819
4719
  }
@@ -3909,6 +4809,7 @@ var ConductorMode = class {
3909
4809
  * @returns Execution result without latency_ms (added by SkillExecutor).
3910
4810
  */
3911
4811
  async execute(config, params, onProgress) {
4812
+ const skillTimeoutMs = typeof config.timeout_ms === "number" ? config.timeout_ms : void 0;
3912
4813
  const conductorSkill = config.conductor_skill;
3913
4814
  if (conductorSkill !== "orchestrate" && conductorSkill !== "plan") {
3914
4815
  return {
@@ -3948,7 +4849,7 @@ var ConductorMode = class {
3948
4849
  decomposition_depth: decompositionDepth + 1,
3949
4850
  orchestration_depth: orchestrationDepth + 1
3950
4851
  },
3951
- timeoutMs: 3e4
4852
+ timeoutMs: skillTimeoutMs ?? 3e4
3952
4853
  });
3953
4854
  if (Array.isArray(response)) {
3954
4855
  const validation = validateAndNormalizeSubtasks(response, {
@@ -4019,6 +4920,7 @@ var ConductorMode = class {
4019
4920
  matches: matchMap,
4020
4921
  gatewayToken: this.gatewayToken,
4021
4922
  resolveAgentUrl: this.resolveAgentUrl,
4923
+ timeoutMs: skillTimeoutMs,
4022
4924
  maxBudget: this.maxBudget,
4023
4925
  team
4024
4926
  });
@@ -4042,18 +4944,18 @@ var ConductorMode = class {
4042
4944
  };
4043
4945
 
4044
4946
  // src/credit/escrow-receipt.ts
4045
- import { z as z3 } from "zod";
4046
- import { randomUUID as randomUUID13 } from "crypto";
4047
- var EscrowReceiptSchema = z3.object({
4048
- requester_owner: z3.string().min(1),
4049
- requester_agent_id: z3.string().optional(),
4050
- requester_public_key: z3.string().min(1),
4051
- amount: z3.number().positive(),
4052
- card_id: z3.string().min(1),
4053
- skill_id: z3.string().optional(),
4054
- timestamp: z3.string(),
4055
- nonce: z3.string().uuid(),
4056
- signature: z3.string().min(1)
4947
+ import { z as z4 } from "zod";
4948
+ import { randomUUID as randomUUID14 } from "crypto";
4949
+ var EscrowReceiptSchema = z4.object({
4950
+ requester_owner: z4.string().min(1),
4951
+ requester_agent_id: z4.string().optional(),
4952
+ requester_public_key: z4.string().min(1),
4953
+ amount: z4.number().positive(),
4954
+ card_id: z4.string().min(1),
4955
+ skill_id: z4.string().optional(),
4956
+ timestamp: z4.string(),
4957
+ nonce: z4.string().uuid(),
4958
+ signature: z4.string().min(1)
4057
4959
  });
4058
4960
  function createSignedEscrowReceipt(db, privateKey, publicKey, opts) {
4059
4961
  const escrowId = holdEscrow(db, opts.owner, opts.amount, opts.cardId);
@@ -4065,7 +4967,7 @@ function createSignedEscrowReceipt(db, privateKey, publicKey, opts) {
4065
4967
  card_id: opts.cardId,
4066
4968
  ...opts.skillId ? { skill_id: opts.skillId } : {},
4067
4969
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4068
- nonce: randomUUID13()
4970
+ nonce: randomUUID14()
4069
4971
  };
4070
4972
  const signature = signEscrowReceipt(receiptData, privateKey);
4071
4973
  const receipt = {
@@ -4075,36 +4977,56 @@ function createSignedEscrowReceipt(db, privateKey, publicKey, opts) {
4075
4977
  return { escrowId, receipt };
4076
4978
  }
4077
4979
 
4980
+ // src/credit/settlement.ts
4981
+ function settleProviderEarning(providerDb, providerOwner, receipt) {
4982
+ const feeAmount = Math.floor(receipt.amount * NETWORK_FEE_RATE);
4983
+ const providerAmount = receipt.amount - feeAmount;
4984
+ recordEarning(
4985
+ providerDb,
4986
+ providerOwner,
4987
+ providerAmount,
4988
+ receipt.card_id,
4989
+ receipt.nonce
4990
+ );
4991
+ return { settled: true };
4992
+ }
4993
+ function settleRequesterEscrow(requesterDb, escrowId) {
4994
+ confirmEscrowDebit(requesterDb, escrowId);
4995
+ }
4996
+ function releaseRequesterEscrow(requesterDb, escrowId) {
4997
+ releaseEscrow(requesterDb, escrowId);
4998
+ }
4999
+
4078
5000
  // src/identity/identity.ts
4079
- import { z as z4 } from "zod";
5001
+ import { z as z5 } from "zod";
4080
5002
  import { createHash as createHash2, createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
4081
5003
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
4082
5004
  import { join as join4 } from "path";
4083
- var AgentIdentitySchema = z4.object({
5005
+ var AgentIdentitySchema = z5.object({
4084
5006
  /** Deterministic ID derived from public key: sha256(hex).slice(0, 16). */
4085
- agent_id: z4.string().min(1),
5007
+ agent_id: z5.string().min(1),
4086
5008
  /** Human-readable owner name (from config or init). */
4087
- owner: z4.string().min(1),
5009
+ owner: z5.string().min(1),
4088
5010
  /** Hex-encoded Ed25519 public key. */
4089
- public_key: z4.string().min(1),
5011
+ public_key: z5.string().min(1),
4090
5012
  /** ISO 8601 timestamp of identity creation. */
4091
- created_at: z4.string().datetime(),
5013
+ created_at: z5.string().datetime(),
4092
5014
  /** Optional guarantor info if linked to a human. */
4093
- guarantor: z4.object({
4094
- github_login: z4.string().min(1),
4095
- verified_at: z4.string().datetime()
5015
+ guarantor: z5.object({
5016
+ github_login: z5.string().min(1),
5017
+ verified_at: z5.string().datetime()
4096
5018
  }).optional()
4097
5019
  });
4098
- var AgentCertificateSchema = z4.object({
5020
+ var AgentCertificateSchema = z5.object({
4099
5021
  identity: AgentIdentitySchema,
4100
5022
  /** ISO 8601 timestamp of certificate issuance. */
4101
- issued_at: z4.string().datetime(),
5023
+ issued_at: z5.string().datetime(),
4102
5024
  /** ISO 8601 timestamp of certificate expiry. */
4103
- expires_at: z4.string().datetime(),
5025
+ expires_at: z5.string().datetime(),
4104
5026
  /** Hex-encoded public key of the issuer (same as identity for self-signed). */
4105
- issuer_public_key: z4.string().min(1),
5027
+ issuer_public_key: z5.string().min(1),
4106
5028
  /** Base64url Ed25519 signature over { identity, issued_at, expires_at, issuer_public_key }. */
4107
- signature: z4.string().min(1)
5029
+ signature: z5.string().min(1)
4108
5030
  });
4109
5031
  var IDENTITY_FILENAME = "identity.json";
4110
5032
  var PRIVATE_KEY_FILENAME = "private.key";
@@ -4293,6 +5215,34 @@ var AgentBnBConsumer = class {
4293
5215
  return this.identity;
4294
5216
  }
4295
5217
  /**
5218
+ * Requests a paid capability via the relay. The relay handles escrow + network fee
5219
+ * server-side, avoiding the fee bypass bug in the signed receipt path.
5220
+ *
5221
+ * @param opts - Relay request options.
5222
+ * @returns The result from the capability execution.
5223
+ * @throws {AgentBnBError} on relay connection failure or execution error.
5224
+ */
5225
+ async requestViaRelay(opts) {
5226
+ const identity = this.getIdentity();
5227
+ const config = loadConfig();
5228
+ const token = config?.token ?? "";
5229
+ return requestViaTemporaryRelay({
5230
+ registryUrl: opts.registryUrl,
5231
+ owner: identity.owner,
5232
+ token,
5233
+ targetOwner: opts.targetOwner,
5234
+ targetAgentId: opts.targetAgentId,
5235
+ cardId: opts.cardId,
5236
+ skillId: opts.skillId,
5237
+ params: opts.params ?? {},
5238
+ timeoutMs: opts.timeoutMs
5239
+ });
5240
+ }
5241
+ /**
5242
+ * @deprecated Use `requestViaRelay()` for paid remote requests. This method uses
5243
+ * local escrow + signed receipts which bypass the network fee. Still works for
5244
+ * free/local requests.
5245
+ *
4296
5246
  * Requests a capability from a remote agent with full escrow lifecycle.
4297
5247
  *
4298
5248
  * 1. Creates a signed escrow receipt (holds credits locally)
@@ -4327,6 +5277,10 @@ var AgentBnBConsumer = class {
4327
5277
  cardId: opts.cardId,
4328
5278
  params: opts.params,
4329
5279
  timeoutMs: opts.timeoutMs,
5280
+ timeoutHint: {
5281
+ expected_duration_ms: opts.expectedDurationMs,
5282
+ hard_timeout_ms: opts.providerHardTimeoutMs
5283
+ },
4330
5284
  escrowReceipt: receipt,
4331
5285
  identity: {
4332
5286
  agentId: identity.agent_id,
@@ -4507,16 +5461,16 @@ var AgentBnBProvider = class {
4507
5461
  };
4508
5462
 
4509
5463
  // src/identity/guarantor.ts
4510
- import { z as z5 } from "zod";
4511
- import { randomUUID as randomUUID14 } from "crypto";
5464
+ import { z as z6 } from "zod";
5465
+ import { randomUUID as randomUUID15 } from "crypto";
4512
5466
  var MAX_AGENTS_PER_GUARANTOR = 10;
4513
5467
  var GUARANTOR_CREDIT_POOL = 50;
4514
- var GuarantorRecordSchema = z5.object({
4515
- id: z5.string().uuid(),
4516
- github_login: z5.string().min(1),
4517
- agent_count: z5.number().int().nonnegative(),
4518
- credit_pool: z5.number().int().nonnegative(),
4519
- created_at: z5.string().datetime()
5468
+ var GuarantorRecordSchema = z6.object({
5469
+ id: z6.string().uuid(),
5470
+ github_login: z6.string().min(1),
5471
+ agent_count: z6.number().int().nonnegative(),
5472
+ credit_pool: z6.number().int().nonnegative(),
5473
+ created_at: z6.string().datetime()
4520
5474
  });
4521
5475
  var GUARANTOR_SCHEMA = `
4522
5476
  CREATE TABLE IF NOT EXISTS guarantors (
@@ -4547,7 +5501,7 @@ function registerGuarantor(db, githubLogin) {
4547
5501
  );
4548
5502
  }
4549
5503
  const record = {
4550
- id: randomUUID14(),
5504
+ id: randomUUID15(),
4551
5505
  github_login: githubLogin,
4552
5506
  agent_count: 0,
4553
5507
  credit_pool: GUARANTOR_CREDIT_POOL,
@@ -4621,12 +5575,12 @@ function getAgentGuarantor(db, agentId) {
4621
5575
  function initiateGithubAuth() {
4622
5576
  return {
4623
5577
  auth_url: "https://github.com/login/oauth/authorize?client_id=PLACEHOLDER&scope=read:user",
4624
- state: randomUUID14()
5578
+ state: randomUUID15()
4625
5579
  };
4626
5580
  }
4627
5581
 
4628
5582
  // src/relay/websocket-relay.ts
4629
- import { randomUUID as randomUUID17 } from "crypto";
5583
+ import { randomUUID as randomUUID18 } from "crypto";
4630
5584
 
4631
5585
  // src/relay/relay-credit.ts
4632
5586
  function lookupCardPrice(registryDb, cardId, skillId) {
@@ -4724,7 +5678,9 @@ function processEscrowSettle(creditDb, escrowId, success, providerAgentId, signa
4724
5678
  throw new Error("Invalid consumer signature on escrow settle");
4725
5679
  }
4726
5680
  }
4727
- const escrowRow = creditDb.prepare("SELECT amount, owner FROM credit_escrow WHERE id = ? AND status = ?").get(escrowId, "held");
5681
+ const escrowRow = creditDb.prepare(
5682
+ "SELECT amount, owner FROM credit_escrow WHERE id = ? AND status IN ('held', 'started', 'progressing', 'abandoned')"
5683
+ ).get(escrowId);
4728
5684
  if (!escrowRow) {
4729
5685
  throw new Error(`Escrow not found or already settled: ${escrowId}`);
4730
5686
  }
@@ -4756,10 +5712,10 @@ function settleWithNetworkFee(creditDb, escrowId, providerOwner) {
4756
5712
  }
4757
5713
 
4758
5714
  // src/hub-agent/relay-bridge.ts
4759
- import { randomUUID as randomUUID16 } from "crypto";
5715
+ import { randomUUID as randomUUID17 } from "crypto";
4760
5716
 
4761
5717
  // src/hub-agent/job-queue.ts
4762
- import { randomUUID as randomUUID15 } from "crypto";
5718
+ import { randomUUID as randomUUID16 } from "crypto";
4763
5719
  function updateJobStatus(db, jobId, status, result) {
4764
5720
  const now = (/* @__PURE__ */ new Date()).toISOString();
4765
5721
  if (result !== void 0) {
@@ -4799,7 +5755,16 @@ function handleJobRelayResponse(opts) {
4799
5755
  // src/relay/websocket-relay.ts
4800
5756
  var RATE_LIMIT_MAX = 60;
4801
5757
  var RATE_LIMIT_WINDOW_MS = 6e4;
4802
- var RELAY_TIMEOUT_MS = 3e5;
5758
+ var RELAY_IDLE_TIMEOUT_MS = 3e4;
5759
+ var RELAY_HARD_TIMEOUT_MS = 3e5;
5760
+ var RELAY_DISCONNECT_GRACE_MS = RELAY_HARD_TIMEOUT_MS + 3e4;
5761
+ function readTimeoutOverride(envKey, fallbackMs) {
5762
+ const raw = process.env[envKey];
5763
+ if (!raw) return fallbackMs;
5764
+ const parsed = Number(raw);
5765
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallbackMs;
5766
+ return Math.floor(parsed);
5767
+ }
4803
5768
  function registerWebSocketRelay(server, db, creditDb) {
4804
5769
  const connections = /* @__PURE__ */ new Map();
4805
5770
  const agentIdToOwner = /* @__PURE__ */ new Map();
@@ -4813,6 +5778,136 @@ function registerWebSocketRelay(server, db, creditDb) {
4813
5778
  if (connections.has(target)) return target;
4814
5779
  return void 0;
4815
5780
  }
5781
+ function clearPendingTimers(pending) {
5782
+ clearTimeout(pending.timeout);
5783
+ if (pending.idleTimeout) clearTimeout(pending.idleTimeout);
5784
+ if (pending.hardTimeout) clearTimeout(pending.hardTimeout);
5785
+ if (pending.graceTimeout) clearTimeout(pending.graceTimeout);
5786
+ }
5787
+ function setPendingTimer(pending, timerType, timer) {
5788
+ const existing = pending[timerType];
5789
+ if (existing) clearTimeout(existing);
5790
+ pending[timerType] = timer;
5791
+ pending.timeout = timer;
5792
+ }
5793
+ function scheduleIdleTimeout(requestId, pending) {
5794
+ const timeoutMs = readTimeoutOverride("AGENTBNB_RELAY_IDLE_TIMEOUT_MS", RELAY_IDLE_TIMEOUT_MS);
5795
+ const timer = setTimeout(() => {
5796
+ const current = pendingRequests.get(requestId);
5797
+ if (!current || (current.lifecycle ?? "held") !== "held") return;
5798
+ pendingRequests.delete(requestId);
5799
+ clearPendingTimers(current);
5800
+ if (current.escrowId && creditDb) {
5801
+ try {
5802
+ releaseForRelay(creditDb, current.escrowId);
5803
+ } catch (e) {
5804
+ console.error("[relay] escrow release on idle timeout failed:", e);
5805
+ }
5806
+ }
5807
+ const originWs = connections.get(current.originOwner);
5808
+ if (originWs && originWs.readyState === 1) {
5809
+ sendMessage(originWs, {
5810
+ type: "response",
5811
+ id: requestId,
5812
+ error: { code: -32603, message: "Relay request timeout" }
5813
+ });
5814
+ }
5815
+ }, timeoutMs);
5816
+ setPendingTimer(pending, "idleTimeout", timer);
5817
+ }
5818
+ function scheduleHardTimeout(requestId, pending) {
5819
+ const timeoutMs = readTimeoutOverride("AGENTBNB_RELAY_HARD_TIMEOUT_MS", RELAY_HARD_TIMEOUT_MS);
5820
+ const timer = setTimeout(() => {
5821
+ const current = pendingRequests.get(requestId);
5822
+ if (!current || current.lifecycle === "abandoned") return;
5823
+ pendingRequests.delete(requestId);
5824
+ clearPendingTimers(current);
5825
+ if (current.escrowId && creditDb) {
5826
+ try {
5827
+ releaseForRelay(creditDb, current.escrowId);
5828
+ } catch (e) {
5829
+ console.error("[relay] escrow release on hard timeout failed:", e);
5830
+ }
5831
+ }
5832
+ const originWs = connections.get(current.originOwner);
5833
+ if (originWs && originWs.readyState === 1) {
5834
+ sendMessage(originWs, {
5835
+ type: "response",
5836
+ id: requestId,
5837
+ error: { code: -32603, message: "Relay request timeout" }
5838
+ });
5839
+ }
5840
+ }, timeoutMs);
5841
+ setPendingTimer(pending, "hardTimeout", timer);
5842
+ }
5843
+ function scheduleGraceTimeout(requestId, pending) {
5844
+ const timeoutMs = readTimeoutOverride(
5845
+ "AGENTBNB_RELAY_DISCONNECT_GRACE_MS",
5846
+ RELAY_DISCONNECT_GRACE_MS
5847
+ );
5848
+ const timer = setTimeout(() => {
5849
+ const current = pendingRequests.get(requestId);
5850
+ if (!current || current.lifecycle !== "abandoned") return;
5851
+ pendingRequests.delete(requestId);
5852
+ clearPendingTimers(current);
5853
+ if (current.escrowId && creditDb) {
5854
+ try {
5855
+ releaseForRelay(creditDb, current.escrowId);
5856
+ } catch (e) {
5857
+ console.error("[relay] escrow release after grace timeout failed:", e);
5858
+ }
5859
+ }
5860
+ }, timeoutMs);
5861
+ setPendingTimer(pending, "graceTimeout", timer);
5862
+ }
5863
+ function transitionPendingToStarted(requestId, pending) {
5864
+ if (pending.lifecycle === "started" || pending.lifecycle === "progressing" || pending.lifecycle === "abandoned") {
5865
+ return;
5866
+ }
5867
+ pending.lifecycle = "started";
5868
+ pending.startedAt = Date.now();
5869
+ if (pending.idleTimeout) clearTimeout(pending.idleTimeout);
5870
+ if (pending.graceTimeout) clearTimeout(pending.graceTimeout);
5871
+ if (pending.escrowId && creditDb) {
5872
+ try {
5873
+ markEscrowStarted(creditDb, pending.escrowId);
5874
+ } catch (e) {
5875
+ console.error("[relay] escrow transition to started failed:", e);
5876
+ }
5877
+ }
5878
+ scheduleHardTimeout(requestId, pending);
5879
+ }
5880
+ function transitionPendingToProgressing(requestId, pending) {
5881
+ if (pending.lifecycle === "abandoned") {
5882
+ return;
5883
+ }
5884
+ if ((pending.lifecycle ?? "held") === "held") {
5885
+ transitionPendingToStarted(requestId, pending);
5886
+ }
5887
+ pending.lifecycle = "progressing";
5888
+ if (pending.escrowId && creditDb) {
5889
+ try {
5890
+ markEscrowProgressing(creditDb, pending.escrowId);
5891
+ } catch (e) {
5892
+ console.error("[relay] escrow transition to progressing failed:", e);
5893
+ }
5894
+ }
5895
+ }
5896
+ function transitionPendingToAbandoned(requestId, pending) {
5897
+ if (pending.lifecycle === "abandoned") return;
5898
+ pending.lifecycle = "abandoned";
5899
+ pending.abandonedAt = Date.now();
5900
+ if (pending.idleTimeout) clearTimeout(pending.idleTimeout);
5901
+ if (pending.hardTimeout) clearTimeout(pending.hardTimeout);
5902
+ if (pending.escrowId && creditDb) {
5903
+ try {
5904
+ markEscrowAbandoned(creditDb, pending.escrowId);
5905
+ } catch (e) {
5906
+ console.error("[relay] escrow transition to abandoned failed:", e);
5907
+ }
5908
+ }
5909
+ scheduleGraceTimeout(requestId, pending);
5910
+ }
4816
5911
  function checkRateLimit(owner) {
4817
5912
  const now = Date.now();
4818
5913
  const entry = rateLimits.get(owner);
@@ -4862,15 +5957,20 @@ function registerWebSocketRelay(server, db, creditDb) {
4862
5957
  } catch {
4863
5958
  }
4864
5959
  }
4865
- function upsertCard(cardData, owner) {
4866
- const parsed = AnyCardSchema.safeParse(cardData);
5960
+ function upsertCard(cardData, owner, agentId) {
5961
+ const parsed = AnyCardSchema.safeParse(
5962
+ agentId ? { ...cardData, agent_id: agentId } : cardData
5963
+ );
4867
5964
  if (!parsed.success) {
4868
5965
  throw new AgentBnBError(
4869
5966
  `Card validation failed: ${parsed.error.message}`,
4870
5967
  "VALIDATION_ERROR"
4871
5968
  );
4872
5969
  }
4873
- const card = { ...parsed.data, availability: { ...parsed.data.availability, online: true } };
5970
+ const card = attachCanonicalAgentId(db, {
5971
+ ...parsed.data,
5972
+ availability: { ...parsed.data.availability, online: true }
5973
+ });
4874
5974
  const cardId = card.id;
4875
5975
  const now = (/* @__PURE__ */ new Date()).toISOString();
4876
5976
  const existing = db.prepare("SELECT id FROM capability_cards WHERE id = ?").get(cardId);
@@ -4888,7 +5988,7 @@ function registerWebSocketRelay(server, db, creditDb) {
4888
5988
  function logAgentJoined(owner, cardName, cardId) {
4889
5989
  try {
4890
5990
  insertRequestLog(db, {
4891
- id: randomUUID17(),
5991
+ id: randomUUID18(),
4892
5992
  card_id: cardId,
4893
5993
  card_name: cardName,
4894
5994
  requester: owner,
@@ -4924,7 +6024,7 @@ function registerWebSocketRelay(server, db, creditDb) {
4924
6024
  agentIdToOwner.set(agentEntry.agent_id, owner);
4925
6025
  for (const agentCard of agentEntry.cards) {
4926
6026
  try {
4927
- upsertCard(agentCard, owner);
6027
+ upsertCard(agentCard, owner, agentEntry.agent_id);
4928
6028
  } catch {
4929
6029
  }
4930
6030
  }
@@ -4938,7 +6038,7 @@ function registerWebSocketRelay(server, db, creditDb) {
4938
6038
  }
4939
6039
  let cardId;
4940
6040
  try {
4941
- cardId = upsertCard(card, owner);
6041
+ cardId = upsertCard(card, owner, msg.agent_id);
4942
6042
  } catch (err) {
4943
6043
  console.error(`[relay] card validation failed for ${owner}:`, err instanceof Error ? err.message : err);
4944
6044
  cardId = card.id ?? owner;
@@ -4948,7 +6048,7 @@ function registerWebSocketRelay(server, db, creditDb) {
4948
6048
  if (msg.cards && msg.cards.length > 0) {
4949
6049
  for (const extraCard of msg.cards) {
4950
6050
  try {
4951
- upsertCard(extraCard, owner);
6051
+ upsertCard(extraCard, owner, msg.agent_id);
4952
6052
  } catch {
4953
6053
  }
4954
6054
  }
@@ -5003,23 +6103,17 @@ function registerWebSocketRelay(server, db, creditDb) {
5003
6103
  console.error("[relay] credit hold error (non-fatal):", err);
5004
6104
  }
5005
6105
  }
5006
- const timeout = setTimeout(() => {
5007
- const pending = pendingRequests.get(msg.id);
5008
- pendingRequests.delete(msg.id);
5009
- if (pending?.escrowId && creditDb) {
5010
- try {
5011
- releaseForRelay(creditDb, pending.escrowId);
5012
- } catch (e) {
5013
- console.error("[relay] escrow release on timeout failed:", e);
5014
- }
5015
- }
5016
- sendMessage(ws, {
5017
- type: "response",
5018
- id: msg.id,
5019
- error: { code: -32603, message: "Relay request timeout" }
5020
- });
5021
- }, RELAY_TIMEOUT_MS);
5022
- pendingRequests.set(msg.id, { originOwner: fromOwner, creditOwner, timeout, escrowId, targetOwner: msg.target_owner });
6106
+ const pending = {
6107
+ originOwner: fromOwner,
6108
+ creditOwner,
6109
+ timeout: setTimeout(() => void 0, 1),
6110
+ escrowId,
6111
+ targetOwner: targetKey ?? msg.target_owner,
6112
+ lifecycle: "held",
6113
+ createdAt: Date.now()
6114
+ };
6115
+ pendingRequests.set(msg.id, pending);
6116
+ scheduleIdleTimeout(msg.id, pending);
5023
6117
  sendMessage(targetWs, {
5024
6118
  type: "incoming_request",
5025
6119
  id: msg.id,
@@ -5031,30 +6125,23 @@ function registerWebSocketRelay(server, db, creditDb) {
5031
6125
  escrow_receipt: msg.escrow_receipt
5032
6126
  });
5033
6127
  }
6128
+ function handleRelayStarted(msg) {
6129
+ const pending = pendingRequests.get(msg.id);
6130
+ if (!pending) return;
6131
+ transitionPendingToStarted(msg.id, pending);
6132
+ const originWs = connections.get(pending.originOwner);
6133
+ if (originWs && originWs.readyState === 1) {
6134
+ sendMessage(originWs, {
6135
+ type: "relay_started",
6136
+ id: msg.id,
6137
+ message: msg.message
6138
+ });
6139
+ }
6140
+ }
5034
6141
  function handleRelayProgress(msg) {
5035
6142
  const pending = pendingRequests.get(msg.id);
5036
6143
  if (!pending) return;
5037
- clearTimeout(pending.timeout);
5038
- const newTimeout = setTimeout(() => {
5039
- const p = pendingRequests.get(msg.id);
5040
- pendingRequests.delete(msg.id);
5041
- if (p?.escrowId && creditDb) {
5042
- try {
5043
- releaseForRelay(creditDb, p.escrowId);
5044
- } catch (e) {
5045
- console.error("[relay] escrow release on progress timeout failed:", e);
5046
- }
5047
- }
5048
- const originWs2 = connections.get(pending.originOwner);
5049
- if (originWs2 && originWs2.readyState === 1) {
5050
- sendMessage(originWs2, {
5051
- type: "response",
5052
- id: msg.id,
5053
- error: { code: -32603, message: "Relay request timeout" }
5054
- });
5055
- }
5056
- }, RELAY_TIMEOUT_MS);
5057
- pending.timeout = newTimeout;
6144
+ transitionPendingToProgressing(msg.id, pending);
5058
6145
  const originWs = connections.get(pending.originOwner);
5059
6146
  if (originWs && originWs.readyState === 1) {
5060
6147
  sendMessage(originWs, {
@@ -5068,7 +6155,7 @@ function registerWebSocketRelay(server, db, creditDb) {
5068
6155
  function handleRelayResponse(msg) {
5069
6156
  const pending = pendingRequests.get(msg.id);
5070
6157
  if (!pending) return;
5071
- clearTimeout(pending.timeout);
6158
+ clearPendingTimers(pending);
5072
6159
  pendingRequests.delete(msg.id);
5073
6160
  if (pending.jobId && creditDb) {
5074
6161
  try {
@@ -5131,16 +6218,15 @@ function registerWebSocketRelay(server, db, creditDb) {
5131
6218
  connections.delete(owner);
5132
6219
  rateLimits.delete(owner);
5133
6220
  agentCapacities.delete(owner);
5134
- for (const [agentId, o] of agentIdToOwner) {
5135
- if (o === owner) {
6221
+ for (const [agentId, mappedOwner] of Array.from(agentIdToOwner.entries())) {
6222
+ if (mappedOwner === owner) {
5136
6223
  agentIdToOwner.delete(agentId);
5137
- break;
5138
6224
  }
5139
6225
  }
5140
6226
  markOwnerOffline(owner);
5141
6227
  for (const [reqId, pending] of pendingRequests) {
5142
6228
  if (pending.targetOwner === owner) {
5143
- clearTimeout(pending.timeout);
6229
+ clearPendingTimers(pending);
5144
6230
  pendingRequests.delete(reqId);
5145
6231
  if (pending.escrowId && creditDb) {
5146
6232
  try {
@@ -5158,14 +6244,19 @@ function registerWebSocketRelay(server, db, creditDb) {
5158
6244
  });
5159
6245
  }
5160
6246
  } else if (pending.originOwner === owner) {
5161
- clearTimeout(pending.timeout);
5162
- pendingRequests.delete(reqId);
5163
- if (pending.escrowId && creditDb) {
5164
- try {
5165
- releaseForRelay(creditDb, pending.escrowId);
5166
- } catch (e) {
5167
- console.error("[relay] escrow release on requester disconnect failed:", e);
6247
+ const lifecycle = pending.lifecycle ?? "held";
6248
+ if (lifecycle === "held") {
6249
+ clearPendingTimers(pending);
6250
+ pendingRequests.delete(reqId);
6251
+ if (pending.escrowId && creditDb) {
6252
+ try {
6253
+ releaseForRelay(creditDb, pending.escrowId);
6254
+ } catch (e) {
6255
+ console.error("[relay] escrow release on requester disconnect failed:", e);
6256
+ }
5168
6257
  }
6258
+ } else {
6259
+ transitionPendingToAbandoned(reqId, pending);
5169
6260
  }
5170
6261
  }
5171
6262
  }
@@ -5208,7 +6299,9 @@ function registerWebSocketRelay(server, db, creditDb) {
5208
6299
  return;
5209
6300
  }
5210
6301
  try {
5211
- const escrow = creditDb.prepare("SELECT card_id FROM credit_escrow WHERE id = ? AND status = ?").get(msg.escrow_id, "held");
6302
+ const escrow = creditDb.prepare(
6303
+ "SELECT card_id FROM credit_escrow WHERE id = ? AND status IN ('held', 'started', 'progressing', 'abandoned')"
6304
+ ).get(msg.escrow_id);
5212
6305
  if (!escrow) {
5213
6306
  sendMessage(ws, { type: "error", code: "escrow_not_found", message: `Escrow not found: ${msg.escrow_id}`, request_id: msg.request_id });
5214
6307
  return;
@@ -5305,6 +6398,9 @@ function registerWebSocketRelay(server, db, creditDb) {
5305
6398
  case "relay_response":
5306
6399
  handleRelayResponse(msg);
5307
6400
  break;
6401
+ case "relay_started":
6402
+ handleRelayStarted(msg);
6403
+ break;
5308
6404
  case "relay_progress":
5309
6405
  handleRelayProgress(msg);
5310
6406
  break;
@@ -5351,7 +6447,7 @@ function registerWebSocketRelay(server, db, creditDb) {
5351
6447
  }
5352
6448
  connections.clear();
5353
6449
  for (const [, pending] of pendingRequests) {
5354
- clearTimeout(pending.timeout);
6450
+ clearPendingTimers(pending);
5355
6451
  }
5356
6452
  pendingRequests.clear();
5357
6453
  rateLimits.clear();
@@ -5371,12 +6467,12 @@ function registerWebSocketRelay(server, db, creditDb) {
5371
6467
  }
5372
6468
 
5373
6469
  // src/onboarding/index.ts
5374
- import { randomUUID as randomUUID19 } from "crypto";
6470
+ import { randomUUID as randomUUID20 } from "crypto";
5375
6471
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
5376
6472
  import { join as join5 } from "path";
5377
6473
 
5378
6474
  // src/cli/onboarding.ts
5379
- import { randomUUID as randomUUID18 } from "crypto";
6475
+ import { randomUUID as randomUUID19 } from "crypto";
5380
6476
  import { createConnection } from "net";
5381
6477
  var KNOWN_API_KEYS = [
5382
6478
  "OPENAI_API_KEY",
@@ -5518,7 +6614,7 @@ function capabilitiesToV2Card(capabilities, owner, agentName) {
5518
6614
  }));
5519
6615
  const card = {
5520
6616
  spec_version: "2.0",
5521
- id: randomUUID19(),
6617
+ id: randomUUID20(),
5522
6618
  owner,
5523
6619
  agent_name: agentName ?? owner,
5524
6620
  skills,