@chorus-aidlc/chorus-openclaw-plugin 0.10.0 → 0.11.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/dist/connection-state.d.ts +35 -0
- package/dist/connection-state.d.ts.map +1 -0
- package/dist/connection-state.js +52 -0
- package/dist/connection-state.js.map +1 -0
- package/dist/control-handler.d.ts +73 -0
- package/dist/control-handler.d.ts.map +1 -0
- package/dist/control-handler.js +135 -0
- package/dist/control-handler.js.map +1 -0
- package/dist/daemon-client.d.ts +203 -0
- package/dist/daemon-client.d.ts.map +1 -0
- package/dist/daemon-client.js +469 -0
- package/dist/daemon-client.js.map +1 -0
- package/dist/daemon-rest-client.d.ts +86 -0
- package/dist/daemon-rest-client.d.ts.map +1 -0
- package/dist/daemon-rest-client.js +196 -0
- package/dist/daemon-rest-client.js.map +1 -0
- package/dist/event-router.d.ts +31 -6
- package/dist/event-router.d.ts.map +1 -1
- package/dist/event-router.js +58 -27
- package/dist/event-router.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -7
- package/dist/index.js.map +1 -1
- package/dist/lineage.d.ts +44 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +116 -0
- package/dist/lineage.js.map +1 -0
- package/dist/mcp-registration.d.ts.map +1 -1
- package/dist/mcp-registration.js +5 -4
- package/dist/mcp-registration.js.map +1 -1
- package/dist/sse-listener.d.ts +34 -0
- package/dist/sse-listener.d.ts.map +1 -1
- package/dist/sse-listener.js +78 -4
- package/dist/sse-listener.js.map +1 -1
- package/dist/wake.d.ts +20 -0
- package/dist/wake.d.ts.map +1 -1
- package/dist/wake.js +56 -0
- package/dist/wake.js.map +1 -1
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/chorus/SKILL.md +39 -8
- package/skills/code-reviewer/SKILL.md +127 -0
- package/skills/develop/SKILL.md +3 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/openspec-aware/SKILL.md +1 -1
- package/skills/proposal/SKILL.md +1 -1
- package/skills/proposal-reviewer/SKILL.md +1 -1
- package/skills/quick-dev/SKILL.md +1 -1
- package/skills/review/SKILL.md +5 -5
- package/skills/task-reviewer/SKILL.md +1 -1
- package/skills/yolo/SKILL.md +26 -1
- package/src/connection-state.ts +66 -0
- package/src/control-handler.ts +219 -0
- package/src/daemon-client.ts +622 -0
- package/src/daemon-rest-client.ts +312 -0
- package/src/event-router.ts +103 -33
- package/src/index.ts +113 -8
- package/src/lineage.ts +157 -0
- package/src/mcp-registration.ts +6 -19
- package/src/openclaw-sdk.d.ts +232 -1
- package/src/sse-listener.ts +117 -5
- package/src/wake.ts +69 -26
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// packages/openclaw-plugin/src/control-handler.ts
|
|
2
|
+
// OpenClaw-host handler for reverse (server→daemon) control commands — the
|
|
3
|
+
// in-process analog of cli/control-handler.mjs. The SSE listener forks a
|
|
4
|
+
// `type:"control"` SSE event here (NOT to the wake router — see sse-listener.ts),
|
|
5
|
+
// so a control command can NEVER spawn a new embedded-agent run for the control
|
|
6
|
+
// event itself. This module is the ROUTING + DOUBLE-CHECK layer only; the actual
|
|
7
|
+
// abort / re-dispatch / pending-turns-sweep BEHAVIORS are injected (filled by the
|
|
8
|
+
// openclaw-daemon-client task, T4).
|
|
9
|
+
//
|
|
10
|
+
// The single safety property this module enforces is the DOUBLE-CHECK, identical
|
|
11
|
+
// to the CLI host (cli/control-handler.mjs): act ONLY when —
|
|
12
|
+
// Check 1 (every command): event.targetConnectionUuid === this plugin's OWN
|
|
13
|
+
// registered connectionUuid (from connection-state). On mismatch — a
|
|
14
|
+
// stale/recycled uuid, another connection's command, or a handshake not
|
|
15
|
+
// yet complete — IGNORE + LOG. This is what stops a recycled connection
|
|
16
|
+
// uuid from aborting the wrong run (Tech Design "Risks": mis-kill after
|
|
17
|
+
// a reconnect).
|
|
18
|
+
// Check 2 (interrupt only): the injected `isEntityRunning(entityType, entityUuid)`
|
|
19
|
+
// predicate returns true — i.e. this plugin currently holds a running
|
|
20
|
+
// embedded-agent run for that entity. (In the CLI host this is "the
|
|
21
|
+
// execution registry holds a running child"; here it is an injected
|
|
22
|
+
// predicate the daemon client backs with its AbortController registry.)
|
|
23
|
+
// On mismatch — only queued, never ran, or already finished — IGNORE + LOG.
|
|
24
|
+
//
|
|
25
|
+
// `resume` and `deliver_turn` carry NO running-entity requirement (mirroring the
|
|
26
|
+
// CLI host): the run for a resumed entity is already gone, and a `deliver_turn`
|
|
27
|
+
// resolves its turn by uuid from the persisted turn table. They only require
|
|
28
|
+
// Check 1.
|
|
29
|
+
//
|
|
30
|
+
// The three verbs route to injected behavior hooks (the seams T4 plugs into):
|
|
31
|
+
// - interrupt → onInterrupt(entityType, entityUuid)
|
|
32
|
+
// - resume → onResume(entityType, entityUuid)
|
|
33
|
+
// - deliver_turn → onDeliverTurn(turnUuid?) (turnUuid present = precise single
|
|
34
|
+
// turn; absent = older server, full connection sweep fallback)
|
|
35
|
+
//
|
|
36
|
+
// Non-throwing: a control event must NEVER crash the SSE loop, so the whole body
|
|
37
|
+
// is wrapped in a try/catch backstop and the hooks' own throws are caught + logged
|
|
38
|
+
// (memory: no-silent-errors — every ignore/failure is logged, never swallowed).
|
|
39
|
+
|
|
40
|
+
import type { ConnectionStateReader } from "./connection-state.js";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The control event shape the server publishes on `control:{connectionUuid}` and
|
|
44
|
+
* forwards verbatim as a `type:"control"` SSE data event. Mirrors the server's
|
|
45
|
+
* `ControlEvent` (src/lib/event-bus.ts). `entityType`/`entityUuid` are present for
|
|
46
|
+
* `interrupt`/`resume`; `turnUuid` is present only for `deliver_turn`.
|
|
47
|
+
*/
|
|
48
|
+
export interface ControlEvent {
|
|
49
|
+
// `string` (not the `"control"` literal) because this is the raw forked SSE
|
|
50
|
+
// event — the handler re-validates `type === "control"` and the command enum at
|
|
51
|
+
// runtime before acting. Matches the listener's `SseControlEvent` shape so the
|
|
52
|
+
// listener can pass its parsed event straight through.
|
|
53
|
+
type: string;
|
|
54
|
+
command?: string;
|
|
55
|
+
targetConnectionUuid?: string;
|
|
56
|
+
entityType?: string;
|
|
57
|
+
entityUuid?: string;
|
|
58
|
+
turnUuid?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Injected behavior hooks. These are the seams the openclaw-daemon-client task
|
|
63
|
+
* (T4) fills with the real abort / re-dispatch / pending-turns-sweep. This task
|
|
64
|
+
* (T3) only ROUTES verified commands to them — it does not implement the
|
|
65
|
+
* behaviors. All hooks are optional so a partially-wired host (or a test) can
|
|
66
|
+
* verify routing without every behavior present.
|
|
67
|
+
*/
|
|
68
|
+
export interface ControlBehaviorHooks {
|
|
69
|
+
/**
|
|
70
|
+
* Whether this plugin currently holds a RUNNING embedded-agent run for the
|
|
71
|
+
* entity — the OpenClaw analog of the CLI host's "execution registry holds a
|
|
72
|
+
* running child" check. Gates `interrupt` (Check 2). Defaults to "nothing is
|
|
73
|
+
* running" (always false) when absent, so an interrupt with no registry is a
|
|
74
|
+
* safe no-op rather than a blind abort.
|
|
75
|
+
*/
|
|
76
|
+
isEntityRunning?: (entityType: string, entityUuid: string) => boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Abort the matching in-flight run for the entity (true mid-run stop via the
|
|
80
|
+
* run's AbortController). Invoked only after BOTH checks pass. (T4)
|
|
81
|
+
*/
|
|
82
|
+
onInterrupt?: (entityType: string, entityUuid: string) => void;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Re-dispatch the entity's wake to continue the same session — the synthetic
|
|
86
|
+
* "resume" wake. Invoked after Check 1 only (no running-entity requirement —
|
|
87
|
+
* the run is gone). (T4)
|
|
88
|
+
*/
|
|
89
|
+
onResume?: (entityType: string, entityUuid: string) => void;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run the connection-scoped pending turn(s). With a `turnUuid` (origin-only
|
|
93
|
+
* live delivery) run PRECISELY that one turn; without one (older server /
|
|
94
|
+
* reconnect) sweep all pending turns. Invoked after Check 1 only. (T4)
|
|
95
|
+
*/
|
|
96
|
+
onDeliverTurn?: (turnUuid?: string) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ControlHandlerOptions {
|
|
100
|
+
/** Live connection identity (connection-state). Provides Check 1's "my uuid". */
|
|
101
|
+
connectionState: ConnectionStateReader;
|
|
102
|
+
/** Injected behavior hooks (filled by T4). */
|
|
103
|
+
hooks: ControlBehaviorHooks;
|
|
104
|
+
logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build the `onControl(event)` callback the SSE listener invokes for a
|
|
109
|
+
* `type:"control"` event. The returned function is synchronous and non-throwing:
|
|
110
|
+
* it performs the double-check and routes to the (injected) behavior hook,
|
|
111
|
+
* returning immediately so the SSE consumer never blocks.
|
|
112
|
+
*
|
|
113
|
+
* A control event NEVER enqueues a wake or spawns a run for the control event
|
|
114
|
+
* itself — that structural guarantee is owned by the SSE listener fork (the
|
|
115
|
+
* control event never reaches `onEvent` / the wake router) and reinforced here
|
|
116
|
+
* by routing ONLY to the abort/resume/deliver hooks, never to the wake path.
|
|
117
|
+
*/
|
|
118
|
+
export function createControlHandler(opts: ControlHandlerOptions): (event: ControlEvent) => void {
|
|
119
|
+
const { connectionState, hooks, logger } = opts;
|
|
120
|
+
const isEntityRunning = hooks.isEntityRunning ?? (() => false);
|
|
121
|
+
|
|
122
|
+
return function onControl(event: ControlEvent): void {
|
|
123
|
+
try {
|
|
124
|
+
if (!event || event.type !== "control") {
|
|
125
|
+
logger.warn(`[Chorus] control-handler received non-control event; ignoring`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
event.command !== "interrupt" &&
|
|
131
|
+
event.command !== "resume" &&
|
|
132
|
+
event.command !== "deliver_turn"
|
|
133
|
+
) {
|
|
134
|
+
// Forward-compatible: the wire enum may grow. Unknown command → ignore + log.
|
|
135
|
+
logger.warn(`[Chorus] control command "${event.command}" not supported; ignoring`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { command, targetConnectionUuid, entityType, entityUuid } = event;
|
|
140
|
+
|
|
141
|
+
// --- Check 1: connection-uuid match (applies to EVERY command) ---
|
|
142
|
+
const myConnectionUuid = connectionState.getConnectionUuid();
|
|
143
|
+
if (!myConnectionUuid || targetConnectionUuid !== myConnectionUuid) {
|
|
144
|
+
// Not ours (stale/recycled uuid, another connection, or handshake not yet
|
|
145
|
+
// complete). Ignore — never abort/resume/deliver for a command that isn't ours.
|
|
146
|
+
logger.info(
|
|
147
|
+
`[Chorus] control: ignoring ${command} for connection ${targetConnectionUuid} ` +
|
|
148
|
+
`(this plugin is ${myConnectionUuid ?? "<unregistered>"})`,
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- deliver_turn: origin-only live delivery. No entity on the wire (the
|
|
154
|
+
// turn is read by uuid) and NO running-entity requirement (mirrors
|
|
155
|
+
// resume). With a turnUuid, run PRECISELY that turn; without one (older
|
|
156
|
+
// server), the hook falls back to a full connection sweep. ---
|
|
157
|
+
if (command === "deliver_turn") {
|
|
158
|
+
const turnUuid = typeof event.turnUuid === "string" ? event.turnUuid : undefined;
|
|
159
|
+
logger.info(
|
|
160
|
+
`[Chorus] control: deliver_turn for connection ${targetConnectionUuid} ` +
|
|
161
|
+
(turnUuid ? `(turn ${turnUuid})` : "(no turnUuid — full sweep fallback)"),
|
|
162
|
+
);
|
|
163
|
+
try {
|
|
164
|
+
hooks.onDeliverTurn?.(turnUuid);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
logger.warn(`[Chorus] control: deliver_turn hook failed: ${err}`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// interrupt / resume both target a specific entity — require both fields.
|
|
172
|
+
if (typeof entityType !== "string" || typeof entityUuid !== "string") {
|
|
173
|
+
logger.warn(`[Chorus] control: ${command} missing entityType/entityUuid; ignoring`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- resume: re-dispatch the wake for this entity. No running-entity check —
|
|
178
|
+
// the run is gone (it was interrupted); the wake path re-enters the same
|
|
179
|
+
// session. Check 1 already passed. ---
|
|
180
|
+
if (command === "resume") {
|
|
181
|
+
logger.info(`[Chorus] control: resuming ${entityType}:${entityUuid} (re-dispatch wake)`);
|
|
182
|
+
try {
|
|
183
|
+
hooks.onResume?.(entityType, entityUuid);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
logger.warn(
|
|
186
|
+
`[Chorus] control: resume re-dispatch failed for ${entityType}:${entityUuid}: ${err}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- interrupt path: Check 2 — this plugin must hold a RUNNING run for the
|
|
193
|
+
// entity. The injected predicate is backed by the daemon client's
|
|
194
|
+
// AbortController registry (T4). ---
|
|
195
|
+
if (!isEntityRunning(entityType, entityUuid)) {
|
|
196
|
+
// Either we never ran this entity, it's only queued, or the run already
|
|
197
|
+
// finished (race: interrupt arrived after completion). Safe no-op.
|
|
198
|
+
logger.info(
|
|
199
|
+
`[Chorus] control: no running embedded-agent run for ${entityType}:${entityUuid} ` +
|
|
200
|
+
`on this plugin; ignoring interrupt`,
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Both checks passed: route to the abort hook (true mid-run stop). ---
|
|
206
|
+
logger.info(`[Chorus] control: interrupting running run for ${entityType}:${entityUuid}`);
|
|
207
|
+
try {
|
|
208
|
+
hooks.onInterrupt?.(entityType, entityUuid);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
logger.warn(
|
|
211
|
+
`[Chorus] control: interrupt hook failed for ${entityType}:${entityUuid}: ${err}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
// Absolute backstop — a control event must never crash the SSE loop.
|
|
216
|
+
logger.error(`[Chorus] control-handler unexpected error: ${err}`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|