crawd 0.8.3 → 0.8.5

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.
@@ -11205,13 +11205,14 @@ var YouTubeChatClient = class extends BaseChatClient {
11205
11205
  // src/backend/coordinator.ts
11206
11206
  import { randomUUID } from "crypto";
11207
11207
  import WebSocket from "ws";
11208
- var BATCH_WINDOW_MS = 2e4;
11208
+ var DEFAULT_BATCH_WINDOW_MS = 2e4;
11209
11209
  var SESSION_KEY = process.env.CRAWD_CHANNEL_ID || "agent:main:crawd:live";
11210
11210
  var DEFAULT_CONFIG = {
11211
11211
  vibeEnabled: true,
11212
11212
  vibeIntervalMs: 3e4,
11213
11213
  idleAfterMs: 18e4,
11214
11214
  sleepAfterIdleMs: 18e4,
11215
+ chatBatchWindowMs: DEFAULT_BATCH_WINDOW_MS,
11215
11216
  vibePrompt: `[CRAWD:VIBE] You are on a livestream. Make sure the crawd skill is loaded. Do one thing on the internet or ask the chat something. Respond with LIVESTREAM_REPLIED after using a tool, or NO_REPLY if you have nothing to say.`
11216
11217
  };
11217
11218
  var realClock = {
@@ -11443,7 +11444,7 @@ var GatewayClient = class _GatewayClient {
11443
11444
  };
11444
11445
  var STARTUP_GRACE_MS = 3e4;
11445
11446
  var SLEEP_CHECK_INTERVAL_MS = 1e4;
11446
- var Coordinator = class {
11447
+ var Coordinator = class _Coordinator {
11447
11448
  buffer = [];
11448
11449
  timer = null;
11449
11450
  triggerFn;
@@ -11482,6 +11483,7 @@ var Coordinator = class {
11482
11483
  this.config = { ...this.config, ...config2 };
11483
11484
  this.logger.log("[Coordinator] Config updated:", {
11484
11485
  vibeIntervalMs: this.config.vibeIntervalMs,
11486
+ chatBatchWindowMs: this.config.chatBatchWindowMs,
11485
11487
  idleAfterMs: this.config.idleAfterMs,
11486
11488
  sleepAfterIdleMs: this.config.sleepAfterIdleMs
11487
11489
  });
@@ -11648,14 +11650,18 @@ var Coordinator = class {
11648
11650
  this._busy = true;
11649
11651
  try {
11650
11652
  const replies = await this.triggerFn(this.config.vibePrompt);
11651
- if (replies.some((r) => r.trim().toUpperCase() === "NO_REPLY")) {
11653
+ const agentReplies = replies.filter((r) => !this.isApiError(r));
11654
+ if (agentReplies.some((r) => r.trim().toUpperCase() === "NO_REPLY")) {
11652
11655
  noReply = true;
11653
- } else if (!this.isCompliantReply(replies)) {
11654
- misaligned = replies.filter((r) => {
11656
+ } else if (!this.isCompliantReply(agentReplies)) {
11657
+ misaligned = agentReplies.filter((r) => {
11655
11658
  const t = r.trim().toUpperCase();
11656
11659
  return t !== "NO_REPLY" && t !== "LIVESTREAM_REPLIED";
11657
11660
  });
11658
11661
  }
11662
+ if (replies.length > agentReplies.length) {
11663
+ this.logger.warn(`[Coordinator] Filtered ${replies.length - agentReplies.length} API error(s) from vibe response`);
11664
+ }
11659
11665
  } catch (err) {
11660
11666
  this.logger.error("[Coordinator] Vibe failed:", err);
11661
11667
  } finally {
@@ -11701,26 +11707,31 @@ var Coordinator = class {
11701
11707
  }
11702
11708
  if (!this.timer) {
11703
11709
  this.flush();
11704
- this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), BATCH_WINDOW_MS);
11710
+ this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.chatBatchWindowMs);
11705
11711
  }
11706
11712
  }
11707
11713
  onCooldownEnd() {
11708
11714
  this.timer = null;
11709
11715
  if (this.buffer.length > 0) {
11710
11716
  this.flush();
11711
- this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), BATCH_WINDOW_MS);
11717
+ this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.chatBatchWindowMs);
11712
11718
  }
11713
11719
  }
11714
11720
  /** Whether the coordinator is busy processing a flush or talk */
11715
11721
  get busy() {
11716
11722
  return this._busy;
11717
11723
  }
11724
+ /** Detect API/gateway errors surfaced as reply strings (e.g. rate limits) */
11725
+ static API_ERROR_RE = /^\d{3}\s+(status\s+code|error)|^rate\s*limit|^too\s+many\s+requests|^overloaded|^server\s+error/i;
11726
+ isApiError(reply) {
11727
+ return _Coordinator.API_ERROR_RE.test(reply.trim());
11728
+ }
11718
11729
  /** Check if agent replies are compliant (NO_REPLY or LIVESTREAM_REPLIED) */
11719
11730
  isCompliantReply(replies) {
11720
11731
  if (replies.length === 0) return true;
11721
11732
  return replies.every((r) => {
11722
11733
  const t = r.trim().toUpperCase();
11723
- return t === "NO_REPLY" || t === "LIVESTREAM_REPLIED";
11734
+ return t === "NO_REPLY" || t === "LIVESTREAM_REPLIED" || this.isApiError(r);
11724
11735
  });
11725
11736
  }
11726
11737
  /** Send misalignment correction when agent responds with plaintext */
@@ -11746,8 +11757,12 @@ var Coordinator = class {
11746
11757
  this._busy = true;
11747
11758
  try {
11748
11759
  const replies = await this.triggerFn(batchText);
11749
- if (!this.isCompliantReply(replies)) {
11750
- await this.sendMisalignment(replies.filter((r) => {
11760
+ const agentReplies = replies.filter((r) => !this.isApiError(r));
11761
+ if (replies.length > agentReplies.length) {
11762
+ this.logger.warn(`[Coordinator] Filtered ${replies.length - agentReplies.length} API error(s) from chat response`);
11763
+ }
11764
+ if (!this.isCompliantReply(agentReplies)) {
11765
+ await this.sendMisalignment(agentReplies.filter((r) => {
11751
11766
  const t = r.trim().toUpperCase();
11752
11767
  return t !== "NO_REPLY" && t !== "LIVESTREAM_REPLIED";
11753
11768
  }));
package/dist/plugin.js CHANGED
@@ -11206,13 +11206,14 @@ var YouTubeChatClient = class extends BaseChatClient {
11206
11206
  // src/backend/coordinator.ts
11207
11207
  import { randomUUID } from "crypto";
11208
11208
  import WebSocket from "ws";
11209
- var BATCH_WINDOW_MS = 2e4;
11209
+ var DEFAULT_BATCH_WINDOW_MS = 2e4;
11210
11210
  var SESSION_KEY = process.env.CRAWD_CHANNEL_ID || "agent:main:crawd:live";
11211
11211
  var DEFAULT_CONFIG = {
11212
11212
  vibeEnabled: true,
11213
11213
  vibeIntervalMs: 3e4,
11214
11214
  idleAfterMs: 18e4,
11215
11215
  sleepAfterIdleMs: 18e4,
11216
+ chatBatchWindowMs: DEFAULT_BATCH_WINDOW_MS,
11216
11217
  vibePrompt: `[CRAWD:VIBE] You are on a livestream. Make sure the crawd skill is loaded. Do one thing on the internet or ask the chat something. Respond with LIVESTREAM_REPLIED after using a tool, or NO_REPLY if you have nothing to say.`
11217
11218
  };
11218
11219
  var realClock = {
@@ -11321,7 +11322,7 @@ var OneShotGateway = class {
11321
11322
  };
11322
11323
  var STARTUP_GRACE_MS = 3e4;
11323
11324
  var SLEEP_CHECK_INTERVAL_MS = 1e4;
11324
- var Coordinator = class {
11325
+ var Coordinator = class _Coordinator {
11325
11326
  buffer = [];
11326
11327
  timer = null;
11327
11328
  triggerFn;
@@ -11360,6 +11361,7 @@ var Coordinator = class {
11360
11361
  this.config = { ...this.config, ...config2 };
11361
11362
  this.logger.log("[Coordinator] Config updated:", {
11362
11363
  vibeIntervalMs: this.config.vibeIntervalMs,
11364
+ chatBatchWindowMs: this.config.chatBatchWindowMs,
11363
11365
  idleAfterMs: this.config.idleAfterMs,
11364
11366
  sleepAfterIdleMs: this.config.sleepAfterIdleMs
11365
11367
  });
@@ -11526,14 +11528,18 @@ var Coordinator = class {
11526
11528
  this._busy = true;
11527
11529
  try {
11528
11530
  const replies = await this.triggerFn(this.config.vibePrompt);
11529
- if (replies.some((r) => r.trim().toUpperCase() === "NO_REPLY")) {
11531
+ const agentReplies = replies.filter((r) => !this.isApiError(r));
11532
+ if (agentReplies.some((r) => r.trim().toUpperCase() === "NO_REPLY")) {
11530
11533
  noReply = true;
11531
- } else if (!this.isCompliantReply(replies)) {
11532
- misaligned = replies.filter((r) => {
11534
+ } else if (!this.isCompliantReply(agentReplies)) {
11535
+ misaligned = agentReplies.filter((r) => {
11533
11536
  const t = r.trim().toUpperCase();
11534
11537
  return t !== "NO_REPLY" && t !== "LIVESTREAM_REPLIED";
11535
11538
  });
11536
11539
  }
11540
+ if (replies.length > agentReplies.length) {
11541
+ this.logger.warn(`[Coordinator] Filtered ${replies.length - agentReplies.length} API error(s) from vibe response`);
11542
+ }
11537
11543
  } catch (err) {
11538
11544
  this.logger.error("[Coordinator] Vibe failed:", err);
11539
11545
  } finally {
@@ -11579,26 +11585,31 @@ var Coordinator = class {
11579
11585
  }
11580
11586
  if (!this.timer) {
11581
11587
  this.flush();
11582
- this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), BATCH_WINDOW_MS);
11588
+ this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.chatBatchWindowMs);
11583
11589
  }
11584
11590
  }
11585
11591
  onCooldownEnd() {
11586
11592
  this.timer = null;
11587
11593
  if (this.buffer.length > 0) {
11588
11594
  this.flush();
11589
- this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), BATCH_WINDOW_MS);
11595
+ this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.chatBatchWindowMs);
11590
11596
  }
11591
11597
  }
11592
11598
  /** Whether the coordinator is busy processing a flush or talk */
11593
11599
  get busy() {
11594
11600
  return this._busy;
11595
11601
  }
11602
+ /** Detect API/gateway errors surfaced as reply strings (e.g. rate limits) */
11603
+ static API_ERROR_RE = /^\d{3}\s+(status\s+code|error)|^rate\s*limit|^too\s+many\s+requests|^overloaded|^server\s+error/i;
11604
+ isApiError(reply) {
11605
+ return _Coordinator.API_ERROR_RE.test(reply.trim());
11606
+ }
11596
11607
  /** Check if agent replies are compliant (NO_REPLY or LIVESTREAM_REPLIED) */
11597
11608
  isCompliantReply(replies) {
11598
11609
  if (replies.length === 0) return true;
11599
11610
  return replies.every((r) => {
11600
11611
  const t = r.trim().toUpperCase();
11601
- return t === "NO_REPLY" || t === "LIVESTREAM_REPLIED";
11612
+ return t === "NO_REPLY" || t === "LIVESTREAM_REPLIED" || this.isApiError(r);
11602
11613
  });
11603
11614
  }
11604
11615
  /** Send misalignment correction when agent responds with plaintext */
@@ -11624,8 +11635,12 @@ var Coordinator = class {
11624
11635
  this._busy = true;
11625
11636
  try {
11626
11637
  const replies = await this.triggerFn(batchText);
11627
- if (!this.isCompliantReply(replies)) {
11628
- await this.sendMisalignment(replies.filter((r) => {
11638
+ const agentReplies = replies.filter((r) => !this.isApiError(r));
11639
+ if (replies.length > agentReplies.length) {
11640
+ this.logger.warn(`[Coordinator] Filtered ${replies.length - agentReplies.length} API error(s) from chat response`);
11641
+ }
11642
+ if (!this.isCompliantReply(agentReplies)) {
11643
+ await this.sendMisalignment(agentReplies.filter((r) => {
11629
11644
  const t = r.trim().toUpperCase();
11630
11645
  return t !== "NO_REPLY" && t !== "LIVESTREAM_REPLIED";
11631
11646
  }));
@@ -12309,6 +12324,33 @@ var plugin = {
12309
12324
  },
12310
12325
  { name: "livestream_reply" }
12311
12326
  );
12327
+ api.registerTool(
12328
+ {
12329
+ name: "livestream_config",
12330
+ label: "Livestream Config",
12331
+ description: "Update livestream coordinator settings at runtime. Use when asked to change vibe speed/frequency, chat throttle/batch window, idle timeout, or sleep timeout.",
12332
+ parameters: Type.Object({
12333
+ vibeIntervalMs: Type.Optional(Type.Number({ description: "Milliseconds between autonomous vibe prompts (lower = more frequent vibes)" })),
12334
+ chatBatchWindowMs: Type.Optional(Type.Number({ description: "Milliseconds to batch chat messages before sending to agent (lower = faster chat response, higher = more messages per batch)" })),
12335
+ idleAfterMs: Type.Optional(Type.Number({ description: "Milliseconds of inactivity before transitioning to idle state" })),
12336
+ sleepAfterIdleMs: Type.Optional(Type.Number({ description: "Milliseconds in idle before transitioning to sleep state" })),
12337
+ vibeEnabled: Type.Optional(Type.Boolean({ description: "Enable or disable autonomous vibe prompts" }))
12338
+ }),
12339
+ async execute(_toolCallId, params) {
12340
+ const b = await ensureBackend();
12341
+ if (!b.coordinator) {
12342
+ return { content: [{ type: "text", text: "Coordinator not running" }] };
12343
+ }
12344
+ const p = params;
12345
+ b.coordinator.updateConfig(p);
12346
+ const { state, config: cfg } = b.coordinator.getState();
12347
+ return {
12348
+ content: [{ type: "text", text: `Config updated. state=${state}, vibeInterval=${cfg.vibeIntervalMs}ms, chatBatch=${cfg.chatBatchWindowMs}ms, idleAfter=${cfg.idleAfterMs}ms, sleepAfter=${cfg.sleepAfterIdleMs}ms, vibes=${cfg.vibeEnabled}` }]
12349
+ };
12350
+ }
12351
+ },
12352
+ { name: "livestream_config" }
12353
+ );
12312
12354
  api.registerService({
12313
12355
  id: "crawd",
12314
12356
  start: async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawd",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "CLI for crawd.bot - AI agent livestreaming platform",
5
5
  "type": "module",
6
6
  "types": "./dist/types.d.ts",
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
2
2
  import WebSocket from 'ws'
3
3
  import type { ChatMessage } from '../lib/chat/types'
4
4
 
5
- const BATCH_WINDOW_MS = 20_000
5
+ const DEFAULT_BATCH_WINDOW_MS = 20_000
6
6
  const SESSION_KEY = process.env.CRAWD_CHANNEL_ID || 'agent:main:crawd:live'
7
7
 
8
8
  /** Coordinator configuration */
@@ -15,6 +15,8 @@ export type CoordinatorConfig = {
15
15
  idleAfterMs: number
16
16
  /** Go sleep after this much inactivity while idle (ms). Default: 60000 (1 min) */
17
17
  sleepAfterIdleMs: number
18
+ /** Chat message batching window (ms). Default: 20000 (20 sec) */
19
+ chatBatchWindowMs: number
18
20
  /** The autonomous "vibe" prompt sent periodically */
19
21
  vibePrompt: string
20
22
  }
@@ -24,6 +26,7 @@ export const DEFAULT_CONFIG: CoordinatorConfig = {
24
26
  vibeIntervalMs: 30_000,
25
27
  idleAfterMs: 180_000,
26
28
  sleepAfterIdleMs: 180_000,
29
+ chatBatchWindowMs: DEFAULT_BATCH_WINDOW_MS,
27
30
  vibePrompt: `[CRAWD:VIBE] You are on a livestream. Make sure the crawd skill is loaded. Do one thing on the internet or ask the chat something. Respond with LIVESTREAM_REPLIED after using a tool, or NO_REPLY if you have nothing to say.`,
28
31
  }
29
32
 
@@ -554,6 +557,7 @@ export class Coordinator {
554
557
  this.config = { ...this.config, ...config }
555
558
  this.logger.log('[Coordinator] Config updated:', {
556
559
  vibeIntervalMs: this.config.vibeIntervalMs,
560
+ chatBatchWindowMs: this.config.chatBatchWindowMs,
557
561
  idleAfterMs: this.config.idleAfterMs,
558
562
  sleepAfterIdleMs: this.config.sleepAfterIdleMs,
559
563
  })
@@ -765,14 +769,19 @@ export class Coordinator {
765
769
  this._busy = true
766
770
  try {
767
771
  const replies = await this.triggerFn(this.config.vibePrompt)
768
- if (replies.some(r => r.trim().toUpperCase() === 'NO_REPLY')) {
772
+ // Filter out API errors (429s, rate limits) — not agent responses
773
+ const agentReplies = replies.filter(r => !this.isApiError(r))
774
+ if (agentReplies.some(r => r.trim().toUpperCase() === 'NO_REPLY')) {
769
775
  noReply = true
770
- } else if (!this.isCompliantReply(replies)) {
771
- misaligned = replies.filter(r => {
776
+ } else if (!this.isCompliantReply(agentReplies)) {
777
+ misaligned = agentReplies.filter(r => {
772
778
  const t = r.trim().toUpperCase()
773
779
  return t !== 'NO_REPLY' && t !== 'LIVESTREAM_REPLIED'
774
780
  })
775
781
  }
782
+ if (replies.length > agentReplies.length) {
783
+ this.logger.warn(`[Coordinator] Filtered ${replies.length - agentReplies.length} API error(s) from vibe response`)
784
+ }
776
785
  } catch (err) {
777
786
  this.logger.error('[Coordinator] Vibe failed:', err)
778
787
  } finally {
@@ -831,7 +840,7 @@ export class Coordinator {
831
840
  // Leading edge: if no timer running, flush immediately and start cooldown
832
841
  if (!this.timer) {
833
842
  this.flush()
834
- this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), BATCH_WINDOW_MS)
843
+ this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.chatBatchWindowMs)
835
844
  }
836
845
  // Otherwise, message is buffered and will be flushed when cooldown ends
837
846
  }
@@ -842,19 +851,26 @@ export class Coordinator {
842
851
  // If messages accumulated during cooldown, flush them and restart cooldown
843
852
  if (this.buffer.length > 0) {
844
853
  this.flush()
845
- this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), BATCH_WINDOW_MS)
854
+ this.timer = this.clock.setTimeout(() => this.onCooldownEnd(), this.config.chatBatchWindowMs)
846
855
  }
847
856
  }
848
857
 
849
858
  /** Whether the coordinator is busy processing a flush or talk */
850
859
  get busy(): boolean { return this._busy }
851
860
 
861
+ /** Detect API/gateway errors surfaced as reply strings (e.g. rate limits) */
862
+ private static readonly API_ERROR_RE = /^\d{3}\s+(status\s+code|error)|^rate\s*limit|^too\s+many\s+requests|^overloaded|^server\s+error/i
863
+
864
+ private isApiError(reply: string): boolean {
865
+ return Coordinator.API_ERROR_RE.test(reply.trim())
866
+ }
867
+
852
868
  /** Check if agent replies are compliant (NO_REPLY or LIVESTREAM_REPLIED) */
853
869
  private isCompliantReply(replies: string[]): boolean {
854
870
  if (replies.length === 0) return true
855
871
  return replies.every(r => {
856
872
  const t = r.trim().toUpperCase()
857
- return t === 'NO_REPLY' || t === 'LIVESTREAM_REPLIED'
873
+ return t === 'NO_REPLY' || t === 'LIVESTREAM_REPLIED' || this.isApiError(r)
858
874
  })
859
875
  }
860
876
 
@@ -889,8 +905,12 @@ export class Coordinator {
889
905
  this._busy = true
890
906
  try {
891
907
  const replies = await this.triggerFn(batchText)
892
- if (!this.isCompliantReply(replies)) {
893
- await this.sendMisalignment(replies.filter(r => {
908
+ const agentReplies = replies.filter(r => !this.isApiError(r))
909
+ if (replies.length > agentReplies.length) {
910
+ this.logger.warn(`[Coordinator] Filtered ${replies.length - agentReplies.length} API error(s) from chat response`)
911
+ }
912
+ if (!this.isCompliantReply(agentReplies)) {
913
+ await this.sendMisalignment(agentReplies.filter(r => {
894
914
  const t = r.trim().toUpperCase()
895
915
  return t !== 'NO_REPLY' && t !== 'LIVESTREAM_REPLIED'
896
916
  }))
package/src/plugin.ts CHANGED
@@ -249,6 +249,36 @@ const plugin: PluginDefinition = {
249
249
  { name: 'livestream_reply' },
250
250
  )
251
251
 
252
+ // livestream_config — runtime coordinator tuning
253
+ api.registerTool(
254
+ {
255
+ name: 'livestream_config',
256
+ label: 'Livestream Config',
257
+ description:
258
+ 'Update livestream coordinator settings at runtime. Use when asked to change vibe speed/frequency, chat throttle/batch window, idle timeout, or sleep timeout.',
259
+ parameters: Type.Object({
260
+ vibeIntervalMs: Type.Optional(Type.Number({ description: 'Milliseconds between autonomous vibe prompts (lower = more frequent vibes)' })),
261
+ chatBatchWindowMs: Type.Optional(Type.Number({ description: 'Milliseconds to batch chat messages before sending to agent (lower = faster chat response, higher = more messages per batch)' })),
262
+ idleAfterMs: Type.Optional(Type.Number({ description: 'Milliseconds of inactivity before transitioning to idle state' })),
263
+ sleepAfterIdleMs: Type.Optional(Type.Number({ description: 'Milliseconds in idle before transitioning to sleep state' })),
264
+ vibeEnabled: Type.Optional(Type.Boolean({ description: 'Enable or disable autonomous vibe prompts' })),
265
+ }),
266
+ async execute(_toolCallId: string, params: unknown) {
267
+ const b = await ensureBackend()
268
+ if (!b.coordinator) {
269
+ return { content: [{ type: 'text', text: 'Coordinator not running' }] }
270
+ }
271
+ const p = params as Record<string, unknown>
272
+ b.coordinator.updateConfig(p)
273
+ const { state, config: cfg } = b.coordinator.getState()
274
+ return {
275
+ content: [{ type: 'text', text: `Config updated. state=${state}, vibeInterval=${cfg.vibeIntervalMs}ms, chatBatch=${cfg.chatBatchWindowMs}ms, idleAfter=${cfg.idleAfterMs}ms, sleepAfter=${cfg.sleepAfterIdleMs}ms, vibes=${cfg.vibeEnabled}` }],
276
+ }
277
+ },
278
+ },
279
+ { name: 'livestream_config' },
280
+ )
281
+
252
282
  // Service lifecycle
253
283
  api.registerService({
254
284
  id: 'crawd',