@pnds/pond 1.9.0 → 1.10.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 +107 -62
- package/src/hooks.ts +54 -47
- package/src/outbound.ts +3 -13
- package/src/runtime.ts +17 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnds/pond",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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/cli": "1.
|
|
19
|
-
"@pnds/sdk": "1.
|
|
18
|
+
"@pnds/cli": "1.10.0",
|
|
19
|
+
"@pnds/sdk": "1.10.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22.0.0",
|
package/src/gateway.ts
CHANGED
|
@@ -6,6 +6,7 @@ type ChannelGatewayAdapter<T = unknown> = NonNullable<ChannelPlugin<T>["gateway"
|
|
|
6
6
|
import { PondClient, PondWs, MENTION_ALL_USER_ID } from "@pnds/sdk";
|
|
7
7
|
import type { Chat, MessageNewData, TextContent, MediaContent, HelloData, ChatUpdateData, AgentConfigUpdateData, TaskAssignedData, Task } from "@pnds/sdk";
|
|
8
8
|
import type { PondChannelConfig } from "./types.js";
|
|
9
|
+
import * as crypto from "node:crypto";
|
|
9
10
|
import * as os from "node:os";
|
|
10
11
|
import * as path from "node:path";
|
|
11
12
|
import { resolvePondAccount } from "./accounts.js";
|
|
@@ -18,6 +19,8 @@ import {
|
|
|
18
19
|
setSessionMessageId,
|
|
19
20
|
setDispatchMessageId,
|
|
20
21
|
setDispatchNoReply,
|
|
22
|
+
setDispatchGroupKey,
|
|
23
|
+
clearDispatchGroupKey,
|
|
21
24
|
} from "./runtime.js";
|
|
22
25
|
import type { PondEvent, DispatchState, ForkResult } from "./runtime.js";
|
|
23
26
|
import { buildOrchestratorSessionKey } from "./session.js";
|
|
@@ -95,7 +98,6 @@ function buildForkResultPrefix(results: ForkResult[]): string {
|
|
|
95
98
|
const lines = [`[Fork result]`];
|
|
96
99
|
lines.push(`[Handled: ${r.sourceEvent.type} — ${r.sourceEvent.summary}]`);
|
|
97
100
|
if (r.actions.length) lines.push(`[Actions: ${r.actions.join("; ")}]`);
|
|
98
|
-
if (r.agentRunId) lines.push(`[Agent run: ${r.agentRunId}]`);
|
|
99
101
|
return lines.join("\n");
|
|
100
102
|
});
|
|
101
103
|
return blocks.join("\n\n") + "\n\n---\n\n";
|
|
@@ -118,60 +120,72 @@ async function dispatchToAgent(opts: {
|
|
|
118
120
|
triggerType?: string;
|
|
119
121
|
triggerRef?: Record<string, unknown>;
|
|
120
122
|
chatId?: string;
|
|
121
|
-
/** For task dispatches: bind the run to a task target for Redis presence + fallback comments. */
|
|
122
123
|
defaultTargetType?: string;
|
|
123
124
|
defaultTargetId?: string;
|
|
125
|
+
senderId?: string;
|
|
126
|
+
senderName?: string;
|
|
127
|
+
messageBody?: string;
|
|
124
128
|
log?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
|
|
125
129
|
}) {
|
|
126
130
|
const { core, cfg, client, config, agentUserId, accountId, sessionKey, messageId, inboundCtx, log } = opts;
|
|
127
131
|
const triggerType = opts.triggerType ?? "mention";
|
|
128
132
|
const triggerRef = opts.triggerRef ?? { message_id: messageId };
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
// Resolve session ID from account state
|
|
135
|
+
const state = getPondAccountState(accountId);
|
|
136
|
+
const sessionId = state?.activeSessionId;
|
|
137
|
+
|
|
138
|
+
// Create input step — captures the user message as a step in the session
|
|
139
|
+
if (sessionId) {
|
|
140
|
+
const targetType = opts.chatId ? "chat" : opts.defaultTargetType ?? "";
|
|
141
|
+
const targetId = opts.chatId ?? opts.defaultTargetId;
|
|
142
|
+
try {
|
|
143
|
+
await client.createAgentStep(config.org_id, agentUserId, sessionId, {
|
|
144
|
+
step_type: "input",
|
|
145
|
+
target_type: targetType,
|
|
146
|
+
target_id: targetId,
|
|
147
|
+
content: {
|
|
148
|
+
trigger_type: triggerType,
|
|
149
|
+
trigger_ref: triggerRef,
|
|
150
|
+
sender_id: opts.senderId ?? "",
|
|
151
|
+
sender_name: opts.senderName ?? "",
|
|
152
|
+
summary: opts.messageBody?.substring(0, 200) ?? "",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
log?.warn(`pond[${accountId}]: failed to create input step: ${String(err)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
132
160
|
let reasoningBuffer = "";
|
|
161
|
+
let turnGroupKey = "";
|
|
133
162
|
try {
|
|
134
163
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
135
164
|
ctx: inboundCtx,
|
|
136
165
|
cfg,
|
|
137
166
|
dispatcherOptions: {
|
|
138
|
-
deliver: async (payload: { text?: string }
|
|
167
|
+
deliver: async (payload: { text?: string }) => {
|
|
139
168
|
// Orchestrator mode: ALL text output is suppressed — agent uses tools to interact.
|
|
140
169
|
// Record as internal step for observability.
|
|
141
170
|
const replyText = payload.text?.trim();
|
|
142
|
-
if (!replyText) return;
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
}
|
|
171
|
+
if (!replyText || !sessionId) return;
|
|
172
|
+
const targetType = opts.chatId ? "chat" : opts.defaultTargetType ?? "";
|
|
173
|
+
const targetId = opts.chatId ?? opts.defaultTargetId;
|
|
174
|
+
try {
|
|
175
|
+
await client.createAgentStep(config.org_id, agentUserId, sessionId, {
|
|
176
|
+
step_type: "text",
|
|
177
|
+
target_type: targetType,
|
|
178
|
+
target_id: targetId,
|
|
179
|
+
content: { text: replyText, suppressed: true },
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
log?.warn(`pond[${accountId}]: failed to create suppressed text step: ${String(err)}`);
|
|
154
183
|
}
|
|
155
184
|
},
|
|
156
185
|
onReplyStart: () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const run = await client.createAgentRun(config.org_id, agentUserId, {
|
|
162
|
-
trigger_type: triggerType,
|
|
163
|
-
trigger_ref: triggerRef,
|
|
164
|
-
...(opts.chatId ? { chat_id: opts.chatId } : {}),
|
|
165
|
-
...(opts.defaultTargetType ? { default_target_type: opts.defaultTargetType, default_target_id: opts.defaultTargetId } : {}),
|
|
166
|
-
});
|
|
167
|
-
return run.id;
|
|
168
|
-
} catch (err) {
|
|
169
|
-
log?.warn(`pond[${accountId}]: failed to create agent run: ${String(err)}`);
|
|
170
|
-
return undefined;
|
|
171
|
-
}
|
|
172
|
-
})();
|
|
173
|
-
const state = getPondAccountState(accountId);
|
|
174
|
-
if (state) state.activeRuns.set(sessionKeyLower, runIdPromise);
|
|
186
|
+
// Generate a new group key per LLM response turn — shared across thinking + tool_call steps
|
|
187
|
+
turnGroupKey = crypto.randomUUID();
|
|
188
|
+
setDispatchGroupKey(sessionKey, turnGroupKey);
|
|
175
189
|
},
|
|
176
190
|
},
|
|
177
191
|
replyOptions: {
|
|
@@ -179,12 +193,16 @@ async function dispatchToAgent(opts: {
|
|
|
179
193
|
reasoningBuffer += payload.text ?? "";
|
|
180
194
|
},
|
|
181
195
|
onReasoningEnd: async () => {
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
if (sessionId && reasoningBuffer) {
|
|
197
|
+
const targetType = opts.chatId ? "chat" : opts.defaultTargetType ?? "";
|
|
198
|
+
const targetId = opts.chatId ?? opts.defaultTargetId;
|
|
184
199
|
try {
|
|
185
|
-
await client.createAgentStep(config.org_id, agentUserId,
|
|
200
|
+
await client.createAgentStep(config.org_id, agentUserId, sessionId, {
|
|
186
201
|
step_type: "thinking",
|
|
202
|
+
target_type: targetType,
|
|
203
|
+
target_id: targetId,
|
|
187
204
|
content: { text: reasoningBuffer },
|
|
205
|
+
group_key: turnGroupKey || undefined,
|
|
188
206
|
});
|
|
189
207
|
} catch (err) {
|
|
190
208
|
log?.warn(`pond[${accountId}]: failed to create thinking step: ${String(err)}`);
|
|
@@ -420,7 +438,6 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
420
438
|
: `${fork.processedEvents.length} events in ${first.targetName ?? fork.targetId}`,
|
|
421
439
|
},
|
|
422
440
|
actions: [],
|
|
423
|
-
agentRunId: undefined,
|
|
424
441
|
});
|
|
425
442
|
}
|
|
426
443
|
// Remove from tracking maps and clean up session files
|
|
@@ -489,6 +506,9 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
489
506
|
chatId: event.targetId.startsWith("cht_") ? event.targetId : undefined,
|
|
490
507
|
defaultTargetType: isTask ? "task" : undefined,
|
|
491
508
|
defaultTargetId: isTask ? event.targetId : undefined,
|
|
509
|
+
senderId: event.senderId,
|
|
510
|
+
senderName: event.senderName,
|
|
511
|
+
messageBody: event.body,
|
|
492
512
|
log,
|
|
493
513
|
});
|
|
494
514
|
if (!ok) throw new Error(`dispatch failed for ${event.messageId}`);
|
|
@@ -652,11 +672,25 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
652
672
|
const count = await fetchAllChats(client, config.org_id, chatInfoMap);
|
|
653
673
|
log?.info(`pond[${ctx.accountId}]: WebSocket connected, ${count} chats cached`);
|
|
654
674
|
|
|
675
|
+
// Create AgentSession on first hello (session lives across dispatches)
|
|
676
|
+
let activeSessionId: string | undefined;
|
|
677
|
+
try {
|
|
678
|
+
const session = await client.createAgentSession(config.org_id, agentUserId, {
|
|
679
|
+
runtime_type: "openclaw",
|
|
680
|
+
runtime_key: orchestratorKey,
|
|
681
|
+
runtime_ref: { hostname: os.hostname(), pid: process.pid },
|
|
682
|
+
});
|
|
683
|
+
activeSessionId = session.id;
|
|
684
|
+
log?.info(`pond[${ctx.accountId}]: created agent session ${session.id}`);
|
|
685
|
+
} catch (err) {
|
|
686
|
+
log?.warn(`pond[${ctx.accountId}]: failed to create agent session: ${String(err)}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
655
689
|
setPondAccountState(ctx.accountId, {
|
|
656
690
|
client,
|
|
657
691
|
orgId: config.org_id,
|
|
658
692
|
agentUserId,
|
|
659
|
-
|
|
693
|
+
activeSessionId,
|
|
660
694
|
wikiMountRoot,
|
|
661
695
|
ws,
|
|
662
696
|
orchestratorSessionKey: orchestratorKey,
|
|
@@ -1036,29 +1070,40 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
1036
1070
|
log?.info(`pond[${ctx.accountId}]: connecting to ${wsUrl}...`);
|
|
1037
1071
|
await ws.connect();
|
|
1038
1072
|
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
activeDispatches.clear();
|
|
1046
|
-
stopWikiHelper?.();
|
|
1047
|
-
ws.disconnect();
|
|
1048
|
-
removePondAccountState(ctx.accountId);
|
|
1049
|
-
for (const [key, value] of Object.entries(previousEnv)) {
|
|
1050
|
-
if (value === undefined) {
|
|
1051
|
-
delete process.env[key];
|
|
1052
|
-
} else {
|
|
1053
|
-
process.env[key] = value;
|
|
1073
|
+
// Keep the gateway alive until aborted; clean up before resolving
|
|
1074
|
+
return new Promise<void>((resolve) => {
|
|
1075
|
+
ctx.abortSignal.addEventListener("abort", async () => {
|
|
1076
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1077
|
+
for (const [, dispatch] of activeDispatches) {
|
|
1078
|
+
clearInterval(dispatch.typingTimer);
|
|
1054
1079
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
});
|
|
1080
|
+
activeDispatches.clear();
|
|
1081
|
+
stopWikiHelper?.();
|
|
1058
1082
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1083
|
+
// Complete the agent session
|
|
1084
|
+
const currentState = getPondAccountState(ctx.accountId);
|
|
1085
|
+
if (currentState?.activeSessionId) {
|
|
1086
|
+
try {
|
|
1087
|
+
await client.updateAgentSession(config.org_id, agentUserId, currentState.activeSessionId, {
|
|
1088
|
+
status: "completed",
|
|
1089
|
+
});
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
log?.warn(`pond[${ctx.accountId}]: failed to complete session: ${String(err)}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
ws.disconnect();
|
|
1096
|
+
removePondAccountState(ctx.accountId);
|
|
1097
|
+
for (const [key, value] of Object.entries(previousEnv)) {
|
|
1098
|
+
if (value === undefined) {
|
|
1099
|
+
delete process.env[key];
|
|
1100
|
+
} else {
|
|
1101
|
+
process.env[key] = value;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
log?.info(`pond[${ctx.accountId}]: disconnected`);
|
|
1105
|
+
resolve();
|
|
1106
|
+
});
|
|
1062
1107
|
});
|
|
1063
1108
|
},
|
|
1064
1109
|
};
|
package/src/hooks.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { extractAccountIdFromSessionKey } from "./session.js";
|
|
3
|
-
import { clearDispatchMessageId, clearSessionMessageId,
|
|
3
|
+
import { clearDispatchGroupKey, clearDispatchMessageId, clearSessionMessageId, getDispatchGroupKey, getDispatchMessageId, getDispatchNoReply, getPondAccountState, getSessionChatId } from "./runtime.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Resolve the PondClient + orgId +
|
|
7
|
-
*
|
|
8
|
-
* completes will block until the run ID is available (not silently drop).
|
|
6
|
+
* Resolve the PondClient + orgId + sessionId for a hook context.
|
|
7
|
+
* Returns undefined if the account/session is not active.
|
|
9
8
|
*/
|
|
10
|
-
|
|
9
|
+
function resolveClientForHook(sessionKey: string | undefined) {
|
|
11
10
|
if (!sessionKey) return undefined;
|
|
12
|
-
// Use stored mapping to recover original (case-sensitive) chat ID,
|
|
13
|
-
// because openclaw normalizes session keys to lowercase.
|
|
14
11
|
const chatId = getSessionChatId(sessionKey);
|
|
15
12
|
if (!chatId) return undefined;
|
|
16
13
|
const accountId = extractAccountIdFromSessionKey(sessionKey);
|
|
@@ -19,9 +16,7 @@ async function resolveClientForHook(sessionKey: string | undefined) {
|
|
|
19
16
|
const state = getPondAccountState(accountId);
|
|
20
17
|
if (!state) return undefined;
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
const activeRunId = await getActiveRunId(state, sessionKey);
|
|
24
|
-
return { ...state, chatId, activeRunId };
|
|
19
|
+
return { ...state, chatId, sessionId: state.activeSessionId };
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
const POND_CHANNEL_CONTEXT = `## Pond Channel — Message Delivery
|
|
@@ -46,17 +41,20 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
46
41
|
return { appendSystemContext: POND_CHANNEL_CONTEXT };
|
|
47
42
|
});
|
|
48
43
|
|
|
49
|
-
// before_tool_call -> (1)
|
|
44
|
+
// before_tool_call -> (1) await step creation to get step ID, (2) ENV injection for exec tool
|
|
50
45
|
api.on("before_tool_call", async (event, ctx) => {
|
|
51
46
|
const sessionKey = ctx.sessionKey;
|
|
52
47
|
|
|
53
|
-
// (1)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
// (1) Create tool_call step and await to get step ID
|
|
49
|
+
let stepId: string | undefined;
|
|
50
|
+
const resolved = resolveClientForHook(sessionKey);
|
|
51
|
+
if (resolved?.sessionId && event.toolCallId) {
|
|
52
|
+
const groupKey = sessionKey ? getDispatchGroupKey(sessionKey) : undefined;
|
|
57
53
|
try {
|
|
58
|
-
await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.
|
|
54
|
+
const step = await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.sessionId, {
|
|
59
55
|
step_type: "tool_call",
|
|
56
|
+
target_type: resolved.chatId.startsWith("cht_") ? "chat" : resolved.chatId.startsWith("tsk_") ? "task" : "",
|
|
57
|
+
target_id: resolved.chatId || undefined,
|
|
60
58
|
content: {
|
|
61
59
|
call_id: event.toolCallId,
|
|
62
60
|
tool_name: event.toolName,
|
|
@@ -64,11 +62,14 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
64
62
|
status: "running",
|
|
65
63
|
started_at: new Date().toISOString(),
|
|
66
64
|
},
|
|
65
|
+
group_key: groupKey,
|
|
66
|
+
runtime_key: event.toolCallId,
|
|
67
67
|
});
|
|
68
|
+
stepId = step.id;
|
|
68
69
|
} catch (err) {
|
|
69
70
|
log?.warn(`pond hook before_tool_call failed: ${String(err)}`);
|
|
70
71
|
}
|
|
71
|
-
}
|
|
72
|
+
}
|
|
72
73
|
|
|
73
74
|
// (2) ENV injection — only for the exec (Bash) tool
|
|
74
75
|
if (event.toolName !== "exec") return;
|
|
@@ -79,13 +80,13 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
79
80
|
if (!state) return;
|
|
80
81
|
|
|
81
82
|
// Dynamic per-dispatch context (changes each dispatch)
|
|
82
|
-
const runId = await getActiveRunId(state, sessionKey);
|
|
83
83
|
const chatId = getSessionChatId(sessionKey);
|
|
84
84
|
const triggerMsgId = getDispatchMessageId(sessionKey);
|
|
85
85
|
const noReply = getDispatchNoReply(sessionKey);
|
|
86
86
|
|
|
87
87
|
const injectedEnv: Record<string, string> = {};
|
|
88
|
-
if (
|
|
88
|
+
if (state.activeSessionId) injectedEnv.POND_SESSION_ID = state.activeSessionId;
|
|
89
|
+
if (stepId) injectedEnv.POND_STEP_ID = stepId;
|
|
89
90
|
if (chatId) injectedEnv.POND_CHAT_ID = chatId;
|
|
90
91
|
if (triggerMsgId) injectedEnv.POND_TRIGGER_MESSAGE_ID = triggerMsgId;
|
|
91
92
|
if (noReply) injectedEnv.POND_NO_REPLY = "1";
|
|
@@ -105,13 +106,15 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
105
106
|
};
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
// after_tool_call -> send tool_result step to
|
|
109
|
+
// after_tool_call -> send tool_result step to AgentSession
|
|
109
110
|
api.on("after_tool_call", async (event, ctx) => {
|
|
110
|
-
const resolved =
|
|
111
|
-
if (!resolved || !
|
|
111
|
+
const resolved = resolveClientForHook(ctx.sessionKey);
|
|
112
|
+
if (!resolved?.sessionId || !event.toolCallId) return;
|
|
112
113
|
try {
|
|
113
|
-
await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.
|
|
114
|
+
await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.sessionId, {
|
|
114
115
|
step_type: "tool_result",
|
|
116
|
+
target_type: resolved.chatId.startsWith("cht_") ? "chat" : resolved.chatId.startsWith("tsk_") ? "task" : "",
|
|
117
|
+
target_id: resolved.chatId || undefined,
|
|
115
118
|
content: {
|
|
116
119
|
call_id: event.toolCallId,
|
|
117
120
|
tool_name: event.toolName,
|
|
@@ -126,31 +129,35 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
126
129
|
}
|
|
127
130
|
});
|
|
128
131
|
|
|
129
|
-
// agent_end ->
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
if (state && ctx.sessionKey) {
|
|
152
|
-
state.activeRuns.delete(ctx.sessionKey.toLowerCase());
|
|
132
|
+
// agent_end -> end of a dispatch cycle, NOT end of session.
|
|
133
|
+
// Session lives across dispatches — don't update session status here.
|
|
134
|
+
// On error: emit a visible error step so the chat user sees the failure.
|
|
135
|
+
api.on("agent_end", async (event, ctx) => {
|
|
136
|
+
// Emit error step if the dispatch failed
|
|
137
|
+
if (event.error && ctx.sessionKey) {
|
|
138
|
+
const accountId = extractAccountIdFromSessionKey(ctx.sessionKey);
|
|
139
|
+
const state = accountId ? getPondAccountState(accountId) : undefined;
|
|
140
|
+
const chatId = getSessionChatId(ctx.sessionKey);
|
|
141
|
+
if (state?.activeSessionId && chatId) {
|
|
142
|
+
try {
|
|
143
|
+
await state.client.createAgentStep(state.orgId, state.agentUserId, state.activeSessionId, {
|
|
144
|
+
step_type: "text",
|
|
145
|
+
target_type: "chat",
|
|
146
|
+
target_id: chatId,
|
|
147
|
+
content: { text: `Dispatch failed: ${String(event.error)}`, suppressed: false },
|
|
148
|
+
projection: true,
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
log?.warn(`pond hook agent_end: failed to emit error step: ${String(err)}`);
|
|
152
|
+
}
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
// Clean up dispatch-specific state
|
|
157
|
+
if (ctx.sessionKey) {
|
|
158
|
+
clearSessionMessageId(ctx.sessionKey);
|
|
159
|
+
clearDispatchMessageId(ctx.sessionKey);
|
|
160
|
+
clearDispatchGroupKey(ctx.sessionKey);
|
|
161
|
+
}
|
|
155
162
|
});
|
|
156
163
|
}
|
package/src/outbound.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
/** Extract ChannelOutboundAdapter from ChannelPlugin (removed from public SDK exports in 2026.3.24). */
|
|
4
4
|
type ChannelOutboundAdapter = NonNullable<ChannelPlugin["outbound"]>;
|
|
5
5
|
import { resolvePondAccount } from "./accounts.js";
|
|
6
|
-
import { getPondAccountState
|
|
6
|
+
import { getPondAccountState } from "./runtime.js";
|
|
7
7
|
import { PondClient } from "@pnds/sdk";
|
|
8
8
|
import type { SendMessageRequest } from "@pnds/sdk";
|
|
9
9
|
|
|
@@ -25,21 +25,11 @@ export const pondOutbound: ChannelOutboundAdapter = {
|
|
|
25
25
|
const orgId = state?.orgId ?? account.config.org_id;
|
|
26
26
|
const chatId = to;
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
if (state) {
|
|
31
|
-
for (const [sessionKey, runPromise] of state.activeRuns.entries()) {
|
|
32
|
-
if (getSessionChatId(sessionKey) === chatId) {
|
|
33
|
-
agentRunId = await runPromise;
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
28
|
+
// Outbound path has no step context — send without agent_step_id.
|
|
29
|
+
// Step linkage happens via the CLI's ENV-injected POND_STEP_ID in the normal dispatch path.
|
|
39
30
|
const req: SendMessageRequest = {
|
|
40
31
|
message_type: "text",
|
|
41
32
|
content: { text },
|
|
42
|
-
agent_run_id: agentRunId,
|
|
43
33
|
};
|
|
44
34
|
const msg = await client.sendMessage(orgId, chatId, req);
|
|
45
35
|
return { channel: "pond", messageId: msg.id, channelId: chatId };
|
package/src/runtime.ts
CHANGED
|
@@ -19,21 +19,12 @@ export type PondAccountState = {
|
|
|
19
19
|
client: PondClient;
|
|
20
20
|
orgId: string;
|
|
21
21
|
agentUserId: string;
|
|
22
|
-
|
|
22
|
+
activeSessionId?: string;
|
|
23
23
|
wikiMountRoot?: string;
|
|
24
24
|
ws?: PondWs;
|
|
25
25
|
orchestratorSessionKey?: string;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
/** Resolve run ID for a session, awaiting if the run is still being created. */
|
|
29
|
-
export async function getActiveRunId(
|
|
30
|
-
state: PondAccountState,
|
|
31
|
-
sessionKey: string,
|
|
32
|
-
): Promise<string | undefined> {
|
|
33
|
-
const promise = state.activeRuns.get(sessionKey.toLowerCase());
|
|
34
|
-
return promise ? await promise : undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
28
|
const accountStates = new Map<string, PondAccountState>();
|
|
38
29
|
|
|
39
30
|
export function setPondAccountState(accountId: string, state: PondAccountState) {
|
|
@@ -94,6 +85,22 @@ export function clearDispatchMessageId(sessionKey: string) {
|
|
|
94
85
|
dispatchMessageIdMap.delete(sessionKey.toLowerCase());
|
|
95
86
|
}
|
|
96
87
|
|
|
88
|
+
// Per-dispatch group key — shared between thinking + tool_call steps for Session Panel grouping.
|
|
89
|
+
// Set per LLM response turn in gateway.ts, read in hooks.ts.
|
|
90
|
+
const dispatchGroupKeyMap = new Map<string, string>();
|
|
91
|
+
|
|
92
|
+
export function setDispatchGroupKey(sessionKey: string, groupKey: string) {
|
|
93
|
+
dispatchGroupKeyMap.set(sessionKey.toLowerCase(), groupKey);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getDispatchGroupKey(sessionKey: string): string | undefined {
|
|
97
|
+
return dispatchGroupKeyMap.get(sessionKey.toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function clearDispatchGroupKey(sessionKey: string) {
|
|
101
|
+
dispatchGroupKeyMap.delete(sessionKey.toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
// Per-dispatch noReply flag — deterministic suppression for agent-to-agent loop prevention.
|
|
98
105
|
// Injected as POND_NO_REPLY=1 into Bash env; the CLI checks and suppresses sends.
|
|
99
106
|
const dispatchNoReplyMap = new Map<string, boolean>();
|
|
@@ -115,7 +122,6 @@ export type ForkResult = {
|
|
|
115
122
|
forkSessionKey: string;
|
|
116
123
|
sourceEvent: { type: string; targetId: string; summary: string };
|
|
117
124
|
actions: string[]; // human-readable list of actions taken (e.g. "replied to cht_xxx")
|
|
118
|
-
agentRunId?: string;
|
|
119
125
|
};
|
|
120
126
|
|
|
121
127
|
/** Mutable dispatch state for the orchestrator gateway. Per-account, managed by gateway.ts. */
|