@gholl-studio/pier-connector 0.2.13 → 0.2.17

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 +108 -25
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gholl-studio/pier-connector",
3
3
  "author": "gholl",
4
- "version": "0.2.13",
4
+ "version": "0.2.17",
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
@@ -188,13 +188,25 @@ export default function register(api) {
188
188
  }
189
189
  });
190
190
 
191
+ const dynamicSessionKey = `pier-job-${jobId}`;
192
+ const injectedPrompt = [
193
+ "=========================================",
194
+ "CRITICAL DIRECTIVE - PIER TRADING NETWORK",
195
+ "=========================================",
196
+ "You are an autonomous AI worker on Pier.",
197
+ "1. You MUST USE the `pier_finish_task` tool to submit the final result when your work is complete.",
198
+ "2. If you need to negotiate or clarify requirements with the employer, use `pier_chat`.",
199
+ "3. Do not just wait in silence. Always officially finish the task if the employer's goal is met.",
200
+ inbound.systemPrompt ? `\nEmployer Guidelines:\n${inbound.systemPrompt}` : ""
201
+ ].join('\n');
202
+
191
203
  const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
192
204
  Body: inbound.text,
193
205
  BodyForAgent: inbound.text,
194
206
  RawBody: inbound.text,
195
207
  From: inbound.senderId,
196
208
  To: `pier:${jobId}`,
197
- SessionKey: route.sessionKey,
209
+ SessionKey: dynamicSessionKey,
198
210
  AccountId: route.accountId,
199
211
  ChatType: 'direct',
200
212
  SenderId: inbound.senderId,
@@ -204,24 +216,34 @@ export default function register(api) {
204
216
  OriginatingTo: `pier:${jobId}`,
205
217
  WasMentioned: true,
206
218
  CommandAuthorized: true,
207
- SystemPrompt: inbound.systemPrompt || undefined,
219
+ SystemPrompt: injectedPrompt,
208
220
  MessageId: inbound.messageId || jobId
209
221
  });
210
222
 
223
+ let collectedResult = '';
224
+
211
225
  // Create a dispatcher to handle the reply lifecycle (fixes sendFinalReply error)
212
226
  const { dispatcher, markDispatchIdle } = api.runtime.channel.reply.createReplyDispatcherWithTyping({
213
227
  cfg: api.config,
214
228
  agentId: route.agentId,
215
229
  onIdle: () => {
216
- logger.debug(`[pier-connector] Dispatcher idle for session ${route.sessionKey}`);
230
+ logger.debug(`[pier-connector] Dispatcher idle for session ${dynamicSessionKey}`);
217
231
  },
218
232
  deliver: async (payload) => {
219
- // Route Agent's reply to outbound.sendText so it reaches NATS
220
- logger.debug(`[pier-connector] deliver() called with text: "${truncate(payload.text, 60)}"`);
233
+ const metadata = activeNodeJobs.get(jobId);
234
+ const isRealtime = metadata?.isRealtimeMsg;
235
+
236
+ if (!isRealtime) {
237
+ collectedResult += payload.text;
238
+ }
239
+
240
+ // Route Agent's reply to outbound.sendText
241
+ // For marketplace jobs, sendText will now just log (content is buffered in collectedResult)
242
+ // For realtime chat, it will publish to the chat subject immediately.
221
243
  await pierChannel.outbound.sendText({
222
244
  text: payload.text,
223
245
  to: `pier:${jobId}`,
224
- metadata: activeNodeJobs.get(jobId),
246
+ metadata,
225
247
  });
226
248
  }
227
249
  });
@@ -229,10 +251,10 @@ export default function register(api) {
229
251
  // Record session meta
230
252
  if (api.runtime.channel.session?.recordSessionMetaFromInbound) {
231
253
  try {
232
- const storePath = api.runtime.channel.session.resolveStorePath(api.config, route.sessionKey);
254
+ const storePath = api.runtime.channel.session.resolveStorePath(api.config, dynamicSessionKey);
233
255
  await api.runtime.channel.session.recordSessionMetaFromInbound({
234
256
  storePath,
235
- sessionKey: route.sessionKey,
257
+ sessionKey: dynamicSessionKey,
236
258
  ctx: ctxPayload
237
259
  });
238
260
  } catch (err) {
@@ -241,14 +263,14 @@ export default function register(api) {
241
263
  }
242
264
 
243
265
  try {
244
- logger.info(`[pier-connector] 🧠 Triggering agent for session ${route.sessionKey}...`);
266
+ logger.info(`[pier-connector] 🧠 Triggering agent for session ${dynamicSessionKey}...`);
245
267
  // Dispatch reply — this will trigger outbound: sendText in pierChannel
246
268
  await api.runtime.channel.reply.dispatchReplyFromConfig({
247
269
  ctx: ctxPayload,
248
270
  cfg: api.config,
249
271
  dispatcher
250
272
  });
251
- logger.info(`[pier-connector] 🧠 Agent dispatch completed for ${route.sessionKey}`);
273
+ logger.info(`[pier-connector] 🧠 Agent dispatch completed for ${dynamicSessionKey}`);
252
274
  } finally {
253
275
  markDispatchIdle();
254
276
  }
@@ -362,17 +384,9 @@ export default function register(api) {
362
384
  logger.error(`[pier-connector] Failed to publish realtime reply to NATS: ${err.message}`);
363
385
  }
364
386
  } else if (js) {
365
- // Publish result to jobs.submit via JetStream (not msg.respond)
366
- try {
367
- await js.publish('jobs.submit', new TextEncoder().encode(JSON.stringify(responsePayload)));
368
- jobsCompleted++;
369
- logger.info(
370
- `[pier-connector] ✔ Job ${jobId} result published to jobs.submit` +
371
- (elapsed ? ` — latency: ${elapsed}ms` : ''),
372
- );
373
- } catch (err) {
374
- logger.error(`[pier-connector] ❌ Failed to publish result for job ${jobId}: ${err.message}`);
375
- }
387
+ // BUG-44: Marketplace results are now buffered in receiveIncoming and submitted ONCE at the end.
388
+ // sendText only logs the progress here.
389
+ logger.debug(`[pier-connector] Marketplace response buffered: "${truncate(text, 40)}"`);
376
390
  }
377
391
 
378
392
  // Delayed stop for job-specific message listener
@@ -544,9 +558,11 @@ export default function register(api) {
544
558
  logger.warn(`[pier-connector] ⚠️ Failed to restore active jobs: ${err.message}`);
545
559
  }
546
560
 
547
- // Public pool with load balancing via JetStream Durable Consumer
561
+ // Public pool with broadcast via JetStream Durable Consumer
548
562
  if (publicSubject) {
549
- const durableName = `pier_marketplace_${config.queueGroup || 'default'}`;
563
+ // For Bidding, every node needs to see the marketplace requests to evaluate them,
564
+ // so we do not use the shared queueGroup. We use a unique durable per node.
565
+ const durableName = `pier_market_${config.nodeId.replace(/-/g, '_')}`;
550
566
  const streamName = 'PIER_JOBS';
551
567
  logger.info(`[pier-connector] 👂 Listening to Marketplace (JetStream): ${publicSubject} (Durable: ${durableName})`);
552
568
 
@@ -565,7 +581,7 @@ export default function register(api) {
565
581
  filter_subject: publicSubject,
566
582
  deliver_policy: DeliverPolicy.New, // SDK-3: Only new messages
567
583
  ack_policy: AckPolicy.Explicit,
568
- deliver_group: config.queueGroup
584
+ // Removed deliver_group to ensure it's a broadcast to all independent nodes
569
585
  });
570
586
  } catch (addErr) {
571
587
  // SDK-5: Config mismatch — delete old and recreate
@@ -576,7 +592,6 @@ export default function register(api) {
576
592
  filter_subject: publicSubject,
577
593
  deliver_policy: DeliverPolicy.New,
578
594
  ack_policy: AckPolicy.Explicit,
579
- deliver_group: config.queueGroup
580
595
  });
581
596
  }
582
597
  consumer = await js.consumers.get(streamName, durableName);
@@ -807,6 +822,7 @@ export default function register(api) {
807
822
 
808
823
  // Anti-Interference: If busy, ignore new marketplace jobs (BUG-34)
809
824
  if (isBusy) {
825
+ logger.debug(`[pier-connector] 🚫 Busy. Ignoring new job payload: ${rawData.substring(0, 50)}...`);
810
826
  // NAK so others in the queue group can take it
811
827
  msg.nak();
812
828
  return;
@@ -1088,6 +1104,73 @@ export default function register(api) {
1088
1104
  { optional: true }
1089
1105
  );
1090
1106
 
1107
+ // ── V2 System Action Tools ────────────────────────────────────────
1108
+
1109
+ const registerSystemActionTool = (name, description, action, extraParams, userRole = 'node') => {
1110
+ api.registerTool({
1111
+ name,
1112
+ description,
1113
+ parameters: {
1114
+ type: 'object',
1115
+ properties: {
1116
+ jobId: { type: 'string', description: 'The ID of the job/chat session' },
1117
+ ...extraParams
1118
+ },
1119
+ required: ['jobId', ...Object.keys(extraParams)]
1120
+ },
1121
+ async execute(_id, params) {
1122
+ if (!nc || connectionStatus !== 'connected') {
1123
+ return { content: [{ type: 'text', text: 'Error: NATS not connected' }] };
1124
+ }
1125
+ try {
1126
+ const config = getActiveConfig();
1127
+ const subject = `jobs.job.${params.jobId}.msg`;
1128
+
1129
+ const { jobId, ...payload } = params;
1130
+
1131
+ // If acting as User (Employer), we need the user API key ideally, but for demo we just pass node ID or secret.
1132
+ // The Backend handles sender_type="node" vs "user".
1133
+ const msgData = {
1134
+ id: ethers.hexlify(ethers.randomBytes(16)),
1135
+ job_id: params.jobId,
1136
+ sender_id: userRole === 'user' ? 'user_' + config.nodeId : config.nodeId,
1137
+ sender_type: userRole,
1138
+ content: JSON.stringify({
1139
+ type: 'system_action',
1140
+ action,
1141
+ payload
1142
+ }),
1143
+ timestamp: new Date().toISOString(),
1144
+ auth_token: config.secretKey, // auth handle in DB needs updating for user role simulation
1145
+ type: 'system_action',
1146
+ action: action
1147
+ };
1148
+
1149
+ await js.publish(subject, new TextEncoder().encode(JSON.stringify(msgData)));
1150
+ return { content: [{ type: 'text', text: `${action} executed successfully` }] };
1151
+ } catch (err) {
1152
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
1153
+ }
1154
+ }
1155
+ }, { optional: true });
1156
+ };
1157
+
1158
+ // For Node (Worker)
1159
+ registerSystemActionTool('pier_bid_task', 'Bid on an open marketplace task to offer your services to the employer', 'task_bid', { message: { type: 'string', description: 'Your pitch explaining why you are a good fit for this task' } });
1160
+ registerSystemActionTool('pier_accept_task', 'Accept an offered task from the employer in the current chat', 'task_accept', {});
1161
+ registerSystemActionTool('pier_finish_task', 'Submit the final result for a task', 'task_submit', { result: { type: 'string', description: 'The final result or file links' } });
1162
+
1163
+ // For User (Employer Robot) - Spoofed identity using Node's config for A2A testing
1164
+ registerSystemActionTool('pier_propose_task', 'Offer a task to a node with a specific price', 'task_offer', {
1165
+ price: { type: 'number', description: 'The price in PIER tokens to offer' },
1166
+ description: { type: 'string', description: 'The formal task description' }
1167
+ }, 'user');
1168
+
1169
+ registerSystemActionTool('pier_rate_task', 'Rate the node after completion', 'task_rate', {
1170
+ score: { type: 'number', description: 'Rating from 1 to 5' },
1171
+ comment: { type: 'string', description: 'A short review' }
1172
+ }, 'user');
1173
+
1091
1174
  // ── 4. Register /pier status command ───────────────────────────────
1092
1175
 
1093
1176
  api.registerCommand({