@agentchatme/agent-core 0.0.1313 → 0.0.1313111

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.
@@ -102,6 +102,14 @@ interface CoordConfig {
102
102
  holder: string;
103
103
  timeoutMs?: number;
104
104
  }
105
+ interface ClaimOutcome {
106
+ claimed: boolean;
107
+ deferred: boolean;
108
+ }
109
+ interface ClaimBatchOutcome {
110
+ claimedCount: number;
111
+ deferred: boolean;
112
+ }
105
113
  declare class ReplyCoord {
106
114
  private readonly cfg;
107
115
  constructor(cfg: CoordConfig);
@@ -109,17 +117,16 @@ declare class ReplyCoord {
109
117
  /** Is the agent's live coding session actively working? Fail-open → FALSE. */
110
118
  isSessionActive(): Promise<boolean>;
111
119
  /**
112
- * Claim the sole right to reply to a message. Returns true if THIS daemon is
113
- * the designated replier, false if a live session already owns it. Fail-open
114
- * → TRUE (reply anyway rather than drop).
120
+ * Claim the sole right to reply to a message, atomically respecting any
121
+ * foreground turn. Fail-open claimed (reply anyway rather than drop).
115
122
  */
116
- claim(messageId: string): Promise<boolean>;
123
+ claim(messageId: string): Promise<ClaimOutcome>;
117
124
  /**
118
125
  * Claim the contiguous oldest-first prefix of one conversation batch.
119
126
  * Falls back to ordered single-message claims against an older API server;
120
127
  * all other coordination failures remain fail-open.
121
128
  */
122
- claimBatch(messageIds: string[]): Promise<number>;
129
+ claimBatch(messageIds: string[]): Promise<ClaimBatchOutcome>;
123
130
  }
124
131
 
125
132
  interface TurnMentionContext {
@@ -281,6 +288,7 @@ declare class Daemon {
281
288
  private pending;
282
289
  private inFlight;
283
290
  private readonly waiters;
291
+ private foregroundClaimsBlockedUntil;
284
292
  private stopping;
285
293
  private heartbeatTimer;
286
294
  constructor(cfg: DaemonConfig, adapter: RuntimeAdapter, ws?: AgentWsClient, // injectable for tests; defaults to a real socket
@@ -295,6 +303,7 @@ declare class Daemon {
295
303
  /** Process bounded backlog snapshots, in arrival order within a conversation. */
296
304
  private drainConversation;
297
305
  private handleNextBatch;
306
+ private waitForForegroundClaimWindow;
298
307
  private turnContext;
299
308
  private markHandled;
300
309
  private markNoLongerPending;
@@ -10,7 +10,7 @@ import {
10
10
  idle,
11
11
  log,
12
12
  resolveIdentity
13
- } from "./chunk-27XDHOL3.js";
13
+ } from "./chunk-YWX7E5VW.js";
14
14
 
15
15
  // src/daemon/ws-client.ts
16
16
  import { WebSocket } from "ws";
@@ -309,20 +309,23 @@ var ReplyCoord = class {
309
309
  }
310
310
  }
311
311
  /**
312
- * Claim the sole right to reply to a message. Returns true if THIS daemon is
313
- * the designated replier, false if a live session already owns it. Fail-open
314
- * → TRUE (reply anyway rather than drop).
312
+ * Claim the sole right to reply to a message, atomically respecting any
313
+ * foreground turn. Fail-open claimed (reply anyway rather than drop).
315
314
  */
316
315
  async claim(messageId) {
317
316
  try {
318
317
  const d = await this.req("POST", "/v1/reply/claim", {
319
318
  message_id: messageId,
320
- holder: this.cfg.holder
319
+ holder: this.cfg.holder,
320
+ defer_if_active: true
321
321
  });
322
- return d?.claimed !== false;
322
+ return {
323
+ claimed: d?.claimed !== false,
324
+ deferred: d?.deferred === true
325
+ };
323
326
  } catch (err) {
324
327
  log.debug(`coord claim failed (proceeding): ${String(err)}`);
325
- return true;
328
+ return { claimed: true, deferred: false };
326
329
  }
327
330
  }
328
331
  /**
@@ -331,26 +334,33 @@ var ReplyCoord = class {
331
334
  * all other coordination failures remain fail-open.
332
335
  */
333
336
  async claimBatch(messageIds) {
334
- if (messageIds.length === 0) return 0;
337
+ if (messageIds.length === 0) return { claimedCount: 0, deferred: false };
335
338
  try {
336
339
  const d = await this.req("POST", "/v1/reply/claim-batch", {
337
340
  message_ids: messageIds,
338
- holder: this.cfg.holder
341
+ holder: this.cfg.holder,
342
+ defer_if_active: true
339
343
  });
340
344
  const count = d?.claimed_count;
341
- return Number.isInteger(count) && count >= 0 && count <= messageIds.length ? count : messageIds.length;
345
+ return {
346
+ claimedCount: Number.isInteger(count) && count >= 0 && count <= messageIds.length ? count : messageIds.length,
347
+ deferred: d?.deferred === true
348
+ };
342
349
  } catch (err) {
343
350
  if (!/reply-coord (404|405)\b/.test(String(err))) {
344
351
  log.debug(`coord batch claim failed (proceeding with all): ${String(err)}`);
345
- return messageIds.length;
352
+ return { claimedCount: messageIds.length, deferred: false };
346
353
  }
347
354
  }
348
355
  let claimed = 0;
349
356
  for (const messageId of messageIds) {
350
- if (!await this.claim(messageId)) break;
357
+ const outcome = await this.claim(messageId);
358
+ if (!outcome.claimed) {
359
+ return { claimedCount: claimed, deferred: outcome.deferred };
360
+ }
351
361
  claimed += 1;
352
362
  }
353
- return claimed;
363
+ return { claimedCount: claimed, deferred: false };
354
364
  }
355
365
  };
356
366
 
@@ -515,7 +525,10 @@ var RETRY_MAX_MS = Math.max(
515
525
  RETRY_BASE_MS,
516
526
  positiveBoundedEnv("AGENTCHATD_RETRY_MAX_MS", 5 * 6e4)
517
527
  );
518
- var YIELD_MS = Number(process.env["AGENTCHATD_YIELD_MS"] ?? 1e4);
528
+ var FOREGROUND_RECHECK_MS = positiveBoundedEnv(
529
+ "AGENTCHATD_FOREGROUND_RECHECK_MS",
530
+ 2e3
531
+ );
519
532
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
520
533
  function retryDelay(attempt) {
521
534
  return Math.min(RETRY_BASE_MS * 2 ** Math.min(20, Math.max(0, attempt - 1)), RETRY_MAX_MS);
@@ -577,6 +590,10 @@ var Daemon = class {
577
590
  pending = 0;
578
591
  inFlight = 0;
579
592
  waiters = [];
593
+ // Identity-wide foreground priority, learned from any deferred claim. Every
594
+ // conversation shares this window so a large multi-conversation backlog
595
+ // cannot turn into one polling loop per conversation.
596
+ foregroundClaimsBlockedUntil = 0;
580
597
  stopping = false;
581
598
  heartbeatTimer = null;
582
599
  async start() {
@@ -648,14 +665,10 @@ var Daemon = class {
648
665
  this.convQueues.get(conversationId)?.shift();
649
666
  return;
650
667
  }
651
- if (await this.coord.isSessionActive()) {
652
- log.info(`msg ${first.id}: live session active \u2014 yielding for ${YIELD_MS}ms`);
653
- await delay(YIELD_MS);
654
- if (this.stopping) return;
655
- }
656
668
  await this.acquireSlot();
657
669
  let slotHeld = true;
658
670
  try {
671
+ await this.waitForForegroundClaimWindow();
659
672
  if (this.stopping) {
660
673
  return;
661
674
  }
@@ -664,23 +677,77 @@ var Daemon = class {
664
677
  const queue = this.convQueues.get(conversationId);
665
678
  if (!queue || queue.length === 0) return;
666
679
  const candidates = queue.splice(0, MAX_BATCH_MESSAGES);
667
- const claimedCount = await this.coord.claimBatch(
680
+ const claim = await this.coord.claimBatch(
668
681
  candidates.map((row) => row.id)
669
682
  );
670
- const batch = candidates.slice(0, claimedCount);
683
+ const claimedCount = claim.claimedCount;
684
+ let batch = candidates.slice(0, claimedCount);
671
685
  if (claimedCount < candidates.length) {
672
- const conflict = candidates[claimedCount];
673
- log.info(`msg ${conflict.id}: claimed by the live session \u2014 standing down`);
674
- this.seen.delete(conflict.id);
675
- this.markNoLongerPending();
676
- const unclaimedTail = candidates.slice(claimedCount + 1);
677
- if (unclaimedTail.length > 0) {
678
- const current = this.convQueues.get(conversationId) ?? [];
679
- this.convQueues.set(conversationId, [...unclaimedTail, ...current]);
686
+ if (claim.deferred) {
687
+ this.foregroundClaimsBlockedUntil = Math.max(
688
+ this.foregroundClaimsBlockedUntil,
689
+ Date.now() + FOREGROUND_RECHECK_MS
690
+ );
691
+ const deferred = candidates.slice(claimedCount);
692
+ if (deferred.length > 0) {
693
+ const current = this.convQueues.get(conversationId) ?? [];
694
+ this.convQueues.set(conversationId, [...deferred, ...current]);
695
+ }
696
+ log.info(
697
+ `msg ${deferred[0]?.id}: foreground turn owns priority \u2014 deferring daemon claim`
698
+ );
699
+ } else {
700
+ const conflict = candidates[claimedCount];
701
+ log.info(`msg ${conflict.id}: claimed by the live session \u2014 standing down`);
702
+ this.seen.delete(conflict.id);
703
+ this.markNoLongerPending();
704
+ const unclaimedTail = candidates.slice(claimedCount + 1);
705
+ if (unclaimedTail.length > 0) {
706
+ const current = this.convQueues.get(conversationId) ?? [];
707
+ this.convQueues.set(conversationId, [...unclaimedTail, ...current]);
708
+ }
680
709
  }
681
710
  }
682
711
  if (batch.length === 0) return;
683
712
  while (!this.stopping) {
713
+ const renewed = await this.coord.claimBatch(batch.map((row) => row.id));
714
+ if (renewed.claimedCount < batch.length) {
715
+ const lost = batch.slice(renewed.claimedCount);
716
+ batch = batch.slice(0, renewed.claimedCount);
717
+ if (renewed.deferred) {
718
+ this.foregroundClaimsBlockedUntil = Math.max(
719
+ this.foregroundClaimsBlockedUntil,
720
+ Date.now() + FOREGROUND_RECHECK_MS
721
+ );
722
+ for (const row of lost) {
723
+ const state = this.seen.get(row.id);
724
+ if (state) {
725
+ state.status = "queued";
726
+ state.updatedAt = Date.now();
727
+ }
728
+ }
729
+ const current = this.convQueues.get(conversationId) ?? [];
730
+ this.convQueues.set(conversationId, [...lost, ...current]);
731
+ } else {
732
+ const conflict = lost[0];
733
+ log.info(`msg ${conflict.id}: renewal lost to a live session \u2014 standing down`);
734
+ this.seen.delete(conflict.id);
735
+ this.markNoLongerPending();
736
+ const tail = lost.slice(1);
737
+ for (const row of tail) {
738
+ const state = this.seen.get(row.id);
739
+ if (state) {
740
+ state.status = "queued";
741
+ state.updatedAt = Date.now();
742
+ }
743
+ }
744
+ if (tail.length > 0) {
745
+ const current = this.convQueues.get(conversationId) ?? [];
746
+ this.convQueues.set(conversationId, [...tail, ...current]);
747
+ }
748
+ }
749
+ }
750
+ if (batch.length === 0) return;
684
751
  const states = batch.map((row) => this.seen.get(row.id));
685
752
  if (states.some(
686
753
  (state) => state === void 0 || state.status === "handled"
@@ -736,6 +803,13 @@ var Daemon = class {
736
803
  if (slotHeld) this.releaseSlot();
737
804
  }
738
805
  }
806
+ async waitForForegroundClaimWindow() {
807
+ while (!this.stopping) {
808
+ const remaining = this.foregroundClaimsBlockedUntil - Date.now();
809
+ if (remaining <= 0) return;
810
+ await delay(remaining);
811
+ }
812
+ }
739
813
  turnContext(batch) {
740
814
  const focus = batch[batch.length - 1];
741
815
  const oldest = batch[0];