@pnds/pond 1.8.0 → 1.9.0
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 +3 -3
- package/src/gateway.ts +80 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnds/pond",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "OpenClaw channel plugin for Pond IM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"openclaw.plugin.json"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@pnds/
|
|
19
|
-
"@pnds/
|
|
18
|
+
"@pnds/cli": "1.9.0",
|
|
19
|
+
"@pnds/sdk": "1.9.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22.0.0",
|
package/src/gateway.ts
CHANGED
|
@@ -402,7 +402,7 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
// Resolve remaining items as not-dispatched (fork stopped after error).
|
|
405
|
-
// These events are NOT acked —
|
|
405
|
+
// These events are NOT acked — dispatch retains them for catch-up replay.
|
|
406
406
|
for (const remaining of fork.queue.splice(0)) {
|
|
407
407
|
remaining.resolve(false);
|
|
408
408
|
}
|
|
@@ -535,10 +535,10 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
535
535
|
|
|
536
536
|
dispatchState.mainCurrentTargetId = event.targetId;
|
|
537
537
|
await runDispatch(event, orchestratorKey, bodyForAgent);
|
|
538
|
-
// Ack
|
|
538
|
+
// Ack dispatch after successful drain — these events were buffered
|
|
539
539
|
// without ack, so this is the first time they're confirmed processed.
|
|
540
540
|
const sourceType = event.type === "task" ? "task_activity" : "message";
|
|
541
|
-
client.
|
|
541
|
+
client.ackDispatch(config.org_id, { source_type: sourceType, source_id: event.messageId }).catch(() => {});
|
|
542
542
|
}
|
|
543
543
|
} finally {
|
|
544
544
|
draining = false;
|
|
@@ -550,8 +550,8 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
550
550
|
/**
|
|
551
551
|
* Route and dispatch an inbound event.
|
|
552
552
|
* Returns true if the event was dispatched (main or fork), false if only buffered.
|
|
553
|
-
* Callers should only ack
|
|
554
|
-
* lost on crash and need to be replayed from
|
|
553
|
+
* Callers should only ack dispatch when this returns true — buffered events may be
|
|
554
|
+
* lost on crash and need to be replayed from dispatch on the next catch-up.
|
|
555
555
|
*/
|
|
556
556
|
async function handleInboundEvent(event: PondEvent): Promise<boolean> {
|
|
557
557
|
const disposition = routeTrigger(event, dispatchState, defaultRoutingStrategy);
|
|
@@ -683,13 +683,13 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
683
683
|
});
|
|
684
684
|
}, intervalSec * 1000);
|
|
685
685
|
|
|
686
|
-
//
|
|
686
|
+
// Dispatch catch-up: fetch pending dispatch events (FIFO).
|
|
687
687
|
// On cold start, skip items older than COLD_START_WINDOW_MS to avoid
|
|
688
688
|
// replaying historical backlog from a previous process.
|
|
689
689
|
const isFirstHello = !hadSuccessfulHello;
|
|
690
690
|
hadSuccessfulHello = true;
|
|
691
|
-
|
|
692
|
-
log?.warn(`pond[${ctx.accountId}]:
|
|
691
|
+
catchUpFromDispatch(isFirstHello).catch((err) => {
|
|
692
|
+
log?.warn(`pond[${ctx.accountId}]: dispatch catch-up failed: ${String(err)}`);
|
|
693
693
|
});
|
|
694
694
|
} catch (err) {
|
|
695
695
|
log?.error(`pond[${ctx.accountId}]: failed to fetch chats: ${String(err)}`);
|
|
@@ -757,7 +757,7 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
757
757
|
(m) => m.user_id === agentUserId || m.user_id === MENTION_ALL_USER_ID,
|
|
758
758
|
);
|
|
759
759
|
if (!isMentioned) {
|
|
760
|
-
// Release claim so catch-up can process this via
|
|
760
|
+
// Release claim so catch-up can process this via dispatch events
|
|
761
761
|
dispatchedMessages.delete(data.id);
|
|
762
762
|
return;
|
|
763
763
|
}
|
|
@@ -802,7 +802,7 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
802
802
|
try {
|
|
803
803
|
const dispatched = await handleInboundEvent(event);
|
|
804
804
|
if (dispatched) {
|
|
805
|
-
client.
|
|
805
|
+
client.ackDispatch(config.org_id, { source_type: "message", source_id: data.id }).catch(() => {});
|
|
806
806
|
} else {
|
|
807
807
|
// Not dispatched (buffered or fork failed) — release claim so catch-up can retry
|
|
808
808
|
dispatchedMessages.delete(data.id);
|
|
@@ -861,7 +861,7 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
861
861
|
// Server event buffer overflowed — events were lost, trigger full catch-up
|
|
862
862
|
ws.on("recovery.overflow", () => {
|
|
863
863
|
log?.warn(`pond[${ctx.accountId}]: recovery.overflow — triggering full catch-up`);
|
|
864
|
-
|
|
864
|
+
catchUpFromDispatch().catch((err) =>
|
|
865
865
|
log?.warn(`pond[${ctx.accountId}]: overflow catch-up failed: ${String(err)}`));
|
|
866
866
|
});
|
|
867
867
|
|
|
@@ -871,33 +871,43 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
871
871
|
try {
|
|
872
872
|
const dispatched = await handleTaskAssignment(data);
|
|
873
873
|
if (dispatched) {
|
|
874
|
-
client.
|
|
874
|
+
client.ackDispatch(config.org_id, { source_type: "task_activity", source_id: data.id }).catch(() => {});
|
|
875
875
|
}
|
|
876
876
|
} catch (err) {
|
|
877
877
|
log?.error(`pond[${ctx.accountId}]: task dispatch failed for ${data.id}: ${String(err)}`);
|
|
878
878
|
}
|
|
879
879
|
});
|
|
880
880
|
|
|
881
|
-
// ──
|
|
882
|
-
// Replays missed
|
|
883
|
-
//
|
|
884
|
-
|
|
881
|
+
// ── Dispatch catch-up ──
|
|
882
|
+
// Replays missed events via the dispatch API (agent-only endpoint, FIFO order).
|
|
883
|
+
// Each dispatch event is a source-pointer; we re-fetch the entity to build a PondEvent.
|
|
884
|
+
//
|
|
885
|
+
// Cold-start window: On a fresh process start (not reconnect), events older than
|
|
886
|
+
// COLD_START_WINDOW_MS are acked without processing. This is an intentional operator
|
|
887
|
+
// decision: a newly started agent should not replay unbounded historical backlog from
|
|
888
|
+
// a previous process lifetime. Reconnects (non-cold-start) replay all pending events.
|
|
889
|
+
//
|
|
890
|
+
// NOTE: RouteToAgents (active/smart group routing) inserts dispatch rows asynchronously
|
|
891
|
+
// after the message.new WS event. A race exists where the live ack-by-source fires
|
|
892
|
+
// before the dispatch row is inserted, leaving an orphan pending row. This is benign:
|
|
893
|
+
// catch-up will process it on next cycle (duplicate delivery, not data loss).
|
|
894
|
+
// TODO: resolve by creating dispatch rows before publishing the live event, or by
|
|
895
|
+
// consuming dispatch.new in the live path.
|
|
896
|
+
async function catchUpFromDispatch(coldStart = false) {
|
|
885
897
|
const minAge = coldStart ? Date.now() - COLD_START_WINDOW_MS : 0;
|
|
886
898
|
let cursor: string | undefined;
|
|
887
899
|
let processed = 0;
|
|
888
900
|
let skippedOld = 0;
|
|
889
901
|
|
|
890
902
|
do {
|
|
891
|
-
const page = await client.
|
|
892
|
-
type: "mention,agent_dm,task_assign,agent_route",
|
|
893
|
-
read: "false",
|
|
903
|
+
const page = await client.getDispatch(config.org_id, {
|
|
894
904
|
limit: 50,
|
|
895
905
|
cursor,
|
|
896
906
|
});
|
|
897
907
|
|
|
898
908
|
for (const item of page.data ?? []) {
|
|
899
|
-
if (minAge > 0 && new Date(item.
|
|
900
|
-
client.
|
|
909
|
+
if (minAge > 0 && new Date(item.created_at).getTime() < minAge) {
|
|
910
|
+
client.ackDispatchByID(config.org_id, item.id).catch(() => {});
|
|
901
911
|
skippedOld++;
|
|
902
912
|
continue;
|
|
903
913
|
}
|
|
@@ -905,21 +915,59 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
905
915
|
processed++;
|
|
906
916
|
try {
|
|
907
917
|
let dispatched = false;
|
|
908
|
-
if (item.
|
|
918
|
+
if (item.event_type === "task_assign" && item.task_id) {
|
|
909
919
|
// Task catch-up: fetch full task and dispatch
|
|
910
|
-
|
|
920
|
+
let task: Awaited<ReturnType<typeof client.getTask>> | null = null;
|
|
921
|
+
let taskFetchFailed = false;
|
|
922
|
+
try {
|
|
923
|
+
task = await client.getTask(config.org_id, item.task_id);
|
|
924
|
+
} catch (err: unknown) {
|
|
925
|
+
// Distinguish 404 (task deleted) from transient failures
|
|
926
|
+
const status = (err as { status?: number })?.status;
|
|
927
|
+
if (status === 404) {
|
|
928
|
+
task = null; // task genuinely deleted
|
|
929
|
+
} else {
|
|
930
|
+
taskFetchFailed = true;
|
|
931
|
+
log?.warn(`pond[${ctx.accountId}]: catch-up task fetch failed for ${item.task_id}, leaving pending: ${String(err)}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (taskFetchFailed) {
|
|
935
|
+
continue; // leave pending for next catch-up cycle
|
|
936
|
+
}
|
|
911
937
|
if (task) {
|
|
938
|
+
// Defense-in-depth (D2): verify task is still assigned to this agent
|
|
939
|
+
if (task.assignee_id !== agentUserId) {
|
|
940
|
+
log?.info(`pond[${ctx.accountId}]: skipping stale task dispatch ${item.id} — reassigned to ${task.assignee_id}`);
|
|
941
|
+
client.ackDispatchByID(config.org_id, item.id).catch(() => {});
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
912
944
|
dispatched = await handleTaskAssignment(task);
|
|
913
945
|
} else {
|
|
914
|
-
dispatched = true; // task deleted —
|
|
946
|
+
dispatched = true; // task genuinely deleted — ack to prevent infinite retry
|
|
915
947
|
}
|
|
916
|
-
} else if (item.source_id && item.chat_id) {
|
|
917
|
-
// Message catch-up
|
|
948
|
+
} else if (item.event_type === "message" && item.source_id && item.chat_id) {
|
|
949
|
+
// Message catch-up: build PondEvent from dispatch source pointer
|
|
918
950
|
if (!tryClaimMessage(item.source_id)) continue;
|
|
919
|
-
|
|
951
|
+
let msg: Awaited<ReturnType<typeof client.getMessage>> | null = null;
|
|
952
|
+
let msgFetchFailed = false;
|
|
953
|
+
try {
|
|
954
|
+
msg = await client.getMessage(item.source_id);
|
|
955
|
+
} catch (err: unknown) {
|
|
956
|
+
const status = (err as { status?: number })?.status;
|
|
957
|
+
if (status === 404) {
|
|
958
|
+
msg = null; // message genuinely deleted
|
|
959
|
+
} else {
|
|
960
|
+
msgFetchFailed = true;
|
|
961
|
+
log?.warn(`pond[${ctx.accountId}]: catch-up message fetch failed for ${item.source_id}, leaving pending: ${String(err)}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (msgFetchFailed) {
|
|
965
|
+
dispatchedMessages.delete(item.source_id);
|
|
966
|
+
continue; // leave pending for next catch-up cycle
|
|
967
|
+
}
|
|
920
968
|
if (!msg || msg.sender_id === agentUserId) {
|
|
921
969
|
dispatchedMessages.delete(item.source_id);
|
|
922
|
-
client.
|
|
970
|
+
client.ackDispatchByID(config.org_id, item.id).catch(() => {});
|
|
923
971
|
continue;
|
|
924
972
|
}
|
|
925
973
|
|
|
@@ -951,7 +999,7 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
951
999
|
}
|
|
952
1000
|
if (!body) {
|
|
953
1001
|
dispatchedMessages.delete(item.source_id);
|
|
954
|
-
client.
|
|
1002
|
+
client.ackDispatchByID(config.org_id, item.id).catch(() => {});
|
|
955
1003
|
continue;
|
|
956
1004
|
}
|
|
957
1005
|
|
|
@@ -971,17 +1019,17 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
971
1019
|
dispatched = await handleInboundEvent(event);
|
|
972
1020
|
}
|
|
973
1021
|
if (dispatched) {
|
|
974
|
-
client.
|
|
1022
|
+
client.ackDispatchByID(config.org_id, item.id).catch(() => {});
|
|
975
1023
|
}
|
|
976
1024
|
} catch (err) {
|
|
977
|
-
log?.warn(`pond[${ctx.accountId}]: catch-up
|
|
1025
|
+
log?.warn(`pond[${ctx.accountId}]: catch-up dispatch ${item.id} (${item.event_type}) failed: ${String(err)}`);
|
|
978
1026
|
}
|
|
979
1027
|
}
|
|
980
1028
|
cursor = page.has_more ? page.next_cursor : undefined;
|
|
981
1029
|
} while (cursor);
|
|
982
1030
|
|
|
983
1031
|
if (processed > 0 || skippedOld > 0) {
|
|
984
|
-
log?.info(`pond[${ctx.accountId}]:
|
|
1032
|
+
log?.info(`pond[${ctx.accountId}]: dispatch catch-up: processed ${processed}, skipped ${skippedOld} old item(s)`);
|
|
985
1033
|
}
|
|
986
1034
|
}
|
|
987
1035
|
|