@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.
Files changed (62) 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 +39 -8
  42. package/skills/code-reviewer/SKILL.md +127 -0
  43. package/skills/develop/SKILL.md +3 -1
  44. package/skills/idea/SKILL.md +1 -1
  45. package/skills/openspec-aware/SKILL.md +1 -1
  46. package/skills/proposal/SKILL.md +1 -1
  47. package/skills/proposal-reviewer/SKILL.md +1 -1
  48. package/skills/quick-dev/SKILL.md +1 -1
  49. package/skills/review/SKILL.md +5 -5
  50. package/skills/task-reviewer/SKILL.md +1 -1
  51. package/skills/yolo/SKILL.md +26 -1
  52. package/src/connection-state.ts +66 -0
  53. package/src/control-handler.ts +219 -0
  54. package/src/daemon-client.ts +622 -0
  55. package/src/daemon-rest-client.ts +312 -0
  56. package/src/event-router.ts +103 -33
  57. package/src/index.ts +113 -8
  58. package/src/lineage.ts +157 -0
  59. package/src/mcp-registration.ts +6 -19
  60. package/src/openclaw-sdk.d.ts +232 -1
  61. package/src/sse-listener.ts +117 -5
  62. 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
+ }