@integrity-labs/agt-cli 0.23.1 → 0.23.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KANBAN_LOOP_COMMAND
3
- } from "./chunk-2TOCO5D2.js";
3
+ } from "./chunk-HSIESZMZ.js";
4
4
 
5
5
  // src/lib/persistent-session.ts
6
6
  import { spawn, execSync, execFileSync as execFileSync2 } from "child_process";
@@ -1167,4 +1167,4 @@ export {
1167
1167
  stopAllSessionsAndWait,
1168
1168
  getProjectDir
1169
1169
  };
1170
- //# sourceMappingURL=chunk-TCCBS3PD.js.map
1170
+ //# sourceMappingURL=chunk-ZKQGDH3T.js.map
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-XEA4SPAN.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-L4YIJJML.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-KUWBZW33.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-XCFWTH6T.js.map
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  SUPERVISOR_RESTART_EXIT_CODE,
3
3
  api,
4
+ estimateActiveTasksTokens,
4
5
  exchangeApiKey,
5
6
  extractCommandNotFound,
6
7
  getApiKey,
@@ -11,7 +12,7 @@ import {
11
12
  provisionOrientHook,
12
13
  provisionStopHook,
13
14
  requireHost
14
- } from "../chunk-Q4QD3TAC.js";
15
+ } from "../chunk-WUYKMPYW.js";
15
16
  import {
16
17
  findTaskByTemplate,
17
18
  getProjectDir as getProjectDir2,
@@ -46,7 +47,7 @@ import {
46
47
  stopAllSessionsAndWait,
47
48
  stopPersistentSession,
48
49
  takeZombieDetection
49
- } from "../chunk-TCCBS3PD.js";
50
+ } from "../chunk-ZKQGDH3T.js";
50
51
  import {
51
52
  KANBAN_CHECK_COMMAND,
52
53
  appendDmFooter,
@@ -62,7 +63,7 @@ import {
62
63
  resolveChannels,
63
64
  resolveDmTarget,
64
65
  wrapScheduledTaskPrompt
65
- } from "../chunk-2TOCO5D2.js";
66
+ } from "../chunk-HSIESZMZ.js";
66
67
 
67
68
  // src/lib/manager-worker.ts
68
69
  import { createHash as createHash2 } from "crypto";
@@ -552,6 +553,108 @@ function decideChannelRestart(input) {
552
553
  return { restart: true, added, removed };
553
554
  }
554
555
 
556
+ // src/lib/restart-breaker.ts
557
+ var DEFAULT_MAX = 2;
558
+ var DEFAULT_WINDOW_MS = 6e5;
559
+ function readEnvNumber(name, fallback) {
560
+ const raw = process.env[name];
561
+ if (!raw) return fallback;
562
+ const parsed = Number(raw);
563
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
564
+ }
565
+ var RestartBreaker = class {
566
+ max;
567
+ windowMs;
568
+ now;
569
+ events = /* @__PURE__ */ new Map();
570
+ trips = /* @__PURE__ */ new Map();
571
+ constructor(opts = {}) {
572
+ this.max = opts.max ?? readEnvNumber("AGT_RESTART_BREAKER_MAX", DEFAULT_MAX);
573
+ this.windowMs = opts.windowMs ?? readEnvNumber("AGT_RESTART_BREAKER_WINDOW_MS", DEFAULT_WINDOW_MS);
574
+ this.now = opts.now ?? Date.now;
575
+ if (!Number.isFinite(this.max) || this.max < 1) {
576
+ throw new Error("restart-breaker max must be a finite number >= 1");
577
+ }
578
+ if (!Number.isFinite(this.windowMs) || this.windowMs < 1e3) {
579
+ throw new Error("restart-breaker windowMs must be a finite number >= 1000");
580
+ }
581
+ }
582
+ /** True if this agent's breaker is currently tripped (manager must skip spawn). */
583
+ isTripped(codeName) {
584
+ return this.trips.has(codeName);
585
+ }
586
+ getTrip(codeName) {
587
+ return this.trips.get(codeName);
588
+ }
589
+ /**
590
+ * Record a restart event. If recording this event puts the count
591
+ * inside the window above `max`, the breaker trips and the call site
592
+ * should NOT respawn.
593
+ *
594
+ * Idempotent on already-tripped breakers: returns the existing trip
595
+ * without double-counting events. Callers may still record the reason
596
+ * via the decision-log for forensics.
597
+ */
598
+ record(codeName, reason) {
599
+ const existing = this.trips.get(codeName);
600
+ if (existing) {
601
+ return { tripped: false, trip: existing, windowCount: existing.eventsAtTrip.length };
602
+ }
603
+ const at = this.now();
604
+ const cutoff = at - this.windowMs;
605
+ const prior = (this.events.get(codeName) ?? []).filter((e) => e.at >= cutoff);
606
+ prior.push({ reason, at });
607
+ this.events.set(codeName, prior);
608
+ if (prior.length > this.max) {
609
+ const trip = {
610
+ trippedAt: at,
611
+ eventsAtTrip: [...prior],
612
+ statusMessage: formatStatusMessage(prior, this.windowMs)
613
+ };
614
+ this.trips.set(codeName, trip);
615
+ this.events.delete(codeName);
616
+ return { tripped: true, trip, windowCount: prior.length };
617
+ }
618
+ return { tripped: false, windowCount: prior.length };
619
+ }
620
+ /** Operator-initiated reset: drops the trip + the events log for this agent. */
621
+ clear(codeName) {
622
+ this.trips.delete(codeName);
623
+ this.events.delete(codeName);
624
+ }
625
+ /** Snapshot tripped agents for `manager-state.json`. */
626
+ serialize() {
627
+ return Object.fromEntries(this.trips.entries());
628
+ }
629
+ /**
630
+ * Rehydrate trip state from `manager-state.json`. Called once at
631
+ * worker startup. Window history is intentionally NOT persisted — only
632
+ * tripped state. A tripped breaker survives manager restart; an
633
+ * un-tripped one starts a fresh window in the new worker.
634
+ */
635
+ hydrate(saved) {
636
+ if (!saved) return;
637
+ for (const [codeName, trip] of Object.entries(saved)) {
638
+ if (trip && typeof trip.trippedAt === "number" && Array.isArray(trip.eventsAtTrip)) {
639
+ this.trips.set(codeName, trip);
640
+ }
641
+ }
642
+ }
643
+ /** Test helper — current in-window event count for `codeName`. */
644
+ windowCount(codeName) {
645
+ const cutoff = this.now() - this.windowMs;
646
+ return (this.events.get(codeName) ?? []).filter((e) => e.at >= cutoff).length;
647
+ }
648
+ };
649
+ function formatStatusMessage(events, windowMs) {
650
+ const last = events[events.length - 1];
651
+ const windowLabel = windowMs < 6e4 ? `${Math.round(windowMs / 1e3)}s` : `${(windowMs / 6e4).toFixed(1).replace(/\.0$/, "")}min`;
652
+ const reasonCounts = /* @__PURE__ */ new Map();
653
+ for (const e of events) reasonCounts.set(e.reason, (reasonCounts.get(e.reason) ?? 0) + 1);
654
+ const breakdown = Array.from(reasonCounts.entries()).map(([r, n]) => `${r}=${n}`).join(", ");
655
+ return `Circuit breaker tripped: ${events.length} restarts in ${windowLabel} (${breakdown}); most recent=${last.reason} at ${new Date(last.at).toISOString()}`;
656
+ }
657
+
555
658
  // src/lib/usage-banner-monitor.ts
556
659
  var MIN_CHECK_INTERVAL_MS = 6e4;
557
660
  var PANE_TAIL_LINES_FOR_BANNER = 200;
@@ -2286,7 +2389,38 @@ var knownVersions = /* @__PURE__ */ new Map();
2286
2389
  var knownStatuses = /* @__PURE__ */ new Map();
2287
2390
  var knownChannels = /* @__PURE__ */ new Map();
2288
2391
  var pendingSessionRestarts = /* @__PURE__ */ new Map();
2289
- function scheduleSessionRestart(codeName, delayMs, reason) {
2392
+ var restartBreaker = new RestartBreaker();
2393
+ var reportedTrips = /* @__PURE__ */ new Map();
2394
+ function recordRestartForBreaker(codeName, reason) {
2395
+ const result = restartBreaker.record(codeName, reason);
2396
+ if (!result.tripped || !result.trip) return;
2397
+ const trip = result.trip;
2398
+ log(
2399
+ `[persistent-session-decision] agent=${codeName} decision=circuit-tripped detail=${JSON.stringify(trip.statusMessage)} (ENG-5441)`
2400
+ );
2401
+ maybeReportCurrentTrip(codeName);
2402
+ }
2403
+ async function reportCircuitTripped(codeName, trip) {
2404
+ if (reportedTrips.get(codeName) === trip.trippedAt) return;
2405
+ const agentId = codeNameToAgentId.get(codeName);
2406
+ if (!agentId) throw new Error(`no agent_id known yet for '${codeName}' \u2014 will retry next poll`);
2407
+ await api.post("/host/circuit-breaker/trip", {
2408
+ agent_id: agentId,
2409
+ status_message: trip.statusMessage,
2410
+ tripped_at: new Date(trip.trippedAt).toISOString(),
2411
+ events: trip.eventsAtTrip.map((e) => ({ reason: e.reason, at: new Date(e.at).toISOString() }))
2412
+ });
2413
+ reportedTrips.set(codeName, trip.trippedAt);
2414
+ }
2415
+ function maybeReportCurrentTrip(codeName) {
2416
+ const trip = restartBreaker.getTrip(codeName);
2417
+ if (!trip) return;
2418
+ if (reportedTrips.get(codeName) === trip.trippedAt) return;
2419
+ reportCircuitTripped(codeName, trip).catch((err) => {
2420
+ log(`[circuit-breaker] Failed to report trip for '${codeName}' to API: ${err.message}`);
2421
+ });
2422
+ }
2423
+ function scheduleSessionRestart(codeName, delayMs, reason, breakerReason = "hot-reload-mcp") {
2290
2424
  const existing = pendingSessionRestarts.get(codeName);
2291
2425
  if (existing) {
2292
2426
  clearTimeout(existing);
@@ -2296,6 +2430,7 @@ function scheduleSessionRestart(codeName, delayMs, reason) {
2296
2430
  pendingSessionRestarts.delete(codeName);
2297
2431
  stopPersistentSession(codeName, log);
2298
2432
  runningMcpHashes.delete(codeName);
2433
+ recordRestartForBreaker(codeName, breakerReason);
2299
2434
  log(`[hot-reload] Session stopped for '${codeName}' \u2014 will respawn with ${reason}`);
2300
2435
  }, delayMs);
2301
2436
  timer.unref?.();
@@ -2331,11 +2466,14 @@ function projectMcpKeys(_codeName, projectDir) {
2331
2466
  return null;
2332
2467
  }
2333
2468
  }
2334
- function stopPersistentSessionAndForgetMcpBaseline(codeName) {
2469
+ function stopPersistentSessionAndForgetMcpBaseline(codeName, breakerReason) {
2335
2470
  cancelPendingSessionRestart(codeName);
2336
2471
  stopPersistentSession(codeName, log);
2337
2472
  runningMcpHashes.delete(codeName);
2338
2473
  runningMcpServerKeys.delete(codeName);
2474
+ if (breakerReason) {
2475
+ recordRestartForBreaker(codeName, breakerReason);
2476
+ }
2339
2477
  }
2340
2478
  function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
2341
2479
  const currentHash = projectMcpHash(codeName, projectDir);
@@ -2472,7 +2610,7 @@ var cachedFrameworkVersion = null;
2472
2610
  var lastVersionCheckAt = 0;
2473
2611
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
2474
2612
  var lastResponsivenessProbeAt = 0;
2475
- var agtCliVersion = true ? "0.23.1" : "dev";
2613
+ var agtCliVersion = true ? "0.23.2" : "dev";
2476
2614
  function resolveBrewPath(execFileSync4) {
2477
2615
  try {
2478
2616
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3368,7 +3506,7 @@ async function pollCycle() {
3368
3506
  log,
3369
3507
  resolveFramework: (codeName) => agentFrameworkCache.get(codeName) ?? null,
3370
3508
  stopSession: (codeName) => {
3371
- stopPersistentSessionAndForgetMcpBaseline(codeName);
3509
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "channel-set-change");
3372
3510
  persistentSessionAgents.delete(codeName);
3373
3511
  claudeAuthTupleBySession.delete(codeName);
3374
3512
  },
@@ -3429,7 +3567,7 @@ async function pollCycle() {
3429
3567
  }
3430
3568
  try {
3431
3569
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
3432
- const { collectDiagnostics } = await import("../persistent-session-XEA4SPAN.js");
3570
+ const { collectDiagnostics } = await import("../persistent-session-L4YIJJML.js");
3433
3571
  const diagCodeNames = [...persistentSessionAgents];
3434
3572
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
3435
3573
  let tailscaleHostname;
@@ -3480,7 +3618,7 @@ async function pollCycle() {
3480
3618
  const {
3481
3619
  collectResponsivenessProbes,
3482
3620
  getResponsivenessIntervalMs
3483
- } = await import("../responsiveness-probe-VWPOCABI.js");
3621
+ } = await import("../responsiveness-probe-YRLESO54.js");
3484
3622
  const probeIntervalMs = getResponsivenessIntervalMs();
3485
3623
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
3486
3624
  const probeCodeNames = [...persistentSessionAgents];
@@ -3678,7 +3816,11 @@ async function pollCycle() {
3678
3816
  ...state2,
3679
3817
  lastPollAt: (/* @__PURE__ */ new Date()).toISOString(),
3680
3818
  pollCount: state2.pollCount + 1,
3681
- agents: agentStates
3819
+ agents: agentStates,
3820
+ // ENG-5441: serialise trip state on every poll so manager restarts
3821
+ // never silently clear a tripped breaker. Cheap — only tripped
3822
+ // entries are stored, and the typical state is zero entries.
3823
+ circuitBreakerTrips: restartBreaker.serialize()
3682
3824
  };
3683
3825
  if (consecutivePollFailures > 0) {
3684
3826
  log(`[poll-backoff] recovered after ${consecutivePollFailures} failure(s), resuming normal interval`);
@@ -3713,6 +3855,7 @@ async function processAgent(agent, agentStates) {
3713
3855
  const previousKnownStatus = knownStatuses.get(agent.agent_id);
3714
3856
  agentDisplayNames.set(agent.code_name, agent.display_name);
3715
3857
  codeNameToAgentId.set(agent.code_name, agent.agent_id);
3858
+ maybeReportCurrentTrip(agent.code_name);
3716
3859
  if (agent.framework) {
3717
3860
  agentFrameworkCache.set(agent.code_name, agent.framework);
3718
3861
  }
@@ -3825,6 +3968,11 @@ async function processAgent(agent, agentStates) {
3825
3968
  if (previousStatus && previousStatus !== agent.status) {
3826
3969
  log(`Agent '${agent.code_name}' status changed: ${previousStatus} \u2192 ${agent.status}`);
3827
3970
  knownVersions.delete(agent.agent_id);
3971
+ if (previousStatus === "paused" && agent.status === "active" && restartBreaker.isTripped(agent.code_name)) {
3972
+ restartBreaker.clear(agent.code_name);
3973
+ reportedTrips.delete(agent.code_name);
3974
+ log(`[circuit-breaker] Cleared trip for '${agent.code_name}' on operator resume (paused \u2192 active)`);
3975
+ }
3828
3976
  let channelCacheMutated = false;
3829
3977
  for (const key of knownChannelConfigHashes.keys()) {
3830
3978
  if (key.startsWith(`${agent.agent_id}:`)) {
@@ -4756,7 +4904,12 @@ async function processAgent(agent, agentStates) {
4756
4904
  codeName: agent.code_name,
4757
4905
  mcpJson: mcpJsonParsed,
4758
4906
  sessionStartedAt: sess?.startedAt ?? null,
4759
- stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName),
4907
+ // ENG-5441: mcp-presence-reaper-driven restart counts against
4908
+ // the breaker. Each declared MCP has its own ENG-5279 give-up
4909
+ // cap (~3 cycles per server), but a thrashing collection of
4910
+ // MCPs can still rotate restarts of the whole session. The
4911
+ // breaker catches the rotating case the per-MCP cap can't see.
4912
+ stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName, "mcp-presence-reaper"),
4760
4913
  // ENG-5292: when the reaper gives up on a managed MCP (cap from
4761
4914
  // ENG-5279 + state-preservation from ENG-5285 both said "this
4762
4915
  // MCP keeps failing after 3 restart cycles"), mark the matching
@@ -5545,6 +5698,15 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5545
5698
  const projectDir = getProjectDir(codeName);
5546
5699
  const mcpConfigPath = join4(projectDir, ".mcp.json");
5547
5700
  const claudeMdPath = join4(projectDir, "CLAUDE.md");
5701
+ if (restartBreaker.isTripped(codeName)) {
5702
+ const trip = restartBreaker.getTrip(codeName);
5703
+ return {
5704
+ decision: "circuit-tripped",
5705
+ spawnAttempted: false,
5706
+ sessionHealthyAfter: isSessionHealthy(codeName),
5707
+ detail: trip.statusMessage
5708
+ };
5709
+ }
5548
5710
  const teamSettingsForTz = refreshData.team?.settings;
5549
5711
  const agentTimezone = (() => {
5550
5712
  const tz = teamSettingsForTz?.["timezone"];
@@ -5627,7 +5789,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5627
5789
  const recordedAuthTuple = claudeAuthTupleBySession.get(codeName);
5628
5790
  if (recordedAuthTuple && recordedAuthTuple !== currentAuthTuple && isSessionHealthy(codeName)) {
5629
5791
  log(`[persistent-session] Auth config changed for '${codeName}' (${recordedAuthTuple} \u2192 ${currentAuthTuple}) \u2014 restarting session`);
5630
- stopPersistentSessionAndForgetMcpBaseline(codeName);
5792
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "auth-tuple-change");
5631
5793
  persistentSessionAgents.delete(codeName);
5632
5794
  restartTrigger = "auth-tuple";
5633
5795
  }
@@ -5639,7 +5801,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5639
5801
  log(
5640
5802
  `[persistent-session] Day rollover for '${codeName}' (yesterday=${current.date}) \u2014 agent idle, restarting to mint fresh session`
5641
5803
  );
5642
- stopPersistentSessionAndForgetMcpBaseline(codeName);
5804
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "day-rollover");
5643
5805
  persistentSessionAgents.delete(codeName);
5644
5806
  restartTrigger = "day-rollover";
5645
5807
  } else {
@@ -5650,6 +5812,15 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5650
5812
  }
5651
5813
  }
5652
5814
  }
5815
+ if (restartBreaker.isTripped(codeName)) {
5816
+ const trip = restartBreaker.getTrip(codeName);
5817
+ return {
5818
+ decision: "circuit-tripped",
5819
+ spawnAttempted: false,
5820
+ sessionHealthyAfter: isSessionHealthy(codeName),
5821
+ detail: trip.statusMessage
5822
+ };
5823
+ }
5653
5824
  if (!isSessionHealthy(codeName)) {
5654
5825
  if (persistentSessionAgents.has(codeName)) {
5655
5826
  const ctx = getLastFailureContext(codeName);
@@ -7136,7 +7307,7 @@ async function processClaudePairSessions(agents) {
7136
7307
  killPairSession,
7137
7308
  pairTmuxSession,
7138
7309
  finalizeClaudePairOnboarding
7139
- } = await import("../claude-pair-runtime-KUWBZW33.js");
7310
+ } = await import("../claude-pair-runtime-XCFWTH6T.js");
7140
7311
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
7141
7312
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
7142
7313
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -7357,6 +7528,14 @@ function generateArtifacts(agent, refreshData, adapter) {
7357
7528
  };
7358
7529
  }
7359
7530
  }
7531
+ const activeTasksInjectEnabled = process.env["AGT_ACTIVE_TASKS_INJECT"] === "1";
7532
+ const activeTasksFromRefresh = activeTasksInjectEnabled ? refreshData.active_tasks ?? [] : void 0;
7533
+ if (activeTasksInjectEnabled) {
7534
+ const tokens = estimateActiveTasksTokens(activeTasksFromRefresh);
7535
+ log(
7536
+ `[provision/active-tasks] agent=${agent.code_name} count=${activeTasksFromRefresh?.length ?? 0} estimated_tokens=${tokens}`
7537
+ );
7538
+ }
7360
7539
  const provisionInput = {
7361
7540
  agent: refreshData.agent,
7362
7541
  charterFrontmatter,
@@ -7367,6 +7546,7 @@ function generateArtifacts(agent, refreshData, adapter) {
7367
7546
  deploymentTarget: "local_docker",
7368
7547
  gatewayPort: 9e3,
7369
7548
  team: refreshData.team ?? void 0,
7549
+ activeTasks: activeTasksFromRefresh,
7370
7550
  // ENG-5009: org name for the identity-preamble. Only present when
7371
7551
  // /host/refresh ships the field — falls through cleanly for older
7372
7552
  // API payloads that don't carry it.
@@ -7715,6 +7895,11 @@ function startManager(opts) {
7715
7895
  state2.agents = parsed.agents;
7716
7896
  log(`[startup] rehydrated ${state2.agents.length} agent state(s) from ${stateFile}`);
7717
7897
  }
7898
+ if (parsed.circuitBreakerTrips && typeof parsed.circuitBreakerTrips === "object") {
7899
+ restartBreaker.hydrate(parsed.circuitBreakerTrips);
7900
+ const n = Object.keys(parsed.circuitBreakerTrips).length;
7901
+ if (n > 0) log(`[startup] rehydrated ${n} circuit-breaker trip(s) \u2014 agents will remain paused until manually resumed`);
7902
+ }
7718
7903
  }
7719
7904
  } catch (err) {
7720
7905
  log(`[startup] state rehydration failed (continuing with empty state): ${err.message}`);