@gholl-studio/pier-connector 0.2.6 → 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 +1 -1
- package/src/index.js +111 -34
- package/src/protocol.js +9 -6
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.
|
|
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.
|
|
@@ -477,24 +516,26 @@ export default function register(api) {
|
|
|
477
516
|
// 5. Subscribe to Subjects
|
|
478
517
|
const publicSubject = config.subject;
|
|
479
518
|
const privateSubject = `jobs.node.${config.nodeId}`;
|
|
480
|
-
|
|
481
519
|
// 5a. Restore active job subscriptions (BUG-32)
|
|
482
520
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
521
|
+
const activeConfig = getActiveConfig();
|
|
522
|
+
if (activeConfig.apiBase) {
|
|
523
|
+
logger.info(`[pier-connector] 🔍 Checking for active jobs to restore for node ${config.nodeId}...`);
|
|
524
|
+
const jobsResp = await fetch(`${activeConfig.apiBase}/jobs?assigned_node_id=${config.nodeId}&limit=50`);
|
|
525
|
+
if (jobsResp.ok) {
|
|
526
|
+
const jobsList = await jobsResp.json();
|
|
527
|
+
const activeJobs = (jobsList || []).filter(j => j.status === 'PENDING' || j.status === 'PROCESSING');
|
|
528
|
+
if (activeJobs.length > 0) {
|
|
529
|
+
logger.info(`[pier-connector] ♻️ Restoring ${activeJobs.length} active job subscriptions...`);
|
|
530
|
+
for (const job of activeJobs) {
|
|
531
|
+
if (!jobSubscriptions.has(job.id)) {
|
|
532
|
+
activeNodeJobs.set(job.id, { pierJobId: job.id });
|
|
533
|
+
await subscribeToJobMessages(job.id);
|
|
534
|
+
}
|
|
494
535
|
}
|
|
536
|
+
} else {
|
|
537
|
+
logger.info('[pier-connector] No active jobs found to restore.');
|
|
495
538
|
}
|
|
496
|
-
} else {
|
|
497
|
-
logger.info('[pier-connector] No active jobs found to restore.');
|
|
498
539
|
}
|
|
499
540
|
}
|
|
500
541
|
} catch (err) {
|
|
@@ -617,14 +658,20 @@ export default function register(api) {
|
|
|
617
658
|
// Use a unique durable name per node and job to avoid replaying acknowledged messages
|
|
618
659
|
const durableName = `pier_chat_${config.nodeId.replace(/[^a-zA-Z0-9]/g, '_')}_${jobId.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
619
660
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
661
|
+
let consumer;
|
|
662
|
+
try {
|
|
663
|
+
consumer = await js.consumers.get(streamName, durableName);
|
|
664
|
+
} catch (err) {
|
|
665
|
+
// Only add if it doesn't exist
|
|
666
|
+
await jsm.consumers.add(streamName, {
|
|
667
|
+
durable_name: durableName,
|
|
668
|
+
filter_subject: msgSubject,
|
|
669
|
+
deliver_policy: DeliverPolicy.New, // Reverted to New to stop message storms (BUG-27)
|
|
670
|
+
ack_policy: AckPolicy.Explicit, // Explicit ACK is crucial for deduplication
|
|
671
|
+
ack_wait: 1000 * 60 * 60, // 1 hour ACK wait to allow for long processing (BUG-30)
|
|
672
|
+
});
|
|
673
|
+
consumer = await js.consumers.get(streamName, durableName);
|
|
674
|
+
}
|
|
628
675
|
|
|
629
676
|
const iter = await consumer.consume();
|
|
630
677
|
jobSubscriptions.set(jobId, iter);
|
|
@@ -756,6 +803,13 @@ export default function register(api) {
|
|
|
756
803
|
return;
|
|
757
804
|
}
|
|
758
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
|
+
|
|
759
813
|
// V1.1 FEATURE: Task Poisoning / Poaching Protection
|
|
760
814
|
if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
|
|
761
815
|
// Not for us, nak it so others can take it
|
|
@@ -763,17 +817,30 @@ export default function register(api) {
|
|
|
763
817
|
return;
|
|
764
818
|
}
|
|
765
819
|
|
|
766
|
-
//
|
|
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;
|
|
767
834
|
msg.ack();
|
|
768
835
|
|
|
769
836
|
jobsReceived++;
|
|
770
837
|
const parsed = parseJob(msg, logger);
|
|
771
838
|
if (!parsed.ok) {
|
|
772
839
|
jobsFailed++;
|
|
840
|
+
isBusy = false; // Reset busy state on parse error
|
|
773
841
|
logger.error(`[pier-connector] Job parse error: ${parsed.error}`);
|
|
774
|
-
// Since we already acked, we must send an error result back to Pier
|
|
775
842
|
safeRespond(msg, createErrorPayload({
|
|
776
|
-
id: 'unknown',
|
|
843
|
+
id: jobIdToClaim || 'unknown',
|
|
777
844
|
errorCode: 'PARSE_ERROR',
|
|
778
845
|
errorMessage: parsed.error,
|
|
779
846
|
workerId: config.nodeId,
|
|
@@ -802,17 +869,24 @@ export default function register(api) {
|
|
|
802
869
|
text: job.task,
|
|
803
870
|
};
|
|
804
871
|
|
|
805
|
-
// SUBSCRIBE to job-specific messages FIRST so we don't miss follow-ups
|
|
806
|
-
// 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
|
|
807
873
|
subscribeToJobMessages(job.id);
|
|
808
874
|
|
|
809
875
|
// Trigger agent run
|
|
810
876
|
await receiveIncoming(inbound, job.id);
|
|
811
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
|
+
|
|
812
885
|
} catch (err) {
|
|
886
|
+
isBusy = false;
|
|
813
887
|
jobsFailed++;
|
|
814
888
|
safeRespond(msg, createErrorPayload({
|
|
815
|
-
id: job.id,
|
|
889
|
+
id: (job && job.id) || 'unknown',
|
|
816
890
|
errorCode: 'EXECUTION_FAILED',
|
|
817
891
|
errorMessage: err.message,
|
|
818
892
|
workerId: config.nodeId,
|
|
@@ -831,6 +905,7 @@ export default function register(api) {
|
|
|
831
905
|
logger.info('[pier-connector] NATS connection closed gracefully');
|
|
832
906
|
}
|
|
833
907
|
});
|
|
908
|
+
|
|
834
909
|
} catch (err) {
|
|
835
910
|
connectionStatus = 'error';
|
|
836
911
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
@@ -843,13 +918,10 @@ export default function register(api) {
|
|
|
843
918
|
logger.info('[pier-connector] 🛑 Stopping background service …');
|
|
844
919
|
connectionStatus = 'stopping';
|
|
845
920
|
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
921
|
+
if (nc) {
|
|
922
|
+
await drainConnection(nc, logger);
|
|
923
|
+
nc = null;
|
|
849
924
|
}
|
|
850
|
-
|
|
851
|
-
await drainConnection(nc, logger);
|
|
852
|
-
nc = null;
|
|
853
925
|
connectionStatus = 'disconnected';
|
|
854
926
|
|
|
855
927
|
logger.info('[pier-connector] ✔ Background service stopped');
|
|
@@ -879,6 +951,10 @@ export default function register(api) {
|
|
|
879
951
|
type: 'number',
|
|
880
952
|
description: 'Timeout in milliseconds to wait for a result (default 60000)',
|
|
881
953
|
},
|
|
954
|
+
parentJobId: {
|
|
955
|
+
type: 'string',
|
|
956
|
+
description: 'Optional ID of the parent job triggering this sub-task (for delegation)',
|
|
957
|
+
},
|
|
882
958
|
},
|
|
883
959
|
required: ['task'],
|
|
884
960
|
},
|
|
@@ -905,6 +981,7 @@ export default function register(api) {
|
|
|
905
981
|
task: params.task,
|
|
906
982
|
timeoutMs: timeout,
|
|
907
983
|
meta: params.meta,
|
|
984
|
+
parentJobId: params.parentJobId,
|
|
908
985
|
});
|
|
909
986
|
|
|
910
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.
|
|
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: '
|
|
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.
|
|
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
|
|
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
|
}
|