@integrity-labs/agt-cli 0.28.1069 → 0.28.1071

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.
Files changed (26) hide show
  1. package/dist/bin/agt.js +5 -5
  2. package/dist/{chunk-FHUYRSCF.js → chunk-3KVS22KX.js} +2 -2
  3. package/dist/{chunk-CFJONCHN.js → chunk-64DXAOQQ.js} +33 -1
  4. package/dist/{chunk-CFJONCHN.js.map → chunk-64DXAOQQ.js.map} +1 -1
  5. package/dist/{chunk-CO2ODT4I.js → chunk-QDJ4K5ZW.js} +4 -4
  6. package/dist/{claude-pair-runtime-Z7SMBMUG.js → claude-pair-runtime-B6DFCLT3.js} +2 -2
  7. package/dist/lib/manager-worker.js +46 -35
  8. package/dist/lib/manager-worker.js.map +1 -1
  9. package/dist/mcp/direct-chat-channel.js +32 -0
  10. package/dist/mcp/index.js +32 -0
  11. package/dist/mcp/origami.js +32 -0
  12. package/dist/mcp/slack-channel.js +32 -0
  13. package/dist/mcp/telegram-channel.js +32 -0
  14. package/dist/{pane-fatal-error-EL336LRO.js → pane-fatal-error-JORQ5OPV.js} +58 -16
  15. package/dist/pane-fatal-error-JORQ5OPV.js.map +1 -0
  16. package/dist/{persistent-session-52A2G7D7.js → persistent-session-75DOT6JV.js} +3 -3
  17. package/dist/{responsiveness-probe-SJCP4V4X.js → responsiveness-probe-YFUIBZZV.js} +3 -3
  18. package/dist/{session-auth-dead-JJPNV3YQ.js → session-auth-dead-JN4EBUCN.js} +2 -2
  19. package/package.json +1 -1
  20. package/dist/pane-fatal-error-EL336LRO.js.map +0 -1
  21. /package/dist/{chunk-FHUYRSCF.js.map → chunk-3KVS22KX.js.map} +0 -0
  22. /package/dist/{chunk-CO2ODT4I.js.map → chunk-QDJ4K5ZW.js.map} +0 -0
  23. /package/dist/{claude-pair-runtime-Z7SMBMUG.js.map → claude-pair-runtime-B6DFCLT3.js.map} +0 -0
  24. /package/dist/{persistent-session-52A2G7D7.js.map → persistent-session-75DOT6JV.js.map} +0 -0
  25. /package/dist/{responsiveness-probe-SJCP4V4X.js.map → responsiveness-probe-YFUIBZZV.js.map} +0 -0
  26. /package/dist/{session-auth-dead-JJPNV3YQ.js.map → session-auth-dead-JN4EBUCN.js.map} +0 -0
@@ -37852,6 +37852,38 @@ var FLAG_REGISTRY = [
37852
37852
  // safety control, so mutations require explicit confirmation.
37853
37853
  sensitive: true
37854
37854
  },
37855
+ {
37856
+ key: "pane-fatal-restart-mode",
37857
+ description: `Pane fatal-error respawn (ENG-11278): the manager reads each claude-code agent pane and, when at least two consecutive most-recent turns ended in Claude Code's own "Prompt is too long \xB7 \u2026 \xB7 /clear to start fresh" / failed /compact result with no model output since, for two consecutive polls, treats the session as unable to answer. off = disabled, shadow = log "would force-fresh respawn" only, enforce = force a FRESH respawn (rotated session UUID, old transcript kept, inbound parked for replay) and raise a reaper_action alert, bounded by a per-agent budget (default 3 per 6h) after which it stops and raises a critical alert. The "Transcript writes are failing" status-bar banner alone only ever warns, in any mode but off. Declared safe value is shadow: observe-only, the absent-flag and flag-DB-error fallback. Flip to enforce centrally (stage or per org) to arm it.`,
37858
+ flagType: "enum",
37859
+ allowedValues: ["off", "shadow", "enforce"],
37860
+ // shadow, not enforce (Obi Wan c734a443aa3f on #6707). This is NEW
37861
+ // auto-respawn behaviour read host-side, and every sibling host-read mode
37862
+ // flag (channel-quarantine-mode, mcp-quarantine-mode, and the pre-flag
37863
+ // AGT_WEDGE_RESTART_MODE) ships shadow; ADR-0022's fail-safe direction is
37864
+ // dark. Enforce is a deliberate, audited central flip, effective within one
37865
+ // heartbeat with no manager restart. The case for enforce (it matches
37866
+ // Claude Code's own turn-error text, not a timing heuristic, and is
37867
+ // budget-bounded) is real, and is exactly what that flip is for.
37868
+ defaultValue: "shadow",
37869
+ // Operator escape hatch (highest precedence on the host). The reader is
37870
+ // hostFlagStore().getString('pane-fatal-restart-mode') in manager-worker;
37871
+ // there is no raw process.env read of it.
37872
+ envVar: "AGT_PANE_FATAL_RESTART_MODE",
37873
+ // Enforcement gate: weakening it (enforce -> shadow/off) drops a recovery
37874
+ // control, so mutations require explicit confirmation.
37875
+ sensitive: true,
37876
+ // DELIBERATE UNREACHABLE PLACEHOLDER - NOT A REAL THRESHOLD YET. Same
37877
+ // convention as `channel-quarantine-auto-resume` above: the reader lands in
37878
+ // apps/cli (manager-worker 1d-3), and the agt-cli version that first ships
37879
+ // it is not knowable before merge. Until bisected off the published dist,
37880
+ // the reach estimate reports every host `stale`, which says "unknown" out
37881
+ // loud instead of claiming reach. BEFORE relying on the reach modal for a
37882
+ // flip: bisect the first agt-cli whose manager-worker reads this key and
37883
+ // replace this value. Advisory only; a flip still takes effect on any host
37884
+ // that runs the reader.
37885
+ since: "0.29.0"
37886
+ },
37855
37887
  {
37856
37888
  key: "docker-hygiene",
37857
37889
  description: "Manager-side docker hygiene on Docker-isolated hosts (ENG-8401): off = no-op, reap-superseded = after a successful agt-runtime retag, remove the prior untagged agt-runtime images that retag orphaned (Layer 1), full = reap-superseded plus a daily dangling-only image+builder prune (Layer 2). agt-runtime:latest and the just-pulled image are never removed and -a/--volumes are never used. Read host-side via hostFlagStore (getString); ships dark.",
package/dist/mcp/index.js CHANGED
@@ -30070,6 +30070,38 @@ var FLAG_REGISTRY = [
30070
30070
  // safety control, so mutations require explicit confirmation.
30071
30071
  sensitive: true
30072
30072
  },
30073
+ {
30074
+ key: "pane-fatal-restart-mode",
30075
+ description: `Pane fatal-error respawn (ENG-11278): the manager reads each claude-code agent pane and, when at least two consecutive most-recent turns ended in Claude Code's own "Prompt is too long \xB7 \u2026 \xB7 /clear to start fresh" / failed /compact result with no model output since, for two consecutive polls, treats the session as unable to answer. off = disabled, shadow = log "would force-fresh respawn" only, enforce = force a FRESH respawn (rotated session UUID, old transcript kept, inbound parked for replay) and raise a reaper_action alert, bounded by a per-agent budget (default 3 per 6h) after which it stops and raises a critical alert. The "Transcript writes are failing" status-bar banner alone only ever warns, in any mode but off. Declared safe value is shadow: observe-only, the absent-flag and flag-DB-error fallback. Flip to enforce centrally (stage or per org) to arm it.`,
30076
+ flagType: "enum",
30077
+ allowedValues: ["off", "shadow", "enforce"],
30078
+ // shadow, not enforce (Obi Wan c734a443aa3f on #6707). This is NEW
30079
+ // auto-respawn behaviour read host-side, and every sibling host-read mode
30080
+ // flag (channel-quarantine-mode, mcp-quarantine-mode, and the pre-flag
30081
+ // AGT_WEDGE_RESTART_MODE) ships shadow; ADR-0022's fail-safe direction is
30082
+ // dark. Enforce is a deliberate, audited central flip, effective within one
30083
+ // heartbeat with no manager restart. The case for enforce (it matches
30084
+ // Claude Code's own turn-error text, not a timing heuristic, and is
30085
+ // budget-bounded) is real, and is exactly what that flip is for.
30086
+ defaultValue: "shadow",
30087
+ // Operator escape hatch (highest precedence on the host). The reader is
30088
+ // hostFlagStore().getString('pane-fatal-restart-mode') in manager-worker;
30089
+ // there is no raw process.env read of it.
30090
+ envVar: "AGT_PANE_FATAL_RESTART_MODE",
30091
+ // Enforcement gate: weakening it (enforce -> shadow/off) drops a recovery
30092
+ // control, so mutations require explicit confirmation.
30093
+ sensitive: true,
30094
+ // DELIBERATE UNREACHABLE PLACEHOLDER - NOT A REAL THRESHOLD YET. Same
30095
+ // convention as `channel-quarantine-auto-resume` above: the reader lands in
30096
+ // apps/cli (manager-worker 1d-3), and the agt-cli version that first ships
30097
+ // it is not knowable before merge. Until bisected off the published dist,
30098
+ // the reach estimate reports every host `stale`, which says "unknown" out
30099
+ // loud instead of claiming reach. BEFORE relying on the reach modal for a
30100
+ // flip: bisect the first agt-cli whose manager-worker reads this key and
30101
+ // replace this value. Advisory only; a flip still takes effect on any host
30102
+ // that runs the reader.
30103
+ since: "0.29.0"
30104
+ },
30073
30105
  {
30074
30106
  key: "docker-hygiene",
30075
30107
  description: "Manager-side docker hygiene on Docker-isolated hosts (ENG-8401): off = no-op, reap-superseded = after a successful agt-runtime retag, remove the prior untagged agt-runtime images that retag orphaned (Layer 1), full = reap-superseded plus a daily dangling-only image+builder prune (Layer 2). agt-runtime:latest and the just-pulled image are never removed and -a/--volumes are never used. Read host-side via hostFlagStore (getString); ships dark.",
@@ -43993,6 +43993,38 @@ var FLAG_REGISTRY = [
43993
43993
  // safety control, so mutations require explicit confirmation.
43994
43994
  sensitive: true
43995
43995
  },
43996
+ {
43997
+ key: "pane-fatal-restart-mode",
43998
+ description: `Pane fatal-error respawn (ENG-11278): the manager reads each claude-code agent pane and, when at least two consecutive most-recent turns ended in Claude Code's own "Prompt is too long \xB7 \u2026 \xB7 /clear to start fresh" / failed /compact result with no model output since, for two consecutive polls, treats the session as unable to answer. off = disabled, shadow = log "would force-fresh respawn" only, enforce = force a FRESH respawn (rotated session UUID, old transcript kept, inbound parked for replay) and raise a reaper_action alert, bounded by a per-agent budget (default 3 per 6h) after which it stops and raises a critical alert. The "Transcript writes are failing" status-bar banner alone only ever warns, in any mode but off. Declared safe value is shadow: observe-only, the absent-flag and flag-DB-error fallback. Flip to enforce centrally (stage or per org) to arm it.`,
43999
+ flagType: "enum",
44000
+ allowedValues: ["off", "shadow", "enforce"],
44001
+ // shadow, not enforce (Obi Wan c734a443aa3f on #6707). This is NEW
44002
+ // auto-respawn behaviour read host-side, and every sibling host-read mode
44003
+ // flag (channel-quarantine-mode, mcp-quarantine-mode, and the pre-flag
44004
+ // AGT_WEDGE_RESTART_MODE) ships shadow; ADR-0022's fail-safe direction is
44005
+ // dark. Enforce is a deliberate, audited central flip, effective within one
44006
+ // heartbeat with no manager restart. The case for enforce (it matches
44007
+ // Claude Code's own turn-error text, not a timing heuristic, and is
44008
+ // budget-bounded) is real, and is exactly what that flip is for.
44009
+ defaultValue: "shadow",
44010
+ // Operator escape hatch (highest precedence on the host). The reader is
44011
+ // hostFlagStore().getString('pane-fatal-restart-mode') in manager-worker;
44012
+ // there is no raw process.env read of it.
44013
+ envVar: "AGT_PANE_FATAL_RESTART_MODE",
44014
+ // Enforcement gate: weakening it (enforce -> shadow/off) drops a recovery
44015
+ // control, so mutations require explicit confirmation.
44016
+ sensitive: true,
44017
+ // DELIBERATE UNREACHABLE PLACEHOLDER - NOT A REAL THRESHOLD YET. Same
44018
+ // convention as `channel-quarantine-auto-resume` above: the reader lands in
44019
+ // apps/cli (manager-worker 1d-3), and the agt-cli version that first ships
44020
+ // it is not knowable before merge. Until bisected off the published dist,
44021
+ // the reach estimate reports every host `stale`, which says "unknown" out
44022
+ // loud instead of claiming reach. BEFORE relying on the reach modal for a
44023
+ // flip: bisect the first agt-cli whose manager-worker reads this key and
44024
+ // replace this value. Advisory only; a flip still takes effect on any host
44025
+ // that runs the reader.
44026
+ since: "0.29.0"
44027
+ },
43996
44028
  {
43997
44029
  key: "docker-hygiene",
43998
44030
  description: "Manager-side docker hygiene on Docker-isolated hosts (ENG-8401): off = no-op, reap-superseded = after a successful agt-runtime retag, remove the prior untagged agt-runtime images that retag orphaned (Layer 1), full = reap-superseded plus a daily dangling-only image+builder prune (Layer 2). agt-runtime:latest and the just-pulled image are never removed and -a/--volumes are never used. Read host-side via hostFlagStore (getString); ships dark.",
@@ -38623,6 +38623,38 @@ var FLAG_REGISTRY = [
38623
38623
  // safety control, so mutations require explicit confirmation.
38624
38624
  sensitive: true
38625
38625
  },
38626
+ {
38627
+ key: "pane-fatal-restart-mode",
38628
+ description: `Pane fatal-error respawn (ENG-11278): the manager reads each claude-code agent pane and, when at least two consecutive most-recent turns ended in Claude Code's own "Prompt is too long \xB7 \u2026 \xB7 /clear to start fresh" / failed /compact result with no model output since, for two consecutive polls, treats the session as unable to answer. off = disabled, shadow = log "would force-fresh respawn" only, enforce = force a FRESH respawn (rotated session UUID, old transcript kept, inbound parked for replay) and raise a reaper_action alert, bounded by a per-agent budget (default 3 per 6h) after which it stops and raises a critical alert. The "Transcript writes are failing" status-bar banner alone only ever warns, in any mode but off. Declared safe value is shadow: observe-only, the absent-flag and flag-DB-error fallback. Flip to enforce centrally (stage or per org) to arm it.`,
38629
+ flagType: "enum",
38630
+ allowedValues: ["off", "shadow", "enforce"],
38631
+ // shadow, not enforce (Obi Wan c734a443aa3f on #6707). This is NEW
38632
+ // auto-respawn behaviour read host-side, and every sibling host-read mode
38633
+ // flag (channel-quarantine-mode, mcp-quarantine-mode, and the pre-flag
38634
+ // AGT_WEDGE_RESTART_MODE) ships shadow; ADR-0022's fail-safe direction is
38635
+ // dark. Enforce is a deliberate, audited central flip, effective within one
38636
+ // heartbeat with no manager restart. The case for enforce (it matches
38637
+ // Claude Code's own turn-error text, not a timing heuristic, and is
38638
+ // budget-bounded) is real, and is exactly what that flip is for.
38639
+ defaultValue: "shadow",
38640
+ // Operator escape hatch (highest precedence on the host). The reader is
38641
+ // hostFlagStore().getString('pane-fatal-restart-mode') in manager-worker;
38642
+ // there is no raw process.env read of it.
38643
+ envVar: "AGT_PANE_FATAL_RESTART_MODE",
38644
+ // Enforcement gate: weakening it (enforce -> shadow/off) drops a recovery
38645
+ // control, so mutations require explicit confirmation.
38646
+ sensitive: true,
38647
+ // DELIBERATE UNREACHABLE PLACEHOLDER - NOT A REAL THRESHOLD YET. Same
38648
+ // convention as `channel-quarantine-auto-resume` above: the reader lands in
38649
+ // apps/cli (manager-worker 1d-3), and the agt-cli version that first ships
38650
+ // it is not knowable before merge. Until bisected off the published dist,
38651
+ // the reach estimate reports every host `stale`, which says "unknown" out
38652
+ // loud instead of claiming reach. BEFORE relying on the reach modal for a
38653
+ // flip: bisect the first agt-cli whose manager-worker reads this key and
38654
+ // replace this value. Advisory only; a flip still takes effect on any host
38655
+ // that runs the reader.
38656
+ since: "0.29.0"
38657
+ },
38626
38658
  {
38627
38659
  key: "docker-hygiene",
38628
38660
  description: "Manager-side docker hygiene on Docker-isolated hosts (ENG-8401): off = no-op, reap-superseded = after a successful agt-runtime retag, remove the prior untagged agt-runtime images that retag orphaned (Layer 1), full = reap-superseded plus a daily dangling-only image+builder prune (Layer 2). agt-runtime:latest and the just-pulled image are never removed and -a/--volumes are never used. Read host-side via hostFlagStore (getString); ships dark.",
@@ -38729,6 +38729,38 @@ var FLAG_REGISTRY = [
38729
38729
  // safety control, so mutations require explicit confirmation.
38730
38730
  sensitive: true
38731
38731
  },
38732
+ {
38733
+ key: "pane-fatal-restart-mode",
38734
+ description: `Pane fatal-error respawn (ENG-11278): the manager reads each claude-code agent pane and, when at least two consecutive most-recent turns ended in Claude Code's own "Prompt is too long \xB7 \u2026 \xB7 /clear to start fresh" / failed /compact result with no model output since, for two consecutive polls, treats the session as unable to answer. off = disabled, shadow = log "would force-fresh respawn" only, enforce = force a FRESH respawn (rotated session UUID, old transcript kept, inbound parked for replay) and raise a reaper_action alert, bounded by a per-agent budget (default 3 per 6h) after which it stops and raises a critical alert. The "Transcript writes are failing" status-bar banner alone only ever warns, in any mode but off. Declared safe value is shadow: observe-only, the absent-flag and flag-DB-error fallback. Flip to enforce centrally (stage or per org) to arm it.`,
38735
+ flagType: "enum",
38736
+ allowedValues: ["off", "shadow", "enforce"],
38737
+ // shadow, not enforce (Obi Wan c734a443aa3f on #6707). This is NEW
38738
+ // auto-respawn behaviour read host-side, and every sibling host-read mode
38739
+ // flag (channel-quarantine-mode, mcp-quarantine-mode, and the pre-flag
38740
+ // AGT_WEDGE_RESTART_MODE) ships shadow; ADR-0022's fail-safe direction is
38741
+ // dark. Enforce is a deliberate, audited central flip, effective within one
38742
+ // heartbeat with no manager restart. The case for enforce (it matches
38743
+ // Claude Code's own turn-error text, not a timing heuristic, and is
38744
+ // budget-bounded) is real, and is exactly what that flip is for.
38745
+ defaultValue: "shadow",
38746
+ // Operator escape hatch (highest precedence on the host). The reader is
38747
+ // hostFlagStore().getString('pane-fatal-restart-mode') in manager-worker;
38748
+ // there is no raw process.env read of it.
38749
+ envVar: "AGT_PANE_FATAL_RESTART_MODE",
38750
+ // Enforcement gate: weakening it (enforce -> shadow/off) drops a recovery
38751
+ // control, so mutations require explicit confirmation.
38752
+ sensitive: true,
38753
+ // DELIBERATE UNREACHABLE PLACEHOLDER - NOT A REAL THRESHOLD YET. Same
38754
+ // convention as `channel-quarantine-auto-resume` above: the reader lands in
38755
+ // apps/cli (manager-worker 1d-3), and the agt-cli version that first ships
38756
+ // it is not knowable before merge. Until bisected off the published dist,
38757
+ // the reach estimate reports every host `stale`, which says "unknown" out
38758
+ // loud instead of claiming reach. BEFORE relying on the reach modal for a
38759
+ // flip: bisect the first agt-cli whose manager-worker reads this key and
38760
+ // replace this value. Advisory only; a flip still takes effect on any host
38761
+ // that runs the reader.
38762
+ since: "0.29.0"
38763
+ },
38732
38764
  {
38733
38765
  key: "docker-hygiene",
38734
38766
  description: "Manager-side docker hygiene on Docker-isolated hosts (ENG-8401): off = no-op, reap-superseded = after a successful agt-runtime retag, remove the prior untagged agt-runtime images that retag orphaned (Layer 1), full = reap-superseded plus a daily dangling-only image+builder prune (Layer 2). agt-runtime:latest and the just-pulled image are never removed and -a/--volumes are never used. Read host-side via hostFlagStore (getString); ships dark.",
@@ -14,8 +14,10 @@ var RULE = /^\s*[─━]{8,}/;
14
14
  var MODEL_HEAD = /^\s*⏺/;
15
15
  var STATUS_HEAD = /^[✻✢✳✶✽✺✹·*]/;
16
16
  var RESULT_LINE = /^\s+⎿\s*(.*)$/;
17
- var COMPACTION_FAILED = /^(?:Prompt is too long · automatic compaction failed\b|Error during compaction\b)/;
18
- var PROMPT_TOO_LONG = /^Prompt is too long\b.*\/clear to start fresh/;
17
+ var PROMPT_TOO_LONG_RENDER = /^Prompt is too long · (.+) · \/clear to start fresh(?: · [^·]+)?$/;
18
+ var COMPACTION_CLAUSE = /^automatic compaction failed\b/;
19
+ var COMPACT_COMMAND_FAILED = /^Error during compaction: \S/;
20
+ var COMPACT_COMMAND_HEAD = /^❯\s*\/compact(?:\s|$)/;
19
21
  var TRANSCRIPT_WRITES_FAILING = /Transcript writes are failing/;
20
22
  function headType(line) {
21
23
  if (MODEL_HEAD.test(line)) return "model";
@@ -31,25 +33,39 @@ function parseBlocks(lines) {
31
33
  for (const line of lines) {
32
34
  const head = headType(line);
33
35
  if (head === null) {
34
- if (turn && !turn.sawContinuation && line.trim().length > 0) {
35
- turn.sawContinuation = true;
36
- const m = RESULT_LINE.exec(line);
37
- turn.result = m ? m[1] : null;
36
+ if (turn) {
37
+ turn.linesAfterHead += 1;
38
+ if (line.trim().length > 0) {
39
+ turn.continuations += 1;
40
+ if (turn.continuations === 1 && turn.linesAfterHead === 1) {
41
+ const m = RESULT_LINE.exec(line);
42
+ turn.result = m ? m[1] : null;
43
+ } else {
44
+ turn.result = null;
45
+ }
46
+ }
38
47
  }
39
48
  continue;
40
49
  }
41
50
  turn = null;
42
51
  if (head === "model") blocks.push({ type: "model" });
43
52
  else if (head === "turn") {
44
- turn = { type: "turn", result: null, sawContinuation: false };
53
+ turn = {
54
+ type: "turn",
55
+ result: null,
56
+ compactCommand: COMPACT_COMMAND_HEAD.test(line),
57
+ linesAfterHead: 0,
58
+ continuations: 0
59
+ };
45
60
  blocks.push(turn);
46
61
  } else blocks.push({ type: "neutral" });
47
62
  }
48
63
  return blocks;
49
64
  }
50
- function fatalKindOf(result) {
51
- if (COMPACTION_FAILED.test(result)) return "compaction-failed";
52
- if (PROMPT_TOO_LONG.test(result)) return "prompt-too-long";
65
+ function fatalKindOf(result, compactCommand) {
66
+ const m = PROMPT_TOO_LONG_RENDER.exec(result);
67
+ if (m) return COMPACTION_CLAUSE.test(m[1]) ? "compaction-failed" : "prompt-too-long";
68
+ if (compactCommand && COMPACT_COMMAND_FAILED.test(result)) return "compaction-failed";
53
69
  return null;
54
70
  }
55
71
  function classifyPane(pane) {
@@ -71,7 +87,7 @@ function classifyPane(pane) {
71
87
  fatalTurns = 0;
72
88
  lastFatal = null;
73
89
  } else if (block.type === "turn" && block.result !== null) {
74
- const kind2 = fatalKindOf(block.result);
90
+ const kind2 = fatalKindOf(block.result, block.compactCommand);
75
91
  if (kind2) {
76
92
  fatalTurns += 1;
77
93
  lastFatal = kind2;
@@ -87,18 +103,18 @@ function classifyPane(pane) {
87
103
  function classifyPaneFatalError(pane) {
88
104
  return classifyPane(pane).kind;
89
105
  }
90
- function parseMode(raw) {
106
+ function parsePaneFatalMode(raw) {
91
107
  const v = (raw ?? "").trim().toLowerCase();
92
108
  if (v === "off" || v === "shadow" || v === "enforce") return v;
93
- return "enforce";
109
+ return "shadow";
94
110
  }
95
111
  function parseIntAtLeast(raw, fallback, floor) {
96
112
  const n = raw ? Number.parseInt(raw, 10) : NaN;
97
113
  return Number.isInteger(n) && n >= floor ? n : fallback;
98
114
  }
99
- function resolvePaneFatalConfig(env = process.env) {
115
+ function resolvePaneFatalConfig(mode, env = process.env) {
100
116
  return {
101
- mode: parseMode(env.AGT_PANE_FATAL_RESTART_MODE),
117
+ mode: parsePaneFatalMode(mode),
102
118
  minCycles: parseIntAtLeast(env.AGT_PANE_FATAL_MIN_CYCLES, DEFAULTS.minCycles, 2),
103
119
  maxRestartsInWindow: parseIntAtLeast(env.AGT_PANE_FATAL_MAX_RESTARTS, DEFAULTS.maxRestartsInWindow, 1),
104
120
  restartWindowMs: parseIntAtLeast(env.AGT_PANE_FATAL_WINDOW_MINUTES, DEFAULTS.restartWindowMs / 6e4, 30) * 6e4
@@ -115,13 +131,39 @@ function decidePaneFatalAction(input, config) {
115
131
  function pruneRestartHistory(history, nowMs, config) {
116
132
  return history.filter((t) => nowMs - t < config.restartWindowMs);
117
133
  }
134
+ var PANE_FATAL_ALERT_HEARTBEAT_MS = 10 * 60 * 1e3;
135
+ var AlertHeartbeat = class {
136
+ constructor(intervalMs = PANE_FATAL_ALERT_HEARTBEAT_MS) {
137
+ this.intervalMs = intervalMs;
138
+ }
139
+ lastSentMs = /* @__PURE__ */ new Map();
140
+ due(key, nowMs) {
141
+ const last = this.lastSentMs.get(key);
142
+ if (last !== void 0 && nowMs - last < this.intervalMs) return false;
143
+ this.lastSentMs.set(key, nowMs);
144
+ return true;
145
+ }
146
+ /** True when an episode is open for `key` (an event was sent and not cleared). */
147
+ active(key) {
148
+ return this.lastSentMs.has(key);
149
+ }
150
+ clear(key) {
151
+ this.lastSentMs.delete(key);
152
+ }
153
+ keys() {
154
+ return [...this.lastSentMs.keys()];
155
+ }
156
+ };
118
157
  export {
158
+ AlertHeartbeat,
119
159
  MIN_FATAL_TURNS,
160
+ PANE_FATAL_ALERT_HEARTBEAT_MS,
120
161
  classifyPane,
121
162
  classifyPaneFatalError,
122
163
  decidePaneFatalAction,
123
164
  isRespawnKind,
165
+ parsePaneFatalMode,
124
166
  pruneRestartHistory,
125
167
  resolvePaneFatalConfig
126
168
  };
127
- //# sourceMappingURL=pane-fatal-error-EL336LRO.js.map
169
+ //# sourceMappingURL=pane-fatal-error-JORQ5OPV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/pane-fatal-error.ts"],"sourcesContent":["/**\n * ENG-11278 — a session that answers every turn with a fatal error is not healthy.\n *\n * ## The incident\n *\n * Pedders host v8, 2026-10-02. marty sat for ~2h20m, and grace for hours, replying\n * to every inbound message with\n *\n * ⎿ Prompt is too long · automatic compaction failed: ENOENT: no such file or\n * directory, mkdir '/root/.claude/projects/-root--augmented-grace-project' ·\n * /clear to start fresh\n *\n * while manager.log said `decision=healthy … session_healthy_after=true` on every\n * poll. Nothing was going to change that: `isSessionHealthy` checks tmux + the\n * claude process (both fine), and the ENG-6153 wedge detector keys on UNDRAINED\n * inbound — but this session drains every message, it just answers each one with\n * the error. Its sibling condition, `⚠ Transcript writes are failing (ENOENT)`,\n * is drawn only in Claude Code's status bar (ENG-10957 found it by accident on\n * ada): no log line, no alert, health green.\n *\n * ## What this module decides\n *\n * Pure, no I/O, so the rules are unit-testable (same split as\n * `wedge-detection.ts`). The poll loop owns the pane capture, the per-agent\n * streak and the restart history, and performs the respawn.\n *\n * - {@link classifyPaneFatalError}: is the pane CURRENTLY showing a fatal,\n * self-perpetuating error? Returns a kind, never pane text.\n * - {@link decidePaneFatalAction}: given that verdict, the streak and the\n * recent restarts, what should the manager do this cycle?\n *\n * ## Reading the pane: blocks, not lines (Obi Wan 1436d0c3a9e5, d2dc0c460de5, 50a5cb896fad)\n *\n * The first version scanned every line for the fatal phrases and only excluded\n * `⏺` heads and `←` heads. That read the phrase anywhere else: on a continuation\n * line of the model's own reply, in a `⎿` tool result (a `Bash` that greps\n * manager.log), in a later line of an inbound message. Any of those could\n * force-respawn a working agent and page, and an untrusted sender could put\n * them there on purpose. So the pane is parsed the way Claude Code draws it.\n *\n * The capture is `tmux capture-pane -p -J` (wrapped lines joined), and splits\n * into three regions around the prompt box, whose rules (`────`) are always the\n * bottom of the screen:\n *\n * …transcript… <- turns, model output, status lines\n * ────────────── <- prompt box top\n * ❯ <what is being typed>\n * ────────────── <- prompt box bottom = the LAST rule on screen\n * ⏵⏵ bypass … · ⚠ Transcript writes are failing (ENOENT) <- status bar\n *\n * The transcript is a sequence of BLOCKS. A block head sits at column 0:\n * `⏺` model output (text or a tool call), `←` an inbound channel message, `❯`\n * a submitted prompt, a spinner glyph (`✻ Cogitated for 1m 29s · done 5:26 PM`)\n * for status. Everything indented under a head belongs to it, including the\n * `⎿` lines: a tool's output under its `⏺`, a tip under a spinner, and a turn's\n * RESULT under its `←`/`❯` head. Claude Code indents every continuation, and\n * `-J` stops a long line wrapping into a fake column-0 head, so a reply line\n * that happens to begin with `←` or `⎿` stays inside its `⏺` block.\n *\n * A genuine turn failure has exactly one shape — the turn's FIRST continuation\n * line is a `⎿` carrying Claude Code's error, attached directly to the inbound\n * or prompt that caused it (grace on v8, verbatim):\n *\n * ← telegram · 8837161157: Yes I think it’s best to start on standard…\n * ⎿ Prompt is too long · automatic compaction failed: ENOENT: … · /clear to start fresh\n *\n * ## Sender-controlled text (Obi Wan 2cfd32120169, 1620303032ce)\n *\n * Could an inbound's own text forge that `⎿` line? Claude Code 2.1.285's\n * `UserChannelMessage` (the `<channel source=…>` renderer) draws a channel\n * inbound as ONE line: `← <server> · <user>: ` + `truncate(body.replace(/\\s+/g,\n * \" \"), 60)`. Every newline in the body is collapsed to a space and the result\n * is cut at 60 columns with `…` (the v8 fixtures show exactly that). So in the\n * default view a sender cannot put anything on a line of its own. The one\n * exception found is the grouped view (`followsSpeakerLabel`), which renders\n * the body in full, multi-line, with no `←` head. That path was not observed on\n * any agent pane, but it is not ruled out, so the parser does not lean on the\n * rendering alone. A turn counts as failed only when ALL of these hold:\n *\n * 1. the `⎿` result is the line IMMEDIATELY after the head (no blank line,\n * no other line between);\n * 2. it is the turn block's ONLY non-blank continuation line. A head followed\n * by more indented lines is not a Claude Code verdict but multi-line text,\n * and the block is not counted at all;\n * 3. the text matches Claude Code's exact render, suffix included:\n * `Prompt is too long · <detail> · /clear to start fresh[ · <warning>]`.\n * `Error during compaction: …` (the result of a manual `/compact`) counts\n * only under a `❯ /compact` head typed at the console, never under a `←`\n * channel inbound, which cannot run a slash command.\n *\n * The residual, stated plainly: a byte-exact forged result as the sole extra\n * line directly under a single-line head, IF some render path drew it as an\n * indented continuation, is structurally identical to a real failure. The\n * rendering above is what closes that; re-check `UserChannelMessage` when\n * Claude Code is upgraded.\n *\n * Only that counts, and only when it is the state of the session NOW: at least\n * {@link MIN_FATAL_TURNS} consecutive turns ending that way with no model output\n * after the first of them (a wedged session fails EVERY turn; a recovered one\n * shows `⏺` after the old error). A single failed turn, a quoted phrase, or a\n * tool printing the log is never enough.\n *\n * The `Transcript writes are failing` banner is matched only in the status bar,\n * and on its own is NOT fatal: a session can show it and keep answering. It\n * yields `transcript-writes-failing`, which the decision maps to a warning\n * alert, never a respawn. Its root cause (an orphaned transcript bind) is\n * repaired by ENG-11277's mount probe.\n *\n * Hysteresis ({@link PaneFatalConfig.minCycles}) covers the remaining window, a\n * capture taken between an error and the model's next output.\n */\n\nexport type PaneFatalKind =\n /** `automatic compaction failed: …` or a manual `/compact` that errored. */\n | 'compaction-failed'\n /** `Prompt is too long · … · /clear to start fresh` without a compaction clause. */\n | 'prompt-too-long'\n /**\n * Claude Code's status-bar warning that the session transcript cannot be\n * written, with no failing turns. Warn-only: never a respawn on its own.\n */\n | 'transcript-writes-failing';\n\n/** Kinds that mean the session cannot answer at all, so a fresh respawn is the remedy. */\nexport function isRespawnKind(kind: PaneFatalKind | null): kind is 'compaction-failed' | 'prompt-too-long' {\n return kind === 'compaction-failed' || kind === 'prompt-too-long';\n}\n\nexport interface PaneFatalVerdict {\n /** Respawn kind when the session is failing every turn; else the banner kind; else null. */\n kind: PaneFatalKind | null;\n /** Consecutive most-recent turns whose result was a fatal error. */\n fatalTurns: number;\n /** `Transcript writes are failing` is in the status bar. */\n transcriptBanner: boolean;\n}\n\nexport type PaneFatalMode = 'off' | 'shadow' | 'enforce';\n\nexport interface PaneFatalConfig {\n mode: PaneFatalMode;\n /** Consecutive poll cycles a kind must be seen before acting. */\n minCycles: number;\n /** Max fatal-error respawns per agent inside {@link restartWindowMs}. */\n maxRestartsInWindow: number;\n /** Sliding window for {@link maxRestartsInWindow}. */\n restartWindowMs: number;\n}\n\nconst DEFAULTS = {\n minCycles: 2,\n maxRestartsInWindow: 3,\n restartWindowMs: 6 * 60 * 60 * 1000,\n} as const;\n\n/**\n * Consecutive failed turns required on one screen. Grace's pane shows the error\n * once per inbound; one failed turn is a bad turn, two in a row with nothing\n * from the model in between is a session that cannot answer.\n */\nexport const MIN_FATAL_TURNS = 2;\n\n/** A prompt-box rule. Claude Code draws it full width; 8 is a floor, not a guess at width. */\nconst RULE = /^\\s*[─━]{8,}/;\n/** `⏺` anywhere a line starts (indent tolerated): any model output resets the streak. */\nconst MODEL_HEAD = /^\\s*⏺/;\n/** Spinner / timing heads: `✻ Cogitated for 1m 29s · done 5:26 PM`, `* Churning…`. */\nconst STATUS_HEAD = /^[✻✢✳✶✽✺✹·*]/;\n/** A turn's result line: indented, then `⎿`. Captures the text after the glyph. */\nconst RESULT_LINE = /^\\s+⎿\\s*(.*)$/;\n/**\n * The error text Claude Code renders as a turn result, anchored at the START of\n * the `⎿` text so a result that merely contains the phrase does not match.\n */\n// Claude Code 2.1.285 renders a turn error as `[v, \" · /clear to start fresh\", q ? ` · ${q}` : \"\"]`\n// where `v` starts `Prompt is too long · `: so the clear suffix is mandatory and\n// may be followed by one ` · <warning>` segment and nothing else.\nconst PROMPT_TOO_LONG_RENDER = /^Prompt is too long · (.+) · \\/clear to start fresh(?: · [^·]+)?$/;\nconst COMPACTION_CLAUSE = /^automatic compaction failed\\b/;\n/** A manual `/compact`'s failure. Only valid under a `❯ /compact` head (see the module doc). */\nconst COMPACT_COMMAND_FAILED = /^Error during compaction: \\S/;\nconst COMPACT_COMMAND_HEAD = /^❯\\s*\\/compact(?:\\s|$)/;\nconst TRANSCRIPT_WRITES_FAILING = /Transcript writes are failing/;\n\ntype Block =\n | { type: 'model' }\n | { type: 'turn'; result: string | null; compactCommand: boolean }\n | { type: 'neutral' };\n\nfunction headType(line: string): 'model' | 'turn' | 'empty-prompt' | 'status' | 'other' | null {\n if (MODEL_HEAD.test(line)) return 'model';\n if (line.length === 0 || /^\\s/.test(line)) return null; // continuation\n if (line.startsWith('←')) return 'turn';\n if (line.startsWith('❯')) return line.slice(1).trim().length > 0 ? 'turn' : 'empty-prompt';\n if (STATUS_HEAD.test(line)) return 'status';\n return 'other';\n}\n\ninterface TurnBlock {\n type: 'turn';\n result: string | null;\n compactCommand: boolean;\n /** Physical lines seen since the head (blank or not). */\n linesAfterHead: number;\n /** Non-blank continuation lines seen since the head. */\n continuations: number;\n}\n\nfunction parseBlocks(lines: readonly string[]): Block[] {\n const blocks: Block[] = [];\n let turn: TurnBlock | null = null;\n for (const line of lines) {\n const head = headType(line);\n if (head === null) {\n // Continuation of the current block. For a turn, a result is only\n // Claude Code's verdict when it is the line IMMEDIATELY after the head\n // and the block's ONLY non-blank continuation (Obi Wan 2cfd32120169,\n // 1620303032ce). Anything else is multi-line text under the head, which\n // a sender can control, so the block is not counted.\n if (turn) {\n turn.linesAfterHead += 1;\n if (line.trim().length > 0) {\n turn.continuations += 1;\n if (turn.continuations === 1 && turn.linesAfterHead === 1) {\n const m = RESULT_LINE.exec(line);\n turn.result = m ? m[1]! : null;\n } else {\n turn.result = null; // a second line, or a gap first: not a verdict\n }\n }\n }\n continue;\n }\n turn = null;\n if (head === 'model') blocks.push({ type: 'model' });\n else if (head === 'turn') {\n turn = {\n type: 'turn',\n result: null,\n compactCommand: COMPACT_COMMAND_HEAD.test(line),\n linesAfterHead: 0,\n continuations: 0,\n };\n blocks.push(turn);\n } else blocks.push({ type: 'neutral' });\n }\n return blocks;\n}\n\nfunction fatalKindOf(\n result: string,\n compactCommand: boolean,\n): 'compaction-failed' | 'prompt-too-long' | null {\n const m = PROMPT_TOO_LONG_RENDER.exec(result);\n if (m) return COMPACTION_CLAUSE.test(m[1]!) ? 'compaction-failed' : 'prompt-too-long';\n if (compactCommand && COMPACT_COMMAND_FAILED.test(result)) return 'compaction-failed';\n return null;\n}\n\n/**\n * Parse a `tmux capture-pane -p -J` capture. Pure; never returns pane text.\n * An unreadable (null/empty) capture is \"can't tell\", which the caller treats\n * as healthy, never as a reason to respawn.\n */\nexport function classifyPane(pane: string | null | undefined): PaneFatalVerdict {\n const clean: PaneFatalVerdict = { kind: null, fatalTurns: 0, transcriptBanner: false };\n if (!pane) return clean;\n const lines = pane.split('\\n').map((l) => l.replace(/\\s+$/, ''));\n\n const rules: number[] = [];\n lines.forEach((l, i) => {\n if (RULE.test(l)) rules.push(i);\n });\n // No prompt box on screen: no status bar to read, so the banner fails closed.\n const lastRule = rules.length > 0 ? rules[rules.length - 1]! : -1;\n const statusBar = lastRule >= 0 ? lines.slice(lastRule + 1) : [];\n const transcriptEnd = rules.length >= 2 ? rules[rules.length - 2]! : lastRule >= 0 ? lastRule : lines.length;\n const transcriptBanner = statusBar.some((l) => TRANSCRIPT_WRITES_FAILING.test(l));\n\n let fatalTurns = 0;\n let lastFatal: 'compaction-failed' | 'prompt-too-long' | null = null;\n for (const block of parseBlocks(lines.slice(0, transcriptEnd))) {\n if (block.type === 'model') {\n fatalTurns = 0; // the model answered: whatever failed before is history\n lastFatal = null;\n } else if (block.type === 'turn' && block.result !== null) {\n const kind = fatalKindOf(block.result, block.compactCommand);\n if (kind) {\n fatalTurns += 1;\n lastFatal = kind;\n } else {\n fatalTurns = 0; // the turn produced a normal result (`/clear`, `/compact` ok)\n lastFatal = null;\n }\n }\n // A turn with no result yet is pending; neutral blocks say nothing.\n }\n\n const kind: PaneFatalKind | null =\n fatalTurns >= MIN_FATAL_TURNS && lastFatal\n ? lastFatal\n : transcriptBanner\n ? 'transcript-writes-failing'\n : null;\n return { kind, fatalTurns, transcriptBanner };\n}\n\n/** {@link classifyPane}'s kind alone. */\nexport function classifyPaneFatalError(pane: string | null | undefined): PaneFatalKind | null {\n return classifyPane(pane).kind;\n}\n\n/**\n * Normalise the `pane-fatal-restart-mode` flag value. The mode is a registry\n * flag (Obi Wan c734a443aa3f), resolved by the manager through the host flag\n * store: `AGT_PANE_FATAL_RESTART_MODE` (operator override) -> heartbeat value\n * (set centrally, per org/team/host) -> compiled default `shadow`. This module\n * stays pure and never reads the store; anything unrecognised is `shadow`, the\n * registry's declared safe value.\n */\nexport function parsePaneFatalMode(raw: string | undefined | null): PaneFatalMode {\n const v = (raw ?? '').trim().toLowerCase();\n if (v === 'off' || v === 'shadow' || v === 'enforce') return v;\n return 'shadow';\n}\n\nfunction parseIntAtLeast(raw: string | undefined, fallback: number, floor: number): number {\n const n = raw ? Number.parseInt(raw, 10) : NaN;\n return Number.isInteger(n) && n >= floor ? n : fallback;\n}\n\n/**\n * Config. `mode` comes from the `pane-fatal-restart-mode` registry flag (the\n * caller resolves it through the host flag store and passes it in). Tuning knobs\n * from the environment (not gates, so they stay env):\n * AGT_PANE_FATAL_MIN_CYCLES default 2, floor 2 (one sighting never acts)\n * AGT_PANE_FATAL_MAX_RESTARTS default 3 per window, floor 1\n * AGT_PANE_FATAL_WINDOW_MINUTES default 360, floor 30\n */\nexport function resolvePaneFatalConfig(\n mode: string | undefined | null,\n env: NodeJS.ProcessEnv = process.env,\n): PaneFatalConfig {\n return {\n mode: parsePaneFatalMode(mode),\n minCycles: parseIntAtLeast(env.AGT_PANE_FATAL_MIN_CYCLES, DEFAULTS.minCycles, 2),\n maxRestartsInWindow: parseIntAtLeast(env.AGT_PANE_FATAL_MAX_RESTARTS, DEFAULTS.maxRestartsInWindow, 1),\n restartWindowMs:\n parseIntAtLeast(env.AGT_PANE_FATAL_WINDOW_MINUTES, DEFAULTS.restartWindowMs / 60_000, 30) * 60_000,\n };\n}\n\nexport type PaneFatalAction =\n /** Nothing to do: no fatal kind, streak too short, or detection off. */\n | 'none'\n /**\n * The transcript-writes banner alone: the session is still answering, so do\n * NOT respawn it. Log and raise a warning recommending a restart (shadow and\n * enforce alike: a warning touches nothing).\n */\n | 'warn'\n /** Shadow mode: would restart. Log once per episode, do nothing. */\n | 'would-restart'\n /** Enforce: force a fresh respawn now. */\n | 'restart'\n /**\n * The restart budget is spent and the condition is back. A respawn has\n * demonstrably not cleared it, so another would be a restart loop: stop\n * restarting and escalate to a human instead.\n */\n | 'give-up';\n\n/**\n * What to do this cycle. `consecutiveCycles` INCLUDES this one; `recentRestartsMs`\n * are the timestamps of earlier fatal-error respawns of this agent.\n */\nexport function decidePaneFatalAction(\n input: {\n kind: PaneFatalKind | null;\n consecutiveCycles: number;\n recentRestartsMs: readonly number[];\n nowMs: number;\n },\n config: PaneFatalConfig,\n): PaneFatalAction {\n if (config.mode === 'off' || input.kind === null) return 'none';\n if (input.consecutiveCycles < config.minCycles) return 'none';\n if (!isRespawnKind(input.kind)) return 'warn';\n if (config.mode === 'shadow') return 'would-restart';\n const inWindow = input.recentRestartsMs.filter((t) => input.nowMs - t < config.restartWindowMs).length;\n return inWindow >= config.maxRestartsInWindow ? 'give-up' : 'restart';\n}\n\n/** Drop restart timestamps that have aged out of the window (bounded memory). */\nexport function pruneRestartHistory(\n history: readonly number[],\n nowMs: number,\n config: PaneFatalConfig,\n): number[] {\n return history.filter((t) => nowMs - t < config.restartWindowMs);\n}\n\n/**\n * How often an ongoing alert-worthy condition (the give-up, the banner warning)\n * is re-reported while it persists (Obi Wan 7dbeeba6c91e). `reaper_action`\n * alerts are closed as `expired` by cron/reaper-alert-sweeper.ts once\n * `payload.last_event_at` is older than AUGMENTED_REAPER_ALERT_QUIET_MINUTES\n * (default 30) and the sweeper never re-checks the condition. Sent once, a\n * give-up alert would expire ~30 min later with the agent still failing every\n * turn: the silent state this module exists to end. Ten minutes keeps\n * last_event_at fresh with two missed beats of headroom inside the default\n * window, the same shape as mcp-presence-reaper's onPaneBlocked, which keeps\n * reporting while the pane stays blocked. When the pane goes clean the beats\n * stop and the alert expires on its own.\n */\nexport const PANE_FATAL_ALERT_HEARTBEAT_MS = 10 * 60 * 1000;\n\n/**\n * Per-agent heartbeat for a condition alert. `due` answers \"send the event this\n * poll?\" and records the send; `clear` ends the episode so the next sighting\n * sends immediately. Pure state, no I/O, so the cadence is unit-testable.\n */\nexport class AlertHeartbeat {\n private readonly lastSentMs = new Map<string, number>();\n\n constructor(private readonly intervalMs: number = PANE_FATAL_ALERT_HEARTBEAT_MS) {}\n\n due(key: string, nowMs: number): boolean {\n const last = this.lastSentMs.get(key);\n if (last !== undefined && nowMs - last < this.intervalMs) return false;\n this.lastSentMs.set(key, nowMs);\n return true;\n }\n\n /** True when an episode is open for `key` (an event was sent and not cleared). */\n active(key: string): boolean {\n return this.lastSentMs.has(key);\n }\n\n clear(key: string): void {\n this.lastSentMs.delete(key);\n }\n\n keys(): string[] {\n return [...this.lastSentMs.keys()];\n }\n}\n"],"mappings":"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","names":["kind"]}
@@ -68,8 +68,8 @@ import {
68
68
  writeDirectChatSessionState,
69
69
  writeEgressAllowlist,
70
70
  writePersistentClaudeWrapper
71
- } from "./chunk-FHUYRSCF.js";
72
- import "./chunk-CFJONCHN.js";
71
+ } from "./chunk-3KVS22KX.js";
72
+ import "./chunk-64DXAOQQ.js";
73
73
  import "./chunk-OCMJ5E6J.js";
74
74
  import "./chunk-XWVM4KPK.js";
75
75
  import "./chunk-EL5MWGGH.js";
@@ -145,4 +145,4 @@ export {
145
145
  writeEgressAllowlist,
146
146
  writePersistentClaudeWrapper
147
147
  };
148
- //# sourceMappingURL=persistent-session-52A2G7D7.js.map
148
+ //# sourceMappingURL=persistent-session-75DOT6JV.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  paneLogPath
3
- } from "./chunk-FHUYRSCF.js";
4
- import "./chunk-CFJONCHN.js";
3
+ } from "./chunk-3KVS22KX.js";
4
+ import "./chunk-64DXAOQQ.js";
5
5
  import "./chunk-OCMJ5E6J.js";
6
6
  import "./chunk-XWVM4KPK.js";
7
7
  import "./chunk-EL5MWGGH.js";
@@ -802,4 +802,4 @@ export {
802
802
  readAndResetSlackReplyBindingClassifications,
803
803
  readAndResetSlackReplyTargetClassifications
804
804
  };
805
- //# sourceMappingURL=responsiveness-probe-SJCP4V4X.js.map
805
+ //# sourceMappingURL=responsiveness-probe-YFUIBZZV.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sessionTranscriptDir
3
- } from "./chunk-CFJONCHN.js";
3
+ } from "./chunk-64DXAOQQ.js";
4
4
  import "./chunk-DGUM43GV.js";
5
5
 
6
6
  // src/lib/session-auth-dead.ts
@@ -204,4 +204,4 @@ export {
204
204
  decideSessionAuthState,
205
205
  probeSessionAuth
206
206
  };
207
- //# sourceMappingURL=session-auth-dead-JJPNV3YQ.js.map
207
+ //# sourceMappingURL=session-auth-dead-JN4EBUCN.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.1069",
3
+ "version": "0.28.1071",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/pane-fatal-error.ts"],"sourcesContent":["/**\n * ENG-11278 — a session that answers every turn with a fatal error is not healthy.\n *\n * ## The incident\n *\n * Pedders host v8, 2026-10-02. marty sat for ~2h20m, and grace for hours, replying\n * to every inbound message with\n *\n * ⎿ Prompt is too long · automatic compaction failed: ENOENT: no such file or\n * directory, mkdir '/root/.claude/projects/-root--augmented-grace-project' ·\n * /clear to start fresh\n *\n * while manager.log said `decision=healthy … session_healthy_after=true` on every\n * poll. Nothing was going to change that: `isSessionHealthy` checks tmux + the\n * claude process (both fine), and the ENG-6153 wedge detector keys on UNDRAINED\n * inbound — but this session drains every message, it just answers each one with\n * the error. Its sibling condition, `⚠ Transcript writes are failing (ENOENT)`,\n * is drawn only in Claude Code's status bar (ENG-10957 found it by accident on\n * ada): no log line, no alert, health green.\n *\n * ## What this module decides\n *\n * Pure, no I/O, so the rules are unit-testable (same split as\n * `wedge-detection.ts`). The poll loop owns the pane capture, the per-agent\n * streak and the restart history, and performs the respawn.\n *\n * - {@link classifyPaneFatalError}: is the pane CURRENTLY showing a fatal,\n * self-perpetuating error? Returns a kind, never pane text.\n * - {@link decidePaneFatalAction}: given that verdict, the streak and the\n * recent restarts, what should the manager do this cycle?\n *\n * ## Reading the pane: blocks, not lines (Obi Wan 1436d0c3a9e5, d2dc0c460de5, 50a5cb896fad)\n *\n * The first version scanned every line for the fatal phrases and only excluded\n * `⏺` heads and `←` heads. That read the phrase anywhere else: on a continuation\n * line of the model's own reply, in a `⎿` tool result (a `Bash` that greps\n * manager.log), in a later line of an inbound message. Any of those could\n * force-respawn a working agent and page, and an untrusted sender could put\n * them there on purpose. So the pane is parsed the way Claude Code draws it.\n *\n * The capture is `tmux capture-pane -p -J` (wrapped lines joined), and splits\n * into three regions around the prompt box, whose rules (`────`) are always the\n * bottom of the screen:\n *\n * …transcript… <- turns, model output, status lines\n * ────────────── <- prompt box top\n * ❯ <what is being typed>\n * ────────────── <- prompt box bottom = the LAST rule on screen\n * ⏵⏵ bypass … · ⚠ Transcript writes are failing (ENOENT) <- status bar\n *\n * The transcript is a sequence of BLOCKS. A block head sits at column 0:\n * `⏺` model output (text or a tool call), `←` an inbound channel message, `❯`\n * a submitted prompt, a spinner glyph (`✻ Cogitated for 1m 29s · done 5:26 PM`)\n * for status. Everything indented under a head belongs to it, including the\n * `⎿` lines: a tool's output under its `⏺`, a tip under a spinner, and a turn's\n * RESULT under its `←`/`❯` head. Claude Code indents every continuation, and\n * `-J` stops a long line wrapping into a fake column-0 head, so a reply line\n * that happens to begin with `←` or `⎿` stays inside its `⏺` block.\n *\n * A genuine turn failure has exactly one shape — the turn's FIRST continuation\n * line is a `⎿` carrying Claude Code's error, attached directly to the inbound\n * or prompt that caused it (grace on v8, verbatim):\n *\n * ← telegram · 8837161157: Yes I think it’s best to start on standard…\n * ⎿ Prompt is too long · automatic compaction failed: ENOENT: … · /clear to start fresh\n *\n * Only that counts, and only when it is the state of the session NOW: at least\n * {@link MIN_FATAL_TURNS} consecutive turns ending that way with no model output\n * after the first of them (a wedged session fails EVERY turn; a recovered one\n * shows `⏺` after the old error). A single failed turn, a quoted phrase, or a\n * tool printing the log is never enough.\n *\n * The `Transcript writes are failing` banner is matched only in the status bar,\n * and on its own is NOT fatal: a session can show it and keep answering. It\n * yields `transcript-writes-failing`, which the decision maps to a warning\n * alert, never a respawn. Its root cause (an orphaned transcript bind) is\n * repaired by ENG-11277's mount probe.\n *\n * Hysteresis ({@link PaneFatalConfig.minCycles}) covers the remaining window, a\n * capture taken between an error and the model's next output.\n */\n\nexport type PaneFatalKind =\n /** `automatic compaction failed: …` or a manual `/compact` that errored. */\n | 'compaction-failed'\n /** `Prompt is too long · … · /clear to start fresh` without a compaction clause. */\n | 'prompt-too-long'\n /**\n * Claude Code's status-bar warning that the session transcript cannot be\n * written, with no failing turns. Warn-only: never a respawn on its own.\n */\n | 'transcript-writes-failing';\n\n/** Kinds that mean the session cannot answer at all, so a fresh respawn is the remedy. */\nexport function isRespawnKind(kind: PaneFatalKind | null): kind is 'compaction-failed' | 'prompt-too-long' {\n return kind === 'compaction-failed' || kind === 'prompt-too-long';\n}\n\nexport interface PaneFatalVerdict {\n /** Respawn kind when the session is failing every turn; else the banner kind; else null. */\n kind: PaneFatalKind | null;\n /** Consecutive most-recent turns whose result was a fatal error. */\n fatalTurns: number;\n /** `Transcript writes are failing` is in the status bar. */\n transcriptBanner: boolean;\n}\n\nexport type PaneFatalMode = 'off' | 'shadow' | 'enforce';\n\nexport interface PaneFatalConfig {\n mode: PaneFatalMode;\n /** Consecutive poll cycles a kind must be seen before acting. */\n minCycles: number;\n /** Max fatal-error respawns per agent inside {@link restartWindowMs}. */\n maxRestartsInWindow: number;\n /** Sliding window for {@link maxRestartsInWindow}. */\n restartWindowMs: number;\n}\n\nconst DEFAULTS = {\n minCycles: 2,\n maxRestartsInWindow: 3,\n restartWindowMs: 6 * 60 * 60 * 1000,\n} as const;\n\n/**\n * Consecutive failed turns required on one screen. Grace's pane shows the error\n * once per inbound; one failed turn is a bad turn, two in a row with nothing\n * from the model in between is a session that cannot answer.\n */\nexport const MIN_FATAL_TURNS = 2;\n\n/** A prompt-box rule. Claude Code draws it full width; 8 is a floor, not a guess at width. */\nconst RULE = /^\\s*[─━]{8,}/;\n/** `⏺` anywhere a line starts (indent tolerated): any model output resets the streak. */\nconst MODEL_HEAD = /^\\s*⏺/;\n/** Spinner / timing heads: `✻ Cogitated for 1m 29s · done 5:26 PM`, `* Churning…`. */\nconst STATUS_HEAD = /^[✻✢✳✶✽✺✹·*]/;\n/** A turn's result line: indented, then `⎿`. Captures the text after the glyph. */\nconst RESULT_LINE = /^\\s+⎿\\s*(.*)$/;\n/**\n * The error text Claude Code renders as a turn result, anchored at the START of\n * the `⎿` text so a result that merely contains the phrase does not match.\n */\nconst COMPACTION_FAILED = /^(?:Prompt is too long · automatic compaction failed\\b|Error during compaction\\b)/;\nconst PROMPT_TOO_LONG = /^Prompt is too long\\b.*\\/clear to start fresh/;\nconst TRANSCRIPT_WRITES_FAILING = /Transcript writes are failing/;\n\ntype Block =\n | { type: 'model' }\n | { type: 'turn'; result: string | null }\n | { type: 'neutral' };\n\nfunction headType(line: string): 'model' | 'turn' | 'empty-prompt' | 'status' | 'other' | null {\n if (MODEL_HEAD.test(line)) return 'model';\n if (line.length === 0 || /^\\s/.test(line)) return null; // continuation\n if (line.startsWith('←')) return 'turn';\n if (line.startsWith('❯')) return line.slice(1).trim().length > 0 ? 'turn' : 'empty-prompt';\n if (STATUS_HEAD.test(line)) return 'status';\n return 'other';\n}\n\nfunction parseBlocks(lines: readonly string[]): Block[] {\n const blocks: Block[] = [];\n let turn: { type: 'turn'; result: string | null; sawContinuation: boolean } | null = null;\n for (const line of lines) {\n const head = headType(line);\n if (head === null) {\n // Continuation of the current block. For a turn, only the FIRST non-blank\n // continuation line can be its result; anything after it is the message's\n // own text or later output, never Claude Code's verdict on the turn.\n if (turn && !turn.sawContinuation && line.trim().length > 0) {\n turn.sawContinuation = true;\n const m = RESULT_LINE.exec(line);\n turn.result = m ? m[1]! : null;\n }\n continue;\n }\n turn = null;\n if (head === 'model') blocks.push({ type: 'model' });\n else if (head === 'turn') {\n turn = { type: 'turn', result: null, sawContinuation: false };\n blocks.push(turn);\n } else blocks.push({ type: 'neutral' });\n }\n return blocks;\n}\n\nfunction fatalKindOf(result: string): 'compaction-failed' | 'prompt-too-long' | null {\n if (COMPACTION_FAILED.test(result)) return 'compaction-failed';\n if (PROMPT_TOO_LONG.test(result)) return 'prompt-too-long';\n return null;\n}\n\n/**\n * Parse a `tmux capture-pane -p -J` capture. Pure; never returns pane text.\n * An unreadable (null/empty) capture is \"can't tell\", which the caller treats\n * as healthy, never as a reason to respawn.\n */\nexport function classifyPane(pane: string | null | undefined): PaneFatalVerdict {\n const clean: PaneFatalVerdict = { kind: null, fatalTurns: 0, transcriptBanner: false };\n if (!pane) return clean;\n const lines = pane.split('\\n').map((l) => l.replace(/\\s+$/, ''));\n\n const rules: number[] = [];\n lines.forEach((l, i) => {\n if (RULE.test(l)) rules.push(i);\n });\n // No prompt box on screen: no status bar to read, so the banner fails closed.\n const lastRule = rules.length > 0 ? rules[rules.length - 1]! : -1;\n const statusBar = lastRule >= 0 ? lines.slice(lastRule + 1) : [];\n const transcriptEnd = rules.length >= 2 ? rules[rules.length - 2]! : lastRule >= 0 ? lastRule : lines.length;\n const transcriptBanner = statusBar.some((l) => TRANSCRIPT_WRITES_FAILING.test(l));\n\n let fatalTurns = 0;\n let lastFatal: 'compaction-failed' | 'prompt-too-long' | null = null;\n for (const block of parseBlocks(lines.slice(0, transcriptEnd))) {\n if (block.type === 'model') {\n fatalTurns = 0; // the model answered: whatever failed before is history\n lastFatal = null;\n } else if (block.type === 'turn' && block.result !== null) {\n const kind = fatalKindOf(block.result);\n if (kind) {\n fatalTurns += 1;\n lastFatal = kind;\n } else {\n fatalTurns = 0; // the turn produced a normal result (`/clear`, `/compact` ok)\n lastFatal = null;\n }\n }\n // A turn with no result yet is pending; neutral blocks say nothing.\n }\n\n const kind: PaneFatalKind | null =\n fatalTurns >= MIN_FATAL_TURNS && lastFatal\n ? lastFatal\n : transcriptBanner\n ? 'transcript-writes-failing'\n : null;\n return { kind, fatalTurns, transcriptBanner };\n}\n\n/** {@link classifyPane}'s kind alone. */\nexport function classifyPaneFatalError(pane: string | null | undefined): PaneFatalKind | null {\n return classifyPane(pane).kind;\n}\n\nfunction parseMode(raw: string | undefined): PaneFatalMode {\n const v = (raw ?? '').trim().toLowerCase();\n if (v === 'off' || v === 'shadow' || v === 'enforce') return v;\n // Default ENFORCE, unlike the ENG-6153 wedge detector's shadow default. That\n // detector infers a wedge from timing heuristics that can misread a long\n // turn; this one matches Claude Code's own fatal-error text, which no\n // working session displays as its latest result. The cost of the wrong\n // default is asymmetric too: shadow here is what v8 already had, an agent\n // answering every message with an error for hours while health read green.\n // The restart budget below bounds the downside of enforce.\n return 'enforce';\n}\n\nfunction parseIntAtLeast(raw: string | undefined, fallback: number, floor: number): number {\n const n = raw ? Number.parseInt(raw, 10) : NaN;\n return Number.isInteger(n) && n >= floor ? n : fallback;\n}\n\n/**\n * Config from the environment:\n * AGT_PANE_FATAL_RESTART_MODE off | shadow | enforce (default enforce)\n * AGT_PANE_FATAL_MIN_CYCLES default 2, floor 2 (one sighting never acts)\n * AGT_PANE_FATAL_MAX_RESTARTS default 3 per window, floor 1\n * AGT_PANE_FATAL_WINDOW_MINUTES default 360, floor 30\n */\nexport function resolvePaneFatalConfig(env: NodeJS.ProcessEnv = process.env): PaneFatalConfig {\n return {\n mode: parseMode(env.AGT_PANE_FATAL_RESTART_MODE),\n minCycles: parseIntAtLeast(env.AGT_PANE_FATAL_MIN_CYCLES, DEFAULTS.minCycles, 2),\n maxRestartsInWindow: parseIntAtLeast(env.AGT_PANE_FATAL_MAX_RESTARTS, DEFAULTS.maxRestartsInWindow, 1),\n restartWindowMs:\n parseIntAtLeast(env.AGT_PANE_FATAL_WINDOW_MINUTES, DEFAULTS.restartWindowMs / 60_000, 30) * 60_000,\n };\n}\n\nexport type PaneFatalAction =\n /** Nothing to do: no fatal kind, streak too short, or detection off. */\n | 'none'\n /**\n * The transcript-writes banner alone: the session is still answering, so do\n * NOT respawn it. Log and raise a warning recommending a restart (shadow and\n * enforce alike: a warning touches nothing).\n */\n | 'warn'\n /** Shadow mode: would restart. Log once per episode, do nothing. */\n | 'would-restart'\n /** Enforce: force a fresh respawn now. */\n | 'restart'\n /**\n * The restart budget is spent and the condition is back. A respawn has\n * demonstrably not cleared it, so another would be a restart loop: stop\n * restarting and escalate to a human instead.\n */\n | 'give-up';\n\n/**\n * What to do this cycle. `consecutiveCycles` INCLUDES this one; `recentRestartsMs`\n * are the timestamps of earlier fatal-error respawns of this agent.\n */\nexport function decidePaneFatalAction(\n input: {\n kind: PaneFatalKind | null;\n consecutiveCycles: number;\n recentRestartsMs: readonly number[];\n nowMs: number;\n },\n config: PaneFatalConfig,\n): PaneFatalAction {\n if (config.mode === 'off' || input.kind === null) return 'none';\n if (input.consecutiveCycles < config.minCycles) return 'none';\n if (!isRespawnKind(input.kind)) return 'warn';\n if (config.mode === 'shadow') return 'would-restart';\n const inWindow = input.recentRestartsMs.filter((t) => input.nowMs - t < config.restartWindowMs).length;\n return inWindow >= config.maxRestartsInWindow ? 'give-up' : 'restart';\n}\n\n/** Drop restart timestamps that have aged out of the window (bounded memory). */\nexport function pruneRestartHistory(\n history: readonly number[],\n nowMs: number,\n config: PaneFatalConfig,\n): number[] {\n return history.filter((t) => nowMs - t < config.restartWindowMs);\n}\n"],"mappings":"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","names":["kind"]}