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 +4 -4
- package/dist/agent.js +131 -236
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* 1.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4
|
-
* 1.
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
// ---
|
|
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
|
|
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.
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
//
|
|
188
|
+
// 5. Graceful shutdown
|
|
210
189
|
setupShutdown(label);
|
|
211
|
-
|
|
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
|
-
// ---
|
|
275
|
-
async function
|
|
276
|
-
const agentName = state.agentName || '
|
|
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}]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
752
|
-
*
|
|
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
|
|
755
|
-
|
|
756
|
-
|
|
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
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
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
|
|
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)
|