@ynhcj/xiaoyi-channel 0.0.203-next → 0.0.204-next

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/index.js CHANGED
@@ -12,6 +12,7 @@ import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
12
12
  import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
13
13
  import { registerCLIHook } from "./src/tools/hmos-cli.js";
14
14
  import { writeSkillUsage } from "./src/utils/skills-logger.js";
15
+ import { getSteerQueue } from "./src/steer-queue.js";
15
16
  /**
16
17
  * Parse a file path string to detect if it refers to a SKILL.md file within
17
18
  * a skills directory. Returns the skill name (parent directory) if so.
@@ -218,6 +219,30 @@ function registerFullHooks(api) {
218
219
  const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
219
220
  api.on("before_prompt_build", beforePromptBuildHandler);
220
221
  registerSelfEvolutionToolResultNudge(api);
222
+ // STEER INJECTION HOOK: reads pending steer messages queued by bot.ts
223
+ // and injects them as appendContext on the next agent turn.
224
+ // This bypasses dispatchReplyFromConfig's admitReplyTurn blocking.
225
+ //
226
+ // appendContext places the steer after the current prompt context (which
227
+ // includes the original user query + tool results), so the model sees:
228
+ // [original context]\n\n用户追加诉求:<steer text>
229
+ //
230
+ // Key is deleted BEFORE splice to avoid a race: if bot.ts pushes a new
231
+ // message between splice and delete, it would be lost when delete removes
232
+ // the (now-repopulated) entry. With delete-first, bot.ts always creates a
233
+ // fresh entry for new messages.
234
+ api.on("agent_turn_prepare", async (_event, ctx) => {
235
+ const sessionKey = ctx.sessionKey;
236
+ if (!sessionKey)
237
+ return;
238
+ const queue = getSteerQueue();
239
+ const pending = queue.get(sessionKey);
240
+ if (!pending || pending.length === 0)
241
+ return;
242
+ queue.delete(sessionKey); // delete first to prevent race
243
+ const text = pending.splice(0).join("\n\n");
244
+ return { appendContext: `\n用户追加诉求:${text}` };
245
+ });
221
246
  }
222
247
  const pluginEntry = definePluginEntry({
223
248
  id: "xiaoyi-channel",
package/dist/src/bot.js CHANGED
@@ -15,6 +15,7 @@ import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
15
15
  import { saveRuntimeInfo } from "./utils/runtime-manager.js";
16
16
  import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
17
17
  import { setCsplSteerContext } from "./cspl/steer-context.js";
18
+ import { getSteerQueue } from "./steer-queue.js";
18
19
  import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
19
20
  import { logger } from "./utils/logger.js";
20
21
  /**
@@ -339,6 +340,36 @@ export async function handleXYMessage(params) {
339
340
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
340
341
  ...mediaPayload,
341
342
  });
343
+ // 🔑 Steer bypass: queue the message for agent_turn_prepare hook injection
344
+ // instead of going through dispatchReplyFromConfig which blocks on admitReplyTurn
345
+ // waiting for the active reply operation to complete.
346
+ //
347
+ // This covers BOTH:
348
+ // 1. Normal A2A steer (isUpdate && !skipReg) — user sends a message during
349
+ // an active agent run
350
+ // 2. CSPL/self-evolution steer (isUpdate && skipReg) — hook-triggered
351
+ // injection via tryInjectSteer. Without the bypass, CSPL steer would
352
+ // deadlock: it runs inside the agent turn, so admitReplyTurn's
353
+ // waitForIdle waits for the reply operation to finish, but the
354
+ // operation is owned by the very same agent turn.
355
+ //
356
+ // The hook fires before every agent prompt build, so the steer text reaches
357
+ // the model on the next turn without blocking.
358
+ if (isUpdate) {
359
+ const steerQueue = getSteerQueue();
360
+ const pending = steerQueue.get(route.sessionKey) ?? [];
361
+ pending.push(textForAgent);
362
+ steerQueue.set(route.sessionKey, pending);
363
+ log.log(`[BOT] Steer bypass: queued for agent_turn_prepare hook, sessionKey=${route.sessionKey}, queueDepth=${pending.length}, skipReg=${skipReg}`);
364
+ // Only decrement refCount when registerTaskId was called (non-skipReg).
365
+ // skipReg callers (CSPL/self-evolution) don't increment refCount.
366
+ if (!skipReg) {
367
+ decrementTaskIdRef(parsed.sessionId);
368
+ }
369
+ // Release the global dispatch init gate — steer doesn't need it.
370
+ params.onInitComplete?.();
371
+ return;
372
+ }
342
373
  // 🔑 For steer messages, pre-set steered=true so the dispatcher skips final
343
374
  // response and cleanup — the first message's dispatcher handles those.
344
375
  const steerState = { steered: isUpdate };
@@ -0,0 +1 @@
1
+ export declare function getSteerQueue(): Map<string, string[]>;
@@ -0,0 +1,9 @@
1
+ // Shared steer injection queue — bot.ts writes, index.ts agent_turn_prepare hook reads.
2
+ // Uses globalThis to survive module deduplication (same pattern as dispatcherUpdaters).
3
+ const _g = globalThis;
4
+ if (!_g.__xySteerInjectionQueue) {
5
+ _g.__xySteerInjectionQueue = new Map();
6
+ }
7
+ export function getSteerQueue() {
8
+ return _g.__xySteerInjectionQueue;
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.203-next",
3
+ "version": "0.0.204-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",