@ynhcj/xiaoyi-channel 0.0.110-next โ†’ 0.0.111-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/src/bot.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface HandleXYMessageParams {
9
9
  message: A2AJsonRpcRequest;
10
10
  accountId: string;
11
11
  webSocketSessionId?: string;
12
+ /** Called after dispatch init is complete (agentTools/wrapStreamFn done). */
13
+ onInitComplete?: () => void;
12
14
  }
13
15
  /**
14
16
  * Handle an incoming A2A message.
package/dist/src/bot.js CHANGED
@@ -209,6 +209,10 @@ export async function handleXYMessage(params) {
209
209
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
210
210
  // Build message body with speaker prefix (following feishu pattern)
211
211
  let messageBody = textForAgent;
212
+ // Embed A2A taskId marker before the user query so the provider can
213
+ // extract it from messages without relying on AsyncLocalStorage.
214
+ // The provider strips this marker before sending to the model.
215
+ messageBody = `xiaoyiA2A info[taskId:${parsed.taskId},deviceType:${deviceType}] ${messageBody}`;
212
216
  // Add speaker prefix for clarity
213
217
  const speaker = parsed.sessionId;
214
218
  messageBody = `${speaker}: ${messageBody}`;
@@ -289,34 +293,42 @@ export async function handleXYMessage(params) {
289
293
  unregisterSession(route.sessionKey);
290
294
  log(`[BOT] โœ… Cleanup completed`);
291
295
  },
292
- run: () =>
293
- // ๐Ÿ” Use AsyncLocalStorage to provide session context to tools
294
- runWithSessionContext(sessionContext, async () => {
295
- log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
296
- log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
297
- log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
298
- log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
299
- log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
300
- log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
301
- try {
302
- const result = await core.channel.reply.dispatchReplyFromConfig({
303
- ctx: ctxPayload,
304
- cfg,
305
- dispatcher,
306
- replyOptions,
307
- });
308
- log(`[BOT-DISPATCH] โœ… dispatchReplyFromConfig returned`);
309
- log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
310
- return result;
311
- }
312
- catch (dispatchErr) {
313
- error(`[BOT-DISPATCH] โŒ dispatchReplyFromConfig threw`);
314
- error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
315
- error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
316
- error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
317
- throw dispatchErr;
318
- }
319
- }),
296
+ run: () => {
297
+ // ๐Ÿ” Use AsyncLocalStorage to provide session context to tools.
298
+ // runWithSessionContext returns after the sync part of dispatch
299
+ // (including agentTools + wrapStreamFn) has executed, so we
300
+ // signal init complete to release the global dispatch gate
301
+ // for the next session.
302
+ const dispatchPromise = runWithSessionContext(sessionContext, async () => {
303
+ log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
304
+ log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
305
+ log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
306
+ log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
307
+ log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
308
+ log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
309
+ try {
310
+ const result = await core.channel.reply.dispatchReplyFromConfig({
311
+ ctx: ctxPayload,
312
+ cfg,
313
+ dispatcher,
314
+ replyOptions,
315
+ });
316
+ log(`[BOT-DISPATCH] โœ… dispatchReplyFromConfig returned`);
317
+ log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
318
+ return result;
319
+ }
320
+ catch (dispatchErr) {
321
+ error(`[BOT-DISPATCH] โŒ dispatchReplyFromConfig threw`);
322
+ error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
323
+ error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
324
+ error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
325
+ throw dispatchErr;
326
+ }
327
+ });
328
+ // Signal init complete โ€” sync part (agentTools, wrapStreamFn) is done
329
+ params.onInitComplete?.();
330
+ return dispatchPromise;
331
+ },
320
332
  });
321
333
  log(`[BOT] โœ… Dispatcher completed for session: ${parsed.sessionId}`);
322
334
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
@@ -68,6 +68,12 @@ export async function monitorXYProvider(opts = {}) {
68
68
  const activeMessages = new Set();
69
69
  // Create session queue for ordered message processing
70
70
  const enqueue = createSessionQueue();
71
+ // Global gate that serializes dispatch initialization across sessions.
72
+ // When a new session starts dispatching, it acquires this gate and holds it
73
+ // until agent setup (agentTools + wrapStreamFn) is complete, then releases it.
74
+ // This prevents lastRegisteredKey races when multiple sessions initialize
75
+ // concurrently.
76
+ let globalDispatchInitGate = Promise.resolve();
71
77
  // Health check interval
72
78
  let healthCheckInterval = null;
73
79
  return new Promise((resolve, reject) => {
@@ -83,6 +89,11 @@ export async function monitorXYProvider(opts = {}) {
83
89
  }
84
90
  activeMessages.add(messageKey);
85
91
  const task = async () => {
92
+ // Wait for the previous session's init to complete (global gate),
93
+ // then acquire the gate for this session's init.
94
+ await globalDispatchInitGate;
95
+ let releaseGate;
96
+ globalDispatchInitGate = new Promise((r) => { releaseGate = r; });
86
97
  try {
87
98
  await handleXYMessage({
88
99
  cfg,
@@ -90,10 +101,12 @@ export async function monitorXYProvider(opts = {}) {
90
101
  message,
91
102
  accountId, // โœ… Pass accountId ("default")
92
103
  webSocketSessionId: sessionId, // โœ… ไผ ้€’ WebSocket ๅฑ‚็บง็š„ sessionId
104
+ onInitComplete: () => releaseGate(),
93
105
  });
94
106
  }
95
107
  catch (err) {
96
108
  // โœ… Only log error, don't re-throw to prevent gateway restart
109
+ releaseGate();
97
110
  error(`XY gateway: error handling message from ${serverId}: ${String(err)}`);
98
111
  }
99
112
  finally {
@@ -8,8 +8,7 @@
8
8
  // models.providers.xiaoyiprovider.api = "openai-completions"
9
9
  // models.providers.xiaoyiprovider.models = [...]
10
10
  import { createHash } from "crypto";
11
- import { activeSessions, asyncLocalStorage, getCurrentSessionContext } from "./tools/session-manager.js";
12
- import { getCurrentTaskId } from "./task-manager.js";
11
+ import { getCurrentSessionContext } from "./tools/session-manager.js";
13
12
  import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
14
13
  // โ”€โ”€ Retry config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
14
  const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000, 60_000];
@@ -443,23 +442,48 @@ export const xiaoyiProvider = {
443
442
  const underlying = ctx.streamFn;
444
443
  if (!underlying)
445
444
  return underlying;
446
- // Capture A2A sessionId at agent setup time for multi-session isolation.
447
- // openclaw calls wrapStreamFn per-agent (per session), so this runs inside
448
- // the correct runWithSessionContext() ALS scope. When multiple sessions are
449
- // active concurrently, getCurrentSessionContext() may later return the WRONG
450
- // session (lastRegisteredKey fallback). The captured sessionId lets us
451
- // bypass that fallback and look up the correct taskId directly from
452
- // task-manager.
453
- const alsStore = asyncLocalStorage.getStore();
454
- const sessionCtx = getCurrentSessionContext();
455
- const capturedA2ASessionId = sessionCtx?.sessionId ?? null;
456
- console.log(`[xiaoyiprovider] wrapStreamFn โ€” ALS: ${alsStore ? "HIT" : "MISS"}, ` +
457
- `capturedSessionId=${capturedA2ASessionId ?? "null"}, ` +
458
- `taskId=${sessionCtx?.taskId ?? "null"}, ` +
459
- `activeSessionsCount=${activeSessions.size}`);
445
+ // โ”€โ”€ Regex to extract A2A taskId/deviceType from user messages โ”€โ”€
446
+ // bot.ts embeds: xiaoyiA2A info[taskId:<id>,deviceType:<type>]
447
+ const A2A_MARKER_RE = /xiaoyiA2A info\[taskId:([^\],]+)(?:,deviceType:([^\]]+))?\]\s*/;
460
448
  return async (model, context, options) => {
461
- // ๆฏๆฌก่ฏทๆฑ‚ๆ—ถไปŽ ctx.extraParams ๅŠจๆ€่ฏปๅ– header
462
449
  const dynamicHeaders = {};
450
+ // โ”€โ”€ Extract A2A taskId/deviceType from user messages โ”€โ”€
451
+ // Scan from last user message backwards; strip the marker from all.
452
+ let resolvedTaskId = null;
453
+ let resolvedDeviceType = null;
454
+ if (context.messages) {
455
+ for (let i = context.messages.length - 1; i >= 0; i--) {
456
+ const msg = context.messages[i];
457
+ if (msg.role !== "user" || !msg.content)
458
+ continue;
459
+ const extract = (text) => {
460
+ const match = text.match(A2A_MARKER_RE);
461
+ if (match) {
462
+ if (!resolvedTaskId)
463
+ resolvedTaskId = match[1] ?? null;
464
+ if (!resolvedDeviceType)
465
+ resolvedDeviceType = match[2] ?? null;
466
+ return text.replace(A2A_MARKER_RE, "");
467
+ }
468
+ return null;
469
+ };
470
+ if (typeof msg.content === "string") {
471
+ const stripped = extract(msg.content);
472
+ if (stripped !== null)
473
+ msg.content = stripped;
474
+ }
475
+ else if (Array.isArray(msg.content)) {
476
+ for (const block of msg.content) {
477
+ if (block.type === "text" && typeof block.text === "string") {
478
+ const stripped = extract(block.text);
479
+ if (stripped !== null)
480
+ block.text = stripped;
481
+ }
482
+ }
483
+ }
484
+ }
485
+ }
486
+ // โ”€โ”€ Build dynamic headers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
463
487
  if (ctx.extraParams) {
464
488
  const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
465
489
  if (typeof fallbackPrefix === "string") {
@@ -477,58 +501,46 @@ export const xiaoyiProvider = {
477
501
  dynamicHeaders["x-cron-flag"] = "begin";
478
502
  }
479
503
  }
480
- else {
481
- // Session mode: resolve taskId for the correct session.
482
- //
483
- // Priority:
484
- // 1. capturedA2ASessionId โ†’ getCurrentTaskId() (most reliable,
485
- // bypasses lastRegisteredKey fallback)
486
- // 2. getCurrentSessionContext()?.taskId (works when ALS
487
- // is intact)
488
- // 3. ctx.extraParams cached values (last resort,
489
- // may be stale / from wrong session)
490
- let resolvedTaskId = null;
491
- if (capturedA2ASessionId) {
492
- resolvedTaskId = getCurrentTaskId(capturedA2ASessionId);
493
- }
494
- if (!resolvedTaskId) {
495
- resolvedTaskId = getCurrentSessionContext()?.taskId ?? null;
496
- }
497
- const traceId = resolvedTaskId ?? ctx.extraParams[HEADER_TRACE_ID];
498
- const sessionId = resolvedTaskId?.split("&")[0]
499
- ?? ctx.extraParams[HEADER_SESSION_ID];
500
- const interactionId = resolvedTaskId?.split("&")[1]
501
- ?? ctx.extraParams[HEADER_INTERACTION_ID]
502
- ?? "";
503
- if (typeof traceId === "string") {
504
- const isCron = isCronTriggered(context.messages);
505
- dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
506
- if (isCron) {
507
- const cronTitle = extractCronTitle(context.messages);
508
- if (cronTitle)
509
- dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
510
- if (context.messages?.length === 1)
511
- dynamicHeaders["x-cron-flag"] = "begin";
512
- }
504
+ else if (resolvedTaskId) {
505
+ // Session mode: taskId extracted from user message marker
506
+ const traceId = resolvedTaskId;
507
+ const sessionId = traceId.split("&")[0];
508
+ const interactionId = traceId.split("&")[1] ?? "";
509
+ const isCron = isCronTriggered(context.messages);
510
+ dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}_${Date.now()}` : traceId;
511
+ if (isCron) {
512
+ const cronTitle = extractCronTitle(context.messages);
513
+ if (cronTitle)
514
+ dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
515
+ if (context.messages?.length === 1)
516
+ dynamicHeaders["x-cron-flag"] = "begin";
513
517
  }
514
518
  if (typeof sessionId === "string")
515
519
  dynamicHeaders[HEADER_SESSION_ID] = sessionId;
516
520
  if (typeof interactionId === "string")
517
521
  dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
518
522
  }
523
+ else {
524
+ // No marker found โ€“ fall back to extraParams cached values
525
+ const traceId = ctx.extraParams[HEADER_TRACE_ID];
526
+ const sessionId = ctx.extraParams[HEADER_SESSION_ID];
527
+ const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
528
+ if (typeof traceId === "string")
529
+ dynamicHeaders[HEADER_TRACE_ID] = traceId;
530
+ if (typeof sessionId === "string")
531
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
532
+ if (typeof interactionId === "string")
533
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
534
+ }
519
535
  }
520
536
  // ่ฎฐๅฝ•่พ“ๅ…ฅ
521
537
  console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
522
538
  if (context.systemPrompt) {
523
539
  console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
524
540
  }
525
- // Prefer deviceType from extraParams (set by prepareExtraParams).
526
- // Fall back to getCurrentSessionContext() because OpenClaw caches
527
- // resolvePreparedExtraParams by provider/modelId โ€“ the cache key does
528
- // not include session-specific data, so deviceType may be missing
529
- // from the cached extraParams even when a session is active.
541
+ // deviceType: prefer marker-extracted value, fall back to extraParams
530
542
  const extraParamsDeviceType = ctx.extraParams?.[DEVICE_TYPE_KEY] || undefined;
531
- const deviceType = extraParamsDeviceType ?? getCurrentSessionContext()?.deviceType;
543
+ const deviceType = resolvedDeviceType || extraParamsDeviceType || undefined;
532
544
  // ๅœจๅ‘้€็ป™ๆจกๅž‹ๅ‰๏ผŒไผ˜ๅŒ– systemPrompt ็ป“ๆž„
533
545
  if (context.systemPrompt) {
534
546
  let sp = context.systemPrompt;
@@ -561,7 +573,7 @@ export const xiaoyiProvider = {
561
573
  const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
562
574
  console.log(`[selfEvolution] selfEvolution flag: ${selfEvolutionEnabled}`);
563
575
  context.systemPrompt = applySelfEvolutionPrompt(context.systemPrompt, selfEvolutionEnabled);
564
- // Append device context to systemPrompt (using pre-captured deviceType from prepareExtraParams)
576
+ // Append device context to systemPrompt
565
577
  if (deviceType) {
566
578
  const displayDevice = (deviceType === "2in1") ? "้ธฟ่’™PC" : deviceType;
567
579
  const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
@@ -598,19 +610,4 @@ export const xiaoyiProvider = {
598
610
  return createRetryingStream(makeStream, cronJob);
599
611
  };
600
612
  },
601
- /**
602
- * Diagnostic-only: log ctx.sessionId format on every transport turn.
603
- * Goal: confirm whether sessionId = sessionKey (so we can use it to
604
- * look up A2A context directly from activeSessions, bypassing ALS).
605
- */
606
- resolveTransportTurnState: (ctx) => {
607
- console.log(`[xiaoyiprovider] resolveTransportTurnState โ€” ` +
608
- `sessionId=${ctx.sessionId ?? "null"}, ` +
609
- `turnId=${ctx.turnId}, ` +
610
- `transport=${ctx.transport}, ` +
611
- `attempt=${ctx.attempt}, ` +
612
- `provider=${ctx.provider}, ` +
613
- `modelId=${ctx.modelId}`);
614
- return null;
615
- },
616
613
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.110-next",
3
+ "version": "0.0.111-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",