@gholl-studio/pier-connector 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gholl-studio/pier-connector",
3
3
  "author": "gholl",
4
- "version": "0.2.7",
4
+ "version": "0.2.9",
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
@@ -46,6 +46,12 @@ export default function register(api) {
46
46
  const jobStopTimeouts = new Map();
47
47
  const activeNodeJobs = new Map();
48
48
 
49
+ /**
50
+ * Anti-Interference Flag (BUG-34)
51
+ * When true, the robot will ignore new marketplace jobs.
52
+ */
53
+ let isBusy = false;
54
+
49
55
  let jobsReceived = 0;
50
56
  let jobsCompleted = 0;
51
57
  let jobsFailed = 0;
@@ -127,6 +133,39 @@ export default function register(api) {
127
133
  }
128
134
  }
129
135
 
136
+ /**
137
+ * Atomically claims a job from the backend before starting execution.
138
+ * Ensures "one-job-at-a-time" exclusivity.
139
+ */
140
+ async function claimJob(jobId) {
141
+ const config = getActiveConfig();
142
+ try {
143
+ const resp = await fetch(`${config.pierApiUrl}/jobs/${jobId}/claim`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'Authorization': `Bearer ${config.secretKey}`,
148
+ }
149
+ });
150
+
151
+ if (!resp.ok) {
152
+ if (resp.status === 409) {
153
+ logger.warn(`[pier-connector] 🚫 Job ${jobId} was already claimed by someone else.`);
154
+ } else {
155
+ const errData = await resp.json().catch(() => ({}));
156
+ logger.error(`[pier-connector] ✖ Failed to claim job ${jobId}: ${errData.error || resp.statusText}`);
157
+ }
158
+ return false;
159
+ }
160
+
161
+ logger.info(`[pier-connector] ✅ Successfully claimed job ${jobId}`);
162
+ return true;
163
+ } catch (err) {
164
+ logger.error(`[pier-connector] ✖ Network error claiming job ${jobId}: ${err.message}`);
165
+ return false;
166
+ }
167
+ }
168
+
130
169
  /**
131
170
  * Routes an incoming message from Pier to the OpenClaw agent.
132
171
  * Replaces the non-existent api.runtime.sendIncoming.
@@ -764,6 +803,13 @@ export default function register(api) {
764
803
  return;
765
804
  }
766
805
 
806
+ // Anti-Interference: If busy, ignore new marketplace jobs (BUG-34)
807
+ if (isBusy) {
808
+ // NAK so others in the queue group can take it
809
+ msg.nak();
810
+ return;
811
+ }
812
+
767
813
  // V1.1 FEATURE: Task Poisoning / Poaching Protection
768
814
  if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
769
815
  // Not for us, nak it so others can take it
@@ -771,17 +817,30 @@ export default function register(api) {
771
817
  return;
772
818
  }
773
819
 
774
- // ACK the job early to 'claim' it and prevent redelivery while we process
820
+ // For Marketplace jobs (those without assigned_node_id, or even those with it),
821
+ // we must ATOMICALLY CLAIM it from the backend before setting isBusy.
822
+ const jobIdToClaim = payload.id;
823
+ if (jobIdToClaim) {
824
+ const success = await claimJob(jobIdToClaim);
825
+ if (!success) {
826
+ // Already taken or error, NAK it just in case someone else can try
827
+ msg.nak();
828
+ return;
829
+ }
830
+ }
831
+
832
+ // Claimed successfully! Set busy state
833
+ isBusy = true;
775
834
  msg.ack();
776
835
 
777
836
  jobsReceived++;
778
837
  const parsed = parseJob(msg, logger);
779
838
  if (!parsed.ok) {
780
839
  jobsFailed++;
840
+ isBusy = false; // Reset busy state on parse error
781
841
  logger.error(`[pier-connector] Job parse error: ${parsed.error}`);
782
- // Since we already acked, we must send an error result back to Pier
783
842
  safeRespond(msg, createErrorPayload({
784
- id: 'unknown',
843
+ id: jobIdToClaim || 'unknown',
785
844
  errorCode: 'PARSE_ERROR',
786
845
  errorMessage: parsed.error,
787
846
  workerId: config.nodeId,
@@ -810,13 +869,21 @@ export default function register(api) {
810
869
  text: job.task,
811
870
  };
812
871
 
813
- // SUBSCRIBE to job-specific messages FIRST so we don't miss follow-ups during the agent run
814
- // This handles the user's "can only receive one message" issue
872
+ // SUBSCRIBE to job-specific messages FIRST so we don't miss follow-ups
815
873
  subscribeToJobMessages(job.id);
816
874
 
817
875
  // Trigger agent run
818
876
  await receiveIncoming(inbound, job.id);
877
+
878
+ // NOTE: isBusy is reset when the agent completes.
879
+ // Because receiveIncoming (via OpenClaw core) might be async or callback-based,
880
+ // we need to be careful. However, for standard MCP tools/agents,
881
+ // we'll assume isBusy should stay true until the job lifecycle is managed.
882
+ // For now, simple reset after agent returns or in error.
883
+ isBusy = false;
884
+
819
885
  } catch (err) {
886
+ isBusy = false;
820
887
  jobsFailed++;
821
888
  safeRespond(msg, createErrorPayload({
822
889
  id: (job && job.id) || 'unknown',
@@ -884,6 +951,10 @@ export default function register(api) {
884
951
  type: 'number',
885
952
  description: 'Timeout in milliseconds to wait for a result (default 60000)',
886
953
  },
954
+ parentJobId: {
955
+ type: 'string',
956
+ description: 'Optional ID of the parent job triggering this sub-task (for delegation)',
957
+ },
887
958
  },
888
959
  required: ['task'],
889
960
  },
@@ -910,6 +981,7 @@ export default function register(api) {
910
981
  task: params.task,
911
982
  timeoutMs: timeout,
912
983
  meta: params.meta,
984
+ parentJobId: params.parentJobId,
913
985
  });
914
986
 
915
987
  try {
package/src/protocol.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Standardizes outbound and inbound JSON boundaries for NATS.
5
5
  */
6
6
 
7
- export const PROTOCOL_VERSION = '1.0';
7
+ export const PROTOCOL_VERSION = '1.1';
8
8
 
9
9
  /**
10
10
  * Creates a standard task request payload to send to Pier.
@@ -15,18 +15,20 @@ export const PROTOCOL_VERSION = '1.0';
15
15
  * @param {number} [params.timeoutMs]
16
16
  * @param {object} [params.meta]
17
17
  * @param {string} [params.targetNodeId]
18
+ * @param {string} [params.parentJobId]
18
19
  * @returns {object}
19
20
  */
20
- export function createRequestPayload({ task, systemPrompt, timeoutMs, meta, targetNodeId }) {
21
+ export function createRequestPayload({ task, systemPrompt, timeoutMs, meta, targetNodeId, parentJobId }) {
21
22
  return {
22
23
  version: PROTOCOL_VERSION,
23
24
  id: crypto.randomUUID(),
24
- type: 'task_request',
25
+ type: 'task', // Changed from task_request to task to align with backend models
25
26
  task,
26
27
  systemPrompt,
27
28
  timeoutMs: timeoutMs || 60000,
28
29
  meta: meta || {},
29
30
  targetNodeId: targetNodeId || null,
31
+ parentJobId: parentJobId || null,
30
32
  };
31
33
  }
32
34
 
@@ -91,10 +93,10 @@ export function createErrorPayload({ id, errorCode, errorMessage, workerId, wall
91
93
  * @returns {{ ok: true, job: object } | { ok: false, error: string }}
92
94
  */
93
95
  export function normalizeInboundPayload(payload) {
94
- // If it's a strict v1.0 protocol request
95
- if (payload.type === 'task_request') {
96
+ // If it's a strict v1.1+ protocol request
97
+ if (payload.type === 'task' || payload.type === 'task_request') {
96
98
  if (!payload.task) {
97
- return { ok: false, error: 'Missing "task" field in task_request payload' };
99
+ return { ok: false, error: 'Missing "task" field in task payload' };
98
100
  }
99
101
  return {
100
102
  ok: true,
@@ -104,6 +106,7 @@ export function normalizeInboundPayload(payload) {
104
106
  systemPrompt: payload.systemPrompt,
105
107
  meta: payload.meta || {},
106
108
  targetNodeId: payload.targetNodeId || null,
109
+ parentJobId: payload.parentJobId || null,
107
110
  },
108
111
  };
109
112
  }