@gholl-studio/pier-connector 0.1.2 → 0.1.4

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/package.json +1 -1
  2. package/src/index.js +145 -60
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gholl-studio/pier-connector",
3
3
  "author": "gholl",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "OpenClaw plugin that connects to the Pier job marketplace. Automatically fetches, executes, and reports distributed tasks for rewards.",
6
6
  "type": "module",
7
7
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -127,6 +127,65 @@ export default function register(api) {
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * Routes an incoming message from Pier to the OpenClaw agent.
132
+ * Replaces the non-existent api.runtime.sendIncoming.
133
+ */
134
+ async function receiveIncoming(inbound, jobId) {
135
+ if (!api.runtime?.channel?.reply) {
136
+ logger.error(`[pier-connector] SDK Error: api.runtime.channel.reply is not available.`);
137
+ return;
138
+ }
139
+
140
+ const route = api.runtime.channel.routing.resolveAgentRoute({
141
+ cfg: api.config,
142
+ channel: 'pier',
143
+ accountId: inbound.accountId,
144
+ peer: {
145
+ kind: 'direct',
146
+ id: jobId
147
+ }
148
+ });
149
+
150
+ const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
151
+ Body: inbound.text,
152
+ BodyForAgent: inbound.text,
153
+ RawBody: inbound.text,
154
+ From: inbound.senderId,
155
+ To: `pier:${jobId}`,
156
+ SessionKey: route.sessionKey,
157
+ AccountId: route.accountId,
158
+ ChatType: 'direct',
159
+ SenderId: inbound.senderId,
160
+ Provider: 'pier',
161
+ Surface: 'pier',
162
+ OriginatingChannel: 'pier',
163
+ OriginatingTo: `pier:${jobId}`,
164
+ WasMentioned: true,
165
+ CommandAuthorized: true
166
+ });
167
+
168
+ // Record session meta
169
+ if (api.runtime.channel.session?.recordSessionMetaFromInbound) {
170
+ try {
171
+ const storePath = api.runtime.channel.session.resolveStorePath(api.config, route.sessionKey);
172
+ await api.runtime.channel.session.recordSessionMetaFromInbound({
173
+ storePath,
174
+ sessionKey: route.sessionKey,
175
+ ctx: ctxPayload
176
+ });
177
+ } catch (err) {
178
+ logger.debug(`[pier-connector] Failed to record session meta: ${err.message}`);
179
+ }
180
+ }
181
+
182
+ // Dispatch reply — this will trigger outbound: sendText in pierChannel
183
+ await api.runtime.channel.reply.dispatchReplyFromConfig({
184
+ ctx: ctxPayload,
185
+ cfg: api.config,
186
+ });
187
+ }
188
+
130
189
  // ── 1. Register messaging channel ──────────────────────────────────
131
190
 
132
191
  const pierChannel = {
@@ -401,8 +460,9 @@ export default function register(api) {
401
460
 
402
461
  const iter = await consumer.consume();
403
462
  for await (const msg of iter) {
404
- await handleMessage(msg);
405
- msg.ack();
463
+ handleMessage(msg).catch(err => {
464
+ logger.error(`[pier-connector] Fatal handleMessage error: ${err.message}`);
465
+ });
406
466
  }
407
467
  } catch (err) {
408
468
  logger.error(`[pier-connector] Marketplace JetStream error: ${err.message}`);
@@ -433,8 +493,9 @@ export default function register(api) {
433
493
 
434
494
  const iter = await consumer.consume();
435
495
  for await (const msg of iter) {
436
- await handleMessage(msg);
437
- msg.ack();
496
+ handleMessage(msg).catch(err => {
497
+ logger.error(`[pier-connector] Fatal handleMessage error: ${err.message}`);
498
+ });
438
499
  }
439
500
  } catch (err) {
440
501
  logger.error(`[pier-connector] Direct JetStream error: ${err.message}`);
@@ -467,53 +528,73 @@ export default function register(api) {
467
528
  const iter = await consumer.consume();
468
529
  jobSubscriptions.set(jobId, iter);
469
530
 
470
- for await (const msg of iter) {
471
- msg.ack();
472
- const rawMsg = new TextDecoder().decode(msg.data);
473
- let msgPayload;
474
- try {
475
- msgPayload = JSON.parse(rawMsg);
476
- } catch (e) { continue; }
477
-
478
- // Ignore my own messages
479
- if (msgPayload.sender_id === config.nodeId) continue;
480
-
481
- if (msgPayload.type === 'receipt') continue;
482
-
483
- // Send read receipt back
484
- if (js && msgPayload.id) {
531
+ (async () => {
532
+ for await (const msg of iter) {
485
533
  try {
486
- const replySubject = `jobs.job.${jobId}.msg`;
487
- await js.publish(replySubject, new TextEncoder().encode(JSON.stringify({
488
- type: 'receipt',
489
- msg_id: msgPayload.id,
490
- reader_id: config.nodeId
491
- })));
492
- logger.info(`[pier-connector] 👁️ Sent read receipt for message ${msgPayload.id}`);
534
+ const rawMsg = new TextDecoder().decode(msg.data);
535
+ let msgPayload;
536
+ try {
537
+ msgPayload = JSON.parse(rawMsg);
538
+ } catch (e) {
539
+ msg.ack();
540
+ continue;
541
+ }
542
+
543
+ // Ignore my own messages
544
+ if (msgPayload.sender_id === config.nodeId) {
545
+ msg.ack();
546
+ continue;
547
+ }
548
+
549
+ if (msgPayload.type === 'receipt') {
550
+ msg.ack();
551
+ continue;
552
+ }
553
+
554
+ // Send read receipt back
555
+ if (js && msgPayload.id) {
556
+ try {
557
+ const replySubject = `jobs.job.${jobId}.msg`;
558
+ await js.publish(replySubject, new TextEncoder().encode(JSON.stringify({
559
+ type: 'receipt',
560
+ msg_id: msgPayload.id,
561
+ reader_id: config.nodeId
562
+ })));
563
+ logger.info(`[pier-connector] 👁️ Sent read receipt for message ${msgPayload.id}`);
564
+ } catch (err) {
565
+ logger.error(`[pier-connector] Failed to send read receipt: ${err.message}`);
566
+ }
567
+ }
568
+
569
+ const content = msgPayload.content;
570
+ logger.info(`[pier-connector] 💬 Message for job ${jobId}: "${truncate(content, 40)}"`);
571
+
572
+ const senderCore = msgPayload.sender_id;
573
+
574
+ activeNodeJobs.set(jobId, {
575
+ pierJobId: jobId,
576
+ isRealtimeMsg: true
577
+ });
578
+
579
+ // Wait for agent to finish before acking - this provides backpressure
580
+ await receiveIncoming({
581
+ accountId: config.accountId || 'default',
582
+ senderId: `pier:${senderCore}`,
583
+ text: content,
584
+ }, jobId);
585
+
586
+ msg.ack();
493
587
  } catch (err) {
494
- logger.error(`[pier-connector] Failed to send read receipt: ${err.message}`);
588
+ logger.error(`[pier-connector] Chat message processing error for ${jobId}: ${err.message}`);
589
+ msg.nak();
495
590
  }
496
591
  }
497
-
498
- const content = msgPayload.content;
499
- logger.info(`[pier-connector] 💬 Message for job ${jobId}: "${truncate(content, 40)}"`);
500
-
501
- const senderCore = msgPayload.sender_id;
502
-
503
- activeNodeJobs.set(senderCore, {
504
- pierJobId: jobId,
505
- isRealtimeMsg: true
506
- });
507
-
508
- await api.runtime.sendIncoming({
509
- channelId: 'pier',
510
- accountId: config.accountId || 'default',
511
- senderId: `pier:${senderCore}`,
512
- text: content,
513
- });
514
- }
592
+ })().catch(err => {
593
+ logger.error(`[pier-connector] Chat iteration died for ${jobId}: ${err.message}`);
594
+ jobSubscriptions.delete(jobId);
595
+ });
515
596
  } catch (err) {
516
- logger.error(`[pier-connector] Job JS message error for ${jobId}: ${err.message}`);
597
+ logger.error(`[pier-connector] Failed to setup chat consumer for ${jobId}: ${err.message}`);
517
598
  }
518
599
  }
519
600
 
@@ -527,6 +608,7 @@ export default function register(api) {
527
608
  payload = JSON.parse(rawData);
528
609
  } catch (e) {
529
610
  logger.error(`[pier-connector] Failed to parse JSON: ${rawData.substring(0, 100)}`);
611
+ msg.ack();
530
612
  return;
531
613
  }
532
614
 
@@ -535,22 +617,30 @@ export default function register(api) {
535
617
  const { jobId } = payload;
536
618
  if (jobId) {
537
619
  logger.info(`[pier-connector] ⏰ Received wakeup signal for job ${jobId}`);
620
+ msg.ack();
538
621
  subscribeToJobMessages(jobId);
622
+ } else {
623
+ msg.ack();
539
624
  }
540
625
  return;
541
626
  }
542
627
 
543
628
  // V1.1 FEATURE: Task Poisoning / Poaching Protection
544
629
  if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
545
- // Silent ignore - don't reply, don't log heavily
630
+ // Not for us, nak it so others can take it
631
+ msg.nak();
546
632
  return;
547
633
  }
548
634
 
635
+ // ACK the job early to 'claim' it and prevent redelivery while we process
636
+ msg.ack();
637
+
549
638
  jobsReceived++;
550
639
  const parsed = parseJob(msg, logger);
551
640
  if (!parsed.ok) {
552
641
  jobsFailed++;
553
642
  logger.error(`[pier-connector] Job parse error: ${parsed.error}`);
643
+ // Since we already acked, we must send an error result back to Pier
554
644
  safeRespond(msg, createErrorPayload({
555
645
  id: 'unknown',
556
646
  errorCode: 'PARSE_ERROR',
@@ -567,32 +657,27 @@ export default function register(api) {
567
657
  try {
568
658
  const senderCore = job.meta?.sender ?? 'anonymous';
569
659
 
570
- activeNodeJobs.set(senderCore, {
660
+ activeNodeJobs.set(job.id, {
571
661
  pierJobId: job.id,
572
662
  pierNatsMsg: msg,
573
- pierStartTime: startTime,
663
+ pierStartTime: performance.now(),
574
664
  pierMeta: job.meta,
575
665
  isRealtimeMsg: false
576
666
  });
577
667
 
578
668
  const inbound = {
579
- channelId: 'pier',
580
669
  accountId: config.accountId || 'default',
581
670
  senderId: `pier:${senderCore}`,
582
671
  text: job.task,
583
- assignedAgentId: config.agentId || undefined,
584
672
  };
585
673
 
586
- if (job.systemPrompt) inbound.systemPrompt = job.systemPrompt;
674
+ // SUBSCRIBE to job-specific messages FIRST so we don't miss follow-ups during the agent run
675
+ // This handles the user's "can only receive one message" issue
676
+ subscribeToJobMessages(job.id);
587
677
 
588
- if (api.runtime?.sendIncoming) {
589
- await api.runtime.sendIncoming(inbound);
590
-
591
- // SUBSCRIBE to job-specific messages for real-time communication
592
- subscribeToJobMessages(job.id);
593
- } else {
594
- throw new Error('Agent runtime not available');
595
- }
678
+ // Trigger agent run
679
+ await receiveIncoming(inbound, job.id);
680
+
596
681
  } catch (err) {
597
682
  jobsFailed++;
598
683
  safeRespond(msg, createErrorPayload({