@oyasmi/pipiclaw 0.6.5 → 0.6.6-beta.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.
@@ -24,6 +24,7 @@ export declare class ChannelRunner implements AgentRunner {
24
24
  private activeModel;
25
25
  private currentSkills;
26
26
  private firstTurnMemoryBootstrapPending;
27
+ private acceptingBusyMessages;
27
28
  private runState;
28
29
  constructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string);
29
30
  run(ctx: DingTalkContext, store: ChannelStore): Promise<{
@@ -53,6 +53,7 @@ function asSdkSettingsManager(manager) {
53
53
  export class ChannelRunner {
54
54
  constructor(sandboxConfig, channelId, channelDir) {
55
55
  this.firstTurnMemoryBootstrapPending = true;
56
+ this.acceptingBusyMessages = false;
56
57
  // --- Per run ---
57
58
  this.runState = createEmptyRunState();
58
59
  this.sandboxConfig = sandboxConfig;
@@ -169,8 +170,10 @@ export class ChannelRunner {
169
170
  // === Public API ===
170
171
  async run(ctx, store) {
171
172
  this.resetRunState(ctx, store);
173
+ this.acceptingBusyMessages = true;
172
174
  const runQueue = createRunQueue(ctx);
173
175
  this.runState.queue = runQueue.queue;
176
+ let promptSubmitted = false;
174
177
  try {
175
178
  await this.ensureSessionReady();
176
179
  this.memoryLifecycle.noteUserTurnStarted();
@@ -225,6 +228,7 @@ export class ChannelRunner {
225
228
  }
226
229
  await this.sessionResourceGate.runPrompt(async () => {
227
230
  await this.session.prompt(promptText);
231
+ promptSubmitted = true;
228
232
  });
229
233
  }
230
234
  catch (err) {
@@ -233,6 +237,14 @@ export class ChannelRunner {
233
237
  log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
234
238
  }
235
239
  finally {
240
+ this.acceptingBusyMessages = false;
241
+ if (!promptSubmitted) {
242
+ const discarded = this.session.clearQueue();
243
+ const discardedCount = discarded.steering.length + discarded.followUp.length;
244
+ if (discardedCount > 0) {
245
+ log.logWarning(`[${this.channelId}] Discarded ${discardedCount} queued busy message(s) after run setup failed`);
246
+ }
247
+ }
236
248
  await runQueue.drain();
237
249
  const finalOutcome = this.runState.finalOutcome;
238
250
  const finalOutcomeText = getFinalOutcomeText(finalOutcome);
@@ -423,20 +435,28 @@ export class ChannelRunner {
423
435
  return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
424
436
  }
425
437
  async queueBusyMessage(delivery, text, userName) {
426
- if (!this.session.isStreaming) {
438
+ if (!this.acceptingBusyMessages) {
427
439
  throw new Error("No task is currently running.");
428
440
  }
441
+ await this.ensureSessionReady();
429
442
  const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
430
443
  if (clippedText !== text.trim()) {
431
444
  log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
432
445
  }
433
446
  const queuedMessage = this.formatUserMessage(clippedText, userName);
434
447
  await this.maybeRunPreventiveCompactionForIncomingText(queuedMessage);
435
- await this.sessionResourceGate.runPrompt(async () => {
436
- await this.session.prompt(queuedMessage, {
437
- streamingBehavior: delivery,
438
- });
439
- });
448
+ if (!this.acceptingBusyMessages) {
449
+ throw new Error("No task is currently running.");
450
+ }
451
+ const queueMessage = async () => {
452
+ if (delivery === "followUp") {
453
+ await this.session.followUp(queuedMessage);
454
+ }
455
+ else {
456
+ await this.session.steer(queuedMessage);
457
+ }
458
+ };
459
+ await this.sessionResourceGate.runPrompt(queueMessage);
440
460
  }
441
461
  resetRunState(ctx, store) {
442
462
  this.runState = createEmptyRunState();
@@ -341,6 +341,9 @@ function flushInactiveChannelMemory(channelStates) {
341
341
  }
342
342
  return flushes;
343
343
  }
344
+ function isNoRunningTaskQueueError(err) {
345
+ return err instanceof Error && err.message === "No task is currently running.";
346
+ }
344
347
  export function createRuntimeContext(options) {
345
348
  const startServices = options.startServices ?? true;
346
349
  const registerSignalHandlers = options.registerSignalHandlers ?? true;
@@ -390,20 +393,10 @@ export function createRuntimeContext(options) {
390
393
  },
391
394
  async handleBusyMessage(event, bot, mode, queueText) {
392
395
  if (shuttingDown) {
393
- return;
396
+ return true;
394
397
  }
395
398
  const state = getState(event.channelId);
396
399
  const trimmedQueueText = queueText.trim();
397
- await archiveIncomingMessage(event.channelId, {
398
- date: new Date().toISOString(),
399
- ts: event.ts,
400
- user: event.user,
401
- userName: event.userName,
402
- text: event.text,
403
- isBot: false,
404
- deliveryMode: mode,
405
- skipContextSync: true,
406
- }, `${mode} message`);
407
400
  try {
408
401
  if (mode === "followUp") {
409
402
  await state.runner.queueFollowUp(trimmedQueueText, event.userName);
@@ -411,6 +404,16 @@ export function createRuntimeContext(options) {
411
404
  else {
412
405
  await state.runner.queueSteer(trimmedQueueText, event.userName);
413
406
  }
407
+ await archiveIncomingMessage(event.channelId, {
408
+ date: new Date().toISOString(),
409
+ ts: event.ts,
410
+ user: event.user,
411
+ userName: event.userName,
412
+ text: event.text,
413
+ isBot: false,
414
+ deliveryMode: mode,
415
+ skipContextSync: true,
416
+ }, `${mode} message`);
414
417
  const confirmation = mode === "followUp"
415
418
  ? event.text.trim().startsWith("/")
416
419
  ? "Queued as follow-up. I’ll handle it after the current task completes."
@@ -420,11 +423,17 @@ export function createRuntimeContext(options) {
420
423
  : "Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.";
421
424
  await bot.sendPlain(event.channelId, confirmation);
422
425
  log.logInfo(`[${event.channelId}] Queued ${mode}: ${trimmedQueueText.substring(0, 80)}`);
426
+ return true;
423
427
  }
424
428
  catch (err) {
425
429
  const errMsg = err instanceof Error ? err.message : String(err);
430
+ if (isNoRunningTaskQueueError(err)) {
431
+ log.logInfo(`[${event.channelId}] Busy ${mode} window closed; requeueing as a normal message`);
432
+ return false;
433
+ }
426
434
  log.logWarning(`[${event.channelId}] Failed to queue ${mode}`, errMsg);
427
435
  await bot.sendPlain(event.channelId, `Could not queue this message: ${errMsg}`);
436
+ return true;
428
437
  }
429
438
  },
430
439
  async handleEvent(event, bot, _isEvent) {
@@ -51,7 +51,7 @@ export interface DingTalkHandler {
51
51
  isRunning(channelId: string): boolean;
52
52
  handleEvent(event: DingTalkEvent, bot: DingTalkBot, isEvent?: boolean): Promise<void>;
53
53
  handleStop(channelId: string, bot: DingTalkBot): Promise<void>;
54
- handleBusyMessage(event: DingTalkEvent, bot: DingTalkBot, mode: BusyMessageMode, queueText: string): Promise<void>;
54
+ handleBusyMessage(event: DingTalkEvent, bot: DingTalkBot, mode: BusyMessageMode, queueText: string): Promise<boolean>;
55
55
  }
56
56
  export declare class DingTalkBot {
57
57
  private handler;
@@ -105,6 +105,7 @@ export declare class DingTalkBot {
105
105
  * Returns true if enqueued, false if queue is full (max 5).
106
106
  */
107
107
  enqueueEvent(event: DingTalkEvent): boolean;
108
+ private enqueueStreamMessage;
108
109
  /**
109
110
  * Get or create an AI Card for a channel.
110
111
  */
@@ -481,6 +481,18 @@ export class DingTalkBot {
481
481
  });
482
482
  return true;
483
483
  }
484
+ enqueueStreamMessage(event) {
485
+ this.getQueue(event.channelId).enqueue(async () => {
486
+ this.activeMessageProcessing = true;
487
+ try {
488
+ await this.handler.handleEvent(event, this);
489
+ }
490
+ finally {
491
+ this.activeMessageProcessing = false;
492
+ this.lastSocketAvailableTime = Date.now();
493
+ }
494
+ });
495
+ }
484
496
  // ==========================================================================
485
497
  // AI Card operations
486
498
  // ==========================================================================
@@ -858,11 +870,17 @@ export class DingTalkBot {
858
870
  return;
859
871
  }
860
872
  if (builtInCommand?.name === "steer") {
861
- await this.handler.handleBusyMessage(event, this, "steer", builtInCommand.args);
873
+ const handled = await this.handler.handleBusyMessage(event, this, "steer", builtInCommand.args);
874
+ if (!handled) {
875
+ this.enqueueStreamMessage(event);
876
+ }
862
877
  return;
863
878
  }
864
879
  if (builtInCommand?.name === "followup") {
865
- await this.handler.handleBusyMessage(event, this, "followUp", builtInCommand.args);
880
+ const handled = await this.handler.handleBusyMessage(event, this, "followUp", builtInCommand.args);
881
+ if (!handled) {
882
+ this.enqueueStreamMessage(event);
883
+ }
866
884
  return;
867
885
  }
868
886
  if (builtInCommand) {
@@ -873,20 +891,14 @@ export class DingTalkBot {
873
891
  await this.sendPlain(channelId, "A task is already running. Only `/stop`, `/steer <message>`, and `/followup <message>` are available while streaming.");
874
892
  return;
875
893
  }
876
- await this.handler.handleBusyMessage(event, this, this.busyMessageDefault, content);
894
+ const handled = await this.handler.handleBusyMessage(event, this, this.busyMessageDefault, content);
895
+ if (!handled) {
896
+ this.enqueueStreamMessage(event);
897
+ }
877
898
  return;
878
899
  }
879
900
  // Enqueue for processing
880
- this.getQueue(channelId).enqueue(async () => {
881
- this.activeMessageProcessing = true;
882
- try {
883
- await this.handler.handleEvent(event, this);
884
- }
885
- finally {
886
- this.activeMessageProcessing = false;
887
- this.lastSocketAvailableTime = Date.now();
888
- }
889
- });
901
+ this.enqueueStreamMessage(event);
890
902
  }
891
903
  getQueue(channelId) {
892
904
  let queue = this.queues.get(channelId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.5",
3
+ "version": "0.6.6-beta.1",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {