@integrity-labs/agt-cli 0.20.3 → 0.20.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.
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-INN5PXU3.js";
25
+ } from "../chunk-LE6K3LZ6.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -1549,6 +1549,56 @@ async function sendError(flag, opts, text) {
1549
1549
  await sendAck(flag, opts, text);
1550
1550
  }
1551
1551
 
1552
+ // src/lib/persistent-session-stuck-tracker.ts
1553
+ var DEFAULT_STUCK_WARN_THRESHOLD = 3;
1554
+ var PersistentSessionStuckTracker = class {
1555
+ counts = /* @__PURE__ */ new Map();
1556
+ warned = /* @__PURE__ */ new Set();
1557
+ threshold;
1558
+ constructor(threshold = DEFAULT_STUCK_WARN_THRESHOLD) {
1559
+ if (threshold < 1) throw new Error("stuck-tracker threshold must be >= 1");
1560
+ this.threshold = threshold;
1561
+ }
1562
+ /**
1563
+ * Record one tick for one agent. Returns whether the caller should log
1564
+ * a stuck-warning this tick. Re-warning only fires when the counter
1565
+ * climbs above the threshold *again* after a recovery, so a stuck
1566
+ * agent doesn't spam the log every 30s — operators get one WARN per
1567
+ * stuck streak.
1568
+ */
1569
+ record(input) {
1570
+ const { codeName, sessionHealthy, spawnAttempted } = input;
1571
+ const wasStuck = (this.counts.get(codeName) ?? 0) >= this.threshold;
1572
+ if (sessionHealthy || spawnAttempted) {
1573
+ this.counts.delete(codeName);
1574
+ const wasWarned = this.warned.delete(codeName);
1575
+ return {
1576
+ consecutiveTicks: 0,
1577
+ shouldWarn: false,
1578
+ recovered: wasStuck && wasWarned
1579
+ };
1580
+ }
1581
+ const next = (this.counts.get(codeName) ?? 0) + 1;
1582
+ this.counts.set(codeName, next);
1583
+ const crossed = next >= this.threshold && !this.warned.has(codeName);
1584
+ if (crossed) this.warned.add(codeName);
1585
+ return {
1586
+ consecutiveTicks: next,
1587
+ shouldWarn: crossed,
1588
+ recovered: false
1589
+ };
1590
+ }
1591
+ /** Test/debug helper: peek at an agent's current streak. */
1592
+ peek(codeName) {
1593
+ return this.counts.get(codeName) ?? 0;
1594
+ }
1595
+ /** Test helper: clear all state. */
1596
+ reset() {
1597
+ this.counts.clear();
1598
+ this.warned.clear();
1599
+ }
1600
+ };
1601
+
1552
1602
  // src/lib/realtime-chat.ts
1553
1603
  import { createClient } from "@supabase/supabase-js";
1554
1604
  var client = null;
@@ -2009,6 +2059,12 @@ function truncateForLog(s) {
2009
2059
  return lines.slice(-PANE_TAIL_PREVIEW_LINES).map((l) => ` | ${l}`).join("\n");
2010
2060
  }
2011
2061
  var KNOWN_SAFE_TAIL_SIGNATURES = /* @__PURE__ */ new Set(["session_id_in_use"]);
2062
+ function shouldSkipRevokedCleanup(previousKnownStatus) {
2063
+ return previousKnownStatus === "revoked";
2064
+ }
2065
+ function hasRevokedResiduals(state2) {
2066
+ return state2.gatewayRunning || state2.portAllocated || state2.provisionDirExists;
2067
+ }
2012
2068
  var knownVersions = /* @__PURE__ */ new Map();
2013
2069
  var knownStatuses = /* @__PURE__ */ new Map();
2014
2070
  var knownChannels = /* @__PURE__ */ new Map();
@@ -2144,7 +2200,7 @@ function clearAgentCaches(agentId, codeName) {
2144
2200
  var cachedFrameworkVersion = null;
2145
2201
  var lastVersionCheckAt = 0;
2146
2202
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
2147
- var agtCliVersion = true ? "0.20.3" : "dev";
2203
+ var agtCliVersion = true ? "0.20.5" : "dev";
2148
2204
  function resolveBrewPath(execFileSync4) {
2149
2205
  try {
2150
2206
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3308,7 +3364,7 @@ async function getOrCacheRegisteredAgents(adapter, profile) {
3308
3364
  }
3309
3365
  async function processAgent(agent, agentStates) {
3310
3366
  if (!config) return;
3311
- log(`==================== ${agent.display_name} (${agent.code_name}) ====================`);
3367
+ const previousKnownStatus = knownStatuses.get(agent.agent_id);
3312
3368
  agentDisplayNames.set(agent.code_name, agent.display_name);
3313
3369
  codeNameToAgentId.set(agent.code_name, agent.agent_id);
3314
3370
  if (agent.framework) {
@@ -3318,7 +3374,9 @@ async function processAgent(agent, agentStates) {
3318
3374
  const adapter = resolveAgentFramework(agent.code_name);
3319
3375
  let agentDir = join4(adapter.getAgentDir(agent.code_name), "provision");
3320
3376
  if (agent.status === "draft" || agent.status === "paused") {
3321
- log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
3377
+ if (previousKnownStatus !== agent.status) {
3378
+ log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
3379
+ }
3322
3380
  await stopGatewayIfRunning(agent.code_name, adapter);
3323
3381
  stopPersistentSessionAndForgetMcpBaseline(agent.code_name);
3324
3382
  try {
@@ -3343,9 +3401,41 @@ async function processAgent(agent, agentStates) {
3343
3401
  gatewayRunning: false,
3344
3402
  acpSessions: []
3345
3403
  });
3404
+ knownStatuses.set(agent.agent_id, agent.status);
3346
3405
  return;
3347
3406
  }
3348
3407
  if (agent.status === "revoked") {
3408
+ if (shouldSkipRevokedCleanup(previousKnownStatus)) {
3409
+ const ports = loadGatewayPorts();
3410
+ const gatewayLiveness = adapter.isGatewayRunning ? await adapter.isGatewayRunning(agent.code_name).catch(() => ({ running: false })) : { running: false };
3411
+ const residuals = {
3412
+ gatewayRunning: gatewayLiveness.running,
3413
+ portAllocated: Object.prototype.hasOwnProperty.call(ports, agent.code_name),
3414
+ provisionDirExists: existsSync3(agentDir)
3415
+ };
3416
+ if (!hasRevokedResiduals(residuals)) {
3417
+ agentStates.push({
3418
+ agentId: agent.agent_id,
3419
+ codeName: agent.code_name,
3420
+ status: agent.status,
3421
+ charterVersion: "",
3422
+ toolsVersion: "",
3423
+ secretsHash: null,
3424
+ lastRefreshAt: now,
3425
+ lastProvisionAt: null,
3426
+ lastDriftCheckAt: null,
3427
+ lastSecretsProvisionAt: null,
3428
+ gatewayPort: null,
3429
+ gatewayPid: null,
3430
+ gatewayRunning: false,
3431
+ acpSessions: []
3432
+ });
3433
+ return;
3434
+ }
3435
+ log(
3436
+ `[revoked] residuals for '${agent.code_name}': gateway=${residuals.gatewayRunning} dir=${residuals.provisionDirExists} port=${residuals.portAllocated ? ports[agent.code_name] : "none"} \u2014 retrying cleanup`
3437
+ );
3438
+ }
3349
3439
  log(`Agent '${agent.code_name}' is revoked, cleaning up`);
3350
3440
  await stopGatewayIfRunning(agent.code_name, adapter);
3351
3441
  stopPersistentSessionAndForgetMcpBaseline(agent.code_name);
@@ -4221,7 +4311,35 @@ async function processAgent(agent, agentStates) {
4221
4311
  const agentFw = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
4222
4312
  const sessionMode = refreshData.agent.session_mode ?? "oneshot";
4223
4313
  if (agentFw === "claude-code" && sessionMode === "persistent") {
4224
- await ensurePersistentSession(agent, tasks, boardItems, refreshData);
4314
+ let psResult;
4315
+ try {
4316
+ psResult = await ensurePersistentSession(agent, tasks, boardItems, refreshData);
4317
+ } catch (err) {
4318
+ const msg = err.message;
4319
+ log(`[persistent-session] EXCEPTION evaluating '${agent.code_name}': ${msg}`);
4320
+ psResult = {
4321
+ decision: "skipped-auth-resolve-failed",
4322
+ spawnAttempted: false,
4323
+ sessionHealthyAfter: false,
4324
+ detail: `exception: ${msg}`
4325
+ };
4326
+ }
4327
+ const detailSuffix = psResult.detail ? ` detail="${psResult.detail}"` : "";
4328
+ log(
4329
+ `[persistent-session-decision] agent=${agent.code_name} decision=${psResult.decision} spawn_attempted=${psResult.spawnAttempted} session_healthy_after=${psResult.sessionHealthyAfter}${detailSuffix}`
4330
+ );
4331
+ const stuck = persistentSessionStuckTracker.record({
4332
+ codeName: agent.code_name,
4333
+ sessionHealthy: psResult.sessionHealthyAfter,
4334
+ spawnAttempted: psResult.spawnAttempted
4335
+ });
4336
+ if (stuck.shouldWarn) {
4337
+ log(
4338
+ `[persistent-session-stuck] WARN: agent=${agent.code_name} unhealthy with no spawn attempt for ${stuck.consecutiveTicks} consecutive ticks (last_decision=${psResult.decision}) \u2014 manager-worker may need restart, or upstream guard is mis-firing (see ENG-5116)`
4339
+ );
4340
+ } else if (stuck.recovered) {
4341
+ log(`[persistent-session-stuck] agent=${agent.code_name} recovered from stuck state`);
4342
+ }
4225
4343
  try {
4226
4344
  checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir2(agent.code_name));
4227
4345
  } catch (err) {
@@ -4954,6 +5072,7 @@ function fireClaudeWorkTrigger(codeName, agentId, boardItems) {
4954
5072
  });
4955
5073
  }
4956
5074
  var persistentSessionAgents = /* @__PURE__ */ new Set();
5075
+ var persistentSessionStuckTracker = new PersistentSessionStuckTracker();
4957
5076
  var claudeAuthTupleBySession = /* @__PURE__ */ new Map();
4958
5077
  async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4959
5078
  const codeName = agent.code_name;
@@ -4986,7 +5105,11 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4986
5105
  const apiKey = getApiKey();
4987
5106
  if (!apiKey) {
4988
5107
  log(`[persistent-session] Skipping '${codeName}' \u2014 AGT_API_KEY not set`);
4989
- return;
5108
+ return {
5109
+ decision: "skipped-no-api-key",
5110
+ spawnAttempted: false,
5111
+ sessionHealthyAfter: isSessionHealthy(codeName)
5112
+ };
4990
5113
  }
4991
5114
  const exchange = await exchangeApiKey(apiKey, false, { forceRefresh: true });
4992
5115
  claudeAuthMode = exchange.claudeAuthMode;
@@ -5000,26 +5123,42 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5000
5123
  persistentSessionAgents.delete(codeName);
5001
5124
  claudeAuthTupleBySession.delete(codeName);
5002
5125
  }
5003
- return;
5126
+ return {
5127
+ decision: "skipped-auth-resolve-failed",
5128
+ spawnAttempted: false,
5129
+ sessionHealthyAfter: isSessionHealthy(codeName),
5130
+ detail: msg
5131
+ };
5004
5132
  }
5005
5133
  if (claudeAuthMode === "subscription") {
5006
5134
  if (!agentRuntimeAuthenticated) {
5007
5135
  agentRuntimeAuthenticated = await checkClaudeAuth();
5008
5136
  if (!agentRuntimeAuthenticated) {
5009
5137
  log(`[persistent-session] Skipping '${codeName}' \u2014 Claude Code not authenticated (subscription mode)`);
5010
- return;
5138
+ return {
5139
+ decision: "skipped-not-authenticated",
5140
+ spawnAttempted: false,
5141
+ sessionHealthyAfter: isSessionHealthy(codeName)
5142
+ };
5011
5143
  }
5012
5144
  }
5013
5145
  } else if (!anthropicApiKey) {
5014
5146
  log(`[persistent-session] Skipping '${codeName}' \u2014 api_key mode but no key returned from exchange`);
5015
- return;
5147
+ return {
5148
+ decision: "skipped-api-key-mode-no-key",
5149
+ spawnAttempted: false,
5150
+ sessionHealthyAfter: isSessionHealthy(codeName)
5151
+ };
5016
5152
  }
5153
+ let restartTrigger = null;
5154
+ let dayRolloverDeferred = false;
5017
5155
  const currentAuthTuple = `${claudeAuthMode}:${anthropicApiKeyFingerprint ?? "none"}`;
5018
5156
  const recordedAuthTuple = claudeAuthTupleBySession.get(codeName);
5019
5157
  if (recordedAuthTuple && recordedAuthTuple !== currentAuthTuple && isSessionHealthy(codeName)) {
5020
5158
  log(`[persistent-session] Auth config changed for '${codeName}' (${recordedAuthTuple} \u2192 ${currentAuthTuple}) \u2014 restarting session`);
5021
5159
  stopPersistentSessionAndForgetMcpBaseline(codeName);
5022
5160
  persistentSessionAgents.delete(codeName);
5161
+ restartTrigger = "auth-tuple";
5023
5162
  }
5024
5163
  if (isStaleForToday(codeName) && isSessionHealthy(codeName)) {
5025
5164
  const current = peekCurrentSession(codeName);
@@ -5031,10 +5170,12 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5031
5170
  );
5032
5171
  stopPersistentSessionAndForgetMcpBaseline(codeName);
5033
5172
  persistentSessionAgents.delete(codeName);
5173
+ restartTrigger = "day-rollover";
5034
5174
  } else {
5035
5175
  log(
5036
5176
  `[persistent-session] Day rollover for '${codeName}' deferred \u2014 agent still active on session ${current.sessionId} (will retry next tick)`
5037
5177
  );
5178
+ dayRolloverDeferred = true;
5038
5179
  }
5039
5180
  }
5040
5181
  }
@@ -5081,7 +5222,12 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash2("sha256").
5081
5222
  });
5082
5223
  persistentSessionAgents.add(codeName);
5083
5224
  claudeAuthTupleBySession.set(codeName, currentAuthTuple);
5084
- return;
5225
+ return {
5226
+ decision: "spawn",
5227
+ spawnAttempted: true,
5228
+ sessionHealthyAfter: isSessionHealthy(codeName),
5229
+ detail: restartTrigger ? `trigger=${restartTrigger}` : void 0
5230
+ };
5085
5231
  }
5086
5232
  resetRestartCount(codeName);
5087
5233
  if (!claudeAuthTupleBySession.has(codeName)) {
@@ -5163,6 +5309,12 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash2("sha256").
5163
5309
  break;
5164
5310
  }
5165
5311
  }
5312
+ const finalDecision = restartTrigger === "auth-tuple" ? "restart-pending" : restartTrigger === "day-rollover" ? "day-rollover-restart" : dayRolloverDeferred ? "day-rollover-deferred" : "healthy";
5313
+ return {
5314
+ decision: finalDecision,
5315
+ spawnAttempted: false,
5316
+ sessionHealthyAfter: isSessionHealthy(codeName)
5317
+ };
5166
5318
  }
5167
5319
  var realtimeStarted = false;
5168
5320
  var realtimeDriftStarted = false;
@@ -7167,7 +7319,9 @@ export {
7167
7319
  applyRestartAcks,
7168
7320
  extractCharterSlackPeers,
7169
7321
  extractCharterTelegramPeers,
7322
+ hasRevokedResiduals,
7170
7323
  markAgentForFreshMemorySync,
7324
+ shouldSkipRevokedCleanup,
7171
7325
  startManager,
7172
7326
  stopManager
7173
7327
  };