2020117-agent 0.5.0 → 0.5.2

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/dist/agent.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Unified Agent Runtime — runs as a long-lived daemon that handles:
4
- * 1. Async platform tasks (inbox polling accept → process → submit result)
3
+ * Agent Runtime — Nostr-native daemon that handles:
4
+ * 1. DVM requests via relay subscription (Kind 5xxx → process → Kind 6xxx result)
5
5
  * 2. P2P sessions (Hyperswarm + Lightning invoice per-minute billing)
6
6
  *
7
- * Both channels share a single capacity counter so the agent never overloads.
7
+ * All agents sign Nostr events with their own private key and publish directly
8
+ * to relays. The platform API is only used for read operations (pipeline sub-tasks).
8
9
  *
9
10
  * Usage:
10
11
  * AGENT=translator DVM_KIND=5302 OLLAMA_MODEL=qwen2.5:0.5b npm run agent
11
12
  * AGENT=my-agent DVM_KIND=5100 MAX_JOBS=5 npm run agent
12
- * DVM_KIND=5100 npm run agent # no API key → P2P-only mode
13
13
  * AGENT=broker DVM_KIND=5302 PROCESSOR=none SUB_KIND=5100 npm run agent
14
14
  * AGENT=custom DVM_KIND=5100 PROCESSOR=exec:./my-model.sh npm run agent
15
15
  * AGENT=remote DVM_KIND=5100 PROCESSOR=http://localhost:8080 npm run agent
package/dist/agent.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Unified Agent Runtime — runs as a long-lived daemon that handles:
4
- * 1. Async platform tasks (inbox polling accept → process → submit result)
3
+ * Agent Runtime — Nostr-native daemon that handles:
4
+ * 1. DVM requests via relay subscription (Kind 5xxx → process → Kind 6xxx result)
5
5
  * 2. P2P sessions (Hyperswarm + Lightning invoice per-minute billing)
6
6
  *
7
- * Both channels share a single capacity counter so the agent never overloads.
7
+ * All agents sign Nostr events with their own private key and publish directly
8
+ * to relays. The platform API is only used for read operations (pipeline sub-tasks).
8
9
  *
9
10
  * Usage:
10
11
  * AGENT=translator DVM_KIND=5302 OLLAMA_MODEL=qwen2.5:0.5b npm run agent
11
12
  * AGENT=my-agent DVM_KIND=5100 MAX_JOBS=5 npm run agent
12
- * DVM_KIND=5100 npm run agent # no API key → P2P-only mode
13
13
  * AGENT=broker DVM_KIND=5302 PROCESSOR=none SUB_KIND=5100 npm run agent
14
14
  * AGENT=custom DVM_KIND=5100 PROCESSOR=exec:./my-model.sh npm run agent
15
15
  * AGENT=remote DVM_KIND=5100 PROCESSOR=http://localhost:8080 npm run agent
@@ -21,8 +21,7 @@ for (const arg of process.argv.slice(2)) {
21
21
  const eq = arg.indexOf('=');
22
22
  if (eq === -1) {
23
23
  // Bare flags (no value)
24
- if (arg === '--sovereign')
25
- process.env.SOVEREIGN = '1';
24
+ if (arg === '--sovereign') { } // legacy flag, all agents are now Nostr-native
26
25
  continue;
27
26
  }
28
27
  const key = arg.slice(0, eq);
@@ -67,9 +66,7 @@ for (const arg of process.argv.slice(2)) {
67
66
  case '--lightning-address':
68
67
  process.env.LIGHTNING_ADDRESS = val;
69
68
  break;
70
- case '--sovereign':
71
- process.env.SOVEREIGN = val || '1';
72
- break;
69
+ case '--sovereign': break; // legacy flag, all agents are now Nostr-native
73
70
  case '--privkey':
74
71
  process.env.NOSTR_PRIVKEY = val;
75
72
  break;
@@ -84,11 +81,11 @@ for (const arg of process.argv.slice(2)) {
84
81
  import { randomBytes } from 'crypto';
85
82
  import { SwarmNode, topicFromKind } from './swarm.js';
86
83
  import { createProcessor } from './processor.js';
87
- import { hasApiKey, loadAgentName, registerService, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, getProfile, reportSession, } from './api.js';
84
+ import { hasApiKey, loadAgentName, getProfile, reportSession, } from './api.js';
88
85
  import { generateInvoice } from './clink.js';
89
86
  import { receiveCashuToken } from './cashu.js';
90
87
  import { generateKeypair, loadSovereignKeys, saveSovereignKeys, signEvent, nip44Encrypt, nip44Decrypt, pubkeyFromPrivkey, RelayPool, } from './nostr.js';
91
- import { parseNwcUri, nwcGetBalance } from './nwc.js';
88
+ import { parseNwcUri, nwcGetBalance, nwcPayLightningAddress } from './nwc.js';
92
89
  import { readFileSync } from 'fs';
93
90
  import WebSocket from 'ws';
94
91
  // Polyfill global WebSocket for Node.js < 22 (needed by ws tunnel)
@@ -97,13 +94,11 @@ if (!globalThis.WebSocket)
97
94
  // --- Config from env ---
98
95
  const KIND = Number(process.env.DVM_KIND) || 5100;
99
96
  const MAX_CONCURRENT = Number(process.env.MAX_JOBS) || 3;
100
- const POLL_INTERVAL = Number(process.env.POLL_INTERVAL) || 30_000;
101
97
  const SATS_PER_CHUNK = Number(process.env.SATS_PER_CHUNK) || 1;
102
98
  const CHUNKS_PER_PAYMENT = Number(process.env.CHUNKS_PER_PAYMENT) || 10;
103
99
  // --- Lightning payment config ---
104
100
  let LIGHTNING_ADDRESS = process.env.LIGHTNING_ADDRESS || '';
105
- // --- Sovereign mode config ---
106
- const SOVEREIGN = process.env.SOVEREIGN === '1' || process.env.SOVEREIGN === 'true';
101
+ // --- Relay config ---
107
102
  const DEFAULT_RELAYS = ['wss://relay.2020117.xyz', 'wss://relay.damus.io', 'wss://nos.lol'];
108
103
  const RELAYS = process.env.NOSTR_RELAYS?.split(',').map(s => s.trim()) || DEFAULT_RELAYS;
109
104
  // --- Sub-task delegation config ---
@@ -135,7 +130,6 @@ const state = {
135
130
  activeJobs: 0,
136
131
  shuttingDown: false,
137
132
  stopHeartbeat: null,
138
- pollTimer: null,
139
133
  swarmNode: null,
140
134
  processor: null,
141
135
  skill: loadSkill(),
@@ -164,7 +158,7 @@ function getAvailableCapacity() {
164
158
  // --- Main ---
165
159
  async function main() {
166
160
  const label = state.agentName || 'agent';
167
- console.log(`[${label}] Starting unified agent runtime`);
161
+ console.log(`[${label}] Starting agent runtime`);
168
162
  // 1. Create and verify processor
169
163
  state.processor = await createProcessor();
170
164
  console.log(`[${label}] kind=${KIND} processor=${state.processor.name} maxJobs=${MAX_CONCURRENT}`);
@@ -187,93 +181,17 @@ async function main() {
187
181
  console.log(`[${label}] Lightning Address loaded from platform: ${LIGHTNING_ADDRESS}`);
188
182
  }
189
183
  }
190
- // 3. Sovereign mode: Nostr identity + relay connections + NIP-XX
191
- if (SOVEREIGN) {
192
- await setupSovereign(label);
193
- }
194
- // 4. Platform registration + heartbeat (skipped in sovereign-only mode)
195
- if (!SOVEREIGN || hasApiKey()) {
196
- await setupPlatform(label);
197
- }
198
- // 5. Async inbox poller (platform mode — fallback when no relay subscription)
199
- // If relay subscription is active, relay is the primary job discovery channel.
200
- // Inbox poller is only needed when agent has no privkey (can't subscribe to relay).
201
- if (!state.sovereignKeys || !state.relayPool) {
202
- startInboxPoller(label);
203
- }
204
- else {
205
- console.log(`[${label}] Relay subscription active — inbox polling disabled (Nostr-native mode)`);
206
- }
207
- // 6. P2P swarm listener
184
+ // 3. Nostr identity + relay + subscriptions (all agents are Nostr-native)
185
+ await setupNostr(label);
186
+ // 4. P2P swarm listener
208
187
  await startSwarmListener(label);
209
- // 7. Graceful shutdown
188
+ // 5. Graceful shutdown
210
189
  setupShutdown(label);
211
- const mode = SOVEREIGN ? 'sovereign' : (hasApiKey() ? 'platform' : 'P2P-only');
212
- console.log(`[${label}] Agent ready — mode=${mode}, channels active\n`);
213
- }
214
- // --- 2. Platform registration ---
215
- async function setupPlatform(label) {
216
- if (!hasApiKey()) {
217
- console.log(`[${label}] No API key — P2P-only mode (inbox polling disabled)`);
218
- return;
219
- }
220
- console.log(`[${label}] Registering on platform...`);
221
- const models = process.env.MODELS ? process.env.MODELS.split(',').map(s => s.trim()) : undefined;
222
- await registerService({
223
- kind: KIND,
224
- satsPerChunk: SATS_PER_CHUNK,
225
- chunksPerPayment: CHUNKS_PER_PAYMENT,
226
- model: state.processor?.name || 'unknown',
227
- models,
228
- skill: state.skill,
229
- });
230
- // Prefer Nostr heartbeat (Kind 30333 direct to relay) over API heartbeat
231
- const keys = loadSovereignKeys(state.agentName || undefined);
232
- if (keys?.privkey && keys?.pubkey) {
233
- // Store keys for Nostr-native operations (relay subscribe, Kind 7000/6xxx signing)
234
- if (!state.sovereignKeys) {
235
- state.sovereignKeys = keys;
236
- // Apply NWC/relays from env if not already in keys
237
- if (!keys.nwc_uri && process.env.NWC_URI)
238
- keys.nwc_uri = process.env.NWC_URI;
239
- if (!keys.relays?.length)
240
- keys.relays = RELAYS;
241
- if (!keys.lightning_address && LIGHTNING_ADDRESS)
242
- keys.lightning_address = LIGHTNING_ADDRESS;
243
- }
244
- // Connect a relay pool if not already connected (sovereign mode)
245
- if (!state.relayPool) {
246
- const relayUrls = keys.relays?.length ? keys.relays : RELAYS;
247
- state.relayPool = new RelayPool(relayUrls);
248
- await state.relayPool.connect();
249
- console.log(`[${label}] Relay pool connected (${state.relayPool.connectedCount} relay(s))`);
250
- }
251
- // Subscribe to DVM requests via relay (Nostr-native job discovery)
252
- subscribeDvmRequests(label);
253
- // Build pricing map from config
254
- const pricing = {};
255
- const priceSats = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
256
- if (priceSats > 0)
257
- pricing[String(KIND)] = priceSats;
258
- state.stopHeartbeat = startNostrHeartbeat(label, keys, state.relayPool, {
259
- pricing,
260
- p2pStatsFn: () => ({
261
- sessions: state.p2pSessionsCompleted,
262
- earned_sats: state.p2pTotalEarnedSats,
263
- active: activeSessions.size > 0,
264
- }),
265
- });
266
- console.log(`[${label}] Heartbeat: Nostr (Kind 30333 → relay)`);
267
- }
268
- else {
269
- console.error(`[${label}] ERROR: No local privkey found in .2020117_keys. Heartbeat disabled.`);
270
- console.error(`[${label}] Generate a Nostr keypair and save privkey/pubkey to .2020117_keys.`);
271
- console.error(`[${label}] See: https://2020117.xyz/skill.md §1 Identity`);
272
- }
190
+ console.log(`[${label}] Agent ready\n`);
273
191
  }
274
- // --- 2a. Sovereign Mode (AIP-0009) ---
275
- async function setupSovereign(label) {
276
- const agentName = state.agentName || 'sovereign-agent';
192
+ // --- 2. Nostr Setup (all agents are Nostr-native) ---
193
+ async function setupNostr(label) {
194
+ const agentName = state.agentName || 'agent';
277
195
  // 1. Load or generate Nostr keys
278
196
  let keys = loadSovereignKeys(agentName);
279
197
  if (!keys?.privkey) {
@@ -310,7 +228,7 @@ async function setupSovereign(label) {
310
228
  if (!keys.lightning_address && LIGHTNING_ADDRESS)
311
229
  keys.lightning_address = LIGHTNING_ADDRESS;
312
230
  state.sovereignKeys = keys;
313
- console.log(`[${label}] Sovereign identity: ${keys.pubkey}`);
231
+ console.log(`[${label}] Identity: ${keys.pubkey}`);
314
232
  // 2. Parse NWC URI if available
315
233
  const nwcUri = keys.nwc_uri || process.env.NWC_URI;
316
234
  if (nwcUri) {
@@ -340,7 +258,21 @@ async function setupSovereign(label) {
340
258
  subscribeNipXX(label);
341
259
  // 8. Subscribe to DVM requests (Kind 5xxx) directly from relay
342
260
  subscribeDvmRequests(label);
343
- // 9. Print startup summary
261
+ subscribeDvmResults(label);
262
+ // 9. Start heartbeat (Kind 30333 to relay)
263
+ const pricing = {};
264
+ const priceSats = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
265
+ if (priceSats > 0)
266
+ pricing[String(KIND)] = priceSats;
267
+ state.stopHeartbeat = startNostrHeartbeat(label, state.sovereignKeys, state.relayPool, {
268
+ pricing,
269
+ p2pStatsFn: () => ({
270
+ sessions: state.p2pSessionsCompleted,
271
+ earned_sats: state.p2pTotalEarnedSats,
272
+ active: activeSessions.size > 0,
273
+ }),
274
+ });
275
+ // 10. Print startup summary
344
276
  const relays = (keys.relays || RELAYS).join(', ');
345
277
  console.log('');
346
278
  console.log(`═══════════════════════════════════════════════`);
@@ -353,21 +285,6 @@ async function setupSovereign(label) {
353
285
  console.log(` Processor: ${state.processor?.name || 'none'}`);
354
286
  console.log(`═══════════════════════════════════════════════`);
355
287
  console.log('');
356
- // 10. Start sovereign heartbeat (Kind 30333 to relay)
357
- if (state.sovereignKeys && state.relayPool) {
358
- const pricing = {};
359
- const priceSats = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
360
- if (priceSats > 0)
361
- pricing[String(KIND)] = priceSats;
362
- state.stopHeartbeat = startNostrHeartbeat(label, state.sovereignKeys, state.relayPool, {
363
- pricing,
364
- p2pStatsFn: () => ({
365
- sessions: state.p2pSessionsCompleted,
366
- earned_sats: state.p2pTotalEarnedSats,
367
- active: activeSessions.size > 0,
368
- }),
369
- });
370
- }
371
288
  }
372
289
  async function publishAiInfo(label) {
373
290
  if (!state.sovereignKeys || !state.relayPool)
@@ -466,6 +383,53 @@ function subscribeDvmRequests(label) {
466
383
  });
467
384
  console.log(`[${label}] Subscribed to DVM requests (Kind ${KIND}) via relay`);
468
385
  }
386
+ // --- Customer: subscribe to DVM results and auto-pay ---
387
+ let dvmResultSubscribed = false;
388
+ function subscribeDvmResults(label) {
389
+ if (!state.sovereignKeys || !state.relayPool)
390
+ return;
391
+ if (dvmResultSubscribed)
392
+ return;
393
+ dvmResultSubscribed = true;
394
+ // Subscribe to Kind 6xxx results directed to us (#p = our pubkey)
395
+ const resultKind = KIND + 1000;
396
+ state.relayPool.subscribe({ kinds: [resultKind], '#p': [state.sovereignKeys.pubkey] }, (event) => {
397
+ handleDvmResult(label, event).catch(e => {
398
+ console.error(`[${label}] DVM result handler error: ${e.message}`);
399
+ });
400
+ });
401
+ console.log(`[${label}] Subscribed to DVM results (Kind ${resultKind}) via relay`);
402
+ }
403
+ async function handleDvmResult(label, event) {
404
+ if (!state.sovereignKeys || !state.relayPool)
405
+ return;
406
+ if (event.pubkey === state.sovereignKeys.pubkey)
407
+ return;
408
+ if (!markSeen(event.id))
409
+ return;
410
+ // Extract job reference, amount, and provider's Lightning Address
411
+ const requestId = event.tags.find(t => t[0] === 'e')?.[1];
412
+ const amountMsats = Number(event.tags.find(t => t[0] === 'amount')?.[1] || '0');
413
+ const amountSats = Math.floor(amountMsats / 1000);
414
+ const lightningAddress = event.tags.find(t => t[0] === 'lightning_address')?.[1];
415
+ console.log(`[${label}] DVM result from ${event.pubkey.slice(0, 8)}: ${event.content.slice(0, 80)}...`);
416
+ // Auto-pay if we have NWC and provider has Lightning Address
417
+ if (amountSats > 0 && lightningAddress && state.nwcParsed) {
418
+ try {
419
+ const { preimage } = await nwcPayLightningAddress(state.nwcParsed, lightningAddress, amountSats);
420
+ console.log(`[${label}] Paid ${amountSats} sats → ${lightningAddress} (preimage: ${preimage.slice(0, 16)}...)`);
421
+ }
422
+ catch (e) {
423
+ console.error(`[${label}] Payment failed: ${e instanceof Error ? e.message : 'Unknown error'}`);
424
+ }
425
+ }
426
+ else if (amountSats > 0 && !lightningAddress) {
427
+ console.warn(`[${label}] Result requires ${amountSats} sats but provider has no Lightning Address`);
428
+ }
429
+ else if (amountSats > 0 && !state.nwcParsed) {
430
+ console.warn(`[${label}] Result requires ${amountSats} sats but no NWC wallet configured`);
431
+ }
432
+ }
469
433
  async function handleAiPrompt(label, event) {
470
434
  if (!state.sovereignKeys || !state.relayPool || !state.processor)
471
435
  return;
@@ -551,8 +515,23 @@ async function handleDvmRequest(label, event) {
551
515
  content: '',
552
516
  }, state.sovereignKeys.privkey);
553
517
  await state.relayPool.publish(feedbackEvent);
554
- // Process
555
- const result = await state.processor.generate({ input });
518
+ // Process (with optional pipeline: delegate sub-task first)
519
+ let result;
520
+ if (SUB_KIND) {
521
+ console.log(`[${label}] Pipeline: delegating to kind ${SUB_KIND}...`);
522
+ try {
523
+ const subResult = await delegateNostr(label, SUB_KIND, input, SUB_BID, SUB_PROVIDER);
524
+ console.log(`[${label}] Sub-task returned ${subResult.length} chars`);
525
+ result = await state.processor.generate({ input: subResult });
526
+ }
527
+ catch (e) {
528
+ console.error(`[${label}] Sub-task failed: ${e.message}, using original input`);
529
+ result = await state.processor.generate({ input });
530
+ }
531
+ }
532
+ else {
533
+ result = await state.processor.generate({ input });
534
+ }
556
535
  console.log(`[${label}] DVM result: ${result.length} chars`);
557
536
  // Send result (Kind 6xxx = request kind + 1000)
558
537
  const resultKind = KIND + 1000;
@@ -658,125 +637,43 @@ function startSovereignHeartbeat(label) {
658
637
  return;
659
638
  startNostrHeartbeat(label, state.sovereignKeys, state.relayPool);
660
639
  }
661
- // --- 3. Async Inbox Poller ---
662
- function startInboxPoller(label) {
663
- if (!hasApiKey())
664
- return;
665
- console.log(`[${label}] Inbox polling every ${POLL_INTERVAL / 1000}s`);
666
- async function poll() {
667
- if (state.shuttingDown)
668
- return;
669
- try {
670
- if (getAvailableCapacity() <= 0) {
671
- // No capacity — skip this round
672
- scheduleNext();
673
- return;
674
- }
675
- const jobs = await getInbox({ kind: KIND, status: 'open', limit: 5 });
676
- for (const job of jobs) {
677
- if (state.shuttingDown)
678
- break;
679
- if (!acquireSlot())
680
- break;
681
- // Check bid meets minimum pricing
682
- const bidSats = job.bid_sats ?? 0;
683
- if (MIN_BID_SATS > 0 && bidSats < MIN_BID_SATS) {
684
- console.log(`[${label}] Skipping job ${job.id}: bid ${bidSats} < min ${MIN_BID_SATS} sats`);
685
- releaseSlot();
686
- continue;
687
- }
688
- // Process in background — don't await
689
- processAsyncJob(label, job.id, job.input, job.params).catch((err) => {
690
- console.error(`[${label}] Async job ${job.id} error: ${err.message}`);
691
- });
692
- }
693
- }
694
- catch (e) {
695
- console.warn(`[${label}] Poll error: ${e.message}`);
696
- }
697
- scheduleNext();
698
- }
699
- function scheduleNext() {
700
- if (state.shuttingDown)
701
- return;
702
- state.pollTimer = setTimeout(poll, POLL_INTERVAL);
703
- }
704
- // First poll after a short delay to let swarm set up
705
- state.pollTimer = setTimeout(poll, 2000);
706
- }
707
- async function processAsyncJob(label, inboxJobId, input, params) {
708
- try {
709
- console.log(`[${label}] Accepting job ${inboxJobId}...`);
710
- const accepted = await acceptJob(inboxJobId);
711
- if (!accepted) {
712
- console.warn(`[${label}] Failed to accept job ${inboxJobId}`);
713
- return;
714
- }
715
- const providerJobId = accepted.job_id;
716
- console.log(`[${label}] Job ${providerJobId}: processing "${input.slice(0, 60)}..."`);
717
- await sendFeedback(providerJobId, 'processing');
718
- let result;
719
- // Pipeline: delegate sub-task via API then process locally
720
- if (SUB_KIND) {
721
- console.log(`[${label}] Job ${providerJobId}: delegating to kind ${SUB_KIND}...`);
722
- try {
723
- const subResult = await delegateAPI(SUB_KIND, input, SUB_BID, SUB_PROVIDER);
724
- console.log(`[${label}] Job ${providerJobId}: sub-task returned ${subResult.length} chars`);
725
- result = await state.processor.generate({ input: subResult, params });
726
- }
727
- catch (e) {
728
- console.error(`[${label}] Job ${providerJobId}: sub-task failed: ${e.message}, using original input`);
729
- result = await state.processor.generate({ input, params });
730
- }
731
- }
732
- else {
733
- // No pipeline — direct local processing
734
- result = await state.processor.generate({ input, params });
735
- }
736
- console.log(`[${label}] Job ${providerJobId}: generated ${result.length} chars`);
737
- const ok = await submitResult(providerJobId, result);
738
- if (ok) {
739
- console.log(`[${label}] Job ${providerJobId}: result submitted`);
740
- }
741
- else {
742
- console.warn(`[${label}] Job ${providerJobId}: failed to submit result`);
743
- }
744
- }
745
- finally {
746
- releaseSlot();
747
- }
748
- }
749
640
  // --- Sub-task delegation ---
750
641
  /**
751
- * Delegate a sub-task via platform API. Creates a job, then polls until
752
- * the result is available (max 120s).
642
+ * Delegate a sub-task via Nostr relay. Publishes Kind 5xxx request,
643
+ * then subscribes for Kind 6xxx result (max 120s timeout).
753
644
  */
754
- async function delegateAPI(kind, input, bidSats, provider) {
755
- const tag = `sub-api`;
756
- const created = await createJob({ kind, input, bid_sats: bidSats, provider });
757
- if (!created) {
758
- throw new Error('Failed to create sub-task via API');
645
+ async function delegateNostr(label, kind, input, bidSats, provider) {
646
+ if (!state.sovereignKeys || !state.relayPool) {
647
+ throw new Error('No Nostr keys or relay pool cannot delegate sub-task');
759
648
  }
760
- const jobId = created.job_id;
761
- console.log(`[${tag}] Created job ${jobId} (kind ${kind}, bid ${bidSats})`);
762
- // Poll for result
763
- const deadline = Date.now() + 120_000;
764
- while (Date.now() < deadline) {
765
- await new Promise(r => setTimeout(r, 5_000));
766
- const job = await getJob(jobId);
767
- if (!job)
768
- continue;
769
- if (job.status === 'completed' || job.status === 'result_available') {
770
- if (job.result) {
771
- console.log(`[${tag}] Job ${jobId}: got result (${job.result.length} chars)`);
772
- return job.result;
773
- }
774
- }
775
- if (job.status === 'cancelled' || job.status === 'rejected') {
776
- throw new Error(`Sub-task ${jobId} was ${job.status}`);
777
- }
649
+ const tags = [
650
+ ['i', input, 'text'],
651
+ ['bid', String(bidSats * 1000)], // msats
652
+ ];
653
+ if (provider) {
654
+ tags.push(['p', provider]);
778
655
  }
779
- throw new Error(`Sub-task ${jobId} timed out after 120s`);
656
+ const requestEvent = signEvent({
657
+ kind,
658
+ tags,
659
+ content: '',
660
+ }, state.sovereignKeys.privkey);
661
+ await state.relayPool.publish(requestEvent);
662
+ console.log(`[${label}] Published sub-task (Kind ${kind}, id ${requestEvent.id.slice(0, 8)})`);
663
+ // Subscribe for result (Kind = request kind + 1000) referencing our request
664
+ const resultKind = kind + 1000;
665
+ return new Promise((resolve, reject) => {
666
+ const timeout = setTimeout(() => {
667
+ sub.close();
668
+ reject(new Error(`Sub-task ${requestEvent.id.slice(0, 8)} timed out after 120s`));
669
+ }, 120_000);
670
+ const sub = state.relayPool.subscribe({ kinds: [resultKind], '#e': [requestEvent.id] }, (event) => {
671
+ clearTimeout(timeout);
672
+ sub.close();
673
+ console.log(`[${label}] Sub-task result from ${event.pubkey.slice(0, 8)}: ${event.content.length} chars`);
674
+ resolve(event.content);
675
+ });
676
+ });
780
677
  }
781
678
  const activeSessions = new Map();
782
679
  // Dedup: track recently seen DVM request event IDs (prevent double-processing from relay + inbox)
@@ -1209,9 +1106,7 @@ function setupShutdown(label) {
1209
1106
  return;
1210
1107
  state.shuttingDown = true;
1211
1108
  console.log(`\n[${label}] Shutting down...`);
1212
- // Stop poller & heartbeat
1213
- if (state.pollTimer)
1214
- clearTimeout(state.pollTimer);
1109
+ // Stop heartbeat
1215
1110
  if (state.stopHeartbeat)
1216
1111
  state.stopHeartbeat();
1217
1112
  // Wait for active jobs to finish (max 10s)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Cashu/Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {