@agentconnect.md/daemon 1.0.0-rc.139 → 1.0.0-rc.140

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/index.js CHANGED
@@ -7383,6 +7383,8 @@ const ConfigSchema = object({
7383
7383
  idleSweepMs: number().int().default(6e4),
7384
7384
  shutdownDrainMs: number().int().default(25e3),
7385
7385
  cancelBackstopMs: number().int().default(3e4),
7386
+ agentStartAttempts: number().int().min(1).default(3),
7387
+ agentStartBackoffMs: number().int().min(0).default(500),
7386
7388
  maxAttachmentBytes: number().int().default(8 * 1024 * 1024)
7387
7389
  }).default({
7388
7390
  maxAgents: 8,
@@ -7391,6 +7393,8 @@ const ConfigSchema = object({
7391
7393
  idleSweepMs: 6e4,
7392
7394
  shutdownDrainMs: 25e3,
7393
7395
  cancelBackstopMs: 3e4,
7396
+ agentStartAttempts: 3,
7397
+ agentStartBackoffMs: 500,
7394
7398
  maxAttachmentBytes: 8 * 1024 * 1024
7395
7399
  })
7396
7400
  });
@@ -13651,7 +13655,8 @@ const FRAME_SCHEMAS = {
13651
13655
  usage: object({
13652
13656
  used: number().int().optional(),
13653
13657
  cost: number().optional()
13654
- }).optional()
13658
+ }).optional(),
13659
+ error: string().optional()
13655
13660
  }),
13656
13661
  "webchat/close": object({ conversationId: string().uuid() }),
13657
13662
  "route/assign": RouteAssign,
@@ -85020,6 +85025,26 @@ var Daemon = class Daemon {
85020
85025
  for (const cpRule of this.cpRouting?.effectiveRules() ?? []) if (!this.resolveCpAgent(cpRule.agentId)) out.add(cpRule.agentId);
85021
85026
  return [...out];
85022
85027
  }
85028
+ /**
85029
+ * Surface a turn that failed before producing a reply (agent couldn't start,
85030
+ * ACP handshake/prompt rejected) to whichever transport owns it. Without this
85031
+ * the failure is invisible: Slack keeps its "is thinking…" status with no
85032
+ * message, and a webchat client never receives a terminal `webchat/done` so its
85033
+ * UI spins forever. Best-effort — a dead Slack connection or absent CP client
85034
+ * just means the (already logged) error goes unshown, never a second throw.
85035
+ */
85036
+ surfaceTurnFailure(err, ctx) {
85037
+ const reason = err?.message ?? String(err);
85038
+ if (ctx.webchat) this.cpClient?.emitWebchatDone({
85039
+ conversationId: ctx.webchat.conversationId,
85040
+ turnId: ctx.webchat.turnId,
85041
+ error: reason
85042
+ });
85043
+ else if (ctx.replyConn) {
85044
+ if (ctx.statusThread) ctx.replyConn.setStatus(ctx.channel, ctx.statusThread, "");
85045
+ ctx.replyConn.postMessage(ctx.channel, `⚠️ Agent failed to respond: ${reason}`, ctx.thread);
85046
+ }
85047
+ }
85023
85048
  async dispatch(agentId, msg, integrationId, webchat) {
85024
85049
  if (this.draining || this.drainingAgents.has(agentId)) {
85025
85050
  this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
@@ -85037,6 +85062,13 @@ var Daemon = class Daemon {
85037
85062
  handled = await this.sessions.handle(agentId, msg);
85038
85063
  } catch (err) {
85039
85064
  this.store.setSessionState(key, "idle", this.clock.now());
85065
+ this.surfaceTurnFailure(err, {
85066
+ webchat,
85067
+ replyConn,
85068
+ channel: msg.channel,
85069
+ thread: msg.thread,
85070
+ statusThread
85071
+ });
85040
85072
  throw err;
85041
85073
  }
85042
85074
  const { sessionId, blocks } = handled;
@@ -85109,6 +85141,16 @@ var Daemon = class Daemon {
85109
85141
  }
85110
85142
  for (const ev of rec.onFinal()) this.recordEvent(agentId, msg.channel, statusThread, ev);
85111
85143
  await p.applyChain;
85144
+ } catch (err) {
85145
+ this.clearIdle(p);
85146
+ this.surfaceTurnFailure(err, {
85147
+ webchat,
85148
+ replyConn,
85149
+ channel: msg.channel,
85150
+ thread: msg.thread,
85151
+ statusThread
85152
+ });
85153
+ throw err;
85112
85154
  } finally {
85113
85155
  this.clearIdle(p);
85114
85156
  this.pending.delete(sessionId);
@@ -85321,14 +85363,48 @@ var Daemon = class Daemon {
85321
85363
  async ensureHostAsync(agentId) {
85322
85364
  const stopping = this.hostStopping.get(agentId);
85323
85365
  if (stopping) await stopping;
85324
- const host = this.ensureHost(agentId, this.cfg);
85325
85366
  let p = this.hostStarts.get(agentId);
85326
85367
  if (!p) {
85327
- p = host.start();
85368
+ p = this.startHostWithRetry(agentId);
85328
85369
  this.hostStarts.set(agentId, p);
85370
+ p.catch(() => {
85371
+ if (this.hostStarts.get(agentId) === p) this.hostStarts.delete(agentId);
85372
+ });
85329
85373
  }
85330
- await p;
85331
- return host;
85374
+ return await p;
85375
+ }
85376
+ /** Launch an agent's ACP host — spawn + the `initialize` handshake — retrying a
85377
+ * failed start up to `agentStartAttempts` times with a fixed backoff. Each attempt
85378
+ * builds a FRESH host; a failed start()'s half-spawned child is reaped before the
85379
+ * next try. On success the started host is left memoized in `this.hosts`; when every
85380
+ * attempt fails the last error is thrown (dispatch surfaces it to the session). */
85381
+ async startHostWithRetry(agentId) {
85382
+ const attempts = Math.max(1, this.cfg.limits.agentStartAttempts);
85383
+ let lastErr;
85384
+ for (let i = 1; i <= attempts; i++) {
85385
+ const host = this.ensureHost(agentId, this.cfg);
85386
+ try {
85387
+ await host.start();
85388
+ return host;
85389
+ } catch (err) {
85390
+ lastErr = err;
85391
+ this.hosts.delete(agentId);
85392
+ await host.stop().catch(() => {});
85393
+ if (i < attempts) {
85394
+ const backoff = this.cfg.limits.agentStartBackoffMs;
85395
+ this.log.warn(`acp: agent "${agentId}" start attempt ${i}/${attempts} failed (${err.message}) — retrying in ${backoff}ms`);
85396
+ await this.sleep(backoff);
85397
+ } else this.log.error(`acp: agent "${agentId}" failed to start after ${attempts} attempt(s): ${formatErr(err)}`);
85398
+ }
85399
+ }
85400
+ throw lastErr;
85401
+ }
85402
+ /** Clock-driven delay (so a FakeClock stays deterministic in tests). */
85403
+ sleep(ms) {
85404
+ const delay = Math.min(Math.max(0, ms), 2147483647);
85405
+ return new Promise((resolve) => {
85406
+ this.clock.setTimeout(() => resolve(), delay);
85407
+ });
85332
85408
  }
85333
85409
  replyConnFor(agentId, integrationId) {
85334
85410
  const intId = integrationId ?? this.agents.get(agentId)?.integrations[0]?.id;