@integrity-labs/agt-cli 0.28.410 → 0.28.412

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/bin/agt.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-QZEA7FWB.js";
43
+ } from "../chunk-5ES25OE6.js";
44
44
  import {
45
45
  AnchorSessionClient,
46
46
  CHANNEL_REGISTRY,
@@ -70,7 +70,7 @@ import {
70
70
  renderTemplate,
71
71
  resolveChannels,
72
72
  serializeManifestForSlackCli
73
- } from "../chunk-YLLK4G3B.js";
73
+ } from "../chunk-QZUKHEMO.js";
74
74
  import "../chunk-XWVM4KPK.js";
75
75
 
76
76
  // src/bin/agt.ts
@@ -4829,7 +4829,7 @@ import { execFileSync, execSync } from "child_process";
4829
4829
  import { existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
4830
4830
  import chalk18 from "chalk";
4831
4831
  import ora16 from "ora";
4832
- var cliVersion = true ? "0.28.410" : "dev";
4832
+ var cliVersion = true ? "0.28.412" : "dev";
4833
4833
  async function fetchLatestVersion() {
4834
4834
  const host2 = getHost();
4835
4835
  if (!host2) return null;
@@ -6001,7 +6001,7 @@ function handleError(err) {
6001
6001
  }
6002
6002
 
6003
6003
  // src/bin/agt.ts
6004
- var cliVersion2 = true ? "0.28.410" : "dev";
6004
+ var cliVersion2 = true ? "0.28.412" : "dev";
6005
6005
  var program = new Command();
6006
6006
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
6007
6007
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -25,7 +25,7 @@ import {
25
25
  resolveConnectivityProbe,
26
26
  worseConnectivityOutcome,
27
27
  wrapScheduledTaskPrompt
28
- } from "./chunk-YLLK4G3B.js";
28
+ } from "./chunk-QZUKHEMO.js";
29
29
  import {
30
30
  parsePsRows
31
31
  } from "./chunk-XWVM4KPK.js";
@@ -6247,7 +6247,7 @@ function requireHost() {
6247
6247
  }
6248
6248
 
6249
6249
  // src/lib/api-client.ts
6250
- var agtCliVersion = true ? "0.28.410" : "dev";
6250
+ var agtCliVersion = true ? "0.28.412" : "dev";
6251
6251
  var lastConfigHash = null;
6252
6252
  function setConfigHash(hash) {
6253
6253
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -8751,4 +8751,4 @@ export {
8751
8751
  managerInstallSystemUnitCommand,
8752
8752
  managerUninstallSystemUnitCommand
8753
8753
  };
8754
- //# sourceMappingURL=chunk-QZEA7FWB.js.map
8754
+ //# sourceMappingURL=chunk-5ES25OE6.js.map
@@ -10242,10 +10242,65 @@ async function execFilePromiseLong(cmd, args, opts) {
10242
10242
  });
10243
10243
  }
10244
10244
 
10245
+ // src/lib/turn-outcome-tracker.ts
10246
+ function isFailure(outcome) {
10247
+ return outcome === "no_reply" || outcome === "failed";
10248
+ }
10249
+ function emptyHealth() {
10250
+ return { lastOutcome: null, lastRepliedAt: null, lastAttemptAt: null, consecutiveFailures: 0 };
10251
+ }
10252
+ var DEFAULT_TURN_FAILURE_WARN_THRESHOLD = 3;
10253
+ var TurnOutcomeTracker = class {
10254
+ health = /* @__PURE__ */ new Map();
10255
+ warned = /* @__PURE__ */ new Set();
10256
+ threshold;
10257
+ constructor(threshold = DEFAULT_TURN_FAILURE_WARN_THRESHOLD) {
10258
+ if (!Number.isInteger(threshold) || threshold < 1) {
10259
+ throw new Error(`turn-outcome threshold must be an integer >= 1 (got ${threshold})`);
10260
+ }
10261
+ this.threshold = threshold;
10262
+ }
10263
+ /** Record one observed turn outcome for one agent. */
10264
+ record(codeName, outcome, now = Date.now()) {
10265
+ const current = this.health.get(codeName) ?? emptyHealth();
10266
+ const next = {
10267
+ lastOutcome: outcome,
10268
+ lastAttemptAt: now,
10269
+ lastRepliedAt: outcome === "replied" ? now : current.lastRepliedAt,
10270
+ // Only a real reply clears the streak. A `declined` or a fire-and-forget
10271
+ // `admitted` leaves it exactly where it was: neither proves the agent can
10272
+ // answer, so neither should be able to mask an ongoing wedge.
10273
+ consecutiveFailures: isFailure(outcome) ? current.consecutiveFailures + 1 : outcome === "replied" ? 0 : current.consecutiveFailures
10274
+ };
10275
+ this.health.set(codeName, next);
10276
+ if (outcome === "replied") {
10277
+ const wasWarned = this.warned.delete(codeName);
10278
+ return { health: next, shouldWarn: false, recovered: wasWarned };
10279
+ }
10280
+ const crossed = next.consecutiveFailures >= this.threshold && !this.warned.has(codeName);
10281
+ if (crossed) this.warned.add(codeName);
10282
+ return { health: next, shouldWarn: crossed, recovered: false };
10283
+ }
10284
+ /** Current turn health for an agent, or null if no turn has been observed. */
10285
+ get(codeName) {
10286
+ return this.health.get(codeName) ?? null;
10287
+ }
10288
+ /**
10289
+ * Drop all state for an agent. Called when its serve is torn down, so a fresh
10290
+ * serve is not born already carrying the dead one's failure streak.
10291
+ */
10292
+ reset(codeName) {
10293
+ this.health.delete(codeName);
10294
+ this.warned.delete(codeName);
10295
+ }
10296
+ };
10297
+
10245
10298
  // src/lib/opencode-session.ts
10246
10299
  var OPENCODE_BIN = process.env["AGT_OPENCODE_BIN"]?.trim() || "opencode";
10247
10300
  var OPENCODE_RUN_TIMEOUT_MS = Number(process.env["AGT_OPENCODE_RUN_TIMEOUT_MS"]) || 18e4;
10248
10301
  var sessions = /* @__PURE__ */ new Map();
10302
+ var loggers = /* @__PURE__ */ new Map();
10303
+ var turnOutcomeTracker = new TurnOutcomeTracker();
10249
10304
  var bridges = /* @__PURE__ */ new Map();
10250
10305
  function opencodeTmuxSession(codeName) {
10251
10306
  return `agt-oc-${codeName}`;
@@ -10425,7 +10480,19 @@ function findFreePort() {
10425
10480
  }
10426
10481
  async function startOpencodeSession(config) {
10427
10482
  const { codeName, log: log2 } = config;
10483
+ loggers.set(codeName, log2);
10428
10484
  const existing = sessions.get(codeName);
10485
+ if (existing && existing.status === "running" && !isOpencodeSessionHealthy(codeName)) {
10486
+ log2(
10487
+ `[opencode-session] '${codeName}' stopped out-of-band (tmux session gone, in-memory state pinned to port ${existing.port ?? "?"}) \u2014 respawning on a fresh port (ENG-7995)`
10488
+ );
10489
+ existing.status = "crashed";
10490
+ existing.restartCount++;
10491
+ existing.port = null;
10492
+ existing.password = null;
10493
+ bridges.delete(codeName);
10494
+ stopTranscriptRefresher(codeName);
10495
+ }
10429
10496
  if (existing && existing.status === "running") return existing;
10430
10497
  const restartCount = existing?.restartCount ?? 0;
10431
10498
  if (existing?.status === "crashed" && existing.startedAt) {
@@ -10574,7 +10641,29 @@ async function injectOpencodeMessage(codeName, msg, opts = {}) {
10574
10641
  return { status: "declined", reason: "server_not_running" };
10575
10642
  }
10576
10643
  const bridge = getBridge(codeName, session.port, session.password);
10577
- return bridge.handleInbound(msg, { gate: opts.gate, awaitReply: opts.awaitReply });
10644
+ try {
10645
+ const result = await bridge.handleInbound(msg, { gate: opts.gate, awaitReply: opts.awaitReply });
10646
+ const outcome = result.status === "declined" ? "declined" : result.status === "replied" && result.reply ? "replied" : opts.awaitReply === false ? "admitted" : "no_reply";
10647
+ noteOpencodeTurnOutcome(codeName, outcome, session);
10648
+ return result;
10649
+ } catch (err) {
10650
+ noteOpencodeTurnOutcome(codeName, "failed", session);
10651
+ throw err;
10652
+ }
10653
+ }
10654
+ function noteOpencodeTurnOutcome(codeName, outcome, startedOn) {
10655
+ if (sessions.get(codeName) !== startedOn || startedOn.status !== "running") return;
10656
+ const { health, shouldWarn, recovered } = turnOutcomeTracker.record(codeName, outcome);
10657
+ const log2 = loggers.get(codeName);
10658
+ if (!log2) return;
10659
+ if (shouldWarn) {
10660
+ const lastOk = health.lastRepliedAt ? `${Math.round((Date.now() - health.lastRepliedAt) / 1e3)}s ago` : "never";
10661
+ log2(
10662
+ `[turn-health] WARN: '${codeName}' has ${health.consecutiveFailures} consecutive turns with no reply (last successful turn: ${lastOk}; serve process alive=${isOpencodeSessionHealthy(codeName)}) \u2014 the serve is up but not completing turns (ENG-7996)`
10663
+ );
10664
+ } else if (recovered) {
10665
+ log2(`[turn-health] '${codeName}' completed a turn again after a no-reply streak (ENG-7996)`);
10666
+ }
10578
10667
  }
10579
10668
  function getBridge(codeName, port, password) {
10580
10669
  const cached = bridges.get(codeName);
@@ -10658,6 +10747,8 @@ function stopOpencodeSession(codeName, log2) {
10658
10747
  }
10659
10748
  stopTranscriptRefresher(codeName);
10660
10749
  bridges.delete(codeName);
10750
+ turnOutcomeTracker.reset(codeName);
10751
+ loggers.delete(codeName);
10661
10752
  const session = sessions.get(codeName);
10662
10753
  if (session) {
10663
10754
  session.status = "stopped";
@@ -10668,6 +10759,9 @@ function stopOpencodeSession(codeName, log2) {
10668
10759
  function getOpencodeSessionState(codeName) {
10669
10760
  return sessions.get(codeName) ?? null;
10670
10761
  }
10762
+ function getOpencodeTurnHealth(codeName) {
10763
+ return turnOutcomeTracker.get(codeName);
10764
+ }
10671
10765
  function stripUndefined(env2) {
10672
10766
  const out = {};
10673
10767
  for (const [k, v] of Object.entries(env2)) if (v !== void 0) out[k] = v;
@@ -12254,6 +12348,7 @@ function collectDiagnostics(codeNames, quarantineEntriesFor) {
12254
12348
  if (oc) {
12255
12349
  const serveAlive = isOpencodeSessionHealthy(codeName);
12256
12350
  const status = serveAlive ? oc.status : oc.status === "running" ? "crashed" : oc.status;
12351
+ const turn = getOpencodeTurnHealth(codeName);
12257
12352
  return {
12258
12353
  codeName,
12259
12354
  framework: "opencode",
@@ -12267,7 +12362,15 @@ function collectDiagnostics(codeNames, quarantineEntriesFor) {
12267
12362
  launchArgs: null,
12268
12363
  channelStatus: null,
12269
12364
  isolated: isolationMode(codeName) === "docker",
12270
- quarantinedChannels
12365
+ quarantinedChannels,
12366
+ ...turn ? {
12367
+ turnHealth: {
12368
+ lastOutcome: turn.lastOutcome,
12369
+ lastRepliedAt: turn.lastRepliedAt ? new Date(turn.lastRepliedAt).toISOString() : null,
12370
+ lastAttemptAt: turn.lastAttemptAt ? new Date(turn.lastAttemptAt).toISOString() : null,
12371
+ consecutiveFailures: turn.consecutiveFailures
12372
+ }
12373
+ } : {}
12271
12374
  };
12272
12375
  }
12273
12376
  const session = sessions2.get(codeName);
@@ -12454,6 +12557,7 @@ export {
12454
12557
  isOpencodeSessionHealthy,
12455
12558
  injectOpencodeMessage,
12456
12559
  stopOpencodeSession,
12560
+ getOpencodeSessionState,
12457
12561
  sessionTranscriptDir,
12458
12562
  transcriptActivityAgeSeconds,
12459
12563
  subagentActivityAgeSeconds,
@@ -12502,4 +12606,4 @@ export {
12502
12606
  stopAllSessionsAndWait,
12503
12607
  getProjectDir
12504
12608
  };
12505
- //# sourceMappingURL=chunk-YLLK4G3B.js.map
12609
+ //# sourceMappingURL=chunk-QZUKHEMO.js.map