2020117-agent 0.6.4 → 0.6.6

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 (2) hide show
  1. package/dist/agent.js +108 -18
  2. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -80,6 +80,7 @@ for (const arg of process.argv.slice(2)) {
80
80
  }
81
81
  }
82
82
  import { randomBytes } from 'crypto';
83
+ import { createConnection } from 'net';
83
84
  import { SwarmNode, topicFromKind } from './swarm.js';
84
85
  import { createProcessor } from './processor.js';
85
86
  import { generateInvoice } from './lnurl.js';
@@ -257,10 +258,11 @@ async function setupNostr(label) {
257
258
  // 7. Subscribe to NIP-XX prompts (Kind 25802)
258
259
  subscribeNipXX(label);
259
260
  // 8. Subscribe to DVM requests (Kind 5xxx)
260
- // P2P-only: still subscribes but handleDvmRequest will ignore broadcast jobs,
261
- // only responding to direct requests (p tag = our pubkey)
262
- subscribeDvmRequests(label);
263
- subscribeDvmResults(label);
261
+ // P2P-only: skip relay subscription entirely only serve P2P customers
262
+ if (!P2P_ONLY) {
263
+ subscribeDvmRequests(label);
264
+ subscribeDvmResults(label);
265
+ }
264
266
  // 9. Start heartbeat (Kind 30333 to relay)
265
267
  const pricing = {};
266
268
  const priceSats = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
@@ -551,12 +553,6 @@ async function handleDvmRequest(label, event) {
551
553
  // Skip own events
552
554
  if (event.pubkey === state.sovereignKeys.pubkey)
553
555
  return;
554
- // P2P-only mode: only handle direct requests (p tag pointing to us)
555
- if (P2P_ONLY) {
556
- const isDirected = event.tags.some(t => t[0] === 'p' && t[1] === state.sovereignKeys.pubkey);
557
- if (!isDirected)
558
- return;
559
- }
560
556
  // Dedup: skip already-seen events
561
557
  if (!markSeen(event.id))
562
558
  return;
@@ -742,6 +738,80 @@ async function delegateNostr(label, kind, input, bidSats, provider) {
742
738
  });
743
739
  });
744
740
  }
741
+ /**
742
+ * Inject think:false into Ollama chat/generate requests.
743
+ * Suppresses reasoning tokens on models like qwen3 that enable thinking by default.
744
+ */
745
+ function injectThinkFalse(rawBytes) {
746
+ const raw = rawBytes.toString();
747
+ const sep = raw.indexOf('\r\n\r\n');
748
+ if (sep === -1)
749
+ return rawBytes;
750
+ const header = raw.slice(0, sep + 4);
751
+ const body = raw.slice(sep + 4);
752
+ if (!header.includes('POST') || (!header.includes('/api/generate') && !header.includes('/api/chat')))
753
+ return rawBytes;
754
+ try {
755
+ const json = JSON.parse(body);
756
+ if (json.think !== undefined)
757
+ return rawBytes;
758
+ json.think = false;
759
+ const newBody = JSON.stringify(json);
760
+ const newHeader = header.replace(/Content-Length:\s*\d+/i, `Content-Length: ${Buffer.byteLength(newBody)}`);
761
+ return Buffer.from(newHeader + newBody);
762
+ }
763
+ catch {
764
+ return rawBytes;
765
+ }
766
+ }
767
+ /**
768
+ * After session payment, switch this socket to a raw TCP pipe to the HTTP backend.
769
+ * The customer then has direct HTTP API access (Ollama, SD-WebUI, etc.) with true streaming.
770
+ * This removes SwarmNode's JSON framing from the socket and replaces it with a bidirectional pipe.
771
+ */
772
+ function startTcpProxy(session, node, processorUrl, label) {
773
+ let addr;
774
+ try {
775
+ const u = new URL(processorUrl);
776
+ addr = { host: u.hostname, port: Number(u.port) || (u.protocol === 'https:' ? 443 : 80) };
777
+ }
778
+ catch {
779
+ console.error(`[${label}] Session ${session.sessionId}: invalid backend URL ${processorUrl}`);
780
+ endSession(node, session, label);
781
+ return;
782
+ }
783
+ session.proxyStarted = true;
784
+ const socket = session.socket;
785
+ const backend = createConnection(addr.port, addr.host);
786
+ backend.setKeepAlive(true);
787
+ // Remove SwarmNode's newline-delimited JSON listener so raw HTTP bytes flow through
788
+ socket.removeAllListeners('data');
789
+ socket.on('data', (chunk) => backend.write(injectThinkFalse(chunk)));
790
+ backend.pipe(socket);
791
+ let cleaned = false;
792
+ const cleanup = () => {
793
+ if (cleaned)
794
+ return;
795
+ cleaned = true;
796
+ try {
797
+ socket.destroy();
798
+ }
799
+ catch { }
800
+ try {
801
+ backend.destroy();
802
+ }
803
+ catch { }
804
+ endSession(node, session, label);
805
+ };
806
+ socket.on('close', cleanup);
807
+ socket.on('error', () => cleanup());
808
+ backend.on('close', cleanup);
809
+ backend.on('error', (e) => {
810
+ console.log(`[${label}] Session ${session.sessionId}: backend error: ${e.message}`);
811
+ cleanup();
812
+ });
813
+ console.log(`[${label}] Session ${session.sessionId}: TCP proxy → ${addr.host}:${addr.port}`);
814
+ }
745
815
  const activeSessions = new Map();
746
816
  // Dedup: track recently seen DVM request event IDs (prevent double-processing from relay + inbox)
747
817
  const seenEventIds = new Set();
@@ -800,7 +870,9 @@ async function startSwarmListener(label) {
800
870
  }
801
871
  // --- Session protocol ---
802
872
  if (msg.type === 'session_start') {
803
- if (!LIGHTNING_ADDRESS) {
873
+ const processorUrl = process.env.PROCESSOR || '';
874
+ const proxyMode = processorUrl.startsWith('http://') || processorUrl.startsWith('https://');
875
+ if (!proxyMode && !LIGHTNING_ADDRESS) {
804
876
  node.send(socket, { type: 'error', id: msg.id, message: 'Provider Lightning Address not configured' });
805
877
  return;
806
878
  }
@@ -812,7 +884,8 @@ async function startSwarmListener(label) {
812
884
  const BILLING_INTERVAL_MIN = 1;
813
885
  const billingAmount = satsPerMinute * BILLING_INTERVAL_MIN;
814
886
  const sessionId = randomBytes(8).toString('hex');
815
- console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${billingAmount} sats)`);
887
+ const modeLabel = proxyMode ? `proxy→${processorUrl}` : `${satsPerMinute} sats/min`;
888
+ console.log(`[${label}] Session ${sessionId} from ${tag}: ${modeLabel}`);
816
889
  const session = {
817
890
  socket,
818
891
  peerId,
@@ -825,6 +898,8 @@ async function startSwarmListener(label) {
825
898
  billingTimer: null,
826
899
  timeoutTimer: null,
827
900
  customerPubkey: msg.pubkey || undefined,
901
+ proxyMode,
902
+ proxyStarted: false,
828
903
  };
829
904
  activeSessions.set(sessionId, session);
830
905
  node.send(socket, {
@@ -835,12 +910,23 @@ async function startSwarmListener(label) {
835
910
  payment_method: paymentMethod,
836
911
  pubkey: state.sovereignKeys?.pubkey,
837
912
  });
838
- // Send first billing tick
839
- await sendBillingTick(node, session, billingAmount, label);
840
- // Recurring billing every 10 minutes
841
- session.billingTimer = setInterval(() => {
842
- sendBillingTick(node, session, billingAmount, label);
843
- }, BILLING_INTERVAL_MIN * 60_000);
913
+ if (proxyMode) {
914
+ if (billingAmount <= 0 || !LIGHTNING_ADDRESS) {
915
+ // Free proxy open TCP pipe immediately
916
+ startTcpProxy(session, node, processorUrl, label);
917
+ }
918
+ else {
919
+ // Paid proxy — one-time session fee, TCP pipe starts on payment (session_tick_ack)
920
+ await sendBillingTick(node, session, billingAmount, label);
921
+ }
922
+ }
923
+ else {
924
+ // Per-minute billing for structured request/result sessions
925
+ await sendBillingTick(node, session, billingAmount, label);
926
+ session.billingTimer = setInterval(() => {
927
+ sendBillingTick(node, session, billingAmount, label);
928
+ }, BILLING_INTERVAL_MIN * 60_000);
929
+ }
844
930
  return;
845
931
  }
846
932
  // Customer sent payment (Lightning preimage)
@@ -854,6 +940,10 @@ async function startSwarmListener(label) {
854
940
  session.lastPaidAt = Date.now();
855
941
  console.log(`[${label}] Session ${session.sessionId}: invoice payment received (+${amount}, total: ${session.totalEarned} sats)`);
856
942
  }
943
+ // Proxy mode: switch to raw TCP pipe after first payment confirmed
944
+ if (session.proxyMode && !session.proxyStarted) {
945
+ startTcpProxy(session, node, process.env.PROCESSOR || '', label);
946
+ }
857
947
  return;
858
948
  }
859
949
  if (msg.type === 'session_end') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {