@oyasmi/pipiclaw 0.6.5 → 0.6.6-beta.2

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,8 @@ export declare class ChannelRunner implements AgentRunner {
24
24
  private activeModel;
25
25
  private currentSkills;
26
26
  private firstTurnMemoryBootstrapPending;
27
+ private acceptingBusyMessages;
28
+ private agentLoopStarted;
27
29
  private runState;
28
30
  constructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string);
29
31
  run(ctx: DingTalkContext, store: ChannelStore): Promise<{
@@ -53,6 +53,8 @@ function asSdkSettingsManager(manager) {
53
53
  export class ChannelRunner {
54
54
  constructor(sandboxConfig, channelId, channelDir) {
55
55
  this.firstTurnMemoryBootstrapPending = true;
56
+ this.acceptingBusyMessages = false;
57
+ this.agentLoopStarted = false;
56
58
  // --- Per run ---
57
59
  this.runState = createEmptyRunState();
58
60
  this.sandboxConfig = sandboxConfig;
@@ -169,8 +171,11 @@ export class ChannelRunner {
169
171
  // === Public API ===
170
172
  async run(ctx, store) {
171
173
  this.resetRunState(ctx, store);
174
+ this.acceptingBusyMessages = true;
175
+ this.agentLoopStarted = false;
172
176
  const runQueue = createRunQueue(ctx);
173
177
  this.runState.queue = runQueue.queue;
178
+ let promptSubmitted = false;
174
179
  try {
175
180
  await this.ensureSessionReady();
176
181
  this.memoryLifecycle.noteUserTurnStarted();
@@ -225,6 +230,7 @@ export class ChannelRunner {
225
230
  }
226
231
  await this.sessionResourceGate.runPrompt(async () => {
227
232
  await this.session.prompt(promptText);
233
+ promptSubmitted = true;
228
234
  });
229
235
  }
230
236
  catch (err) {
@@ -233,6 +239,15 @@ export class ChannelRunner {
233
239
  log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
234
240
  }
235
241
  finally {
242
+ this.acceptingBusyMessages = false;
243
+ this.agentLoopStarted = false;
244
+ if (!promptSubmitted) {
245
+ const discarded = this.session.clearQueue();
246
+ const discardedCount = discarded.steering.length + discarded.followUp.length;
247
+ if (discardedCount > 0) {
248
+ log.logWarning(`[${this.channelId}] Discarded ${discardedCount} queued busy message(s) after run setup failed`);
249
+ }
250
+ }
236
251
  await runQueue.drain();
237
252
  const finalOutcome = this.runState.finalOutcome;
238
253
  const finalOutcomeText = getFinalOutcomeText(finalOutcome);
@@ -423,20 +438,37 @@ export class ChannelRunner {
423
438
  return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
424
439
  }
425
440
  async queueBusyMessage(delivery, text, userName) {
426
- if (!this.session.isStreaming) {
441
+ if (!this.acceptingBusyMessages) {
442
+ throw new Error("No task is currently running.");
443
+ }
444
+ if (this.agentLoopStarted && !this.session.isStreaming) {
427
445
  throw new Error("No task is currently running.");
428
446
  }
447
+ await this.ensureSessionReady();
429
448
  const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
430
449
  if (clippedText !== text.trim()) {
431
450
  log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
432
451
  }
433
452
  const queuedMessage = this.formatUserMessage(clippedText, userName);
434
453
  await this.maybeRunPreventiveCompactionForIncomingText(queuedMessage);
435
- await this.sessionResourceGate.runPrompt(async () => {
436
- await this.session.prompt(queuedMessage, {
437
- streamingBehavior: delivery,
438
- });
439
- });
454
+ if (!this.acceptingBusyMessages) {
455
+ throw new Error("No task is currently running.");
456
+ }
457
+ if (this.agentLoopStarted && !this.session.isStreaming) {
458
+ throw new Error("No task is currently running.");
459
+ }
460
+ const queueMessage = async () => {
461
+ if (this.agentLoopStarted && !this.session.isStreaming) {
462
+ throw new Error("No task is currently running.");
463
+ }
464
+ if (delivery === "followUp") {
465
+ await this.session.followUp(queuedMessage);
466
+ }
467
+ else {
468
+ await this.session.steer(queuedMessage);
469
+ }
470
+ };
471
+ await this.sessionResourceGate.runPrompt(queueMessage);
440
472
  }
441
473
  resetRunState(ctx, store) {
442
474
  this.runState = createEmptyRunState();
@@ -564,6 +596,9 @@ export class ChannelRunner {
564
596
  // === Session event subscription ===
565
597
  subscribeToSessionEvents() {
566
598
  this.session.subscribe(async (event) => {
599
+ if (isRecord(event) && event.type === "message_start") {
600
+ this.agentLoopStarted = true;
601
+ }
567
602
  if (isRecord(event) && "reason" in event && event.reason === "new") {
568
603
  this.firstTurnMemoryBootstrapPending = true;
569
604
  }
@@ -127,6 +127,7 @@ export async function handleSessionEvent(event, context) {
127
127
  const commandResultText = extractCustomCommandResultText(event.message);
128
128
  if (commandResultText) {
129
129
  runState.finalOutcome = { kind: "final", text: commandResultText };
130
+ runState.finalResponseDelivered = false;
130
131
  log.logResponse(logCtx, commandResultText);
131
132
  queue.enqueue(async () => {
132
133
  const delivered = await ctx.respondPlain(commandResultText);
@@ -195,6 +196,7 @@ export async function handleSessionEvent(event, context) {
195
196
  return;
196
197
  }
197
198
  runState.finalOutcome = { kind: "final", text: finalText };
199
+ runState.finalResponseDelivered = false;
198
200
  memoryLifecycle.noteCompletedAssistantTurn();
199
201
  log.logResponse(logCtx, finalText);
200
202
  queue.enqueue(async () => {
@@ -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) {
@@ -96,10 +96,28 @@ class ChannelDeliveryController {
96
96
  log.logWarning(`[${this.event.channelId}] Failed to archive bot response`, err instanceof Error ? err.message : String(err));
97
97
  });
98
98
  }
99
+ resetProgressAfterFinal() {
100
+ if (!this.finalResponseDelivered) {
101
+ return;
102
+ }
103
+ this.progressSegments = [];
104
+ this.cachedProgressText = "";
105
+ this.progressTextDirty = false;
106
+ this.mode = "progress";
107
+ this.progressStartedAt = 0;
108
+ this.progressWindowStartedAt = 0;
109
+ this.toolCallCount = 0;
110
+ this.sentProgressChars = 0;
111
+ this.replayRequired = false;
112
+ this.finalReplacementText = "";
113
+ this.cardWarmupTriggered = false;
114
+ this.finalResponseDelivered = false;
115
+ }
99
116
  async appendProgress(text, shouldLog) {
100
- if (this.closed || this.finalResponseDelivered || !text.trim())
117
+ if (this.closed || !text.trim())
101
118
  return;
102
119
  this.clearCardWarmup();
120
+ this.resetProgressAfterFinal();
103
121
  if (this.progressStartedAt === 0) {
104
122
  this.progressStartedAt = Date.now();
105
123
  }
@@ -124,8 +142,8 @@ class ChannelDeliveryController {
124
142
  this.bumpRevision(false);
125
143
  }
126
144
  async sendFinal(text, shouldLog) {
127
- if (this.closed || this.finalResponseDelivered)
128
- return this.finalResponseDelivered;
145
+ if (this.closed)
146
+ return false;
129
147
  this.clearCardWarmup();
130
148
  if (shouldLog) {
131
149
  this.archiveBotResponse(text);
@@ -140,7 +158,7 @@ class ChannelDeliveryController {
140
158
  return true;
141
159
  }
142
160
  async replaceWithFinal(text) {
143
- if (this.closed || this.finalResponseDelivered)
161
+ if (this.closed)
144
162
  return;
145
163
  this.clearCardWarmup();
146
164
  this.finalReplacementText = text;
@@ -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.2",
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": {