@pnds/pond 1.9.0 → 1.10.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnds/pond",
3
- "version": "1.9.0",
3
+ "version": "1.10.1",
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.9.0",
19
- "@pnds/sdk": "1.9.0"
18
+ "@pnds/sdk": "1.10.1",
19
+ "@pnds/cli": "1.10.1"
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
- const sessionKeyLower = sessionKey.toLowerCase();
130
- let thinkingSent = false;
131
- let runIdPromise: Promise<string | undefined> | undefined;
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 }, info: { kind: 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 runId = runIdPromise ? await runIdPromise : undefined;
144
- if (runId) {
145
- try {
146
- await client.createAgentStep(config.org_id, agentUserId, runId, {
147
- step_type: "text",
148
- content: { text: replyText, suppressed: true },
149
- ...(info.kind === "block" ? { chat_projection: false } : {}),
150
- });
151
- } catch (err) {
152
- log?.warn(`pond[${accountId}]: failed to create suppressed text step: ${String(err)}`);
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
- if (thinkingSent) return;
158
- thinkingSent = true;
159
- runIdPromise = (async () => {
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
- const runId = runIdPromise ? await runIdPromise : undefined;
183
- if (runId && reasoningBuffer) {
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, runId, {
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,29 @@ 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
+ // On WS reconnect, preserve the existing session — only create if none exists.
677
+ const existingState = getPondAccountState(ctx.accountId);
678
+ let activeSessionId: string | undefined = existingState?.activeSessionId;
679
+ if (!activeSessionId) {
680
+ try {
681
+ const session = await client.createAgentSession(config.org_id, agentUserId, {
682
+ runtime_type: "openclaw",
683
+ runtime_key: orchestratorKey,
684
+ runtime_ref: { hostname: os.hostname(), pid: process.pid },
685
+ });
686
+ activeSessionId = session.id;
687
+ log?.info(`pond[${ctx.accountId}]: created agent session ${session.id}`);
688
+ } catch (err) {
689
+ log?.warn(`pond[${ctx.accountId}]: failed to create agent session: ${String(err)}`);
690
+ }
691
+ }
692
+
655
693
  setPondAccountState(ctx.accountId, {
656
694
  client,
657
695
  orgId: config.org_id,
658
696
  agentUserId,
659
- activeRuns: new Map(),
697
+ activeSessionId,
660
698
  wikiMountRoot,
661
699
  ws,
662
700
  orchestratorSessionKey: orchestratorKey,
@@ -1036,29 +1074,40 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
1036
1074
  log?.info(`pond[${ctx.accountId}]: connecting to ${wsUrl}...`);
1037
1075
  await ws.connect();
1038
1076
 
1039
- // Clean up on abort
1040
- ctx.abortSignal.addEventListener("abort", () => {
1041
- if (heartbeatTimer) clearInterval(heartbeatTimer);
1042
- for (const [, dispatch] of activeDispatches) {
1043
- clearInterval(dispatch.typingTimer);
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;
1077
+ // Keep the gateway alive until aborted; clean up before resolving
1078
+ return new Promise<void>((resolve) => {
1079
+ ctx.abortSignal.addEventListener("abort", async () => {
1080
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
1081
+ for (const [, dispatch] of activeDispatches) {
1082
+ clearInterval(dispatch.typingTimer);
1054
1083
  }
1055
- }
1056
- log?.info(`pond[${ctx.accountId}]: disconnected`);
1057
- });
1084
+ activeDispatches.clear();
1085
+ stopWikiHelper?.();
1058
1086
 
1059
- // Keep the gateway alive until aborted
1060
- return new Promise<void>((resolve) => {
1061
- ctx.abortSignal.addEventListener("abort", () => resolve());
1087
+ // Complete the agent session
1088
+ const currentState = getPondAccountState(ctx.accountId);
1089
+ if (currentState?.activeSessionId) {
1090
+ try {
1091
+ await client.updateAgentSession(config.org_id, agentUserId, currentState.activeSessionId, {
1092
+ status: "completed",
1093
+ });
1094
+ } catch (err) {
1095
+ log?.warn(`pond[${ctx.accountId}]: failed to complete session: ${String(err)}`);
1096
+ }
1097
+ }
1098
+
1099
+ ws.disconnect();
1100
+ removePondAccountState(ctx.accountId);
1101
+ for (const [key, value] of Object.entries(previousEnv)) {
1102
+ if (value === undefined) {
1103
+ delete process.env[key];
1104
+ } else {
1105
+ process.env[key] = value;
1106
+ }
1107
+ }
1108
+ log?.info(`pond[${ctx.accountId}]: disconnected`);
1109
+ resolve();
1110
+ });
1062
1111
  });
1063
1112
  },
1064
1113
  };
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, getActiveRunId, getDispatchMessageId, getDispatchNoReply, getPondAccountState, getSessionChatId } from "./runtime.js";
3
+ import { clearDispatchGroupKey, clearDispatchMessageId, clearSessionMessageId, getDispatchGroupKey, getDispatchMessageId, getDispatchNoReply, getPondAccountState, getSessionChatId } from "./runtime.js";
4
4
 
5
5
  /**
6
- * Resolve the PondClient + orgId + runId for a hook context.
7
- * Awaits the run ID promise so hooks that fire before createAgentRun
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
- async function resolveClientForHook(sessionKey: string | undefined) {
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
- // Await the run ID promise — may block briefly if the run is still being created
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) fire-and-forget step creation, (2) ENV injection for exec tool
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) Fire-and-forget step creation (all tools)
54
- void (async () => {
55
- const resolved = await resolveClientForHook(sessionKey);
56
- if (!resolved || !resolved.activeRunId || !event.toolCallId) return;
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.activeRunId, {
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 (runId) injectedEnv.POND_RUN_ID = runId;
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 AgentRun
109
+ // after_tool_call -> send tool_result step to AgentSession
109
110
  api.on("after_tool_call", async (event, ctx) => {
110
- const resolved = await resolveClientForHook(ctx.sessionKey);
111
- if (!resolved || !resolved.activeRunId || !event.toolCallId) return;
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.activeRunId, {
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 -> complete the AgentRun
130
- api.on("agent_end", async (_event, ctx) => {
131
- const resolved = await resolveClientForHook(ctx.sessionKey);
132
- if (!resolved) {
133
- log?.warn(`pond hook agent_end: could not resolve client (sessionKey=${ctx.sessionKey})`);
134
- return;
135
- }
136
- if (!resolved.activeRunId) return;
137
- const accountId = extractAccountIdFromSessionKey(ctx.sessionKey ?? "");
138
- const state = accountId ? getPondAccountState(accountId) : undefined;
139
- try {
140
- await resolved.client.updateAgentRun(resolved.orgId, resolved.agentUserId, resolved.activeRunId, {
141
- status: "completed",
142
- });
143
- } catch (err) {
144
- log?.warn(`pond hook agent_end failed: ${String(err)}`);
145
- } finally {
146
- // Always clear local state — even if the server call failed, the run is over locally
147
- if (ctx.sessionKey) {
148
- clearSessionMessageId(ctx.sessionKey);
149
- clearDispatchMessageId(ctx.sessionKey);
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, getSessionChatId } from "./runtime.js";
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
- // Match active run by chatId (outbound path has no sessionKey, but has chatId via `to`)
29
- let agentRunId: string | undefined;
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
- activeRuns: Map<string, Promise<string | undefined>>; // sessionKey (lowercased) → runId promise
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. */