@chorus-aidlc/chorus-openclaw-plugin 0.5.3 → 0.11.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.
Files changed (61) hide show
  1. package/dist/connection-state.d.ts +35 -0
  2. package/dist/connection-state.d.ts.map +1 -0
  3. package/dist/connection-state.js +52 -0
  4. package/dist/connection-state.js.map +1 -0
  5. package/dist/control-handler.d.ts +73 -0
  6. package/dist/control-handler.d.ts.map +1 -0
  7. package/dist/control-handler.js +135 -0
  8. package/dist/control-handler.js.map +1 -0
  9. package/dist/daemon-client.d.ts +203 -0
  10. package/dist/daemon-client.d.ts.map +1 -0
  11. package/dist/daemon-client.js +469 -0
  12. package/dist/daemon-client.js.map +1 -0
  13. package/dist/daemon-rest-client.d.ts +86 -0
  14. package/dist/daemon-rest-client.d.ts.map +1 -0
  15. package/dist/daemon-rest-client.js +196 -0
  16. package/dist/daemon-rest-client.js.map +1 -0
  17. package/dist/event-router.d.ts +31 -6
  18. package/dist/event-router.d.ts.map +1 -1
  19. package/dist/event-router.js +58 -27
  20. package/dist/event-router.js.map +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +106 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/lineage.d.ts +44 -0
  25. package/dist/lineage.d.ts.map +1 -0
  26. package/dist/lineage.js +116 -0
  27. package/dist/lineage.js.map +1 -0
  28. package/dist/mcp-registration.d.ts.map +1 -1
  29. package/dist/mcp-registration.js +5 -4
  30. package/dist/mcp-registration.js.map +1 -1
  31. package/dist/sse-listener.d.ts +34 -0
  32. package/dist/sse-listener.d.ts.map +1 -1
  33. package/dist/sse-listener.js +78 -4
  34. package/dist/sse-listener.js.map +1 -1
  35. package/dist/wake.d.ts +20 -0
  36. package/dist/wake.d.ts.map +1 -1
  37. package/dist/wake.js +56 -0
  38. package/dist/wake.js.map +1 -1
  39. package/package.json +1 -1
  40. package/skills/brainstorm/SKILL.md +1 -1
  41. package/skills/chorus/SKILL.md +37 -6
  42. package/skills/develop/SKILL.md +1 -1
  43. package/skills/idea/SKILL.md +18 -3
  44. package/skills/openspec-aware/SKILL.md +1 -1
  45. package/skills/proposal/SKILL.md +1 -1
  46. package/skills/proposal-reviewer/SKILL.md +1 -1
  47. package/skills/quick-dev/SKILL.md +1 -1
  48. package/skills/review/SKILL.md +1 -1
  49. package/skills/task-reviewer/SKILL.md +1 -1
  50. package/skills/yolo/SKILL.md +1 -1
  51. package/src/connection-state.ts +66 -0
  52. package/src/control-handler.ts +219 -0
  53. package/src/daemon-client.ts +622 -0
  54. package/src/daemon-rest-client.ts +312 -0
  55. package/src/event-router.ts +103 -33
  56. package/src/index.ts +113 -8
  57. package/src/lineage.ts +157 -0
  58. package/src/mcp-registration.ts +6 -19
  59. package/src/openclaw-sdk.d.ts +232 -1
  60. package/src/sse-listener.ts +117 -5
  61. package/src/wake.ts +69 -26
package/src/index.ts CHANGED
@@ -5,9 +5,15 @@ import { resolveConfig, validateConfigWithWarnings } from "./config.js";
5
5
  import { ensureChorusMcpServer } from "./mcp-registration.js";
6
6
  import { ChorusMcpClient } from "./mcp-client.js";
7
7
  import { ChorusSseListener } from "./sse-listener.js";
8
- import { ChorusEventRouter } from "./event-router.js";
9
- import { createWake } from "./wake.js";
8
+ import { ChorusEventRouter, type WakeAttribution } from "./event-router.js";
9
+ import { resolveWakeRunContext } from "./wake.js";
10
10
  import { registerChorusCommands } from "./commands.js";
11
+ import { ConnectionState } from "./connection-state.js";
12
+ import { createControlHandler, type ControlBehaviorHooks } from "./control-handler.js";
13
+ import { createDaemonRestClient } from "./daemon-rest-client.js";
14
+ import { LineageResolver } from "./lineage.js";
15
+ import { OpenClawDaemonClient, type WakeRequest } from "./daemon-client.js";
16
+ import type { DaemonPendingTurn } from "./daemon-rest-client.js";
11
17
 
12
18
  /**
13
19
  * JSON-Schema config contract for the Chorus plugin.
@@ -70,15 +76,97 @@ export default definePluginEntry({
70
76
  // assignments, notifications back-fill).
71
77
  const mcpClient = new ChorusMcpClient({ chorusUrl, apiKey, logger });
72
78
 
73
- // 5. Event router. Wakes the agent in-process by running an embedded agent
74
- // turn via `api.runtime.agent.runEmbeddedAgent` (see wake.ts). `createWake`
75
- // resolves the main agent session + configured model on each wake and
76
- // gracefully DROPS (logs + returns) when it cannot run it never throws,
77
- // so the SSE service stays alive even on a host that exposes no session.
79
+ // 4b. Connection identity + reverse control channel + daemon reporting (parity).
80
+ // `connectionState` holds the DaemonConnection uuid the server reports
81
+ // post-handshake (captured by the listener's onConnectionId); it is the
82
+ // single source of truth for "which connection am I", read by the control
83
+ // handler's double-check AND the daemon REST reporter (lazily, so order
84
+ // doesn't matter — both predate the handshake).
85
+ const connectionState = new ConnectionState();
86
+
87
+ // The shared pure-REST daemon client owns the `/api/daemon/*` payload shapes
88
+ // (turn-advance / transcript / execution-state / report-interrupt /
89
+ // pending-turns). It reads the connectionUuid lazily from connectionState.
90
+ const restClient = createDaemonRestClient({
91
+ url: chorusUrl,
92
+ apiKey,
93
+ getConnectionUuid: () => connectionState.getConnectionUuid(),
94
+ logger,
95
+ });
96
+
97
+ // Lineage resolver: per-notification { rootIdeaUuid, directIdeaUuid } via the
98
+ // root-idea REST endpoint, so the daemon client anchors the session on the
99
+ // DIRECT idea (resume/deliver_turn continuity) and reports the ROOT idea in
100
+ // its execution snapshot (the two-id contract).
101
+ const lineage = new LineageResolver({ url: chorusUrl, apiKey, logger });
102
+
103
+ // The in-process daemon client wraps runEmbeddedAgent with full reporting,
104
+ // the AbortController registry (real mid-run interrupt), the execution
105
+ // snapshot source, deterministic session-key mapping, and the at-most-once
106
+ // pending-turns backfill. `resolveRunContext` is the ONE place that reaches
107
+ // into api.config/api.runtime (kept out of the client so it stays testable).
108
+ // `redispatch` resolves lineage for a synthetic resume so it continues the
109
+ // SAME session, then runs the wake; a delivered turn already carries its ids.
110
+ let daemonClient: OpenClawDaemonClient;
111
+ const redispatch = (req: WakeRequest): void => {
112
+ void (async () => {
113
+ let enriched = req;
114
+ // A resume only knows the entity — resolve its lineage so the wake anchors on
115
+ // the same business key (direct idea) the original run used. A delivered turn
116
+ // already carries directIdeaUuid, so we skip the round-trip when present.
117
+ if (req.directIdeaUuid == null && req.entityType && req.entityUuid) {
118
+ try {
119
+ const { rootIdeaUuid, directIdeaUuid } = await lineage.resolve({
120
+ entityType: req.entityType,
121
+ entityUuid: req.entityUuid,
122
+ });
123
+ enriched = { ...req, rootIdeaUuid, directIdeaUuid };
124
+ } catch (err) {
125
+ logger.warn(`[Chorus] resume lineage resolve failed: ${err}`);
126
+ }
127
+ }
128
+ await daemonClient.runWake(enriched);
129
+ })();
130
+ };
131
+ daemonClient = new OpenClawDaemonClient({
132
+ restClient,
133
+ resolveRunContext: () => resolveWakeRunContext(api, logger),
134
+ redispatch,
135
+ // Build the prompt for a delivered human_instruction turn. The free-text body
136
+ // lives only on the turn (promptText); fall back to a generic nudge if absent.
137
+ buildTurnPrompt: (turn: DaemonPendingTurn) =>
138
+ turn.promptText && turn.promptText.trim()
139
+ ? `[Chorus] A human sent you an instruction in this conversation:\n\n${turn.promptText}`
140
+ : `[Chorus] A human sent you a new instruction in this conversation (session ${turn.sessionId}). Review the latest comments and respond.`,
141
+ logger,
142
+ });
143
+
144
+ // The control handler ROUTES verified control commands to the daemon client's
145
+ // behavior hooks (real abort / resume re-dispatch / pending-turns sweep),
146
+ // after its own double-check (own connection + held entity).
147
+ const controlHooks: ControlBehaviorHooks = daemonClient.controlHooks;
148
+ const onControl = createControlHandler({ connectionState, hooks: controlHooks, logger });
149
+
150
+ // 5. Event router. Wakes the agent in-process by running an embedded agent turn
151
+ // via the daemon client (which calls api.runtime.agent.runEmbeddedAgent and
152
+ // reports lifecycle/transcript). The router resolves each notification's
153
+ // lineage, then the daemon client's runWake gracefully DROPS (logs + returns)
154
+ // when it cannot run — it never throws, so the SSE service stays alive.
155
+ const wakeFn = (message: string, contextKey: string, attribution?: WakeAttribution): void => {
156
+ void daemonClient.runWake({
157
+ prompt: message,
158
+ contextKey,
159
+ entityType: attribution?.entityType,
160
+ entityUuid: attribution?.entityUuid,
161
+ directIdeaUuid: attribution?.directIdeaUuid,
162
+ rootIdeaUuid: attribution?.rootIdeaUuid,
163
+ });
164
+ };
78
165
  const eventRouter = new ChorusEventRouter({
79
166
  mcpClient,
80
167
  logger,
81
- wake: createWake(api, logger),
168
+ lineage,
169
+ wake: wakeFn,
82
170
  });
83
171
 
84
172
  // 6. Background SSE service. The SSE socket opens only inside start(), which
@@ -92,7 +180,23 @@ export default definePluginEntry({
92
180
  apiKey,
93
181
  logger,
94
182
  onEvent: (event) => eventRouter.dispatch(event),
183
+ // Capture (and refresh on reconnect) the DaemonConnection identity the
184
+ // server reports post-handshake. NOT a wake — forked by the listener.
185
+ onConnectionId: (connectionUuid) => {
186
+ connectionState.setConnectionUuid(connectionUuid);
187
+ logger.info(`[Chorus] registered as daemon connection ${connectionUuid}`);
188
+ },
189
+ // Reverse control channel. The handler does the double-check and routes
190
+ // to the behavior hooks — NEVER the wake path.
191
+ onControl,
95
192
  onReconnect: async () => {
193
+ // (1) Notification backfill — re-pull unread notifications missed during
194
+ // the gap (autonomous wakes). (2) Pending-turns backfill — re-derive
195
+ // this connection's unstarted human_instruction turns from the turn
196
+ // table and run each (the lost-deliver_turn-ping safety net). The two
197
+ // share the daemon client's seen-set so a turn is run at most once
198
+ // across live delivery + backfill. Each swallows its own errors so one
199
+ // failing source never aborts the other.
96
200
  try {
97
201
  const result = (await mcpClient.callTool("chorus_get_notifications", {
98
202
  status: "unread",
@@ -105,6 +209,7 @@ export default definePluginEntry({
105
209
  } catch (err) {
106
210
  logger.warn(`Failed to back-fill notifications: ${err}`);
107
211
  }
212
+ await daemonClient.onReconnect();
108
213
  },
109
214
  });
110
215
  await sseListener.connect();
package/src/lineage.ts ADDED
@@ -0,0 +1,157 @@
1
+ // packages/openclaw-plugin/src/lineage.ts
2
+ // Resolves any inbound Chorus notification/entity to its idea attribution, so the
3
+ // OpenClaw daemon client can anchor ONE embedded-agent session per DIRECT idea (the
4
+ // in-process analog of `claude --resume <directIdeaUuid>`) while reporting the ROOT
5
+ // idea in its execution snapshot for observability.
6
+ //
7
+ // TS mirror of `cli/lineage.mjs` (`LineageResolver`) — same single-source-of-truth
8
+ // REST contract, re-stated in TS because the plugin publishes standalone (it cannot
9
+ // import a file under `cli/`; see daemon-rest-client.ts for the same rationale).
10
+ //
11
+ // Resolution is fully SERVER-SIDE: every entity is resolved by a single call to the
12
+ // standalone REST endpoint
13
+ // GET /api/entities/{type}/{uuid}/root-idea (Bearer <cho_ agent key>)
14
+ // which returns BOTH `rootIdeaUuid` (topmost ancestor) and `directIdeaUuid` (the
15
+ // first idea node on the lineage). There is intentionally NO client-side lineage
16
+ // walk. On any failure (unreachable server, non-2xx, malformed body) it returns both
17
+ // ids as null so the caller falls back to a per-entity session key — "no idea
18
+ // ancestor" is a normal, non-fatal outcome. Uses global fetch (Node 18+) → no new dep.
19
+
20
+ export interface LineageLogger {
21
+ info: (msg: string) => void;
22
+ warn: (msg: string) => void;
23
+ error: (msg: string) => void;
24
+ }
25
+
26
+ const NOOP_LOGGER: LineageLogger = { info() {}, warn() {}, error() {} };
27
+
28
+ /** The idea attribution of an entity. Both null when there's no idea ancestor. */
29
+ export interface LineageAttribution {
30
+ rootIdeaUuid: string | null;
31
+ directIdeaUuid: string | null;
32
+ }
33
+
34
+ const NONE: LineageAttribution = { rootIdeaUuid: null, directIdeaUuid: null };
35
+
36
+ export interface LineageResolverOptions {
37
+ /** Chorus base URL. */
38
+ url: string;
39
+ /** `cho_` agent API key. */
40
+ apiKey: string;
41
+ logger?: LineageLogger;
42
+ /** Injectable for tests (defaults to global fetch). */
43
+ fetchImpl?: typeof fetch;
44
+ }
45
+
46
+ export class LineageResolver {
47
+ private readonly url: string;
48
+ private readonly apiKey: string;
49
+ private readonly logger: LineageLogger;
50
+ private readonly fetchImpl: typeof fetch;
51
+ /** Per-run cache keyed by `${type}:${uuid}` so repeats single-flight. */
52
+ private readonly cache = new Map<string, LineageAttribution>();
53
+
54
+ constructor(opts: LineageResolverOptions) {
55
+ this.url = opts.url.replace(/\/$/, "");
56
+ this.apiKey = opts.apiKey;
57
+ this.logger = opts.logger ?? NOOP_LOGGER;
58
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
59
+ }
60
+
61
+ /**
62
+ * Resolve an entity to its idea attribution `{ rootIdeaUuid, directIdeaUuid }`.
63
+ * One REST call per entity; the cache single-flights repeats. On any failure both
64
+ * ids are null (caller falls back to a per-entity key). Never throws.
65
+ */
66
+ async resolve(event: {
67
+ entityType?: string;
68
+ entityUuid?: string;
69
+ }): Promise<LineageAttribution> {
70
+ const entityType = event?.entityType;
71
+ const entityUuid = event?.entityUuid;
72
+ if (!entityType || !entityUuid) {
73
+ this.logger.warn("[Chorus] lineage: event missing entityType/entityUuid");
74
+ return NONE;
75
+ }
76
+ // An ad-hoc conversation (`daemon_session`) has NO idea ancestor by definition,
77
+ // and the root-idea endpoint does not accept it (it would 400). Short-circuit to
78
+ // the null attribution the caller would fall back to anyway — avoiding a
79
+ // guaranteed-failing round-trip + a spurious warn on every ad-hoc resume. The
80
+ // caller then anchors the session on the entity uuid (= the ad-hoc sessionId).
81
+ if (entityType === "daemon_session") {
82
+ return NONE;
83
+ }
84
+ const cacheKey = `${entityType}:${entityUuid}`;
85
+ const cached = this.cache.get(cacheKey);
86
+ if (cached) return cached;
87
+
88
+ const result = await this.resolveViaServer(entityType, entityUuid);
89
+ this.cache.set(cacheKey, result);
90
+ return result;
91
+ }
92
+
93
+ /**
94
+ * Call GET /api/entities/{type}/{uuid}/root-idea and return
95
+ * `{ rootIdeaUuid, directIdeaUuid }` (each string | null). Returns both null on any
96
+ * error so the caller degrades to a per-entity session key — never throws.
97
+ */
98
+ private async resolveViaServer(
99
+ entityType: string,
100
+ entityUuid: string,
101
+ ): Promise<LineageAttribution> {
102
+ const endpoint =
103
+ `${this.url}/api/entities/${encodeURIComponent(entityType)}/` +
104
+ `${encodeURIComponent(entityUuid)}/root-idea`;
105
+ let response: Response;
106
+ try {
107
+ response = await this.fetchImpl(endpoint, {
108
+ headers: { Authorization: `Bearer ${this.apiKey}`, Accept: "application/json" },
109
+ });
110
+ } catch (err) {
111
+ this.logger.warn(`[Chorus] lineage: request failed for ${entityType}:${entityUuid}: ${err}`);
112
+ return NONE;
113
+ }
114
+ if (!response.ok) {
115
+ this.logger.warn(
116
+ `[Chorus] lineage: server returned ${response.status} for ${entityType}:${entityUuid}`,
117
+ );
118
+ return NONE;
119
+ }
120
+ let body: unknown;
121
+ try {
122
+ body = await response.json();
123
+ } catch (err) {
124
+ this.logger.warn(`[Chorus] lineage: bad JSON for ${entityType}:${entityUuid}: ${err}`);
125
+ return NONE;
126
+ }
127
+ // API envelope: { success: true, data: { rootIdeaUuid, directIdeaUuid, ... } }.
128
+ const data =
129
+ body && typeof body === "object" ? (body as { data?: unknown }).data : undefined;
130
+ if (!data || typeof data !== "object" || !("rootIdeaUuid" in data)) {
131
+ this.logger.warn(
132
+ `[Chorus] lineage: unexpected response shape for ${entityType}:${entityUuid}`,
133
+ );
134
+ return NONE;
135
+ }
136
+ const root = (data as { rootIdeaUuid: unknown }).rootIdeaUuid;
137
+ if (root !== null && typeof root !== "string") {
138
+ this.logger.warn(`[Chorus] lineage: non-string rootIdeaUuid for ${entityType}:${entityUuid}`);
139
+ return NONE;
140
+ }
141
+ // directIdeaUuid is the session anchor. Older servers may omit it: treat a
142
+ // missing/non-string value as null so the caller falls back to a per-entity key.
143
+ const directRaw = (data as { directIdeaUuid?: unknown }).directIdeaUuid;
144
+ const direct = typeof directRaw === "string" ? directRaw : null;
145
+ if (directRaw !== undefined && directRaw !== null && typeof directRaw !== "string") {
146
+ this.logger.warn(
147
+ `[Chorus] lineage: non-string directIdeaUuid for ${entityType}:${entityUuid}`,
148
+ );
149
+ }
150
+ const resolvedVia = (data as { resolvedVia?: unknown }).resolvedVia;
151
+ this.logger.info(
152
+ `[Chorus] lineage: ${entityType}:${entityUuid} → root ${root ?? "none"}, direct ${direct ?? "none"}` +
153
+ (typeof resolvedVia === "string" ? ` (${resolvedVia})` : ""),
154
+ );
155
+ return { rootIdeaUuid: root, directIdeaUuid: direct };
156
+ }
157
+ }
@@ -86,25 +86,12 @@ export async function ensureChorusMcpServer(
86
86
  const desired = buildDesiredEntry(cfg.chorusUrl!, cfg.apiKey!);
87
87
 
88
88
  try {
89
- // `api.runtime` is permissively typed via the SDK shim; narrow to the
90
- // config surface we use (current() + mutateConfigFile). At runtime the host
91
- // provides the real `PluginRuntimeCore.config` API
92
- // (../openclaw/src/plugins/runtime/types-core.ts:145).
93
- const runtimeConfig = (
94
- api.runtime as
95
- | {
96
- config?: {
97
- current?: () => { mcp?: { servers?: Record<string, unknown> } } | undefined;
98
- mutateConfigFile?: (params: {
99
- afterWrite: { mode: "auto" };
100
- mutate: (draft: {
101
- mcp?: { servers?: Record<string, unknown> };
102
- }) => void;
103
- }) => Promise<unknown>;
104
- };
105
- }
106
- | undefined
107
- )?.config;
89
+ // `api.runtime` is now typed (`OpenClawPluginRuntime`) via the SDK shim; its
90
+ // `config` slice mirrors the real `PluginRuntimeCore.config` API
91
+ // (../openclaw/src/plugins/runtime/types-core.ts:145) `current()` reads the
92
+ // live snapshot, `mutateConfigFile(...)` writes the `mcp.servers.chorus`
93
+ // entry. No `unknown` cast needed.
94
+ const runtimeConfig = api.runtime?.config;
108
95
 
109
96
  if (!runtimeConfig?.mutateConfigFile) {
110
97
  logger.error(
@@ -42,11 +42,236 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
42
42
  jsonSchema?: Record<string, unknown>;
43
43
  };
44
44
 
45
+ // ===========================================================================
46
+ // Runtime SDK surface (`api.runtime`) — typed against the REAL ../openclaw
47
+ // source so the daemon call sites are compile-time checked rather than
48
+ // `unknown`-cast. We declare ONLY the minimal slice the plugin consumes; the
49
+ // full `PluginRuntime` graph (subagent/nodes/channel/media/...) is not pulled
50
+ // in. Every shape below was verified field-by-field against the real source
51
+ // (file:line citations inline) — NOT from memory.
52
+ //
53
+ // WHY HAND-DECLARED (not imported from `openclaw/plugin-sdk`): the real
54
+ // package exports `./plugin-sdk` (package.json exports map) but NOT the
55
+ // `./plugin-sdk/plugin-entry` subpath this plugin imports, and the `openclaw`
56
+ // build resolvable in this workspace is an older 2026.3.x that lacks both.
57
+ // Importing the real defs would also couple this separately-published package
58
+ // to the full `openclaw` type graph (llm-core, markdown-core, …) and turn the
59
+ // peer into a build-time hard dependency. The plugin must build/pack
60
+ // standalone (peerDependencies.openclaw only), so we mirror the minimal slice.
61
+ // ===========================================================================
62
+
63
+ /**
64
+ * `BlockReplyPayload` — payload for the `onBlockReply` streaming callback.
65
+ * Verified: ../openclaw/src/agents/embedded-agent-payloads.ts:1-11.
66
+ * The plugin's transcript reporter reads only `.text`.
67
+ */
68
+ export type OpenClawBlockReplyPayload = {
69
+ text?: string;
70
+ mediaUrls?: string[];
71
+ audioAsVoice?: boolean;
72
+ trustedLocalMedia?: boolean;
73
+ sensitiveMedia?: boolean;
74
+ isReasoning?: boolean;
75
+ replyToId?: string;
76
+ replyToTag?: boolean;
77
+ replyToCurrent?: boolean;
78
+ };
79
+
80
+ /**
81
+ * `ReplyPayload` (subset) — payload for the `onToolResult` streaming callback.
82
+ * Verified: ../openclaw/src/auto-reply/reply-payload.ts:7-60 (only the fields
83
+ * the plugin may read are declared; the rest of the large union is omitted via
84
+ * the index signature).
85
+ */
86
+ export type OpenClawReplyPayload = {
87
+ text?: string;
88
+ mediaUrl?: string;
89
+ mediaUrls?: string[];
90
+ isError?: boolean;
91
+ isReasoning?: boolean;
92
+ isReasoningSnapshot?: boolean;
93
+ channelData?: Record<string, unknown>;
94
+ [key: string]: unknown;
95
+ };
96
+
97
+ /**
98
+ * What initiated an embedded run.
99
+ * Verified: ../openclaw/src/agents/embedded-agent-runner/run/params.ts:32
100
+ * (`EmbeddedRunTrigger`).
101
+ */
102
+ export type OpenClawEmbeddedRunTrigger =
103
+ | "cron"
104
+ | "heartbeat"
105
+ | "manual"
106
+ | "memory"
107
+ | "overflow"
108
+ | "user";
109
+
110
+ /**
111
+ * Params for `runtime.agent.runEmbeddedAgent`.
112
+ *
113
+ * The real `RunEmbeddedAgentParams` has ~100 fields, almost all optional;
114
+ * verified against ../openclaw/src/agents/embedded-agent-runner/run/params.ts.
115
+ * We declare exactly the fields the plugin passes/uses (required ones with
116
+ * their real required/optional-ness) plus the daemon-parity fields
117
+ * (`abortSignal` + per-message streaming callbacks) the dependent tasks
118
+ * consume, and keep an index signature for the untouched remainder so the
119
+ * type stays a faithful subset rather than a closed shape.
120
+ *
121
+ * Required-field cites (params.ts): sessionId:40, sessionFile:104,
122
+ * workspaceDir:105, prompt:111, timeoutMs:155, runId:166.
123
+ * Optional-field cites: sessionKey:41, agentId:46, trigger:51, agentDir:108,
124
+ * config:109, provider:122, model:123, disableMessageTool:91,
125
+ * runTimeoutOverrideMs:165, abortSignal:167, onAssistantMessageStart:190,
126
+ * onBlockReply:191, onReasoningStream:195-199, onToolResult:201.
127
+ */
128
+ export type RunEmbeddedAgentParams = {
129
+ sessionId: string;
130
+ sessionFile: string;
131
+ workspaceDir: string;
132
+ prompt: string;
133
+ timeoutMs: number;
134
+ runId: string;
135
+ sessionKey?: string;
136
+ agentId?: string;
137
+ trigger?: OpenClawEmbeddedRunTrigger;
138
+ agentDir?: string;
139
+ // The real type is `OpenClawConfig`; the plugin passes through the opaque
140
+ // `api.config` snapshot, so `unknown` keeps it pass-through-safe.
141
+ config?: unknown;
142
+ provider?: string;
143
+ model?: string;
144
+ disableMessageTool?: boolean;
145
+ runTimeoutOverrideMs?: number;
146
+ /** Cooperative mid-run interrupt; relayed through the whole run. (params.ts:167) */
147
+ abortSignal?: AbortSignal;
148
+ /** Fires when the assistant begins a message. (params.ts:190) */
149
+ onAssistantMessageStart?: () => void | Promise<void>;
150
+ /** Fires per finalized assistant text block — the transcript source. (params.ts:191) */
151
+ onBlockReply?: (payload: OpenClawBlockReplyPayload) => void | Promise<void>;
152
+ /** Fires for reasoning/thinking deltas; NOT posted to the transcript. (params.ts:195) */
153
+ onReasoningStream?: (payload: {
154
+ text?: string;
155
+ mediaUrls?: string[];
156
+ isReasoningSnapshot?: boolean;
157
+ }) => void | Promise<void>;
158
+ /** Fires per tool result. (params.ts:201) */
159
+ onToolResult?: (payload: OpenClawReplyPayload) => void | Promise<void>;
160
+ // The real type carries many more optional fields; allow them without
161
+ // re-declaring the full graph.
162
+ [key: string]: unknown;
163
+ };
164
+
165
+ /**
166
+ * Result of `runtime.agent.runEmbeddedAgent`.
167
+ * Verified: ../openclaw/src/agents/embedded-agent-runner/types.ts:179-212
168
+ * (`EmbeddedAgentRunResult`) and its `meta: EmbeddedAgentRunMeta` at :137-177.
169
+ * The plugin reads `meta.aborted` to distinguish a user-abort from a crash.
170
+ */
171
+ export type EmbeddedAgentRunMeta = {
172
+ durationMs: number;
173
+ /** True when the run was aborted via `abortSignal`. (types.ts:140) */
174
+ aborted?: boolean;
175
+ finalAssistantVisibleText?: string;
176
+ stopReason?: string;
177
+ [key: string]: unknown;
178
+ };
179
+ export type EmbeddedAgentRunResult = {
180
+ meta: EmbeddedAgentRunMeta;
181
+ payloads?: Array<{ text?: string; isError?: boolean; isReasoning?: boolean }>;
182
+ [key: string]: unknown;
183
+ };
184
+
185
+ /**
186
+ * Session store entry (subset).
187
+ * Verified: ../openclaw/src/config/sessions/types.ts — `sessionId:254`,
188
+ * `sessionFile?:256`. Returned by `getSessionEntry`.
189
+ */
190
+ export type OpenClawSessionEntry = {
191
+ sessionId: string;
192
+ sessionFile?: string;
193
+ [key: string]: unknown;
194
+ };
195
+
196
+ /**
197
+ * The `runtime.agent` slice the plugin consumes.
198
+ * Verified: ../openclaw/src/plugins/runtime/types-core.ts:180-221
199
+ * (`PluginRuntimeCore.agent`). Each member's signature confirmed at its real
200
+ * definition site (cited per member).
201
+ */
202
+ export type OpenClawRuntimeAgent = {
203
+ /** params.ts/run.ts:458 — `(params) => Promise<EmbeddedAgentRunResult>`. */
204
+ runEmbeddedAgent: (params: RunEmbeddedAgentParams) => Promise<EmbeddedAgentRunResult>;
205
+ /** agent-scope-config.ts:195 — positional `(cfg, agentId)`, returns the agent dir. */
206
+ resolveAgentDir: (cfg: unknown, agentId: string) => string;
207
+ /** agent-scope-config.ts:170 — positional `(cfg, agentId)`, returns the workspace dir. */
208
+ resolveAgentWorkspaceDir: (cfg: unknown, agentId: string) => string;
209
+ /** timeout.ts:15 — options-object `({ cfg }) => number`. */
210
+ resolveAgentTimeoutMs: (opts: {
211
+ cfg?: unknown;
212
+ overrideMs?: number | null;
213
+ overrideSeconds?: number | null;
214
+ minMs?: number;
215
+ }) => number;
216
+ session: {
217
+ /** store.ts:210 — `({ sessionKey, agentId? }) => SessionEntry | undefined`. */
218
+ getSessionEntry: (options: {
219
+ sessionKey: string;
220
+ agentId?: string;
221
+ env?: NodeJS.ProcessEnv;
222
+ storePath?: string;
223
+ }) => OpenClawSessionEntry | undefined;
224
+ /** paths.ts:267 — `(sessionId, entry?, opts?) => string`. */
225
+ resolveSessionFilePath: (
226
+ sessionId: string,
227
+ entry?: { sessionFile?: string },
228
+ opts?: { agentId?: string; sessionsDir?: string },
229
+ ) => string;
230
+ [key: string]: unknown;
231
+ };
232
+ [key: string]: unknown;
233
+ };
234
+
235
+ /**
236
+ * The `runtime.config` slice the plugin consumes (MCP-server registration).
237
+ * Verified: ../openclaw/src/plugins/runtime/types-core.ts:145-178
238
+ * (`PluginRuntimeCore.config`). The plugin uses `current()` to read the live
239
+ * config snapshot and `mutateConfigFile(...)` to write the `mcp.servers.chorus`
240
+ * entry; the real `mutateConfigFile` is generic — `mutate(draft)` mutates a
241
+ * `DeepReadonly`-cloned draft in place. We narrow `draft`/return to the MCP
242
+ * slice the plugin touches.
243
+ */
244
+ export type OpenClawRuntimeConfig = {
245
+ current?: () => { mcp?: { servers?: Record<string, unknown> } } | undefined;
246
+ mutateConfigFile?: (params: {
247
+ afterWrite: { mode: "auto" };
248
+ mutate: (draft: { mcp?: { servers?: Record<string, unknown> } }) => void;
249
+ }) => Promise<unknown>;
250
+ [key: string]: unknown;
251
+ };
252
+
253
+ /**
254
+ * The slice of `PluginRuntime` (`api.runtime`) the plugin consumes.
255
+ * Verified: ../openclaw/src/plugins/types.ts:2600 (`runtime: PluginRuntime`)
256
+ * → ../openclaw/src/plugins/runtime/types.ts (PluginRuntime = PluginRuntimeCore
257
+ * & …) → ../openclaw/src/plugins/runtime/types-core.ts (`agent` :180,
258
+ * `config` :145). The reverse control channel is NOT part of this runtime
259
+ * surface — it arrives over the SSE stream (`type: "control"`), handled in
260
+ * `sse-listener.ts` / the control handler, not via `api.runtime`. Confirmed:
261
+ * no `control`/`connection` member exists on `PluginRuntimeCore`.
262
+ */
263
+ export type OpenClawPluginRuntime = {
264
+ agent: OpenClawRuntimeAgent;
265
+ config: OpenClawRuntimeConfig;
266
+ [key: string]: unknown;
267
+ };
268
+
45
269
  /**
46
270
  * Permissive subset of `OpenClawPluginApi` used by this plugin's entry.
47
271
  *
48
272
  * Only the members this plugin touches are typed; the index signature keeps
49
273
  * the rest of the (large) host API accessible without importing it.
274
+ * Verified: ../openclaw/src/plugins/types.ts:2584-2600 (`OpenClawPluginApi`).
50
275
  */
51
276
  export type OpenClawPluginApi = {
52
277
  registrationMode: PluginRegistrationMode;
@@ -68,7 +293,13 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
68
293
  }) => void;
69
294
  registerCommand: (command: unknown) => void;
70
295
  registerTool: (tool: unknown, opts?: unknown) => void;
71
- runtime?: unknown;
296
+ /**
297
+ * In-process runtime helpers. Typed to the minimal slice the plugin uses
298
+ * (`agent`, `config`) — no longer bare `unknown`. Optional because the
299
+ * narrow registration modes (discovery/cli-metadata) may not expose it; the
300
+ * plugin guards `api.runtime` before use.
301
+ */
302
+ runtime?: OpenClawPluginRuntime;
72
303
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
304
  [key: string]: any;
74
305
  };