@pnds/pond 1.1.0 → 1.2.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/TOOLS.md +44 -0
- package/package.json +17 -4
- package/src/action-tools.ts +256 -0
- package/src/channel.ts +2 -2
- package/src/config-manager.ts +18 -0
- package/src/fork.ts +213 -0
- package/src/gateway.ts +717 -222
- package/src/hooks.ts +61 -25
- package/src/index.ts +5 -1
- package/src/outbound.ts +4 -1
- package/src/routing.ts +48 -0
- package/src/runtime.ts +84 -1
- package/src/session.ts +5 -0
- package/src/tool-helpers.ts +11 -0
- package/src/tools-md.ts +7 -0
- package/src/wiki-helper.ts +157 -0
- package/src/wiki-tools.ts +370 -0
package/src/hooks.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { extractAccountIdFromSessionKey } from "./session.js";
|
|
3
|
-
import { getActiveRunId, getPondAccountState, getSessionChatId } from "./runtime.js";
|
|
3
|
+
import { clearDispatchMessageId, clearSessionMessageId, getActiveRunId, getPondAccountState, getSessionChatId } from "./runtime.js";
|
|
4
|
+
import { loadToolsMarkdown } from "./tools-md.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Resolve the PondClient + orgId + runId for a hook context.
|
|
@@ -24,42 +25,73 @@ async function resolveClientForHook(sessionKey: string | undefined) {
|
|
|
24
25
|
return { ...state, chatId, activeRunId };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const ORCHESTRATOR_PREFIX = `## CRITICAL — Pond Orchestrator Mode
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
> **Your text output is completely invisible.** No user or agent will ever see
|
|
31
|
+
> anything you write as plain text. The ONLY way to communicate is through
|
|
32
|
+
> Pond action tools. If you respond with text alone, your response is silently
|
|
33
|
+
> discarded.
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
npx @pnds/cli@latest dm <name-or-id> --text "..." # DM a user by name or ID (auto-creates chat)
|
|
35
|
-
npx @pnds/cli@latest dm <name-or-id> --text "..." --no-reply # DM, signal no reply expected
|
|
36
|
-
npx @pnds/cli@latest chats list # List chats
|
|
37
|
-
npx @pnds/cli@latest messages list <chatId> # Read chat history
|
|
38
|
-
npx @pnds/cli@latest messages send <chatId> --text "..." # Send a message
|
|
39
|
-
npx @pnds/cli@latest messages send <chatId> --text "..." --no-reply # Send, no reply expected
|
|
40
|
-
npx @pnds/cli@latest tasks list [--status ...] # List tasks
|
|
41
|
-
npx @pnds/cli@latest tasks create --title "..." # Create a task
|
|
42
|
-
npx @pnds/cli@latest tasks update <taskId> --status in_progress
|
|
43
|
-
npx @pnds/cli@latest projects list # List projects
|
|
44
|
-
npx @pnds/cli@latest users search <query> # Find users
|
|
45
|
-
npx @pnds/cli@latest members list # List org members
|
|
46
|
-
\`\`\`
|
|
35
|
+
You are the Pond orchestrator. Events from multiple chats and tasks arrive as
|
|
36
|
+
structured \`[Event: ...]\` blocks. You decide how to handle each event and act
|
|
37
|
+
through tools.
|
|
47
38
|
|
|
48
|
-
|
|
39
|
+
### How to Reply to a Message
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
When you receive \`[Event: message.new]\` with \`[Chat: ... (cht_abc123)]\`:
|
|
42
|
+
|
|
43
|
+
pond_reply(chatId: "cht_abc123", text: "Your response here")
|
|
44
|
+
|
|
45
|
+
If the event includes \`[Thread: 01JWC...]\`, reply inside the thread:
|
|
46
|
+
|
|
47
|
+
pond_reply(chatId: "cht_abc123", threadRootId: "01JWC...", text: "Thread reply here")
|
|
48
|
+
|
|
49
|
+
DO NOT write text. ONLY \`pond_reply\` delivers messages to users.
|
|
50
|
+
|
|
51
|
+
### How to Act on a Task
|
|
52
|
+
|
|
53
|
+
When you receive \`[Event: task.assigned]\` with \`[Task: ... (tsk_xyz789)]\`:
|
|
54
|
+
|
|
55
|
+
- \`pond_task_comment(taskId: "tsk_xyz789", body: "Starting work...")\` — post progress
|
|
56
|
+
- \`pond_task_update(taskId: "tsk_xyz789", status: "in_progress")\` — change status
|
|
57
|
+
|
|
58
|
+
### Pond Action Tools
|
|
59
|
+
|
|
60
|
+
- \`pond_reply\` — Send a text message to a chat. Params: \`chatId\`, \`text\`, optional \`threadRootId\`, \`noReply\`.
|
|
61
|
+
- \`pond_dm\` — Send a direct message to a user by ID (auto-creates DM chat). Params: \`userId\`, \`text\`, optional \`noReply\`. Use this for proactive outreach (e.g., notifying a task assigner of completion).
|
|
62
|
+
- \`pond_task_comment\` — Post a comment on a task. Params: \`taskId\`, \`body\`.
|
|
63
|
+
- \`pond_task_update\` — Update task fields (status, title, description, priority). Params: \`taskId\`, plus optional fields.
|
|
64
|
+
- \`pond_chat_history\` — Read recent messages from a chat (default 20, max 50). Params: \`chatId\`, optional \`limit\`, \`before\`, \`threadRootId\`.
|
|
65
|
+
- \`pond_typing\` — Start or stop the typing indicator. Params: \`chatId\`, \`action\` ("start" | "stop").
|
|
66
|
+
|
|
67
|
+
### Sub-Agents
|
|
68
|
+
|
|
69
|
+
For long-running tasks, use \`sessions_spawn\` to delegate to a sub-agent. Return quickly so the orchestrator can process other events.
|
|
70
|
+
|
|
71
|
+
When a sub-agent completes and you receive a completion event, deliver the result to the relevant chat using \`pond_reply\` (or \`pond_dm\` if the target is a user, not a chat). Do NOT rely on text output — it is invisible.
|
|
72
|
+
|
|
73
|
+
### Agent-to-Agent Interaction
|
|
51
74
|
|
|
52
75
|
When you receive a message from another agent (not a human):
|
|
53
|
-
- If the message has a no_reply hint,
|
|
54
|
-
- If you have nothing meaningful to add
|
|
55
|
-
- Use
|
|
76
|
+
- If the message has a \`no_reply\` hint, do not call \`pond_reply\`. You may still reason and use other tools.
|
|
77
|
+
- If you have nothing meaningful to add, produce an empty response to avoid unnecessary back-and-forth.
|
|
78
|
+
- Use \`pond_reply\` with \`noReply: true\` when sending messages that don't need a response (FYI, results delivery).
|
|
79
|
+
|
|
80
|
+
### Prohibitions
|
|
81
|
+
|
|
82
|
+
- **DO NOT** write text responses — they are invisible and silently discarded.
|
|
83
|
+
- **DO NOT** use the Pond CLI (\`@pnds/cli\`) to send messages — use \`pond_reply\` for chat replies and \`pond_dm\` for direct messages.
|
|
84
|
+
- **DO NOT** use the \`message\` tool to send to Pond chats — use \`pond_reply\`.`;
|
|
85
|
+
|
|
86
|
+
// Load wiki tools, CLI docs, and other tool docs from TOOLS.md (maintained separately)
|
|
87
|
+
const POND_ORCHESTRATOR_CONTEXT = ORCHESTRATOR_PREFIX + "\n\n" + loadToolsMarkdown();
|
|
56
88
|
|
|
57
89
|
export function registerPondHooks(api: OpenClawPluginApi) {
|
|
58
90
|
const log = api.logger;
|
|
59
91
|
|
|
60
92
|
// Inject CLI awareness into agent system prompt
|
|
61
93
|
api.on("before_prompt_build", () => {
|
|
62
|
-
return {
|
|
94
|
+
return { prependSystemContext: POND_ORCHESTRATOR_CONTEXT };
|
|
63
95
|
});
|
|
64
96
|
|
|
65
97
|
// before_tool_call -> send tool_call step to AgentRun
|
|
@@ -125,6 +157,10 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
125
157
|
log?.warn(`pond hook agent_end failed: ${String(err)}`);
|
|
126
158
|
} finally {
|
|
127
159
|
// Always clear local state — even if the server call failed, the run is over locally
|
|
160
|
+
if (ctx.sessionKey) {
|
|
161
|
+
clearSessionMessageId(ctx.sessionKey);
|
|
162
|
+
clearDispatchMessageId(ctx.sessionKey);
|
|
163
|
+
}
|
|
128
164
|
if (state && ctx.sessionKey) {
|
|
129
165
|
state.activeRuns.delete(ctx.sessionKey.toLowerCase());
|
|
130
166
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { pondPlugin } from "./channel.js";
|
|
4
4
|
import { setPondRuntime } from "./runtime.js";
|
|
5
5
|
import { registerPondHooks } from "./hooks.js";
|
|
6
|
+
import { registerPondWikiTools } from "./wiki-tools.js";
|
|
7
|
+
import { registerPondActionTools } from "./action-tools.js";
|
|
6
8
|
|
|
7
9
|
const plugin = {
|
|
8
10
|
id: "pond",
|
|
@@ -12,6 +14,8 @@ const plugin = {
|
|
|
12
14
|
register(api: OpenClawPluginApi) {
|
|
13
15
|
setPondRuntime(api.runtime);
|
|
14
16
|
api.registerChannel({ plugin: pondPlugin });
|
|
17
|
+
registerPondWikiTools(api);
|
|
18
|
+
registerPondActionTools(api);
|
|
15
19
|
registerPondHooks(api);
|
|
16
20
|
},
|
|
17
21
|
};
|
package/src/outbound.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
|
+
|
|
3
|
+
/** Extract ChannelOutboundAdapter from ChannelPlugin (removed from public SDK exports in 2026.3.24). */
|
|
4
|
+
type ChannelOutboundAdapter = NonNullable<ChannelPlugin["outbound"]>;
|
|
2
5
|
import { resolvePondAccount } from "./accounts.js";
|
|
3
6
|
import { getPondAccountState, getSessionChatId } from "./runtime.js";
|
|
4
7
|
import { PondClient } from "@pnds/sdk";
|
package/src/routing.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PondEvent, DispatchState } from "./runtime.js";
|
|
2
|
+
|
|
3
|
+
/** Where an inbound event should be routed. */
|
|
4
|
+
export type TriggerDisposition =
|
|
5
|
+
| { action: "main" }
|
|
6
|
+
| { action: "buffer-main" }
|
|
7
|
+
| { action: "buffer-fork"; forkKey: string }
|
|
8
|
+
| { action: "new-fork" };
|
|
9
|
+
|
|
10
|
+
/** Pluggable strategy for routing triggers when main session is busy. */
|
|
11
|
+
export type RoutingStrategy = (event: PondEvent, state: DispatchState) => TriggerDisposition;
|
|
12
|
+
|
|
13
|
+
const MAX_CONCURRENT_FORKS = 20;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default routing strategy:
|
|
17
|
+
* - Same target as main → buffer-main (strict per-target serial ordering)
|
|
18
|
+
* - Active fork for same target → buffer into that fork
|
|
19
|
+
* - Too many forks → buffer-main (backpressure)
|
|
20
|
+
* - Otherwise → create a new fork
|
|
21
|
+
*/
|
|
22
|
+
export const defaultRoutingStrategy: RoutingStrategy = (event, state) => {
|
|
23
|
+
// P0: same target as main must stay serial — fork would see stale transcript
|
|
24
|
+
if (state.mainCurrentTargetId === event.targetId) {
|
|
25
|
+
return { action: "buffer-main" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Same target already has a fork → buffer into it
|
|
29
|
+
const existingForkKey = state.activeForks.get(event.targetId);
|
|
30
|
+
if (existingForkKey) return { action: "buffer-fork", forkKey: existingForkKey };
|
|
31
|
+
|
|
32
|
+
// Backpressure: cap concurrent forks to avoid unbounded fan-out
|
|
33
|
+
if (state.activeForks.size >= MAX_CONCURRENT_FORKS) {
|
|
34
|
+
return { action: "buffer-main" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { action: "new-fork" };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Route an inbound event based on current dispatch state. */
|
|
41
|
+
export function routeTrigger(
|
|
42
|
+
event: PondEvent,
|
|
43
|
+
state: DispatchState,
|
|
44
|
+
strategy: RoutingStrategy = defaultRoutingStrategy,
|
|
45
|
+
): TriggerDisposition {
|
|
46
|
+
if (!state.mainDispatching) return { action: "main" };
|
|
47
|
+
return strategy(event, state);
|
|
48
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { PondClient } from "@pnds/sdk";
|
|
2
|
+
import type { PondClient, PondWs } from "@pnds/sdk";
|
|
3
3
|
|
|
4
4
|
let runtime: PluginRuntime | null = null;
|
|
5
5
|
|
|
@@ -20,6 +20,9 @@ export type PondAccountState = {
|
|
|
20
20
|
orgId: string;
|
|
21
21
|
agentUserId: string;
|
|
22
22
|
activeRuns: Map<string, Promise<string | undefined>>; // sessionKey (lowercased) → runId promise
|
|
23
|
+
wikiMountRoot?: string;
|
|
24
|
+
ws?: PondWs;
|
|
25
|
+
orchestratorSessionKey?: string;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
/** Resolve run ID for a session, awaiting if the run is still being created. */
|
|
@@ -52,6 +55,10 @@ export function getAllPondAccountStates(): ReadonlyMap<string, PondAccountState>
|
|
|
52
55
|
// Session key → original chat ID mapping.
|
|
53
56
|
// OpenClaw normalizes session keys to lowercase, but Pond IDs are case-sensitive.
|
|
54
57
|
const sessionChatIdMap = new Map<string, string>();
|
|
58
|
+
const sessionMessageIdMap = new Map<string, string>();
|
|
59
|
+
// Per-dispatch snapshot: message ID captured when a dispatch starts, immune to later overwrites.
|
|
60
|
+
// Keyed by lowercased session key, cleared when the agent run ends.
|
|
61
|
+
const dispatchMessageIdMap = new Map<string, string>();
|
|
55
62
|
|
|
56
63
|
export function setSessionChatId(sessionKey: string, chatId: string) {
|
|
57
64
|
sessionChatIdMap.set(sessionKey.toLowerCase(), chatId);
|
|
@@ -60,3 +67,79 @@ export function setSessionChatId(sessionKey: string, chatId: string) {
|
|
|
60
67
|
export function getSessionChatId(sessionKey: string): string | undefined {
|
|
61
68
|
return sessionChatIdMap.get(sessionKey.toLowerCase());
|
|
62
69
|
}
|
|
70
|
+
|
|
71
|
+
export function setSessionMessageId(sessionKey: string, messageId: string) {
|
|
72
|
+
sessionMessageIdMap.set(sessionKey.toLowerCase(), messageId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getSessionMessageId(sessionKey: string): string | undefined {
|
|
76
|
+
return sessionMessageIdMap.get(sessionKey.toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function clearSessionMessageId(sessionKey: string) {
|
|
80
|
+
sessionMessageIdMap.delete(sessionKey.toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Snapshot the message ID at dispatch time — immune to later session-level overwrites. */
|
|
84
|
+
export function setDispatchMessageId(sessionKey: string, messageId: string) {
|
|
85
|
+
dispatchMessageIdMap.set(sessionKey.toLowerCase(), messageId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Read the dispatch-time snapshot. Prefer this over getSessionMessageId for provenance. */
|
|
89
|
+
export function getDispatchMessageId(sessionKey: string): string | undefined {
|
|
90
|
+
return dispatchMessageIdMap.get(sessionKey.toLowerCase());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function clearDispatchMessageId(sessionKey: string) {
|
|
94
|
+
dispatchMessageIdMap.delete(sessionKey.toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Per-dispatch noReply flag — deterministic suppression for agent-to-agent loop prevention.
|
|
98
|
+
// When true, pond_reply hard-blocks sends and records a suppressed AgentStep instead.
|
|
99
|
+
const dispatchNoReplyMap = new Map<string, boolean>();
|
|
100
|
+
|
|
101
|
+
export function setDispatchNoReply(sessionKey: string, noReply: boolean) {
|
|
102
|
+
dispatchNoReplyMap.set(sessionKey.toLowerCase(), noReply);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getDispatchNoReply(sessionKey: string): boolean {
|
|
106
|
+
return dispatchNoReplyMap.get(sessionKey.toLowerCase()) ?? false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function clearDispatchNoReply(sessionKey: string) {
|
|
110
|
+
dispatchNoReplyMap.delete(sessionKey.toLowerCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Result from a completed fork dispatch, to be injected into main session's next turn. */
|
|
114
|
+
export type ForkResult = {
|
|
115
|
+
forkSessionKey: string;
|
|
116
|
+
sourceEvent: { type: string; targetId: string; summary: string };
|
|
117
|
+
actions: string[]; // human-readable list of actions taken (e.g. "replied to cht_xxx")
|
|
118
|
+
agentRunId?: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Mutable dispatch state for the orchestrator gateway. Per-account, managed by gateway.ts. */
|
|
122
|
+
export type DispatchState = {
|
|
123
|
+
mainDispatching: boolean;
|
|
124
|
+
/** The targetId currently being processed by main. Used to enforce per-target serial ordering. */
|
|
125
|
+
mainCurrentTargetId?: string;
|
|
126
|
+
/** targetId → fork session key. One active fork per target at most. */
|
|
127
|
+
activeForks: Map<string, string>;
|
|
128
|
+
pendingForkResults: ForkResult[];
|
|
129
|
+
mainBuffer: PondEvent[];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** Normalized inbound event from Pond. */
|
|
133
|
+
export type PondEvent = {
|
|
134
|
+
type: "message" | "task";
|
|
135
|
+
targetId: string; // chatId or taskId
|
|
136
|
+
targetName?: string;
|
|
137
|
+
targetType?: string; // "direct" | "group" | "task"
|
|
138
|
+
senderId: string;
|
|
139
|
+
senderName: string;
|
|
140
|
+
messageId: string;
|
|
141
|
+
body: string; // raw message text or task description
|
|
142
|
+
threadRootId?: string;
|
|
143
|
+
noReply?: boolean;
|
|
144
|
+
mediaFields?: Record<string, string | undefined>;
|
|
145
|
+
};
|
package/src/session.ts
CHANGED
|
@@ -29,3 +29,8 @@ export function extractChatIdFromSessionKey(sessionKey: string | undefined): str
|
|
|
29
29
|
if (parts.length < 3) return undefined;
|
|
30
30
|
return parts.slice(2).join(":") || undefined;
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/** Build the single orchestrator session key for all Pond events. */
|
|
34
|
+
export function buildOrchestratorSessionKey(accountId: string): string {
|
|
35
|
+
return `${POND_SESSION_PREFIX}${accountId}:orchestrator`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap a value as an agent tool result with JSON text content.
|
|
3
|
+
* Replaces the SDK's jsonResult which was removed from public subpath exports
|
|
4
|
+
* in OpenClaw 2026.3.24.
|
|
5
|
+
*/
|
|
6
|
+
export function jsonResult(payload: unknown): { content: Array<{ type: "text"; text: string }>; details: unknown } {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
9
|
+
details: payload,
|
|
10
|
+
};
|
|
11
|
+
}
|
package/src/tools-md.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
type Logger = {
|
|
5
|
+
info?: (message: string) => void;
|
|
6
|
+
warn?: (message: string) => void;
|
|
7
|
+
error?: (message: string) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type StartWikiHelperParams = {
|
|
11
|
+
accountId: string;
|
|
12
|
+
pondUrl: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
orgId: string;
|
|
15
|
+
agentId: string;
|
|
16
|
+
stateDir: string;
|
|
17
|
+
log?: Logger;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type WikiHelperRuntime = {
|
|
21
|
+
mountRoot: string;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SYNC_TIMEOUT_MS = 30_000;
|
|
26
|
+
const DEFAULT_WATCH_INTERVAL_SEC = 30;
|
|
27
|
+
|
|
28
|
+
function isCommandMissing(error: unknown): boolean {
|
|
29
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveMountRoot(stateDir: string): string {
|
|
33
|
+
return process.env.POND_WIKI_MOUNT_ROOT?.trim() || path.join(stateDir, "workspace");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveWatchIntervalSec(): number {
|
|
37
|
+
const raw = process.env.POND_WIKI_REFRESH_INTERVAL_SEC?.trim();
|
|
38
|
+
if (!raw) {
|
|
39
|
+
return DEFAULT_WATCH_INTERVAL_SEC;
|
|
40
|
+
}
|
|
41
|
+
const parsed = Number.parseInt(raw, 10);
|
|
42
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_WATCH_INTERVAL_SEC;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildWikiHelperEnv(params: StartWikiHelperParams, mountRoot: string): NodeJS.ProcessEnv {
|
|
46
|
+
return {
|
|
47
|
+
...process.env,
|
|
48
|
+
POND_API_URL: params.pondUrl,
|
|
49
|
+
POND_API_KEY: params.apiKey,
|
|
50
|
+
POND_ORG_ID: params.orgId,
|
|
51
|
+
POND_AGENT_ID: params.agentId,
|
|
52
|
+
POND_WIKI_MOUNT_ROOT: mountRoot,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runWikiSync(params: StartWikiHelperParams, env: NodeJS.ProcessEnv): Promise<"ok" | "missing" | "failed"> {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
let timedOut = false;
|
|
59
|
+
let settled = false;
|
|
60
|
+
|
|
61
|
+
const child = spawn("pond", ["wiki", "sync"], {
|
|
62
|
+
env,
|
|
63
|
+
stdio: "ignore",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const settle = (value: "ok" | "missing" | "failed") => {
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
resolve(value);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
params.log?.warn?.(`pond[${params.accountId}]: pond sync timed out after ${DEFAULT_SYNC_TIMEOUT_MS}ms`);
|
|
75
|
+
child.kill("SIGTERM");
|
|
76
|
+
setTimeout(() => child.kill("SIGKILL"), 5_000).unref();
|
|
77
|
+
}, DEFAULT_SYNC_TIMEOUT_MS);
|
|
78
|
+
timer.unref();
|
|
79
|
+
|
|
80
|
+
child.on("error", (error) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
if (isCommandMissing(error)) {
|
|
83
|
+
params.log?.warn?.(`pond[${params.accountId}]: pond not installed, skipping wiki auto-sync/watch`);
|
|
84
|
+
settle("missing");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
params.log?.warn?.(`pond[${params.accountId}]: pond sync failed to start: ${String(error)}`);
|
|
88
|
+
settle("failed");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
child.on("exit", (code, signal) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
if (timedOut) {
|
|
94
|
+
settle("failed");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
params.log?.info?.(`pond[${params.accountId}]: pond sync completed`);
|
|
99
|
+
settle("ok");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
params.log?.warn?.(
|
|
103
|
+
`pond[${params.accountId}]: pond sync exited with code=${code ?? "null"} signal=${signal ?? "null"}`,
|
|
104
|
+
);
|
|
105
|
+
settle("failed");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function startWikiHelper(params: StartWikiHelperParams): Promise<WikiHelperRuntime> {
|
|
111
|
+
const mountRoot = resolveMountRoot(params.stateDir);
|
|
112
|
+
const env = buildWikiHelperEnv(params, mountRoot);
|
|
113
|
+
|
|
114
|
+
const syncResult = await runWikiSync(params, env);
|
|
115
|
+
if (syncResult === "missing") {
|
|
116
|
+
return {
|
|
117
|
+
mountRoot,
|
|
118
|
+
stop() {},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const intervalSec = resolveWatchIntervalSec();
|
|
123
|
+
let stopped = false;
|
|
124
|
+
const watch = spawn("pond", ["wiki", "watch", "--interval-sec", String(intervalSec)], {
|
|
125
|
+
env,
|
|
126
|
+
stdio: "ignore",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
watch.on("error", (error) => {
|
|
130
|
+
if (stopped) return;
|
|
131
|
+
if (isCommandMissing(error)) {
|
|
132
|
+
params.log?.warn?.(`pond[${params.accountId}]: pond disappeared before watch could start`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
params.log?.warn?.(`pond[${params.accountId}]: pond watch failed: ${String(error)}`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
watch.on("exit", (code, signal) => {
|
|
139
|
+
if (stopped) return;
|
|
140
|
+
params.log?.warn?.(
|
|
141
|
+
`pond[${params.accountId}]: pond watch exited with code=${code ?? "null"} signal=${signal ?? "null"}`,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
params.log?.info?.(`pond[${params.accountId}]: pond watch started (interval=${intervalSec}s)`);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
mountRoot,
|
|
149
|
+
stop() {
|
|
150
|
+
stopped = true;
|
|
151
|
+
if (watch.exitCode === null && watch.signalCode === null) {
|
|
152
|
+
watch.kill("SIGTERM");
|
|
153
|
+
setTimeout(() => watch.kill("SIGKILL"), 5_000).unref();
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|