@pnds/pond 1.0.1 → 1.2.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/TOOLS.md +54 -0
- package/package.json +17 -4
- package/src/action-tools.ts +223 -0
- package/src/channel.ts +2 -2
- package/src/fork.ts +213 -0
- package/src/gateway.ts +721 -143
- package/src/hooks.ts +33 -17
- 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,31 +25,42 @@ async function resolveClientForHook(sessionKey: string | undefined) {
|
|
|
24
25
|
return { ...state, chatId, activeRunId };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const ORCHESTRATOR_PREFIX = `## Orchestrator Model
|
|
28
29
|
|
|
29
|
-
You
|
|
30
|
+
You are an orchestrator agent. Events arrive as structured \`[Event: ...]\` blocks. Your text output is internal reasoning only — users cannot see it. All visible actions must go through tools.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
npx @pnds/cli@latest whoami # Check your identity
|
|
33
|
-
npx @pnds/cli@latest chats list # List chats
|
|
34
|
-
npx @pnds/cli@latest messages list <chatId> # Read chat history
|
|
35
|
-
npx @pnds/cli@latest messages send <chatId> --text "..." # Send a message
|
|
36
|
-
npx @pnds/cli@latest tasks list [--status ...] # List tasks
|
|
37
|
-
npx @pnds/cli@latest tasks create --title "..." # Create a task
|
|
38
|
-
npx @pnds/cli@latest tasks update <taskId> --status in_progress
|
|
39
|
-
npx @pnds/cli@latest projects list # List projects
|
|
40
|
-
npx @pnds/cli@latest users search <query> # Find users
|
|
41
|
-
npx @pnds/cli@latest members list # List org members
|
|
42
|
-
\`\`\`
|
|
32
|
+
## Pond Action Tools
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
- \`pond_reply\` — Send a text message to a chat. Params: \`chatId\`, \`text\`, optional \`noReply\`.
|
|
35
|
+
- \`pond_task_comment\` — Post a comment on a task. Params: \`taskId\`, \`body\`.
|
|
36
|
+
- \`pond_task_update\` — Update task fields (status, title, description, priority). Params: \`taskId\`, plus optional fields.
|
|
37
|
+
- \`pond_chat_history\` — Read recent messages from a chat (default 20, max 50). Use this to get context on unfamiliar chats. Params: \`chatId\`, optional \`limit\`, \`before\`, \`threadRootId\`.
|
|
38
|
+
- \`pond_typing\` — Start or stop the typing indicator. Params: \`chatId\`, \`action\` ("start" | "stop").
|
|
39
|
+
|
|
40
|
+
## Task Handling
|
|
41
|
+
|
|
42
|
+
Use \`pond_task_comment\` to communicate progress on tasks. Use \`pond_task_update\` to change task status (e.g., in_progress, done), title, description, or priority.
|
|
43
|
+
|
|
44
|
+
## Sub-Agents
|
|
45
|
+
|
|
46
|
+
For long-running tasks, use \`sessions_spawn\` to delegate to a sub-agent. Return quickly so the orchestrator can process other events.
|
|
47
|
+
|
|
48
|
+
## Agent-to-Agent Interaction
|
|
49
|
+
|
|
50
|
+
When you receive a message from another agent (not a human):
|
|
51
|
+
- If the message has a no_reply hint, you may still reason and use tools, but your response will not be sent to chat.
|
|
52
|
+
- If you have nothing meaningful to add to the conversation, produce an empty response to avoid unnecessary back-and-forth.
|
|
53
|
+
- Replies go through \`pond_reply\` — use \`noReply: true\` when sending messages that don't require a response (e.g., delivering results, FYI notifications).`;
|
|
54
|
+
|
|
55
|
+
// Load wiki tools, CLI docs, and other tool docs from TOOLS.md (maintained separately)
|
|
56
|
+
const POND_ORCHESTRATOR_CONTEXT = ORCHESTRATOR_PREFIX + "\n\n" + loadToolsMarkdown();
|
|
45
57
|
|
|
46
58
|
export function registerPondHooks(api: OpenClawPluginApi) {
|
|
47
59
|
const log = api.logger;
|
|
48
60
|
|
|
49
61
|
// Inject CLI awareness into agent system prompt
|
|
50
62
|
api.on("before_prompt_build", () => {
|
|
51
|
-
return { prependContext:
|
|
63
|
+
return { prependContext: POND_ORCHESTRATOR_CONTEXT };
|
|
52
64
|
});
|
|
53
65
|
|
|
54
66
|
// before_tool_call -> send tool_call step to AgentRun
|
|
@@ -114,6 +126,10 @@ export function registerPondHooks(api: OpenClawPluginApi) {
|
|
|
114
126
|
log?.warn(`pond hook agent_end failed: ${String(err)}`);
|
|
115
127
|
} finally {
|
|
116
128
|
// Always clear local state — even if the server call failed, the run is over locally
|
|
129
|
+
if (ctx.sessionKey) {
|
|
130
|
+
clearSessionMessageId(ctx.sessionKey);
|
|
131
|
+
clearDispatchMessageId(ctx.sessionKey);
|
|
132
|
+
}
|
|
117
133
|
if (state && ctx.sessionKey) {
|
|
118
134
|
state.activeRuns.delete(ctx.sessionKey.toLowerCase());
|
|
119
135
|
}
|
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
|
+
}
|