clinch-core 0.6.2 → 0.7.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.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  The Agent Negotiation Protocol (ANP) governs how two autonomous software agents establish identity, declare capabilities, and reach a cryptographically verifiable agreement on price and terms.
12
12
 
13
13
  ### 1.1 Address Formatting & Routing
14
- All destination routing in Clinch relies on strict, structured addressing:
14
+ All destination routing in Clinch relies on structured, prefixed addresses:
15
15
 
16
16
  $$\text{PROTOCOL\_MODE} \mathbin{.} \text{domain} \mathbin{.} \text{TLD}$$
17
17
 
@@ -36,9 +36,10 @@ The SDK maintains an isolated, turn-based state machine for each active session.
36
36
  * `OFFLINE`: Client is completely disconnected.
37
37
  * `CONNECTING`: Authenticating identity and performing local Proof-of-Work (PoW) verification.
38
38
  * `IDLE`: Connected and authenticated. Ready to initialize new sessions.
39
+ * `RECONNECTING`: Socket connection lost; executing exponential backoff.
39
40
  * `NEGOTIATING`: Active, turn-based bargaining sequence in progress.
40
41
  * `CONVERGED`: Mathematical convergence reached; agreement co-signed.
41
- * `STALEMATE`: Handshake or negotiation sequence aborted or timed out.
42
+ * `STALEMATE`: Negotiation terminated; max turns reached without convergence.
42
43
  * `ERROR`: Internal network, compilation, or cryptographic validation failure.
43
44
 
44
45
  ### Developer Event Subscriptions
@@ -114,7 +115,7 @@ async function runAutonomousSession() {
114
115
  runAutonomousSession();
115
116
  ```
116
117
 
117
- ### 4.2 Cloud Webhook & Horizontal Scale Pattern
118
+ ### 4.2 Webhook & Horizontal Scale Pattern
118
119
  For enterprise cloud environments where processes must remain stateless or scale horizontally across server pods, you must serialize and rehydrate active sessions dynamically. This ensures that when an asynchronous callback arrives, any pod can load the session state and use the matching ephemeral key to sign the next turn.
119
120
 
120
121
  ```javascript
@@ -167,11 +168,51 @@ webhookCore.on('callback_received', async (event) => {
167
168
  });
168
169
  ```
169
170
 
171
+ ### 4.3 Cascading Multi-Seller Squeeze and Races
172
+ To implement market discovery, the SDK supports cascading negotiations across multiple matching sellers. The Core orchestrates two distinct optimization strategies:
173
+
174
+ #### Sequential Squeeze (Default)
175
+ Useful for high-value items where time is secondary to price. The SDK negotiates sequentially with each seller, using the converged deal price of the previous seller as the strict, maximum budget ceiling for the next. This dynamically forces sellers to underbid one another.
176
+
177
+ #### Parallel Race
178
+ Necessary for real-time services like ride-hailing or logistics. The SDK handshakes and conducts negotiations with all selected sellers concurrently in parallel. Once all sessions finish, it selects the absolute lowest price converged under your budget.
179
+
180
+ ```javascript
181
+ // Triggers the cascading loop
182
+ const bestDeal = await core.negotiateCascade(
183
+ 'domain_name', // Category to discover
184
+ { intent: 'purchase', item: 'mybrand.io', max_budget: 150.00 },
185
+ 3, // Max sellers to target
186
+ 'sequential' // 'sequential' | 'parallel'
187
+ );
188
+
189
+ if (bestDeal) {
190
+ console.log(`🏆 Optimal deal secured with ${bestDeal.sellerId} at $${bestDeal.finalPrice}`);
191
+ }
192
+ ```
193
+
194
+ ### 4.4 Blind Key Pass (Credential Injection)
195
+ For sessions with services that require authorization tokens or API keys to operate (such as platform execution nodes), you can register these credentials locally.
196
+
197
+ The Core library securely stores these secrets and silently injects them into the handshake transport layer during session initialization. This prevents the API keys from ever being exposed to the AI model's context window, safeguarding you against prompt injection attacks.
198
+
199
+ ```javascript
200
+ // Register the API credential locally bound to the domain namespace
201
+ core.registerSecret('apify.anp', 'apify_sec_key_xyz987', 'Apify Production Token');
202
+
203
+ // Handshaking with this address now automatically injects the credential silently
204
+ const sessionId = await core.negotiate('ANP/C.apify.anp', {
205
+ intent: 'purchase',
206
+ item: 'actor-scraper-run',
207
+ max_budget: 5.00
208
+ });
209
+ ```
210
+
170
211
  ---
171
212
 
172
213
  ## 5. Integration Guide: Seller Nodes
173
214
 
174
- Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
215
+ Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
175
216
 
176
217
  ### 5.1 Decoupled Routing Architecture
177
218
  To prevent centralized token expiration failures, Clinch separates ownership from routing:
@@ -241,6 +282,13 @@ Initializes the cryptographic handshake with a seller. Returns `sessionId`.
241
282
  #### `exportSessionState(sessionId)` / `importSessionState(serializedData)`
242
283
  Serializes or de-serializes all in-flight state for a given session, including ephemeral Ed25519 keys, state metrics, and local sandbox parameters, into an exchangeable JSON string.
243
284
 
285
+ #### `registerSecret(domain, key, name?)` / `clearSecret(domain)`
286
+ Saves or clears an API secret locally. The key is never visible to the AI agent and is silently passed in handshake payloads to the designated domain.
287
+
288
+ #### `async negotiateCascade(category, constraints, maxSellers?, strategy?)`
289
+ Queries the discovery register and cascade-negotiates with matching nodes.
290
+ * `strategy`: `'sequential'` (Sequential Squeeze) or `'parallel'` (Concurrent Race). Default: `'sequential'`.
291
+
244
292
  #### `buildAgentPrompt(sessionId, incomingMessage)`
245
293
  Assembles a contextual, structure-compliant system prompt for external LLMs to ensure compliant JSON execution.
246
294
 
package/dist/index.d.ts CHANGED
@@ -48,12 +48,15 @@ export declare class ClinchCore extends EventEmitter {
48
48
  identityPubKey: string;
49
49
  private activeSessions;
50
50
  private ws;
51
+ private localSecrets;
51
52
  private isSandboxMode;
52
53
  private sandboxModelContext;
53
54
  private sandboxMaxTurns;
54
55
  get activeNegotiationId(): string | null;
55
56
  constructor(config?: ClinchConfig);
56
57
  private setStatus;
58
+ registerSecret(domain: string, key: string, name?: string): void;
59
+ clearSecret(domain: string): void;
57
60
  initialize(cachedToken?: string): Promise<void>;
58
61
  private getRegistryUrl;
59
62
  private networkRequest;
@@ -69,6 +72,12 @@ export declare class ClinchCore extends EventEmitter {
69
72
  negotiate(address: string, constraints: ConstraintVector): Promise<string>;
70
73
  sendCounter(sessionId: string, price: number, reason: string): Promise<void>;
71
74
  exitSession(sessionId: string): Promise<string>;
75
+ negotiateCascade(category: string, constraints: ConstraintVector, maxSellers?: number, strategy?: 'sequential' | 'parallel'): Promise<{
76
+ sessionId: string;
77
+ sellerId: string;
78
+ finalPrice: number;
79
+ } | null>;
80
+ private waitForSession;
72
81
  sandbox(config?: SandboxConfig): Promise<void>;
73
82
  private setupSandbox;
74
83
  private handleAutomaticSandboxTurn;
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ const ws_1 = __importDefault(require("ws"));
47
47
  const PROTOCOL_VERSION = "0.1.0";
48
48
  const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
49
49
  function toHex(arr) {
50
- return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
50
+ return Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('');
51
51
  }
52
52
  function fromHex(hex) {
53
53
  // Strips all spaces, newlines, and rogue quotes from .env strings
@@ -55,7 +55,7 @@ function fromHex(hex) {
55
55
  const match = clean.match(/.{1,2}/g);
56
56
  if (!match)
57
57
  return new Uint8Array(0);
58
- return new Uint8Array(match.map(byte => parseInt(byte, 16)));
58
+ return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
59
59
  }
60
60
  // ============================================================================
61
61
  // THE CLINCH CORE LIBRARY (Buyer)
@@ -71,6 +71,8 @@ class ClinchCore extends events_1.EventEmitter {
71
71
  identityPubKey;
72
72
  activeSessions = new Map();
73
73
  ws = null;
74
+ // Blind Key Pass Local Secret Store
75
+ localSecrets = new Map();
74
76
  isSandboxMode = false;
75
77
  sandboxModelContext = null;
76
78
  sandboxMaxTurns = 6;
@@ -99,6 +101,18 @@ class ClinchCore extends events_1.EventEmitter {
99
101
  this.emit('log', `🟡 [State] ${this.status}`);
100
102
  }
101
103
  }
104
+ // --------------------------------------------------------------------------
105
+ // BLIND KEY PASS MANAGERS (Silent API Key Handshake)
106
+ // --------------------------------------------------------------------------
107
+ registerSecret(domain, key, name) {
108
+ const normalizedDomain = domain.toLowerCase().trim();
109
+ this.localSecrets.set(normalizedDomain, { key, name });
110
+ this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
111
+ }
112
+ clearSecret(domain) {
113
+ const normalizedDomain = domain.toLowerCase().trim();
114
+ this.localSecrets.delete(normalizedDomain);
115
+ }
102
116
  async initialize(cachedToken) {
103
117
  if (this.status === 'IDLE' || this.status === 'CONNECTING')
104
118
  return;
@@ -311,6 +325,7 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
311
325
  const parsed = this.parseAddress(address);
312
326
  const ephemeralKeys = tweetnacl_1.default.sign.keyPair();
313
327
  const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
328
+ const blindSecret = this.localSecrets.get(parsed.domain);
314
329
  const initPayload = {
315
330
  clinch_version: PROTOCOL_VERSION,
316
331
  mode: parsed.mode,
@@ -318,6 +333,10 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
318
333
  session_pub_key: ephemeralPubHex,
319
334
  timestamp_utc: new Date().toISOString()
320
335
  };
336
+ if (blindSecret) {
337
+ initPayload.blind_auth_token = blindSecret.key;
338
+ this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
339
+ }
321
340
  const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
322
341
  const signature = toHex(tweetnacl_1.default.sign.detached(msgUint8, ephemeralKeys.secretKey));
323
342
  const response = await this.networkRequest(`/api/route/${parsed.domain}/handshake`, {
@@ -368,6 +387,100 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
368
387
  session.exitTokenHash = res.token_hash;
369
388
  return res.token_hash;
370
389
  }
390
+ // --------------------------------------------------------------------------
391
+ // CASCADING ITERATIVE NEGOTIATION (Squeeze vs. Parallel Concurrency)
392
+ // --------------------------------------------------------------------------
393
+ async negotiateCascade(category, constraints, maxSellers = 3, strategy = 'sequential') {
394
+ this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
395
+ const discovery = await this.search(category);
396
+ const sellers = (discovery.results || []).slice(0, maxSellers);
397
+ if (sellers.length === 0) {
398
+ this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
399
+ return null;
400
+ }
401
+ // ── STRATEGY 1: PARALLEL RACE (High Urgency / Ride Hailing) ──
402
+ if (strategy === 'parallel') {
403
+ this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
404
+ const sessionPromises = sellers.map(async (seller) => {
405
+ const targetAddress = `ANP/C.${seller.agent_id}`;
406
+ try {
407
+ const sessionId = await this.negotiate(targetAddress, constraints);
408
+ const result = await this.waitForSession(sessionId);
409
+ return { sellerId: seller.agent_id, sessionId, ...result };
410
+ }
411
+ catch (e) {
412
+ this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
413
+ return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate', price: Infinity };
414
+ }
415
+ });
416
+ const results = await Promise.all(sessionPromises);
417
+ const successfulDeals = results.filter((r) => r.outcome === 'deal' && r.price <= constraints.max_budget);
418
+ if (successfulDeals.length === 0) {
419
+ this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
420
+ return null;
421
+ }
422
+ successfulDeals.sort((a, b) => a.price - b.price);
423
+ const winner = successfulDeals[0];
424
+ this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
425
+ return {
426
+ sessionId: winner.sessionId,
427
+ sellerId: winner.sellerId,
428
+ finalPrice: winner.price
429
+ };
430
+ }
431
+ // ── STRATEGY 2: SEQUENTIAL SQUEEZE (Low Urgency / Price Optimization) ──
432
+ this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
433
+ let bestDeal = null;
434
+ let currentBudgetCeiling = constraints.max_budget;
435
+ for (const seller of sellers) {
436
+ const targetAddress = `ANP/C.${seller.agent_id}`;
437
+ this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
438
+ const sessionConstraints = {
439
+ ...constraints,
440
+ max_budget: currentBudgetCeiling
441
+ };
442
+ try {
443
+ const sessionId = await this.negotiate(targetAddress, sessionConstraints);
444
+ const result = await this.waitForSession(sessionId);
445
+ if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
446
+ this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
447
+ bestDeal = {
448
+ sessionId,
449
+ sellerId: seller.agent_id,
450
+ finalPrice: result.price
451
+ };
452
+ currentBudgetCeiling = result.price;
453
+ }
454
+ else {
455
+ this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
456
+ }
457
+ }
458
+ catch (e) {
459
+ this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
460
+ }
461
+ }
462
+ return bestDeal;
463
+ }
464
+ waitForSession(sessionId) {
465
+ return new Promise((resolve) => {
466
+ const onClosed = (event) => {
467
+ if (event.sessionId === sessionId) {
468
+ this.off('session_closed', onClosed);
469
+ this.off('status_changed', onStatus);
470
+ resolve({ outcome: 'deal', price: event.finalPrice });
471
+ }
472
+ };
473
+ const onStatus = (status) => {
474
+ if (status === 'STALEMATE') {
475
+ this.off('session_closed', onClosed);
476
+ this.off('status_changed', onStatus);
477
+ resolve({ outcome: 'stalemate', price: Infinity });
478
+ }
479
+ };
480
+ this.on('session_closed', onClosed);
481
+ this.on('status_changed', onStatus);
482
+ });
483
+ }
371
484
  async sandbox(config = {}) {
372
485
  this.isSandboxMode = true;
373
486
  this.sandboxMaxTurns = config.maxTurns || 6;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clinch-core",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
 
5
5
  "description": "Clinch Protocol Edge Client",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ const PROTOCOL_VERSION = "0.1.0";
10
10
  const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
11
11
 
12
12
  function toHex(arr: Uint8Array | number[]): string {
13
- return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
13
+ return Array.from(arr).map((b: number) => b.toString(16).padStart(2, '0')).join('');
14
14
  }
15
15
 
16
16
  function fromHex(hex: string): Uint8Array {
@@ -18,7 +18,7 @@ function fromHex(hex: string): Uint8Array {
18
18
  const clean = hex.replace(/[^0-9a-fA-F]/g, '');
19
19
  const match = clean.match(/.{1,2}/g);
20
20
  if (!match) return new Uint8Array(0);
21
- return new Uint8Array(match.map(byte => parseInt(byte, 16)));
21
+ return new Uint8Array(match.map((byte: string) => parseInt(byte, 16)));
22
22
  }
23
23
 
24
24
  // ============================================================================
@@ -97,6 +97,9 @@ export class ClinchCore extends EventEmitter {
97
97
  private activeSessions = new Map<string, SessionState>();
98
98
  private ws: WebSocket | null = null;
99
99
 
100
+ // Blind Key Pass Local Secret Store
101
+ private localSecrets = new Map<string, { key: string, name?: string }>();
102
+
100
103
  private isSandboxMode = false;
101
104
  private sandboxModelContext: any = null;
102
105
  private sandboxMaxTurns = 6;
@@ -129,6 +132,20 @@ export class ClinchCore extends EventEmitter {
129
132
  }
130
133
  }
131
134
 
135
+ // --------------------------------------------------------------------------
136
+ // BLIND KEY PASS MANAGERS (Silent API Key Handshake)
137
+ // --------------------------------------------------------------------------
138
+ public registerSecret(domain: string, key: string, name?: string): void {
139
+ const normalizedDomain = domain.toLowerCase().trim();
140
+ this.localSecrets.set(normalizedDomain, { key, name });
141
+ this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
142
+ }
143
+
144
+ public clearSecret(domain: string): void {
145
+ const normalizedDomain = domain.toLowerCase().trim();
146
+ this.localSecrets.delete(normalizedDomain);
147
+ }
148
+
132
149
  async initialize(cachedToken?: string): Promise<void> {
133
150
  if (this.status === 'IDLE' || this.status === 'CONNECTING') return;
134
151
  this.setStatus('CONNECTING');
@@ -356,7 +373,9 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
356
373
  const ephemeralKeys = nacl.sign.keyPair();
357
374
  const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
358
375
 
359
- const initPayload = {
376
+ const blindSecret = this.localSecrets.get(parsed.domain);
377
+
378
+ const initPayload: any = {
360
379
  clinch_version: PROTOCOL_VERSION,
361
380
  mode: parsed.mode,
362
381
  constraints,
@@ -364,6 +383,11 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
364
383
  timestamp_utc: new Date().toISOString()
365
384
  };
366
385
 
386
+ if (blindSecret) {
387
+ initPayload.blind_auth_token = blindSecret.key;
388
+ this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
389
+ }
390
+
367
391
  const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
368
392
  const signature = toHex(nacl.sign.detached(msgUint8, ephemeralKeys.secretKey));
369
393
 
@@ -426,6 +450,117 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
426
450
  return res.token_hash;
427
451
  }
428
452
 
453
+ // --------------------------------------------------------------------------
454
+ // CASCADING ITERATIVE NEGOTIATION (Squeeze vs. Parallel Concurrency)
455
+ // --------------------------------------------------------------------------
456
+ public async negotiateCascade(
457
+ category: string,
458
+ constraints: ConstraintVector,
459
+ maxSellers = 3,
460
+ strategy: 'sequential' | 'parallel' = 'sequential'
461
+ ): Promise<{ sessionId: string, sellerId: string, finalPrice: number } | null> {
462
+ this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
463
+ const discovery = await this.search(category);
464
+ const sellers: any[] = (discovery.results || []).slice(0, maxSellers);
465
+
466
+ if (sellers.length === 0) {
467
+ this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
468
+ return null;
469
+ }
470
+
471
+ // ── STRATEGY 1: PARALLEL RACE (High Urgency / Ride Hailing) ──
472
+ if (strategy === 'parallel') {
473
+ this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
474
+
475
+ const sessionPromises = sellers.map(async (seller: any) => {
476
+ const targetAddress = `ANP/C.${seller.agent_id}`;
477
+ try {
478
+ const sessionId = await this.negotiate(targetAddress, constraints);
479
+ const result = await this.waitForSession(sessionId);
480
+ return { sellerId: seller.agent_id, sessionId, ...result };
481
+ } catch (e: any) {
482
+ this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
483
+ return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate' as const, price: Infinity };
484
+ }
485
+ });
486
+
487
+ const results = await Promise.all(sessionPromises);
488
+ const successfulDeals = results.filter((r: any) => r.outcome === 'deal' && r.price <= constraints.max_budget);
489
+
490
+ if (successfulDeals.length === 0) {
491
+ this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
492
+ return null;
493
+ }
494
+
495
+ successfulDeals.sort((a: any, b: any) => a.price - b.price);
496
+ const winner = successfulDeals[0];
497
+
498
+ this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
499
+ return {
500
+ sessionId: winner.sessionId,
501
+ sellerId: winner.sellerId,
502
+ finalPrice: winner.price
503
+ };
504
+ }
505
+
506
+ // ── STRATEGY 2: SEQUENTIAL SQUEEZE (Low Urgency / Price Optimization) ──
507
+ this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
508
+ let bestDeal: { sessionId: string, sellerId: string, finalPrice: number } | null = null;
509
+ let currentBudgetCeiling = constraints.max_budget;
510
+
511
+ for (const seller of sellers) {
512
+ const targetAddress = `ANP/C.${seller.agent_id}`;
513
+ this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
514
+
515
+ const sessionConstraints = {
516
+ ...constraints,
517
+ max_budget: currentBudgetCeiling
518
+ };
519
+
520
+ try {
521
+ const sessionId = await this.negotiate(targetAddress, sessionConstraints);
522
+ const result = await this.waitForSession(sessionId);
523
+
524
+ if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
525
+ this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
526
+ bestDeal = {
527
+ sessionId,
528
+ sellerId: seller.agent_id,
529
+ finalPrice: result.price
530
+ };
531
+ currentBudgetCeiling = result.price;
532
+ } else {
533
+ this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
534
+ }
535
+ } catch (e: any) {
536
+ this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
537
+ }
538
+ }
539
+
540
+ return bestDeal;
541
+ }
542
+
543
+ private waitForSession(sessionId: string): Promise<{ outcome: 'deal' | 'stalemate', price: number }> {
544
+ return new Promise((resolve) => {
545
+ const onClosed = (event: any) => {
546
+ if (event.sessionId === sessionId) {
547
+ this.off('session_closed', onClosed);
548
+ this.off('status_changed', onStatus);
549
+ resolve({ outcome: 'deal', price: event.finalPrice });
550
+ }
551
+ };
552
+ const onStatus = (status: CoreStatus) => {
553
+ if (status === 'STALEMATE') {
554
+ this.off('session_closed', onClosed);
555
+ this.off('status_changed', onStatus);
556
+ resolve({ outcome: 'stalemate', price: Infinity });
557
+ }
558
+ };
559
+ this.on('session_closed', onClosed);
560
+ this.on('status_changed', onStatus);
561
+ });
562
+ }
563
+
429
564
  async sandbox(config: SandboxConfig = {}): Promise<void> {
430
565
  this.isSandboxMode = true;
431
566
  this.sandboxMaxTurns = config.maxTurns || 6;