@integrity-labs/agt-cli 0.28.946 → 0.28.948

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 +1 @@
1
- {"version":3,"sources":["../src/lib/responsiveness-probe.ts","../src/lib/interim-promise-scan.ts","../../../packages/core/src/channels/interim-promise.ts","../../../packages/core/src/channels/deflection-causes.ts"],"sourcesContent":["/**\n * ENG-5399 — Tier 1 responsiveness probe (manager-side).\n *\n * Cheap, fast-cadence canary that catches \"agent went silent\" inside\n * minutes, well before the existing synthetic-probe cron's ~35 min\n * staleness window (`SyntheticReplyAgeSeconds`, ENG-5122).\n *\n * Mechanism: for each managed agent, read the mtime of the agent's\n * `pane.log` and report `now - mtime` as `PaneActivityAgeSeconds` via\n * a new `/host/responsiveness-probe` endpoint. `pane.log` is the\n * tmux pipe-pane sink set up by `setupPaneLog()` — any visible\n * activity (assistant turns, tool calls, in-place progress\n * heartbeats) bumps its mtime. A silent agent has a steadily\n * climbing age that lands in CloudWatch and trips a per-agent alarm.\n *\n * ENG-6017 adds a second per-agent signal on the same cadence:\n * `pending_inbound_oldest_age_seconds` — the age of the oldest marker\n * file across the agent's `*-pending-inbound/` directories (written by\n * the channel MCP servers for inbounds awaiting delivery). This is the\n * one artifact of the \"message typed but never submitted\" failure mode\n * that every other canary is blind to: in the koda incident\n * (2026-06-04) an operator Slack DM sat undelivered for 40+ minutes\n * while pane-activity stayed fresh (health checks), synthetic probes\n * were answered by the one-shot fallback, and heartbeat/session-alive\n * only reflect manager health. The field is OMITTED (not zero) when the\n * agent has no pending-inbound markers — the API treats absent as\n * \"no signal\", never as \"healthy\" (absent-vs-zero matters for\n * mixed-version fleets where old CLIs don't report it at all).\n *\n * Run from `pollCycle()` in `manager-worker.ts` on a configurable\n * interval (default 60s via `AUGMENTED_RESPONSIVENESS_INTERVAL_MS`;\n * lowered from 5 min by ENG-7550 - this post is what refreshes\n * `agents.last_pane_activity_at`, the minutely busy-sampling substrate).\n */\n\nimport { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { outstandingPromiseOldestAgeSeconds } from './interim-promise-scan.js';\nimport { paneLogPath } from './persistent-session.js';\nimport { DEFLECTION_CAUSES } from '@augmented/core/channels/deflection-causes.js';\n\nexport interface ResponsivenessProbeResult {\n code_name: string;\n pane_activity_age_seconds: number;\n /**\n * ENG-6017: age (s) of the oldest marker file across the agent's\n * `*-pending-inbound/` directories. Omitted when no markers exist —\n * absent means \"no signal\", NOT \"zero / healthy\".\n */\n pending_inbound_oldest_age_seconds?: number;\n /**\n * ENG-10045: how many DEAD-LETTERED inbound markers are on the host for this\n * agent RIGHT NOW - the files in its `*-pending-inbound-stale/` dirs, which is\n * the `store: stale` set `debug_inspect_dead_letters` reads.\n *\n * ALWAYS PRESENT, INCLUDING ZERO, and that is the whole point of the field\n * rather than an oversight against the convention its neighbours follow.\n *\n * Every other counter here is omitted when empty, because for an AGE an absent\n * field honestly means \"no signal\". This one feeds a CloudWatch gauge whose\n * alarm carries `OKActions`, and on that path \"no datapoint\" and \"the publisher\n * stopped\" are the same observation - `TreatMissingData: 'notBreaching'` reads\n * both as fine and CLOSES the alert. An affirmative zero is the difference\n * between an OK that means \"nothing is stranded\" and an OK that means \"nobody\n * told me\". ENG-10045 exists because an alarm said the first while meaning\n * something closer to the second.\n */\n dead_lettered_inbound_markers?: number;\n /**\n * ENG-8265: age (s) of the oldest OUTSTANDING interim promise — an inbound the\n * agent answered with `interim: true` (\"the real answer is still coming\") and\n * has not yet answered substantively. Measured from the marker's `promised_at`,\n * so it survives a mid-turn respawn and cannot be reset by unrelated engagement.\n * Omitted when the agent owes no promise (the healthy steady state) — absent\n * means \"no signal\", NOT \"zero / healthy\". See\n * `@augmented/core/channels/interim-promise.js` for why a `seen_at` marker is\n * not a promise.\n */\n promise_outstanding_oldest_age_seconds?: number;\n /**\n * ENG-6327: per-cause inbound deflection counts since the last probe (read +\n * RESET from the channel servers' `<channel>-deflections.json` counter files).\n * Omitted when there were none. Keyed by DeflectionCause.\n */\n deflections?: Record<string, number>;\n /**\n * ENG-6408 / ADR-0024 Slice 1.5: shadow lane-classification counts since the\n * last probe (read + RESET from `<channel>-lane-classifications.json`). Keyed\n * by `<lane>|<expects_reply>|<source>` plus the reserved\n * `suspected_misclassification` canary. Omitted when there were none.\n */\n lane_classifications?: Record<string, number>;\n /**\n * WS0 (Slack cross-thread reply routing): SHADOW reply-target classification\n * counts since the last probe (read + RESET from\n * `<channel>-reply-target-classifications.json`). Keyed by\n * `<result>|<pending_threads_bucket>`. Omitted when there were none.\n */\n slack_reply_target_classifications?: Record<string, number>;\n /**\n * WS2 (Slack cross-thread reply routing): SHADOW reply-BINDING classification\n * counts since the last probe (read + RESET from\n * `<channel>-reply-binding-classifications.json`). Keyed by the binding\n * classification (bound / coords_corrected / ... / unknown_inbound_id).\n * Omitted when there were none.\n */\n slack_reply_binding_classifications?: Record<string, number>;\n /**\n * ENG-7855: slack-hot-thread-guard (ENG-7462) SHADOW/enforce outcome counts\n * since the last probe (read + RESET from\n * `<channel>-hot-thread-classifications.json`). Keyed by\n * `<mode>|<outcome>|<proactive>`. Omitted when there were none (the guard only\n * records in shadow/enforce, so off-mode agents never populate this).\n */\n slack_hot_thread_classifications?: Record<string, number>;\n /**\n * ENG-8346: direct-chat cursor advances that moved FEWER rows than were asked\n * for, since the last probe (read + RESET from\n * `<source>-cursor-advance-classifications.json`). Keyed by\n * `<route>|<reason>|<partial>`. Omitted when there were none — which is the\n * healthy state, so a populated field is always worth reading.\n */\n direct_chat_cursor_shortfall_classifications?: Record<string, number>;\n}\n\n/**\n * ENG-8090: what actually goes on the wire to `/host/responsiveness-probe`.\n *\n * Differs from `ResponsivenessProbeResult` in two ways, both because opencode\n * agents have no `pane.log`:\n *\n * - `pane_activity_age_seconds` is OPTIONAL. An opencode agent reports\n * occupancy and turn completion instead; the API tolerates its absence and\n * simply writes no `last_pane_activity_at` for that agent.\n * - two opencode-only fields carry the split signals.\n *\n * Kept as a separate type so `collectResponsivenessProbes` — whose result the\n * WEDGE detector consumes and which must keep requiring a real pane age — is not\n * widened. See `collectPanelessActivityProbes`.\n */\nexport interface ResponsivenessProbePayloadEntry\n extends Omit<ResponsivenessProbeResult, 'pane_activity_age_seconds'> {\n pane_activity_age_seconds?: number;\n /**\n * ENG-8090: seconds since the agent was last doing work — 0 while a turn is in\n * flight. The OCCUPANCY signal, and the substrate for busy/idle sampling and\n * agent-hours billing. Omitted when the manager has observed no turn for this\n * agent: absent is \"no signal\", never \"idle\".\n *\n * Deliberately NOT a liveness signal. A serve wedged mid-turn reports 0 here.\n */\n busy_activity_age_seconds?: number;\n /**\n * ENG-8090: seconds since a turn last produced real reply text. The LIVENESS\n * signal — \"turns are completing\", not \"the process printed something\" — and\n * the only one of the two that may resolve a synthetic probe. Omitted when no\n * turn has ever succeeded in this manager generation.\n */\n turn_completion_age_seconds?: number;\n /**\n * ENG-8116: MEASURED occupancy, as whole closed wall-clock minutes.\n *\n * This is what replaces cadence-counted buckets. The two `*_age_seconds`\n * fields above describe an INSTANT, which forced the API to infer duration\n * from how often probes arrived — and probe arrival is 87s apart at the fleet\n * median, 182s at p90, against a 60s sampling window. Those buckets are\n * measured host-side instead, so the total is invariant to how often this\n * payload is sent.\n *\n * Only closed minutes appear here; the in-progress minute is still accruing\n * and is reported once it closes. Absent means \"no occupancy to report\",\n * which is distinct from an agent that reported none because it is on an old\n * CLI — the API keeps the legacy freshness path for those.\n */\n busy_buckets?: Array<{ bucket: string; seconds: number }>;\n /**\n * ENG-8465: SHADOW of the occupancy-qualification gate — per-drain counts of\n * how the reported `busy_buckets` split against transcript evidence of real\n * work (`occupancy-gate.ts` + the merged `@augmented/core` qualifier). Keyed by\n * `qualified` (a bucket with a real turn nearby — the positive control),\n * `unqualified_dropped` (no qualifying turn near it — dropped under `enforce`,\n * still billed under `shadow`), and `unreadable` (transcripts unreadable, so\n * the buckets were credited UNGATED — fail-open). Omitted when there were none.\n * See KNOWN_OCCUPANCY_QUALIFICATION_OUTCOMES; loss on a failed POST is accepted\n * (shadow telemetry, never re-credited like `busy_buckets`).\n */\n occupancy_qualification_classifications?: Record<string, number>;\n}\n\n/**\n * ENG-8465: the only occupancy-qualification outcome keys. Mirrored EXACTLY in\n * packages/api/src/routes/host-runtime.ts and enforced by\n * scripts/check-classification-allowlist-parity.mjs — a key present on one side\n * only is silently dropped, reading as a zero that means \"never ingested\". Add a\n * new key to BOTH sets in the same change.\n */\nconst KNOWN_OCCUPANCY_QUALIFICATION_OUTCOMES = new Set([\n 'qualified',\n 'unqualified_dropped',\n 'unreadable',\n // ENG-8571: bucket kept (never dropped) because the bounded transcript tail did\n // not reach back far enough to judge it. MUST also be in the API-side set\n // (routes/host-runtime.ts) or the parity guard drops it to a silent zero.\n 'coverage_shortfall',\n // ENG-9178: bucket kept because it overlaps a COMPLETED tool-call bracket with\n // no qualifying turn in reach — the middle of a long tool call. Separate from\n // `qualified` so the flip can be sized against turn evidence and bracket\n // evidence independently. Mirror in the API set.\n 'qualified_by_bracket',\n // ENG-9178: bucket HELD (neither reported nor dropped) because it sits inside a\n // tool call that has not returned. Under `enforce` this is also the signal that\n // stands the legacy cron down for a drain that reported no buckets at all —\n // see the ENG-8625 note on hasOccupancyQualification. Mirror in the API set.\n 'bracket_deferred',\n]);\n\n/**\n * Build the wire classification record from a {@link QualifyBucketsResult}-shaped\n * count, keeping only known, positive-integer outcomes. Returns null when empty\n * so the caller omits the field entirely (absent = \"no signal\"). Keeping the\n * builder here — not in occupancy-gate.ts — keeps KNOWN_OCCUPANCY_QUALIFICATION_\n * OUTCOMES the single CLI-side allowlist the parity guard diffs against the API.\n */\nexport function occupancyQualificationClassifications(counts: {\n qualified: number;\n unqualifiedDropped: number;\n unreadable: number;\n coverageShortfall: number;\n qualifiedByBracket?: number;\n bracketDeferred?: number;\n}): Record<string, number> | null {\n const raw: Record<string, number> = {\n qualified: counts.qualified,\n unqualified_dropped: counts.unqualifiedDropped,\n unreadable: counts.unreadable,\n coverage_shortfall: counts.coverageShortfall,\n // ENG-9178. Optional on the input type so the manager's fail-open fallback\n // (which builds this record by hand when the qualifier throws) does not have\n // to know about outcomes it can never produce.\n qualified_by_bracket: counts.qualifiedByBracket ?? 0,\n bracket_deferred: counts.bracketDeferred ?? 0,\n };\n const out: Record<string, number> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (KNOWN_OCCUPANCY_QUALIFICATION_OUTCOMES.has(key) && Number.isSafeInteger(value) && value > 0) {\n out[key] = value;\n }\n }\n return Object.keys(out).length > 0 ? out : null;\n}\n\n/**\n * ENG-6327 — per-channel deflection counter file-layout contract. The channel\n * MCP servers increment `<channel>-deflections.json` in the agent home (via\n * recordChannelDeflection in packages/mcp/ack-reaction.ts); apps/cli does not\n * depend on @integrity-labs/mcp, so the manager re-implements the read against\n * this contract — the same read-only, no-IPC posture as the `*-pending-inbound`\n * scan above. Body is a JSON object keyed by cause with integer counts.\n */\nconst DEFLECTION_COUNTER_SUFFIX = '-deflections.json';\n// ENG-10054: this list used to be re-declared here. The comment it carried was\n// right about the danger - \"adding a cause in packages/mcp WITHOUT adding it\n// here produces a metric that is faithfully written and never observed\" - and a\n// parity test was built to enforce it. But the test only ever compared THIS copy\n// with the mcp one, and the drift happened in the THIRD copy nobody was\n// watching: the API ingest filter, missing `aged_out_unrecoverable` and\n// `marker_corrupt`. The warning was correct and the guard was pointed at the\n// wrong pair. There is now one list, in core, and no pair to keep in step.\n//\n// (The note above about apps/cli not depending on @integrity-labs/mcp still\n// holds - this imports @augmented/core, which apps/cli already depends on, so\n// the no-IPC read-only posture is unchanged.)\nconst KNOWN_DEFLECTION_CAUSES = DEFLECTION_CAUSES;\n\n/**\n * Read and RESET every `<channel>-deflections.json` in an agent home, returning\n * the summed counts by cause (or null when there are none). Consumes via atomic\n * rename-then-read-then-unlink so an increment racing the reset is carried into\n * the next window rather than lost. Only known causes are accumulated so a\n * corrupt file can't smuggle an unbounded dimension into CloudWatch.\n *\n * Reset-on-read is required: these are deltas-since-last-consume, so the metric\n * would over-count if they weren't cleared each cycle. The trade-off is that a\n * failed POST loses that cycle's counts (under-count) — acceptable for a\n * frequency metric and consistent with the other best-effort probe siblings;\n * unlike the in-memory give-up counter, file-based counts aren't re-credited\n * on POST failure.\n */\nexport function readAndResetChannelDeflections(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(DEFLECTION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming); // atomic; replaces any stale .consuming from a crashed cycle\n } catch {\n continue; // vanished / not a regular file — skip\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const cause of KNOWN_DEFLECTION_CAUSES) {\n const n = parsed[cause];\n if (typeof n === 'number' && Number.isFinite(n) && n > 0) {\n total[cause] = (total[cause] ?? 0) + Math.floor(n);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt — drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n/**\n * ENG-6408 / ADR-0024 Slice 1.5 - per-channel lane-classification counter\n * file-layout contract. The channel MCP servers increment\n * `<channel>-lane-classifications.json` in the agent home (via\n * recordLaneClassification in packages/mcp/inbound-lane-telemetry.ts); apps/cli\n * does not depend on @integrity-labs/mcp, so the manager re-implements the read\n * against this contract (same posture as the deflection scan above). Body is a\n * JSON object keyed by `<lane>|<expects_reply>|<source>` (plus the reserved\n * `suspected_misclassification`) with integer counts.\n */\nconst LANE_CLASSIFICATION_COUNTER_SUFFIX = '-lane-classifications.json';\nconst SUSPECTED_MISCLASSIFICATION_KEY = 'suspected_misclassification';\nconst KNOWN_LANES = new Set(['conversational', 'directive', 'liveness']);\nconst KNOWN_LANE_SOURCES = new Set(['slack', 'telegram', 'msteams', 'direct-chat']);\n\n/**\n * Only well-formed composite keys (and the misclassification canary) are\n * accumulated, so a corrupt counter file can't smuggle an unbounded dimension\n * into CloudWatch - the same bounding the deflection reader applies.\n */\nfunction isKnownLaneClassificationKey(key: string): boolean {\n if (key === SUSPECTED_MISCLASSIFICATION_KEY) return true;\n const parts = key.split('|');\n if (parts.length !== 3) return false;\n const [lane, expectsReply, source] = parts as [string, string, string];\n return (\n KNOWN_LANES.has(lane) &&\n (expectsReply === 'true' || expectsReply === 'false') &&\n KNOWN_LANE_SOURCES.has(source)\n );\n}\n\n/**\n * Read and RESET every `<channel>-lane-classifications.json` in an agent home,\n * returning the summed counts by key (or null when there are none). Mirrors\n * readAndResetChannelDeflections exactly: atomic rename-then-read-then-unlink so\n * an increment racing the reset carries into the next window, and only known\n * keys are accumulated. SHADOW telemetry - informs the Slice 2 gate, gates\n * nothing itself.\n */\nexport function readAndResetChannelLaneClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(LANE_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming); // atomic; replaces any stale .consuming from a crashed cycle\n } catch {\n continue; // vanished / not a regular file — skip\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownLaneClassificationKey(key)) continue;\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + Math.floor(raw);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt — drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n/**\n * WS0 (Slack cross-thread reply routing) - per-channel reply-target counter\n * file-layout contract. The channel MCP servers increment\n * `<channel>-reply-target-classifications.json` in the agent home (via\n * recordReplyTargetClassification in packages/mcp/slack-reply-target-telemetry.ts);\n * apps/cli does not depend on @integrity-labs/mcp, so the manager re-implements\n * the read against this contract (same posture as the lane-classification scan\n * above). Body is a JSON object keyed by `<result>|<pending_threads_bucket>`\n * with integer counts.\n */\nconst REPLY_TARGET_CLASSIFICATION_COUNTER_SUFFIX = '-reply-target-classifications.json';\nconst KNOWN_REPLY_TARGET_RESULTS = new Set([\n 'matched_pending',\n 'matched_recent',\n 'crossed_candidate',\n 'unknown_target',\n]);\nconst KNOWN_PENDING_THREAD_BUCKETS = new Set(['0', '1', '2', '3plus']);\n\n/**\n * Only well-formed `<result>|<bucket>` keys are accumulated, so a corrupt counter\n * file can't smuggle an unbounded dimension into CloudWatch - the same bounding\n * the lane-classification / deflection readers apply.\n */\nfunction isKnownReplyTargetKey(key: string): boolean {\n const parts = key.split('|');\n if (parts.length !== 2) return false;\n const [result, bucket] = parts as [string, string];\n return KNOWN_REPLY_TARGET_RESULTS.has(result) && KNOWN_PENDING_THREAD_BUCKETS.has(bucket);\n}\n\n/**\n * Read and RESET every `<channel>-reply-target-classifications.json` in an agent\n * home, returning the summed counts by key (or null when there are none). Mirrors\n * readAndResetChannelLaneClassifications exactly: atomic\n * rename-then-read-then-unlink so an increment racing the reset carries into the\n * next window, and only known keys are accumulated. SHADOW telemetry - informs\n * the WS2 binding gate, gates nothing itself.\n */\nexport function readAndResetSlackReplyTargetClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(REPLY_TARGET_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming); // atomic; replaces any stale .consuming from a crashed cycle\n } catch {\n continue; // vanished / not a regular file - skip\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownReplyTargetKey(key)) continue;\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + Math.floor(raw);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// WS2 (Slack cross-thread reply routing) - per-channel reply-BINDING counter\n// file-layout contract. The slack MCP increments\n// `<channel>-reply-binding-classifications.json` (via\n// recordReplyBindingClassification in packages/mcp/slack-reply-binding-telemetry.ts);\n// apps/cli re-implements the read against this contract, same posture as the\n// reply-target scan above. Body is a JSON object keyed by the binding\n// classification (plus the reserved unknown_inbound_id canary) with integer counts.\nconst REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX = '-reply-binding-classifications.json';\nconst KNOWN_REPLY_BINDING_KEYS = new Set([\n 'bound',\n 'coords_corrected',\n 'coords_match',\n 'proactive',\n 'unknown_target',\n 'legacy',\n 'unknown_inbound_id', // reserved canary\n // ENG-7716: the ENG-7542 DM-channel guard writes these into the same counter\n // file. They are additional reserved keys (channel_mistarget_corrected counts\n // every cross-channel rewrite; dm_channel_mistarget its DM subset), overlapping\n // the main classifications the same way unknown_inbound_id does. Allowlisted so\n // the DM->channel leak signal survives the read/reset fold and reaches CloudWatch.\n 'channel_mistarget_corrected',\n 'dm_channel_mistarget',\n // ENG-7806 (fm1): the inbound-delivery-ledger verdicts the Stop hook records for\n // each recover_*_for decision (self = this inbound already delivered; moved_on =\n // a different conversation was answered after this inbound; clear = ledger says\n // recover; fallback = no ledger data, used the transcript scan). Folded into the\n // same counter-file pipe so ledger coverage reaches CloudWatch as Classification\n // dimensions on the existing SlackReplyBindingClassifications metric.\n 'delivery_ledger_self',\n 'delivery_ledger_moved_on',\n 'delivery_ledger_clear',\n 'delivery_ledger_fallback',\n // ENG-8137: the scheduled-turn channel guard writes these into the same counter\n // file (recordScheduledChannelOverride). `_shadow` counts detections while the\n // flag observes without changing routing - the false-positive signal that gates\n // the enforce flip; `_blocked` counts sends actually refused under enforce.\n // Allowlisted so both reach CloudWatch: without them this guard would fire\n // fleet-wide with no observable rate, which is the blindness that let the\n // original misroute reach customers unnoticed.\n 'scheduled_channel_override_shadow',\n 'scheduled_channel_override_blocked',\n]);\n\n/**\n * Read and RESET every `<channel>-reply-binding-classifications.json` in an agent\n * home, returning the summed counts by classification (or null when there are\n * none). Mirrors readAndResetSlackReplyTargetClassifications; only known keys are\n * accumulated so a corrupt file can't add an unbounded CloudWatch dimension.\n * SHADOW telemetry - informs the enforce flip, gates nothing itself.\n */\nexport function readAndResetSlackReplyBindingClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming);\n } catch {\n continue;\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!KNOWN_REPLY_BINDING_KEYS.has(key)) continue;\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + Math.floor(raw);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// ENG-7855 - per-channel hot-thread-guard (ENG-7462) counter file-layout contract.\n// The slack MCP increments `<channel>-hot-thread-classifications.json` (via\n// recordHotThreadClassification in packages/mcp/slack-hot-thread-telemetry.ts);\n// apps/cli re-implements the read against this contract, same posture as the\n// reply-target / reply-binding scans above. Body is a JSON object keyed by\n// `<mode>|<outcome>|<proactive>` with integer counts.\nconst HOT_THREAD_CLASSIFICATION_COUNTER_SUFFIX = '-hot-thread-classifications.json';\n// Mirrored by the API-side sets of the same names in routes/host-runtime.ts.\n// A value accepted by only one side is dropped silently and the metric reads a\n// clean zero meaning \"never ingested\" - the ENG-7716 / ENG-8214 failure. Any\n// new mode or outcome must land in BOTH, and in the mcp HotThreadGuardResult\n// union, in the same change.\nconst KNOWN_HOT_THREAD_MODES = new Set(['shadow', 'enforce']);\nconst KNOWN_HOT_THREAD_OUTCOMES = new Set([\n 'redirected_to_hot_thread',\n 'root_forced',\n 'root_no_hot_thread',\n 'not_applicable',\n]);\n\n/**\n * Only well-formed `<mode>|<outcome>|<proactive>` keys are accumulated, so a\n * corrupt counter file can't smuggle an unbounded dimension into CloudWatch - the\n * same bounding the reply-target / reply-binding readers apply.\n */\nfunction isKnownHotThreadKey(key: string): boolean {\n const parts = key.split('|');\n if (parts.length !== 3) return false;\n const [mode, outcome, proactive] = parts as [string, string, string];\n return (\n KNOWN_HOT_THREAD_MODES.has(mode) &&\n KNOWN_HOT_THREAD_OUTCOMES.has(outcome) &&\n (proactive === 'true' || proactive === 'false')\n );\n}\n\n/**\n * Read and RESET every `<channel>-hot-thread-classifications.json` in an agent\n * home, returning the summed counts by key (or null when there are none). Mirrors\n * readAndResetSlackReplyTargetClassifications exactly: atomic\n * rename-then-read-then-unlink so an increment racing the reset carries into the\n * next window, and only known keys are accumulated. SHADOW/enforce observability\n * for the hot-thread guard - gates nothing itself.\n */\nexport function readAndResetSlackHotThreadClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(HOT_THREAD_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming);\n } catch {\n continue;\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownHotThreadKey(key)) continue;\n // ENG-7855 (CodeRabbit): require a positive SAFE INTEGER. The writer only\n // ever emits integer counts, so a fractional/unsafe value is corruption -\n // flooring it would emit a spurious 0 datapoint (0.5) or silently alter a\n // count (1.9 → 1). Drop it instead.\n if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + raw;\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// ENG-8346 - direct-chat cursor-advance shortfall counter file-layout contract.\n// Two writers increment `<source>-cursor-advance-classifications.json` via\n// recordCursorAdvanceOutcome (@augmented/core/direct-chat/cursor-advance-\n// telemetry.js): the direct-chat channel MCP writes `direct-chat-...` and the\n// manager's ADR-0047 opencode arm writes `direct-chat-manager-...`. Separate\n// files because they are separate PROCESSES and read-modify-write on one file\n// loses increments. Both are summed here. Body is a JSON object keyed by\n// `<route>|<reason>|<partial>` with integer counts.\nconst CURSOR_SHORTFALL_COUNTER_SUFFIX = '-cursor-advance-classifications.json';\n// Mirrored by the API-side sets of the same names in routes/host-runtime.ts, and\n// sourced from CURSOR_SHORTFALL_REASONS in @augmented/core's direct-chat/\n// cursor-advance.ts (which the WRITER normalises against, so an unrecognised\n// server reason arrives here already folded into `other` rather than dropped).\n// A value accepted by only one side is filtered out with no error and no log,\n// and the metric then reads a clean zero meaning \"never ingested\" - the\n// ENG-7716 / ENG-8214 failure. Any new route or reason must land in BOTH.\n// ENG-8393: the last three are the NON-SHORTFALL outcomes. `advanced` and\n// `advanced_unreported` are benign; `failed` is not - it records an advance\n// that did not happen, and is counted because a fleet where every advance\n// failed would otherwise report zero shortfalls AND zero successes. The name of\n// this set now under-describes it - since that ticket the reason slot also\n// carries the non-shortfall verdicts, which is what gives the metric a\n// denominator and makes a zero on the shortfall reasons mean something. Kept\n// as-is because the name is load-bearing in the parity guard's REQUIRED_PAIRS\n// and mirrored on the API side.\nconst KNOWN_CURSOR_ADVANCE_ROUTES = new Set(['reply', 'consume']);\nconst KNOWN_CURSOR_SHORTFALL_REASONS = new Set([\n 'not_found',\n 'unclaimed',\n 'gave_up',\n 'already_delivered',\n 'error',\n 'undiagnosed',\n 'unknown',\n 'unreported',\n 'malformed_count',\n 'other',\n 'advanced',\n 'advanced_unreported',\n 'failed',\n]);\n\n/**\n * Only well-formed `<route>|<reason>|<partial>` keys are accumulated, so a\n * corrupt counter file cannot smuggle an unbounded dimension into CloudWatch -\n * the same bounding the reply-target / reply-binding / hot-thread readers apply.\n */\nfunction isKnownCursorShortfallKey(key: string): boolean {\n const parts = key.split('|');\n if (parts.length !== 3) return false;\n const [route, reason, partial] = parts as [string, string, string];\n return (\n KNOWN_CURSOR_ADVANCE_ROUTES.has(route) &&\n KNOWN_CURSOR_SHORTFALL_REASONS.has(reason) &&\n (partial === 'true' || partial === 'false')\n );\n}\n\n/**\n * Read and RESET every `<source>-cursor-advance-classifications.json` in an agent\n * home, returning the summed counts by key (or null when there are none).\n *\n * Mirrors readAndResetSlackHotThreadClassifications exactly: atomic\n * rename-then-read-then-unlink so an increment racing the reset carries into the\n * next window, and only known keys are accumulated.\n *\n * This is the numerator AND (since ENG-8393) the denominator for ENG-8347. A\n * direct-chat cursor that does not advance strands a message the agent has\n * ALREADY answered, and the only other trace is a stderr line in a log nobody\n * watches - which is how the ENG-8337 strand went unnoticed.\n *\n * ENG-8393 changed the cost profile deliberately: the writer used to record only\n * shortfalls, so a healthy agent wrote no file at all - and that was the defect,\n * not a saving. It made \"no shortfalls happened\" and \"this host never ingested\"\n * byte-identical, which for a metric that exists to gate a decision means the\n * decision is taken blind. A quiet agent still writes nothing (it advances no\n * cursors); an ACTIVE one now writes a small file per probe window either way.\n */\nexport function readAndResetDirectChatCursorShortfallClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(CURSOR_SHORTFALL_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming);\n } catch {\n continue;\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownCursorShortfallKey(key)) continue;\n // Require a positive SAFE INTEGER, same as the hot-thread reader: the\n // writer only ever emits integer counts, so a fractional/unsafe value\n // is corruption - flooring it would emit a spurious 0 datapoint or\n // silently alter a count.\n if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + raw;\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// ENG-7550: 60s default (was 5 min). The probe post is what refreshes\n// agents.last_pane_activity_at, so its cadence is the resolution ceiling for\n// minutely busy sampling (sample_agent_activity at p_window_seconds=60).\n// Sized before lowering: the per-agent CloudWatch alarms already run\n// Period:60/EvaluationPeriods:1 (built for 1-min datapoints), CloudWatch\n// bills per metric not per datapoint, and the 5x Lambda/API-GW invocation\n// increase is trivial at fleet scale. The reset-on-read delta counters\n// (deflections, lane/reply classifications) are cadence-agnostic - smaller\n// batches, same totals.\nconst DEFAULT_INTERVAL_MS = 60 * 1000;\n\nexport function getResponsivenessIntervalMs(): number {\n const raw = process.env.AUGMENTED_RESPONSIVENESS_INTERVAL_MS;\n if (!raw) return DEFAULT_INTERVAL_MS;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_INTERVAL_MS;\n}\n\n/**\n * ENG-10048: the age basis for one marker — its TRUE arrival time where the\n * marker records one, falling back to the file's mtime where it does not.\n *\n * WHY NOT JUST mtime. `pending-inbound-stale` measured mtime, and every\n * in-place marker rewrite advances it: `seen_at`, `promised_at` (ENG-8265), a\n * `replay_count` bump. So reading a stuck thread reset the clock that reported\n * it, and the gauge said \"one hour\" about a three-hour-old undelivered message.\n * The standard diagnostic disarmed the alarm, and nothing said so. Measured on\n * sherlock's own markers: `received_at` 17:01:56, mtime 19:00:59, `seen_at`\n * 19:00:59 — 8ms apart, so the read is unambiguously what moved it.\n *\n * WHY NOT JUST received_at, WHICH IS THE TRAP. The ticket's own AC said a\n * missing `received_at` must fail closed and be treated as old. That is right\n * for a corrupt marker and wrong for a whole CHANNEL: msteams markers carry the\n * raw Bot Framework Activity and have no `received_at` at all, while\n * `msteams-inbound-puller.ts` deliberately stamps their mtime from `created_at`\n * so boot-drain preserves arrival order. For that channel mtime ALREADY IS the\n * arrival time, and failing closed would have fired this alarm on every Teams\n * agent on the first evaluation with nothing stranded anywhere.\n *\n * So: prefer the semantic field, fall back to the incidental one per marker.\n * A marker is never SKIPPED for lacking a clock — it always contributes an age,\n * which preserves the fail-closed intent (a corrupt marker cannot mask a wedge)\n * without inventing one for a channel that never had the field.\n *\n * A future-dated `received_at` (clock skew) falls back to mtime rather than\n * reporting a negative age, matching `isPendingMarkerStale`'s treatment.\n *\n * `nowMs` is REQUIRED and is the caller's sampled clock, not `Date.now()` read\n * here (CodeRabbit on #5691). The collector computes the reported age from a\n * `nowMs` it captured earlier in the tick; reading the clock again inside this\n * function opens a window where a `received_at` that is future relative to the\n * SAMPLE is already past by the time it is checked. It then passes the\n * future-date guard, yields a negative age, and `Math.max(0, ...)` in the\n * collector clamps that to 0 - reporting a stranded marker as brand new and\n * suppressing the very alarm this change exists to make honest. Taking the\n * clock as a parameter makes the two reads the same read, and making it\n * required stops a caller silently reintroducing the skew.\n */\nexport function markerArrivalMs(markerPath: string, mtimeMs: number, nowMs: number): number {\n let receivedAt: unknown;\n try {\n receivedAt = (JSON.parse(readFileSync(markerPath, 'utf8')) as { received_at?: unknown })\n ?.received_at;\n } catch {\n return mtimeMs; // unreadable/malformed — mtime is the only clock available\n }\n if (typeof receivedAt !== 'string') return mtimeMs;\n const parsed = Date.parse(receivedAt);\n if (!Number.isFinite(parsed)) return mtimeMs;\n // Future-dated: skew, not a real arrival. mtime is the safer basis.\n if (parsed > nowMs) return mtimeMs;\n return parsed;\n}\n\n/**\n * ENG-10048: which markers the `pending-inbound-stale` ALARM counts.\n *\n * Named and exported rather than inlined because \"which markers count\" is the\n * decision that went wrong when this alarm was first armed, and it deserves one\n * home and a test rather than a condition inside a loop.\n *\n * This filter is DELIBERATELY NOT the wedge detector's filter, because the two\n * ask different questions. `oldestLivePendingInboundMtimeMs` asks \"is THIS\n * SESSION failing to drain?\"; the alarm asks \"is a customer message stranded?\".\n *\n * - `discretionary: true` → EXCLUDED. Auto-followed participant chatter the\n * agent may legitimately skip. A deliberate skip is not a stranded ask, and\n * paging on one trains people to ignore the alarm.\n * - `undeliverable: true` → EXCLUDED. Already dead-lettered by the channel and\n * covered by its own `dead-lettered-inbound` alarm; counting it here pages\n * twice for one problem. NOTE the standing caveat: that sibling alarm is\n * itself reported to self-close while the message is still stranded, so\n * coverage of dead-lettered markers rests on that being fixed, not on this.\n * - pre-session markers → INCLUDED, unlike the wedge scan. A message that\n * outlived a respawn is MORE stranded, not less. The wedge detector excludes\n * them because a leftover cannot prove this session is stuck; the alarm does\n * not care which session was up when the customer wrote in.\n */\nexport function isAlarmRelevantMarker(flags: MarkerFlags): boolean {\n return !flags.undeliverable && !flags.discretionary;\n}\n\n/**\n * ENG-6017: oldest pending-inbound marker ARRIVAL time (ms epoch) for an\n * agent, or null when it has no counted markers / no pending-inbound dirs.\n *\n * ENG-10048 changed TWO things here, and both were load-bearing:\n * - the CLOCK is now `markerArrivalMs`, not the raw mtime, so an observer\n * reading a stuck thread can no longer reset the age this alarm reports;\n * - the marker SET is now filtered by `isAlarmRelevantMarker`. That is not\n * optional tidying: without it, moving to a clock nobody can reset makes\n * dead-lettered and discretionary markers - which never drain, by design -\n * report their true age and hold the alarm on forever. The old mtime clock\n * was accidentally masking them, so the filter has to arrive with it.\n *\n * The channel MCP servers (slack-channel, telegram-channel, …) write one\n * marker file per inbound into `~/.augmented/<codeName>/<channel>-pending-\n * inbound/` and clear it when the agent acknowledges the message. The\n * directory layout is the contract here — read-only, no IPC with the MCP\n * (the MCP and CLI release independently; file mtimes need no protocol).\n *\n * ENG-6072: only plain, non-hidden files count as markers. The msteams MCP\n * keeps `.markers/` and `.processed/` housekeeping SUBDIRECTORIES inside its\n * pending-inbound dir; their mtimes never advance, so statting every dirent\n * made the gauge climb forever and fired pending-inbound-stale on agents with\n * zero stranded messages (kylie ~3.4d / scout ~34h false ALARMs the moment\n * ENG-6023 activated the alarm). Dot-entries are skipped wholesale — the\n * hidden namespace is reserved for MCP bookkeeping, never for markers.\n */\nfunction oldestPendingInboundArrivalMs(agentHomeDir: string, nowMs: number): number | null {\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // agent home missing — nothing to report\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const full = join(dir, file.name);\n let mtimeMs: number;\n try {\n mtimeMs = statSync(full).mtimeMs;\n } catch {\n // Marker drained between readdir and stat — that's the happy path.\n continue;\n }\n const flags = readMarkerFlags(full);\n if (flags === null) continue; // vanished mid-scan — drained, exclude\n if (flags !== 'malformed' && !isAlarmRelevantMarker(flags)) continue;\n const arrivalMs = markerArrivalMs(full, mtimeMs, nowMs);\n if (oldest === null || arrivalMs < oldest) oldest = arrivalMs;\n }\n }\n return oldest;\n}\n\n/**\n * ENG-6160 / ENG-6319: read a marker's classification flags.\n * - `{...}` → parsed flags.\n * - `null` → vanished mid-scan (ENOENT) — drained between stat and\n * read, the happy path; callers exclude it.\n * - `'malformed'` → present but unreadable for another reason. The live\n * scan treats this as LIVE (a corrupt marker can never\n * mask a real wedge); park leaves it in place (a corrupt\n * marker must never become a dropped message).\n */\ninterface MarkerFlags {\n /** ENG-5846: dead-lettered by the channel (⏳-noticed, never drainable). */\n undeliverable: boolean;\n /**\n * ENG-6319: auto-followed participant-thread inbound the agent may\n * legitimately skip. Excluded from the wedge live-scan (a deliberate skip\n * must not read as \"failing to drain\" and force-respawn a healthy\n * session) but still parked across respawns — it may be a real\n * operator message awaiting a reply.\n */\n discretionary: boolean;\n}\n\nfunction readMarkerFlags(markerPath: string): MarkerFlags | null | 'malformed' {\n try {\n const parsed = JSON.parse(readFileSync(markerPath, 'utf8')) as {\n undeliverable?: unknown;\n discretionary?: unknown;\n };\n return {\n undeliverable: parsed?.undeliverable === true,\n discretionary: parsed?.discretionary === true,\n };\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'ENOENT' ? null : 'malformed';\n }\n}\n\n/**\n * ENG-6160: oldest *LIVE* pending-inbound marker mtime (ms epoch) for an agent,\n * or null when there is no live marker. \"Live\" excludes:\n *\n * - markers older than `sessionStartMs` — a marker written before the current\n * session started is a leftover from a PREVIOUS session and cannot mean\n * *this* session is failing to drain. This is the load-bearing exclusion:\n * without it, an orphan marker survives a fresh respawn and the wedge\n * detector re-fires forever on a healthy idle agent (the sherlock enforce\n * loop, 2026-06-08: `inboundAge=3389s` on a `● Ready.` session).\n * - markers flagged `undeliverable: true` — already dead-lettered by the channel.\n * - markers flagged `discretionary: true` (ENG-6319) — skippable auto-followed\n * participant-thread inbound; a deliberate skip must not force-respawn a\n * healthy session.\n *\n * Distinct from `oldestPendingInboundArrivalMs`, which feeds the ENG-6017\n * `pending-inbound-stale` CloudWatch alarm.\n *\n * ENG-10048: this note used to say that alarm's \"semantics must NOT change\",\n * and it was read as protecting the FILTER — which was fine — while the CLOCK\n * was resettable all along. The comment steering editors away pointed at the\n * half that was already correct, which is part of why the defect survived. The\n * two functions now differ deliberately on BOTH axes:\n * - clock: the alarm keys on arrival; this one still keys on mtime, because a\n * wedge is about THIS session's drain behaviour and an in-session rewrite is\n * genuine evidence of engagement.\n * - filter: the alarm INCLUDES pre-session markers (a message that outlived a\n * respawn is more stranded, not less); this one excludes them, because a\n * leftover cannot prove the current session is failing.\n */\nexport function oldestLivePendingInboundMtimeMs(\n agentHomeDir: string,\n opts: { sessionStartMs?: number | null } = {},\n): number | null {\n const sessionStartMs = opts.sessionStartMs ?? null;\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const full = join(dir, file.name);\n let mtimeMs: number;\n try {\n mtimeMs = statSync(full).mtimeMs;\n } catch {\n continue; // drained between readdir and stat — happy path\n }\n if (sessionStartMs !== null && mtimeMs < sessionStartMs) continue; // pre-session leftover\n const flags = readMarkerFlags(full);\n if (flags === null) continue; // vanished between stat and read — drained, exclude\n if (flags !== 'malformed' && (flags.undeliverable || flags.discretionary)) continue;\n if (oldest === null || mtimeMs < oldest) oldest = mtimeMs;\n }\n }\n return oldest;\n}\n\n/**\n * ENG-10045 — how many DEAD-LETTERED inbound markers are on this host for this\n * agent RIGHT NOW.\n *\n * ## Why this function exists at all\n *\n * `DEAD_LETTERED_INBOUND_HOURLY_METRIC` is a SUM of dead-letter EVENTS over a\n * trailing hour. An hour after the last event the datapoint is zero, CloudWatch\n * fires `OKActions`, the bridge closes the alert — and the undelivered customer\n * message is still sitting in `*-pending-inbound-stale/`, and will be forever.\n * The alarm never measured whether anything was still stranded. This is the\n * gauge that lets it: the count is the artifact's CURRENT population, so an OK\n * means the artifact is gone, which is what the ENG-10045 close contract in\n * `packages/api/src/lib/agent-heartbeat-alarms.ts` requires.\n *\n * ## The predicate, stated rather than inferred\n *\n * Sherlock's correction on ENG-10045 was that `mtime` looked like an oversight\n * and was a deliberate choice nobody had written down, so the next editor\n * \"fixed\" it into a known bug. Three choices here, each with its reason:\n *\n * 1. **`-stale` dirs only, never the live `*-pending-inbound/` dirs.** A live\n * marker is a message still queued for delivery — undelivered, but not\n * stranded. It has its own signal (`PENDING_INBOUND_AGE_METRIC`) and its own\n * alarm. Counting both here would make this gauge fire for the healthy case\n * where a message arrived four seconds ago.\n *\n * 2. **`msteams-pending-inbound-stale` IS counted**, even though\n * `parkPendingInbound` skips the LIVE msteams dir. Those are not the same\n * decision. The live skip exists because msteams top-level files are raw Bot\n * Framework activity payloads that the teams channel server re-drains on\n * boot, so moving them aside defeated the one channel with native respawn\n * recovery. Nothing re-drains a `-stale` dir: anything that reached it was\n * put there by a dead-letter, which is exactly the population this counts. A\n * skip here would make msteams the one channel whose lost messages are\n * invisible.\n *\n * 3. **A malformed marker IS counted.** It is the same rule this file already\n * applies in the other direction — `parkPendingInbound` parks a corrupt\n * marker rather than dead-lettering it, because \"corrupt must never become a\n * drop\". The mirror of that is that corrupt must never mask a drop either: a\n * file we cannot parse, sitting in a dead-letter dir, is a customer message\n * we cannot prove was delivered. It counts. This also means the count needs\n * no marker parse at all, which is why it is a `readdir` and not a `readFile`\n * per entry — it runs on the probe cadence for every agent on the host.\n *\n * ## A FAILED SCAN IS NOT AN EMPTY ONE (CodeRabbit, #5931 — and it was right)\n *\n * The first version returned 0 when `readdirSync` threw, matching every other\n * scan in this file. That is safe for the AGE scans, whose 0 means \"no marker so\n * no age\" and whose field is omitted anyway. It is NOT safe here, because a 0\n * from this function is published as an affirmative datapoint and an affirmative\n * zero is what CLEARS the alarm. A host that could not read its own marker\n * directory would have told CloudWatch \"nothing is stranded\" — the same false\n * all-clear as the trailing-hour SUM, arriving by a different route.\n *\n * So the return is `number | null`, and the rule is asymmetric on purpose:\n *\n * - a count we ESTABLISHED (every stale dir read cleanly) → that number,\n * including 0, which is the datapoint that legitimately clears the alarm;\n * - a count of ZERO we could NOT establish (the home or any stale dir threw) →\n * `null`, \"no signal\", and the caller omits the field entirely;\n * - a POSITIVE count with some dir unreadable → still that number. A partial\n * positive can only under-report, and under-reporting one stranded message\n * while reporting another still leaves the alarm open. Suppressing it to\n * `null` would throw away a true alarm to avoid an imprecise one.\n *\n * Paired with `TreatMissingData: 'ignore'` on the alarm (also CodeRabbit), the\n * `null` case means the alarm RETAINS its state rather than drifting to OK. The\n * two halves are one fix: this decides when to say nothing, and that decides\n * what silence means.\n */\nexport function deadLetteredInboundMarkerCount(agentHomeDir: string): number | null {\n let count = 0;\n let scanIncomplete = false;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // could not even list the home — we have established nothing\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound-stale')) continue;\n let files;\n try {\n files = readdirSync(join(agentHomeDir, entry.name), { withFileTypes: true });\n } catch {\n scanIncomplete = true;\n continue;\n }\n for (const file of files) {\n // Dotfiles are bookkeeping, never markers — the same exclusion every\n // other scan in this file makes.\n if (!file.isFile() || file.name.startsWith('.')) continue;\n count++;\n }\n }\n // Only a ZERO needs to have been established. A positive is reported however\n // partial it is — see the asymmetry note above.\n if (count === 0 && scanIncomplete) return null;\n return count;\n}\n\n/**\n * ENG-6160: age (s) of the oldest LIVE pending-inbound marker for an agent, or\n * null when none. The wedge detector uses this instead of the alarm-facing\n * `pending_inbound_oldest_age_seconds` so a stale/dead-letter marker can't\n * false-fire a respawn.\n */\nexport function livePendingInboundOldestAgeSeconds(\n codeName: string,\n sessionStartMs: number | null,\n now: Date = new Date(),\n): number | null {\n const oldest = oldestLivePendingInboundMtimeMs(dirname(paneLogPath(codeName)), { sessionStartMs });\n if (oldest === null) return null;\n return Math.max(0, Math.floor((now.getTime() - oldest) / 1000));\n}\n\n/**\n * ENG-8881: age of the most recent channel inbound ACTIVITY for an agent, in ms\n * since epoch — arrival *or* drain, across every channel.\n *\n * WHY NOT THE OBVIOUS SIGNALS. The incident (ENG-8881) is an agent that accepted\n * work on Telegram and was force-restarted three seconds later, before it could\n * write the kanban row. Neither existing signal sees that moment:\n *\n * - `lastInboundMs` / `inboundAgeSecondsFor` in manager-worker is stamped ONLY\n * from `processDirectChatMessage`. Slack and Telegram arrive through MCP\n * children, so for the channel this incident happened on it is never set.\n * - `oldestLivePendingInboundMtimeMs` counts UNANSWERED markers. The agent had\n * already replied, so its marker was drained and the signal was empty at the\n * instant of the kill. Correctly empty — and useless here.\n *\n * The window we must protect is exactly \"a message arrived and was answered just\n * now\", which is the moment a marker is created and then removed. Both mutate the\n * CONTAINING DIRECTORY's mtime, so the directory timestamp survives the drain\n * that erases the marker itself. That is the signal this returns.\n *\n * Deliberately reads directory mtimes and not file mtimes: the files are gone by\n * the time we need to know they existed.\n *\n * Returns null when the agent has no channel directories at all (never messaged).\n */\nexport function newestPendingInboundActivityMtimeMs(agentHomeDir: string): number | null {\n let newest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n // Same predicate as oldestLivePendingInboundMtimeMs, so the two agree on\n // what counts as a channel dir. `-pending-inbound-stale` does not match.\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n try {\n const mtimeMs = statSync(join(agentHomeDir, entry.name)).mtimeMs;\n if (newest === null || mtimeMs > newest) newest = mtimeMs;\n } catch {\n continue;\n }\n }\n return newest;\n}\n\n/**\n * ENG-6160: move every pending-inbound marker for an agent aside into a sibling\n * `<channel>-pending-inbound-stale/` directory (NOT silently deleted — the\n * payload pointer is preserved for forensics), returning the count moved.\n *\n * ENG-6289: no longer called on wedge respawn — the wedge path parks instead\n * (see `parkPendingInbound` above; blanket dead-letter permanently dropped the\n * user's message). Kept as the explicit \"move everything aside\" seam for tests\n * and operator emergencies. The stale dir does not end in `-pending-inbound`,\n * so neither the probe nor this scan re-counts moved markers.\n */\n/**\n * ENG-6289: park-not-drop. On a force-fresh wedge respawn, KEEP undrained\n * pending-inbound markers in their live dirs instead of dead-lettering them —\n * the fresh session's orient hook surfaces them (\"N queued messages\" + details)\n * and ENG-5969 replay (when enabled) re-pushes their payloads. Markers are NOT\n * rewritten (no counter, no mtime bump): the ENG-6160 pre-session exclusion in\n * `oldestLivePendingInboundMtimeMs` already keeps an untouched parked marker\n * out of the fresh session's wedge signal, and re-delivery stays bounded by\n * the existing machinery — the channel-side `replay_count` cap (≤3) and the\n * orphan sweep's received_at TTL. A manager-side rewrite would be the first\n * cross-process writer into marker files, where a torn read in the channel\n * sweep DELETES the marker (`unlinkSync` on parse failure) — the exact drop\n * this function exists to prevent.\n *\n * Only markers already flagged `undeliverable: true` are dead-lettered (moved\n * to `-stale`) — the channel already gave the user the ⏳ notice for those, so\n * nothing can ever drain them. Malformed markers are LEFT IN PLACE, matching\n * the live-scan philosophy above (a corrupt marker must never mask — or\n * become — a dropped message).\n *\n * The msteams dir is skipped wholesale: its top-level files are raw Bot\n * Framework activity payloads (the transport queue, not bookkeeping — real\n * markers live in the hidden `.markers/` subdir this scan never touches), and\n * the teams channel server's boot drain redelivers them to the fresh session\n * on its own. Moving them aside (what the pre-ENG-6289 dead-letter did) was\n * actively defeating the one channel with native respawn recovery.\n */\nexport interface ParkPendingInboundResult {\n parked: number;\n deadLettered: number;\n}\n\n/**\n * Move one marker into the sibling `-stale` dead-letter dir (moved, not\n * deleted — preserved for forensics). Returns true on success; best-effort —\n * a marker that vanished or can't move is left as-is.\n */\nfunction moveMarkerToStale(dir: string, deadDir: string, name: string): boolean {\n try {\n mkdirSync(deadDir, { recursive: true });\n renameSync(join(dir, name), join(deadDir, name));\n return true;\n } catch {\n return false;\n }\n}\n\nexport function parkPendingInbound(codeName: string, _now: Date = new Date()): ParkPendingInboundResult {\n const home = dirname(paneLogPath(codeName));\n const result: ParkPendingInboundResult = { parked: 0, deadLettered: 0 };\n let entries;\n try {\n entries = readdirSync(home, { withFileTypes: true });\n } catch {\n return result;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n if (entry.name === 'msteams-pending-inbound') continue; // transport queue — boot drain owns recovery\n const dir = join(home, entry.name);\n const deadDir = join(home, `${entry.name}-stale`);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const flags = readMarkerFlags(join(dir, file.name));\n if (flags === null) continue; // drained mid-scan — already gone\n // Only undeliverable markers dead-letter. Discretionary markers\n // (ENG-6319) PARK like engaged ones — they may be a real operator\n // message awaiting a reply (the live silent-loss class); malformed\n // markers park too (corrupt must never become a drop).\n if (flags !== 'malformed' && flags.undeliverable) {\n if (moveMarkerToStale(dir, deadDir, file.name)) result.deadLettered++;\n } else {\n result.parked++;\n }\n }\n }\n return result;\n}\n\nexport function deadLetterPendingInbound(codeName: string, _now: Date = new Date()): number {\n const home = dirname(paneLogPath(codeName));\n let moved = 0;\n let entries;\n try {\n entries = readdirSync(home, { withFileTypes: true });\n } catch {\n return 0;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(home, entry.name);\n const deadDir = join(home, `${entry.name}-stale`);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n if (moveMarkerToStale(dir, deadDir, file.name)) moved++;\n }\n }\n return moved;\n}\n\n/**\n * Compute the pane.log age for each agent. Missing or unreadable\n * pane.log returns null — the caller should drop those entries\n * rather than fabricate a \"fresh\" or \"ancient\" value. A missing\n * file means the agent has never spawned in this manager generation,\n * which is a separate problem covered by SessionAliveAgeSeconds.\n */\nexport function collectResponsivenessProbes(\n codeNames: string[],\n now: Date = new Date(),\n): ResponsivenessProbeResult[] {\n const nowMs = now.getTime();\n const results: ResponsivenessProbeResult[] = [];\n for (const codeName of codeNames) {\n try {\n const panePath = paneLogPath(codeName);\n const mtimeMs = statSync(panePath).mtimeMs;\n const ageSeconds = Math.max(0, Math.floor((nowMs - mtimeMs) / 1000));\n const result: ResponsivenessProbeResult = {\n code_name: codeName,\n pane_activity_age_seconds: ageSeconds,\n };\n attachSupplementalCounters(result, dirname(panePath), nowMs);\n results.push(result);\n } catch {\n // No pane.log yet (fresh agent, never spawned) — skip. The\n // session-alive monitor already covers the \"should be running\n // but isn't\" case.\n //\n // ENG-8090: an OPENCODE agent lands here on every cycle, permanently —\n // its serve writes `opencode-serve.log`, so this stat always throws. That\n // is what `collectPanelessActivityProbes` exists to cover; this function\n // stays pane-only because the wedge detector consumes its output and must\n // never act on an agent it cannot read a pane age for.\n }\n }\n return results;\n}\n\n/**\n * ENG-8090: read + RESET the per-agent counter files that ride the probe's\n * cadence, and attach the ones that produced anything.\n *\n * Extracted from the loop above because these reads were inside the same `try`\n * as the `pane.log` stat: an agent with no `pane.log` lost not just its pane age\n * but its deflection, lane-classification and reply-binding counters too. For\n * opencode that meant those telemetry streams — including the ADR-0024\n * misclassification canary — were silently empty for the whole fleet, a wider\n * blast radius than the liveness gap ENG-8090 was filed for.\n *\n * The reads are DESTRUCTIVE (read-and-reset), so exactly one collector may run\n * them per agent per cycle. That is guaranteed structurally, not by convention:\n * `collectResponsivenessProbes` handles agents whose `pane.log` stat succeeds\n * and `collectPanelessActivityProbes` handles those whose stat fails, and the\n * two sets are complements of each other.\n */\nfunction attachSupplementalCounters(\n result: ResponsivenessProbeResult | ResponsivenessProbePayloadEntry,\n agentHome: string,\n nowMs: number,\n): void {\n // ENG-6017: piggyback the pending-inbound drain-age scan on the same\n // cadence. Field omitted (not 0) when there are no markers.\n const oldestMarkerMs = oldestPendingInboundArrivalMs(agentHome, nowMs);\n if (oldestMarkerMs !== null) {\n result.pending_inbound_oldest_age_seconds = Math.max(\n 0,\n Math.floor((nowMs - oldestMarkerMs) / 1000),\n );\n }\n // ENG-10045: the stranded-marker gauge, set UNCONDITIONALLY — including 0.\n //\n // Every other field in this function is omitted when empty. This one is not,\n // and the asymmetry is the fix rather than an inconsistency: an omitted field\n // reaches CloudWatch as a missing datapoint, `TreatMissingData: 'notBreaching'`\n // reads that as fine, and the alarm's `OKActions` CLOSE the alert. So for this\n // one signal \"I have nothing to say\" would be indistinguishable from \"nothing\n // is stranded\" — which is the exact false all-clear ENG-10045 is about.\n //\n // The one case that IS omitted: a scan we could not complete that found\n // nothing. `null` means \"we established nothing\", and publishing a 0 there\n // would be the host asserting an all-clear it never checked.\n const stranded = deadLetteredInboundMarkerCount(agentHome);\n if (stranded !== null) result.dead_lettered_inbound_markers = stranded;\n // ENG-8265: the oldest OUTSTANDING interim promise — an inbound the agent\n // acked with `interim: true` and never answered substantively. Rides the same\n // cadence as the pending-inbound scan but is a DIFFERENT signal and must not be\n // conflated with it. ENG-8265 wrote this to explain why the promise clock had\n // to be separate: the pending-inbound age WAS measured from the marker's\n // mtime, which this very ack's in-place rewrite advances, so an ack-then-drop\n // reset that clock while leaving the user unanswered. ENG-10048 fixed that\n // clock for every channel that records `received_at`. It is NOT a blanket\n // immunity: msteams markers carry no `received_at`, so they fall back to\n // mtime and an in-place rewrite still resets their reported age (CodeRabbit\n // on #5691 - the PR body claimed the residual gap, this comment did not).\n // The two remain different signals regardless and must not be conflated.\n // This one is measured\n // from the marker's `promised_at`, is non-destructive (read-only, no reset), and\n // survives the mid-turn respawn that destroyed the work in the motivating\n // incident. Omitted (never 0) when no promise is outstanding.\n const promiseAge = outstandingPromiseOldestAgeSeconds(agentHome, new Date(nowMs));\n if (promiseAge !== null) {\n result.promise_outstanding_oldest_age_seconds = promiseAge;\n }\n // ENG-6327: read + RESET the per-channel deflection counters on the same\n // cadence so each cause's frequency reaches CloudWatch (ChannelDeflections,\n // Cause dimension). Omitted when there were none this window.\n const deflections = readAndResetChannelDeflections(agentHome);\n if (deflections) {\n result.deflections = deflections;\n }\n // ENG-6408 / ADR-0024 Slice 1.5: read + RESET the shadow lane-classification\n // counters on the same cadence so the classification mix (and the\n // misclassification canary) reaches CloudWatch. Omitted when none this window.\n const laneClassifications = readAndResetChannelLaneClassifications(agentHome);\n if (laneClassifications) {\n result.lane_classifications = laneClassifications;\n }\n // WS0: read + RESET the shadow reply-target counters on the same cadence so\n // the cross-thread classification mix reaches CloudWatch. Omitted when none.\n const replyTargetClassifications = readAndResetSlackReplyTargetClassifications(agentHome);\n if (replyTargetClassifications) {\n result.slack_reply_target_classifications = replyTargetClassifications;\n }\n // WS2: read + RESET the shadow reply-binding counters on the same cadence\n // so the bound / coords_corrected / unknown_target mix (the enforce-flip\n // gate) reaches CloudWatch. Omitted when none this window.\n const replyBindingClassifications = readAndResetSlackReplyBindingClassifications(agentHome);\n if (replyBindingClassifications) {\n result.slack_reply_binding_classifications = replyBindingClassifications;\n }\n // ENG-7855: read + RESET the hot-thread-guard outcome counters on the same\n // cadence so the redirect / root / not-applicable mix (the fire rate that\n // tells \"correctly quiet\" from \"never had the chance\") reaches CloudWatch.\n // Omitted when none this window (off-mode agents never write these).\n const hotThreadClassifications = readAndResetSlackHotThreadClassifications(agentHome);\n if (hotThreadClassifications) {\n result.slack_hot_thread_classifications = hotThreadClassifications;\n }\n // ENG-8346: read + RESET the direct-chat cursor-advance shortfall counters, so\n // \"the reply POST succeeded but the cursor did not move\" reaches CloudWatch\n // instead of only a per-host stderr line. Omitted when none this window.\n //\n // It matters that this lives HERE rather than in either collector: an opencode\n // agent has no pane.log, and site 9 (the manager's ADR-0047 arm, which writes\n // the `direct-chat-manager-` file) is the opencode path SPECIFICALLY. Putting\n // the read in `collectResponsivenessProbes` would have collected every agent\n // except the one whose writer this is.\n const cursorShortfalls = readAndResetDirectChatCursorShortfallClassifications(agentHome);\n if (cursorShortfalls) {\n result.direct_chat_cursor_shortfall_classifications = cursorShortfalls;\n }\n}\n\n/**\n * ENG-8090: activity probes for agents that have NO readable `pane.log`.\n *\n * The opencode runtime is the whole reason this exists. Its serve is a headless\n * HTTP server, not a tmux TUI, so there is no pane whose mtime tracks agent\n * work — and `opencode-serve.log`, the obvious substitute, is the wrong signal\n * twice over: it advances for HTTP-server reasons unrelated to any turn (so an\n * idle agent would bill as permanently busy), and it kept advancing all through\n * ENG-8058's deadlock (so a wedged agent would read alive). Both consumers are\n * fed from the turn lifecycle instead, via the two `*AgeSeconds` resolvers.\n *\n * Agents whose `pane.log` IS readable are skipped here and reported by\n * `collectResponsivenessProbes` — see the destructive-read note on\n * `attachSupplementalCounters`. A Claude Code agent that has simply never\n * spawned also has no `pane.log`; it lands here, both resolvers return null for\n * it, and it emits an entry carrying only whatever counters exist. That is a\n * strict improvement on today (those counters were dropped), and it writes no\n * activity timestamp, so it cannot make an unspawned agent look alive or busy.\n */\nexport function collectPanelessActivityProbes(\n codeNames: string[],\n resolvers: {\n /** Seconds since the agent was last occupied; 0 while a turn is in flight. */\n busyActivityAgeSeconds: (codeName: string) => number | null;\n /** Seconds since a turn last produced real reply text. */\n turnCompletionAgeSeconds: (codeName: string) => number | null;\n },\n now: Date = new Date(),\n): ResponsivenessProbePayloadEntry[] {\n const nowMs = now.getTime();\n const results: ResponsivenessProbePayloadEntry[] = [];\n for (const codeName of codeNames) {\n const panePath = paneLogPath(codeName);\n try {\n statSync(panePath);\n // Has a pane.log — `collectResponsivenessProbes` owns this agent, and\n // running the destructive counter reads here too would halve its counts.\n continue;\n } catch {\n // No pane.log — this collector owns it. Fall through.\n }\n const entry: ResponsivenessProbePayloadEntry = { code_name: codeName };\n attachSupplementalCounters(entry, dirname(panePath), nowMs);\n\n // ENG-10045: hold the stranded-marker gauge back until we know whether this\n // entry is going out at all, then re-attach it under the rule stated below.\n //\n // It is the one counter attached UNCONDITIONALLY (an affirmative zero is\n // what clears its alarm), so on a paneless agent with no other signal it was\n // the only field besides the name — and every never-spawned agent started\n // emitting a payload entry every cycle, which is the per-cycle iteration\n // ENG-8090 removed.\n const strandedMarkers = entry.dead_lettered_inbound_markers;\n delete entry.dead_lettered_inbound_markers;\n\n const busy = resolvers.busyActivityAgeSeconds(codeName);\n if (busy !== null && Number.isFinite(busy)) {\n entry.busy_activity_age_seconds = Math.max(0, Math.floor(busy));\n }\n const turn = resolvers.turnCompletionAgeSeconds(codeName);\n if (turn !== null && Number.isFinite(turn)) {\n entry.turn_completion_age_seconds = Math.max(0, Math.floor(turn));\n }\n // ENG-10045 — THE RULE, stated rather than counted (Koda on #5931).\n //\n // The gauge RIDES ALONG with an entry that exists on its own merits.\n // A POSITIVE gauge is itself a reason for the entry to exist.\n // A ZERO gauge is never a reason.\n //\n // The first draft of this was `Object.keys(entry).length === 2`, which is the\n // same rule expressed as arithmetic over the object's shape — and it is\n // wrong the moment anyone adds another field to this collector, because the\n // magic number silently stops matching and every never-spawned agent starts\n // emitting again. Nothing would fail; the entries would just come back. The\n // form below cannot drift that way: it asks the question directly.\n //\n // Why a zero is safe to drop: there is nothing to clear. An agent this quiet\n // has no alarm state to move, and `TreatMissingData: 'ignore'` means a\n // missing datapoint leaves the alarm exactly where it is. A POSITIVE count\n // must always get through — a stranded message is a signal by definition, and\n // losing it here would be this whole ticket again.\n const hasSignalOfItsOwn = Object.keys(entry).length > 1;\n if (strandedMarkers !== undefined && (strandedMarkers > 0 || hasSignalOfItsOwn)) {\n entry.dead_lettered_inbound_markers = strandedMarkers;\n }\n // Nothing but a name. Sending it would have the API iterate a payload entry\n // that can produce no metric and no write — on every cycle, for every agent\n // that has never spawned. Absent is already the API's \"no signal\", so say\n // nothing rather than say nothing at length.\n if (Object.keys(entry).length === 1) continue;\n results.push(entry);\n }\n return results;\n}\n","/**\n * ENG-8265 — scan an agent's pending-inbound marker dirs for the oldest\n * OUTSTANDING interim promise, and report its age on the responsiveness probe.\n *\n * The predicate itself (what counts as a promise, and why `seen_at` does not) is\n * in `@augmented/core/channels/interim-promise.js` — read that file first; it\n * carries the design rationale and the full list of shapes that must never\n * count. This module is only the filesystem half: walk `<agentHome>/*-pending-\n * inbound/`, parse each marker, and take the earliest `promised_at`.\n *\n * Deliberate differences from the two sibling scans in `responsiveness-probe.ts`:\n *\n * - **No `sessionStartMs` exclusion.** `oldestLivePendingInboundMtimeMs` skips\n * markers older than the current session because a leftover marker cannot\n * prove *this* session is failing to drain. Applying that here would have made\n * ENG-8265 a no-op in the incident that motivated it: stirling's promise was\n * made before two mid-turn respawns, so a session-relative measure would\n * discard the only evidence. A promise survives a restart precisely because\n * the user's expectation does.\n * - **Age from the marker's `promised_at`, not its mtime.** Every in-place\n * rewrite (a reaction, a `read_thread`, a replay-payload attach) bumps mtime;\n * none of them discharge a promise.\n * - **A malformed marker is excluded, not included.** The live wedge scan treats\n * an unreadable marker as LIVE so a corrupt file can never mask a real wedge.\n * Here the asymmetry is reversed: this signal opens an alert, and a false alert\n * about a healthy conversation is the expensive outcome, so an unparseable\n * marker fails quiet.\n */\nimport { readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport {\n interimPromiseAgeSeconds,\n outstandingPromiseAtMs,\n type InterimPromiseMarkerFields,\n} from '@augmented/core/channels/interim-promise.js';\n\n/**\n * Earliest `promised_at` (ms epoch) across every marker under `agentHomeDir` that\n * carries an outstanding promise, or `null` when the agent owes none.\n *\n * `null` is the healthy steady state and the reason the probe field is OMITTED\n * rather than sent as 0 — the absent-vs-zero contract every other probe counter\n * follows (a 0 would read as \"promise made 0s ago\", i.e. permanently healthy, and\n * would re-mask the failure on mixed-version fleets).\n */\nexport function oldestOutstandingPromiseAtMs(agentHomeDir: string): number | null {\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // agent home missing — nothing to report\n }\n for (const entry of entries) {\n // Same directory contract as the sibling scans: only `<channel>-pending-\n // inbound/`. The `-stale` / `-dead-letter` siblings deliberately do not match\n // (a parked-aside marker is no longer an outstanding promise), and hidden\n // entries are MCP bookkeeping (`.markers/`, `.processed/`), never markers.\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n let marker: InterimPromiseMarkerFields;\n try {\n marker = JSON.parse(readFileSync(join(dir, file.name), 'utf8')) as InterimPromiseMarkerFields;\n } catch {\n // Vanished between readdir and read (a final reply landed — the happy\n // path), or unparseable. Either way: not a promise. See the header note\n // on why this fails quiet where the wedge scan fails loud.\n continue;\n }\n const promisedAt = outstandingPromiseAtMs(marker);\n if (promisedAt === null) continue;\n if (oldest === null || promisedAt < oldest) oldest = promisedAt;\n }\n }\n return oldest;\n}\n\n/**\n * Age (seconds) of the oldest outstanding promise under `agentHomeDir`, or `null`\n * when there is none. This is the value the responsiveness probe reports as\n * `promise_outstanding_oldest_age_seconds`.\n */\nexport function outstandingPromiseOldestAgeSeconds(\n agentHomeDir: string,\n now: Date = new Date(),\n): number | null {\n const promisedAt = oldestOutstandingPromiseAtMs(agentHomeDir);\n if (promisedAt === null) return null;\n return interimPromiseAgeSeconds(promisedAt, now.getTime());\n}\n\n/**\n * Convenience form keyed on an agent code name, for callers that only have the\n * pane-log path convention to hand. `paneLogPathFn` is injected rather than\n * imported so this module stays free of `responsiveness-probe.ts`'s import graph\n * (and testable without a real agent home).\n */\nexport function outstandingPromiseOldestAgeSecondsForAgent(\n codeName: string,\n paneLogPathFn: (codeName: string) => string,\n now: Date = new Date(),\n): number | null {\n return outstandingPromiseOldestAgeSeconds(dirname(paneLogPathFn(codeName)), now);\n}\n","/**\n * ENG-8265 — the outstanding-interim-promise predicate.\n *\n * ## The failure this closes\n *\n * An agent that replies *\"On it — pulling your v10 first…\"* and then never\n * delivers trips **nothing**. `agent_stall` only selects cards with\n * `status = 'in_progress'` and the agent had none; `agent_pending_inbound`\n * measures marker age from the marker's mtime, which the interim ack's own\n * in-place rewrite advances; and `agent_channel_silent_loss` never measured\n * \"did a reply go out\" at all (it counts `ChannelDeflections Cause=replay_orphaned`\n * — a recoverable inbound GC'd before the replay scanner re-pushed it, which an\n * interim ack neither causes nor suppresses). So the one failure a customer\n * noticed by hand was the one the platform could not see.\n *\n * ## Why `seen_at` is NOT the signal\n *\n * ENG-6567 already stopped an interim ack from DELETING the pending marker: it\n * downgrades it in place by stamping `seen_at`, so the Stop-hook ghost-reply\n * recovery stays armed for the eventual answer. It is tempting to read a\n * lingering `seen_at` marker as an outstanding promise. **It is not.**\n * `seen_at` is stamped by every form of engagement — an interim ack, a\n * `slack.read_thread`, an emoji reaction, `noteThreadActivityByMessageTs` on a\n * reaction to any message in the thread. An agent that reads a thread, decides\n * the message is not for it and correctly says nothing owes no follow-up\n * whatsoever, and alerting on that would page a human about a conversation that\n * was fine — repeatedly, on every skipped auto-followed thread, which is exactly\n * how an alert gets trained into background noise.\n *\n * So this module keys off a DISTINCT field, `promised_at`, written by exactly one\n * caller: the channel reply tool on the `interim: true` path. `interim: true` is\n * the agent explicitly declaring \"the real answer is still coming\" (it is what\n * the tool's own schema says), so `promised_at` present + the marker still on\n * disk is a promise the agent made and has not discharged. A final reply\n * (`interim` omitted/false) DELETES the marker via the existing settle path —\n * including the thread fan-out, so any substantive reply anywhere in the thread\n * discharges the promise, not just one keyed to the original message_ts.\n *\n * ## Why the age is measured from `promised_at`, not from mtime\n *\n * Two reasons, both load-bearing:\n *\n * 1. **mtime is resettable by unrelated engagement.** A reaction or a\n * `read_thread` rewrites the marker (to stamp `seen_at`) and bumps its\n * mtime. Measuring the promise from mtime would let an agent that keeps\n * touching the thread hold a broken promise open forever without the age\n * ever climbing.\n * 2. **mtime does not survive the incident that motivated the ticket.**\n * Stirling's session was killed twice mid-turn by token rotations\n * (ENG-8264). `promised_at` is a value inside the file, so the clock keeps\n * running across a respawn; an mtime- or session-relative measure would not.\n *\n * For the same reason this predicate deliberately does **NOT** reuse the\n * wedge-detector's `sessionStartMs` exclusion (`oldestLivePendingInboundMtimeMs`\n * skips markers older than the current session). That exclusion is correct for\n * \"is THIS session failing to drain\" and would have made this fix a no-op in the\n * exact incident it was filed for — stirling's promise predates both respawns.\n *\n * ## Shapes that must never count as an outstanding promise\n *\n * - no `promised_at` (write-once marker, `seen_at`-only engagement, a\n * read_thread, a reaction, an unrelated participant's new message)\n * - `discretionary: true` — a skippable auto-followed participant-thread\n * inbound the agent may legitimately never answer\n * - `undeliverable: true` — already dead-lettered by the channel; the user got\n * a ⏳ notice, so no promise is outstanding\n * - a marker that no longer exists — a final reply deleted it, which is the\n * normal ack-then-deliver path and the single most important negative case\n * - a malformed / unparseable `promised_at` — a corrupt value must not\n * manufacture a promise (contrast the wedge live-scan, which treats a corrupt\n * marker as LIVE because there a corrupt marker must never MASK a real wedge;\n * here the asymmetry runs the other way, so it fails quiet)\n * - `promised_at` in the future (clock skew) — clamped to age 0, never negative\n */\n\n/**\n * The subset of a channel pending-inbound marker this predicate reads. Every\n * field is optional: markers written before ENG-8265, and by channels with no\n * interim concept (direct-chat has no `interim` argument), carry none of them and\n * must parse cleanly to \"no promise\".\n */\nexport interface InterimPromiseMarkerFields {\n /**\n * ENG-8265: ISO timestamp stamped when the agent posted an INTERIM reply on\n * this inbound — an explicit \"the real answer is still coming\". Written only\n * by the reply tool's `interim: true` path, never by `read_thread` / reaction\n * engagement (those stamp `seen_at` only). First write wins: a second interim\n * ack on the same inbound must not restart the promise clock, or an agent that\n * keeps saying \"still working\" would never be visible as having dropped it.\n */\n promised_at?: unknown;\n /** ENG-6567 engagement stamp. Present for ALL engagement — not a promise. */\n seen_at?: unknown;\n /** ENG-5846: dead-lettered by the channel; the user was told, nothing owed. */\n undeliverable?: unknown;\n /** ENG-6319: skippable auto-followed inbound; a deliberate skip owes nothing. */\n discretionary?: unknown;\n}\n\n/**\n * Default promise window, in seconds. The ticket's guidance is \"start generous,\n * ~30 min, tune down\"; 30 min is also what the agent's own instructions imply\n * (\"I'll come back shortly\"), so it is the threshold that is easiest to defend.\n *\n * Shared between the CLI reporter (which needs it only for its own tests and\n * docs — CloudWatch does the comparing) and the API alarm, so the two can never\n * drift into a state where the reported age and the alarmed threshold disagree.\n */\nexport const DEFAULT_INTERIM_PROMISE_THRESHOLD_SECONDS = 1800;\n\n/**\n * `promised_at` as ms-epoch when this marker carries an OUTSTANDING promise,\n * else `null`. Pure: the caller supplies the parsed marker, so this is trivially\n * exhaustible over every shape above.\n */\nexport function outstandingPromiseAtMs(marker: InterimPromiseMarkerFields | null | undefined): number | null {\n if (!marker || typeof marker !== 'object') return null;\n // A skip and a dead-letter both mean \"nothing is owed here\", regardless of\n // whether an interim ack was posted earlier.\n if (marker.discretionary === true) return null;\n if (marker.undeliverable === true) return null;\n if (typeof marker.promised_at !== 'string' || marker.promised_at === '') return null;\n const ms = Date.parse(marker.promised_at);\n // Unparseable timestamp: fail quiet. A corrupt value must not invent a promise\n // — the cost of a false alert here is a human paged about a healthy thread.\n if (!Number.isFinite(ms)) return null;\n return ms;\n}\n\n/**\n * Age (whole seconds) of a promise made at `promisedAtMs`, clamped at 0 so clock\n * skew between the marker writer and the probe can never produce a negative age\n * (which would sail under any threshold and read as healthy).\n */\nexport function interimPromiseAgeSeconds(promisedAtMs: number, nowMs: number): number {\n return Math.max(0, Math.floor((nowMs - promisedAtMs) / 1000));\n}\n\n/**\n * Whether a reported promise age breaches the window. Mirrors the CloudWatch\n * alarm's `GreaterThanOrEqualToThreshold` **exactly**, so a test can assert \"an\n * alert by T+30min\" against the same comparison the alarm performs rather than\n * against a re-derived one. `>=` (not `>`) is deliberate: the acceptance\n * criterion is \"alert **by** T+30min\", and with `>` a promise sitting at exactly\n * 1800s would wait for the next publish cycle.\n */\nexport function interimPromiseBreaches(\n ageSeconds: number,\n thresholdSeconds: number = DEFAULT_INTERIM_PROMISE_THRESHOLD_SECONDS,\n): boolean {\n return Number.isFinite(ageSeconds) && ageSeconds >= thresholdSeconds;\n}\n\n/**\n * What kind of engagement is being recorded on a marker.\n *\n * - `interim_ack` — the agent replied with `interim: true`, explicitly promising\n * a substantive answer still to come. The ONLY kind that records a promise.\n * - `seen` — every other form of engagement: a `read_thread`, a reaction, a\n * deliberate skip. Proves the agent saw the inbound; promises nothing.\n */\nexport type MarkerEngagementKind = 'interim_ack' | 'seen';\n\nexport interface MarkerEngagementDecision {\n /** Fields to merge into the marker, or `null` when no rewrite is needed. */\n stamp: { seen_at?: string; promised_at?: string } | null;\n /**\n * Whether the marker carries an outstanding promise once this decision is\n * applied — `true` also when it was ALREADY promised and no rewrite is needed,\n * so a caller can treat \"already in the desired state\" as success rather than\n * as a failed write.\n */\n promised: boolean;\n}\n\n/**\n * Decide what an engagement stamps on a pending-inbound marker. Shared by the\n * Slack and Telegram channel servers so the two can never drift on the one\n * question that matters here — which engagements create a promise and which do\n * not. Pure; the caller owns reading and writing the file.\n *\n * Rules, each of which a negative test depends on:\n *\n * - `seen` never writes `promised_at`. A read_thread or a reaction must not make\n * a thread the agent correctly skipped look like a broken promise.\n * - `seen` on an already-seen marker rewrites NOTHING (keeps the first\n * engagement timestamp, and keeps a reaction storm from bumping mtime).\n * - `interim_ack` on an already-promised marker rewrites NOTHING and reports\n * `promised: true`. Keeping the FIRST promise timestamp is what makes the\n * elapsed-time signal measure the whole outstanding window; restarting it on\n * every \"still working\" ack would hide the drop indefinitely.\n * - `interim_ack` on a seen-but-unpromised marker adds `promised_at` while\n * leaving the earlier `seen_at` intact.\n */\nexport function decideMarkerEngagement(\n marker: InterimPromiseMarkerFields | null | undefined,\n kind: MarkerEngagementKind,\n nowIso: string,\n): MarkerEngagementDecision {\n const alreadySeen = typeof marker?.seen_at === 'string' && marker.seen_at !== '';\n const alreadyPromised = typeof marker?.promised_at === 'string' && marker.promised_at !== '';\n\n if (kind === 'seen') {\n return alreadySeen\n ? { stamp: null, promised: alreadyPromised }\n : { stamp: { seen_at: nowIso }, promised: alreadyPromised };\n }\n if (alreadyPromised) return { stamp: null, promised: true };\n return {\n stamp: alreadySeen ? { promised_at: nowIso } : { seen_at: nowIso, promised_at: nowIso },\n promised: true,\n };\n}\n\n/**\n * Per-settle tally of interim-promise stamp attempts, as each channel server\n * records it while fanning out over the markers a reply settles.\n */\nexport interface PromiseStampTally {\n /** Markers the stamp op was applied to. */\n attempted: number;\n /** Of those, how many now carry `promised_at` on disk. */\n stamped: number;\n}\n\n/**\n * The sentence a reply tool appends to its result after an `interim: true` send,\n * or `null` when nothing should be said.\n *\n * Shared by the Slack and Telegram servers for the same reason\n * {@link decideMarkerEngagement} is: an interim ack that the platform failed to\n * RECORD is the very failure this ticket closes, one layer down — nothing is\n * tracking the owed reply, and no alarm will ever fire because `promised_at` was\n * never written. A bare `sent` in that case tells the agent the obligation is\n * being watched when it is not. Slack surfaced this from day one and Telegram did\n * not (it only wrote the operator-facing stderr line), which is precisely the\n * cross-channel drift this module exists to prevent — so the text lives here and\n * both channels read it.\n *\n * Three outcomes:\n *\n * - `attempted > stamped` — a marker matched but the promise did not persist.\n * WARN: the agent must not rely on a reminder that cannot arrive, so tell it to\n * answer in this same turn.\n * - `stamped > 0` — recorded. Say so, so the agent knows the final reply is still\n * owed and is being tracked.\n * - `attempted === 0` — no marker matched this settle (a proactive interim post,\n * or the inbound was already discharged). Not an error, and not worth a line.\n */\nexport function describePromiseStampOutcome(\n tally: PromiseStampTally,\n /**\n * The channel's own reply tool (`slack.reply` / `telegram.reply`). Only the\n * success line names it — an instruction to \"send a separate reply\" is\n * actionable only if the agent is told which tool call discharges the promise,\n * and naming the wrong channel's tool would be worse than naming none.\n */\n replyToolName: string,\n): string | null {\n if (tally.attempted > tally.stamped) {\n return 'WARNING: this was recorded as an interim ack but the outstanding-promise marker could NOT be written, so nothing is tracking that you still owe a final reply. Do not rely on a reminder — post your substantive answer in this same turn.';\n }\n if (tally.stamped > 0) {\n return `Recorded as an interim ack — your final answer is still owed and is being tracked until you send it as a separate ${replyToolName} with interim omitted.`;\n }\n return null;\n}\n","/**\n * Deflection causes — the ONE definition (ENG-10054).\n *\n * WHY THIS LIVES IN CORE. This list had three copies: the writer\n * (`packages/mcp/src/ack-reaction.ts`), the probe reader\n * (`apps/cli/src/lib/responsiveness-probe.ts`), and the API ingest filter\n * (`packages/api/src/routes/host-runtime.ts`). The third had drifted to TEN of\n * the twelve, missing `aged_out_unrecoverable` and `marker_corrupt`, and the\n * ingest drops an unlisted cause with a bare `continue` — no log, no counter —\n * before `PutMetricData`.\n *\n * So those two CloudWatch series read zero, and the zero meant \"never\n * ingested\", not \"never happened\". They are ENG-9414's message-DESTRUCTION\n * counters, added precisely because \"four destroyed customer inbounds in one\n * hour produced zero alerts\". The counters shipped; the metric never arrived;\n * the silence they were built to end continued. A drifted copy of a list is\n * how a fix gets quietly undone.\n *\n * The ingest filter itself was right to be an allowlist — its comment explains\n * that an unknown key from a corrupt counter file would otherwise add an\n * unbounded CloudWatch dimension value. That reasoning is preserved: this is\n * still a closed allowlist, it is simply the SAME allowlist everywhere.\n *\n * RULE FOR WHOEVER ADDS THE NEXT CAUSE: add it here and nowhere else. If you\n * find yourself typing a cause string into a second file, that is the defect\n * this module exists to prevent.\n */\n\n/**\n * Why an inbound could not be delivered at all. The three hard-failure classes\n * `classifyUndeliverableCause` attributes; everything else is a healthy path.\n *\n * Moved here with `DeflectionCause` (ENG-10054) because the latter is a union\n * over it, and splitting a type across packages to keep a constant in one place\n * would just trade one drift surface for another. `ack-reaction.ts` re-exports\n * both, so existing importers are unaffected.\n */\nexport type UndeliverableCause = 'integration_down' | 'session_dead' | 'wedged';\n\n/**\n * The attributed trigger for a deflection. The undeliverable subset comes\n * straight from classifyUndeliverableCause; 'busy' is the busy-ack path;\n * 'duplicate' is a redundant re-delivery dropped at fresh ingress (ENG-6270);\n * 'replay_orphaned' is a recoverable inbound (a marker carrying a replay\n * payload) the orphan sweep GC'd WITHOUT it ever being replayed (ENG-6355 —\n * the candidate-silent-loss canary); 'replay_exhausted' (ENG-7736) is its\n * sibling for a marker GC'd AFTER one or more replay pushes that still never\n * cleared - under respawn churn a forced push can land in a session that is\n * killed before the turn starts, and without this cause those losses would be\n * invisible (replay_orphaned requires replay_count === 0, so bumping the\n * count would otherwise silence the alarm that caught the Sherlock loss); and\n * 'unknown' is the fail-safe when the cause genuinely can't be determined\n * (un-probeable host whose liveness reads 'unknown') — honest\n * under-attribution rather than a confident wrong label.\n *\n * ENG-6270/6355: 'duplicate' and 'replay_orphaned' share this cause-dimensioned\n * pipeline (rather than sibling metrics) so the ChannelDeflections{Cause=…}\n * series give the pre-flip baselines the ENG-6354 replay soak watches — dup-rate\n * (Cause=duplicate) must stay ~0 and candidate-silent-loss (Cause=replay_orphaned)\n * must fall when replay is enabled. No new counter-file kind, probe field, or\n * PutMetricData path to maintain.\n */\nexport type DeflectionCause =\n | UndeliverableCause\n | 'busy'\n | 'duplicate'\n | 'duplicate_mention'\n // ENG-8387 AC3: an echo we dropped whose app_mention twin NEVER arrived -\n // a mention thrown away and never delivered. Distinct from duplicate_mention,\n // which is a mention that did arrive and was dropped as a re-delivery.\n | 'mention_echo_orphan'\n | 'replay_orphaned'\n | 'replay_exhausted'\n // ENG-9414: a genuinely-aged, non-discretionary marker GC'd with NO replay\n // payload. `replay_orphaned` cannot see this class, by construction: it\n // requires `marker.payload`, and this is precisely the marker that has none.\n //\n // It is not the mild case. The payload is attached ~180 lines and three\n // awaits AFTER the marker is written (slack-channel.ts:9519 vs :9701), and\n // the in-session push to the agent happens after the attach - so a throw in\n // that window leaves a marker with no payload whose message THE AGENT WAS\n // NEVER NOTIFIED OF AT ALL. Unreplayable and undelivered. Under the old gate\n // it was deleted at 30 minutes with no record anywhere that it had existed.\n | 'aged_out_unrecoverable'\n // ENG-9414: a marker deleted WITHOUT being aged - structurally broken\n // (missing channel/thread/message ts), unparseable, or carrying a\n // future-dated `received_at`. `isPendingMarkerStale` deletes all three;\n // `isMarkerGenuinelyAged` returns false for all three, so none was counted.\n //\n // The clock-skew case is the one to watch: an NTP step backwards\n // future-dates every marker written in that window, so a single skew event\n // deletes an agent's entire live queue silently. A non-zero rate here means\n // messages are being destroyed for a reason that has nothing to do with age.\n | 'marker_corrupt'\n | 'unknown';\n\nexport const DEFLECTION_CAUSES: readonly DeflectionCause[] = [\n 'integration_down',\n 'session_dead',\n 'wedged',\n 'busy',\n 'duplicate',\n // ENG-8387: an `app_mention` — the CANONICAL delivery of an @mention — dropped\n // by the fresh-ingress dedup. Split out of the generic `duplicate` bucket\n // because the two mean very different things: `duplicate` is dominated by the\n // deliberate, high-volume ENG-6378 echo drop and by routine reconnect\n // redelivery, both benign, while a dropped app_mention has the exact shape of\n // a lost mention. Conflated, the second is invisible inside the first — which\n // is how ENG-6378's silent loss ran unobserved.\n 'duplicate_mention',\n 'mention_echo_orphan',\n 'replay_orphaned',\n 'replay_exhausted',\n // ENG-9414 — see the type union above for why these two exist. Both are\n // message-loss classes the pre-existing causes were structurally unable to\n // observe, which is why four destroyed customer inbounds in one hour\n // produced zero alerts.\n 'aged_out_unrecoverable',\n 'marker_corrupt',\n 'unknown',\n];\n\n/**\n * The same causes as a Set, for the membership tests the readers do.\n *\n * Typed `ReadonlySet<string>` rather than `ReadonlySet<DeflectionCause>` on\n * purpose: every caller is checking an UNTRUSTED string off the wire (a probe\n * payload, a counter file) against the allowlist. A `Set<DeflectionCause>`\n * would force each of them to cast the untrusted value to the very type the\n * check exists to establish, which defeats the check.\n */\nexport const DEFLECTION_CAUSE_SET: ReadonlySet<string> = new Set<string>(DEFLECTION_CAUSES);\n\n/** Narrowing membership test over the one allowlist. */\nexport function isDeflectionCause(value: string): value is DeflectionCause {\n return DEFLECTION_CAUSE_SET.has(value);\n}\n"],"mappings":"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","names":["readdirSync","readFileSync","dirname","join","readdirSync","join","readFileSync","dirname"]}
1
+ {"version":3,"sources":["../src/lib/responsiveness-probe.ts","../src/lib/interim-promise-scan.ts","../../../packages/core/src/channels/interim-promise.ts","../../../packages/core/src/channels/deflection-causes.ts"],"sourcesContent":["/**\n * ENG-5399 — Tier 1 responsiveness probe (manager-side).\n *\n * Cheap, fast-cadence canary that catches \"agent went silent\" inside\n * minutes, well before the existing synthetic-probe cron's ~35 min\n * staleness window (`SyntheticReplyAgeSeconds`, ENG-5122).\n *\n * Mechanism: for each managed agent, read the mtime of the agent's\n * `pane.log` and report `now - mtime` as `PaneActivityAgeSeconds` via\n * a new `/host/responsiveness-probe` endpoint. `pane.log` is the\n * tmux pipe-pane sink set up by `setupPaneLog()` — any visible\n * activity (assistant turns, tool calls, in-place progress\n * heartbeats) bumps its mtime. A silent agent has a steadily\n * climbing age that lands in CloudWatch and trips a per-agent alarm.\n *\n * ENG-6017 adds a second per-agent signal on the same cadence:\n * `pending_inbound_oldest_age_seconds` — the age of the oldest marker\n * file across the agent's `*-pending-inbound/` directories (written by\n * the channel MCP servers for inbounds awaiting delivery). This is the\n * one artifact of the \"message typed but never submitted\" failure mode\n * that every other canary is blind to: in the koda incident\n * (2026-06-04) an operator Slack DM sat undelivered for 40+ minutes\n * while pane-activity stayed fresh (health checks), synthetic probes\n * were answered by the one-shot fallback, and heartbeat/session-alive\n * only reflect manager health. The field is OMITTED (not zero) when the\n * agent has no pending-inbound markers — the API treats absent as\n * \"no signal\", never as \"healthy\" (absent-vs-zero matters for\n * mixed-version fleets where old CLIs don't report it at all).\n *\n * Run from `pollCycle()` in `manager-worker.ts` on a configurable\n * interval (default 60s via `AUGMENTED_RESPONSIVENESS_INTERVAL_MS`;\n * lowered from 5 min by ENG-7550 - this post is what refreshes\n * `agents.last_pane_activity_at`, the minutely busy-sampling substrate).\n */\n\nimport { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { outstandingPromiseOldestAgeSeconds } from './interim-promise-scan.js';\nimport { paneLogPath } from './persistent-session.js';\nimport { DEFLECTION_CAUSES } from '@augmented/core/channels/deflection-causes.js';\n\nexport interface ResponsivenessProbeResult {\n code_name: string;\n pane_activity_age_seconds: number;\n /**\n * ENG-6017: age (s) of the oldest marker file across the agent's\n * `*-pending-inbound/` directories. Omitted when no markers exist —\n * absent means \"no signal\", NOT \"zero / healthy\".\n */\n pending_inbound_oldest_age_seconds?: number;\n /**\n * ENG-10045: how many DEAD-LETTERED inbound markers are on the host for this\n * agent RIGHT NOW - the files in its `*-pending-inbound-stale/` dirs, which is\n * the `store: stale` set `debug_inspect_dead_letters` reads.\n *\n * ALWAYS PRESENT, INCLUDING ZERO, and that is the whole point of the field\n * rather than an oversight against the convention its neighbours follow.\n *\n * Every other counter here is omitted when empty, because for an AGE an absent\n * field honestly means \"no signal\". This one feeds a CloudWatch gauge whose\n * alarm carries `OKActions`, and on that path \"no datapoint\" and \"the publisher\n * stopped\" are the same observation - `TreatMissingData: 'notBreaching'` reads\n * both as fine and CLOSES the alert. An affirmative zero is the difference\n * between an OK that means \"nothing is stranded\" and an OK that means \"nobody\n * told me\". ENG-10045 exists because an alarm said the first while meaning\n * something closer to the second.\n */\n dead_lettered_inbound_markers?: number;\n /**\n * ENG-8265: age (s) of the oldest OUTSTANDING interim promise — an inbound the\n * agent answered with `interim: true` (\"the real answer is still coming\") and\n * has not yet answered substantively. Measured from the marker's `promised_at`,\n * so it survives a mid-turn respawn and cannot be reset by unrelated engagement.\n * Omitted when the agent owes no promise (the healthy steady state) — absent\n * means \"no signal\", NOT \"zero / healthy\". See\n * `@augmented/core/channels/interim-promise.js` for why a `seen_at` marker is\n * not a promise.\n */\n promise_outstanding_oldest_age_seconds?: number;\n /**\n * ENG-6327: per-cause inbound deflection counts since the last probe (read +\n * RESET from the channel servers' `<channel>-deflections.json` counter files).\n * Omitted when there were none. Keyed by DeflectionCause.\n */\n deflections?: Record<string, number>;\n /**\n * ENG-6408 / ADR-0024 Slice 1.5: shadow lane-classification counts since the\n * last probe (read + RESET from `<channel>-lane-classifications.json`). Keyed\n * by `<lane>|<expects_reply>|<source>` plus the reserved\n * `suspected_misclassification` canary. Omitted when there were none.\n */\n lane_classifications?: Record<string, number>;\n /**\n * WS0 (Slack cross-thread reply routing): SHADOW reply-target classification\n * counts since the last probe (read + RESET from\n * `<channel>-reply-target-classifications.json`). Keyed by\n * `<result>|<pending_threads_bucket>`. Omitted when there were none.\n */\n slack_reply_target_classifications?: Record<string, number>;\n /**\n * WS2 (Slack cross-thread reply routing): SHADOW reply-BINDING classification\n * counts since the last probe (read + RESET from\n * `<channel>-reply-binding-classifications.json`). Keyed by the binding\n * classification (bound / coords_corrected / ... / unknown_inbound_id).\n * Omitted when there were none.\n */\n slack_reply_binding_classifications?: Record<string, number>;\n /**\n * ENG-7855: slack-hot-thread-guard (ENG-7462) SHADOW/enforce outcome counts\n * since the last probe (read + RESET from\n * `<channel>-hot-thread-classifications.json`). Keyed by\n * `<mode>|<outcome>|<proactive>`. Omitted when there were none (the guard only\n * records in shadow/enforce, so off-mode agents never populate this).\n */\n slack_hot_thread_classifications?: Record<string, number>;\n /**\n * ENG-8346: direct-chat cursor advances that moved FEWER rows than were asked\n * for, since the last probe (read + RESET from\n * `<source>-cursor-advance-classifications.json`). Keyed by\n * `<route>|<reason>|<partial>`. Omitted when there were none — which is the\n * healthy state, so a populated field is always worth reading.\n */\n direct_chat_cursor_shortfall_classifications?: Record<string, number>;\n}\n\n/**\n * ENG-8090: what actually goes on the wire to `/host/responsiveness-probe`.\n *\n * Differs from `ResponsivenessProbeResult` in two ways, both because opencode\n * agents have no `pane.log`:\n *\n * - `pane_activity_age_seconds` is OPTIONAL. An opencode agent reports\n * occupancy and turn completion instead; the API tolerates its absence and\n * simply writes no `last_pane_activity_at` for that agent.\n * - two opencode-only fields carry the split signals.\n *\n * Kept as a separate type so `collectResponsivenessProbes` — whose result the\n * WEDGE detector consumes and which must keep requiring a real pane age — is not\n * widened. See `collectPanelessActivityProbes`.\n */\nexport interface ResponsivenessProbePayloadEntry\n extends Omit<ResponsivenessProbeResult, 'pane_activity_age_seconds'> {\n pane_activity_age_seconds?: number;\n /**\n * ENG-8090: seconds since the agent was last doing work — 0 while a turn is in\n * flight. The OCCUPANCY signal, and the substrate for busy/idle sampling and\n * agent-hours billing. Omitted when the manager has observed no turn for this\n * agent: absent is \"no signal\", never \"idle\".\n *\n * Deliberately NOT a liveness signal. A serve wedged mid-turn reports 0 here.\n */\n busy_activity_age_seconds?: number;\n /**\n * ENG-8090: seconds since a turn last produced real reply text. The LIVENESS\n * signal — \"turns are completing\", not \"the process printed something\" — and\n * the only one of the two that may resolve a synthetic probe. Omitted when no\n * turn has ever succeeded in this manager generation.\n */\n turn_completion_age_seconds?: number;\n /**\n * ENG-8116: MEASURED occupancy, as whole closed wall-clock minutes.\n *\n * This is what replaces cadence-counted buckets. The two `*_age_seconds`\n * fields above describe an INSTANT, which forced the API to infer duration\n * from how often probes arrived — and probe arrival is 87s apart at the fleet\n * median, 182s at p90, against a 60s sampling window. Those buckets are\n * measured host-side instead, so the total is invariant to how often this\n * payload is sent.\n *\n * Only closed minutes appear here; the in-progress minute is still accruing\n * and is reported once it closes. Absent means \"no occupancy to report\",\n * which is distinct from an agent that reported none because it is on an old\n * CLI — the API keeps the legacy freshness path for those.\n */\n busy_buckets?: Array<{ bucket: string; seconds: number }>;\n /**\n * ENG-8465: SHADOW of the occupancy-qualification gate — per-drain counts of\n * how the reported `busy_buckets` split against transcript evidence of real\n * work (`occupancy-gate.ts` + the merged `@augmented/core` qualifier). Keyed by\n * `qualified` (a bucket with a real turn nearby — the positive control),\n * `unqualified_dropped` (no qualifying turn near it — dropped under `enforce`,\n * still billed under `shadow`), and `unreadable` (transcripts unreadable, so\n * the buckets were credited UNGATED — fail-open). Omitted when there were none.\n * See KNOWN_OCCUPANCY_QUALIFICATION_OUTCOMES; loss on a failed POST is accepted\n * (shadow telemetry, never re-credited like `busy_buckets`).\n */\n occupancy_qualification_classifications?: Record<string, number>;\n}\n\n/**\n * ENG-8465: the only occupancy-qualification outcome keys. Mirrored EXACTLY in\n * packages/api/src/routes/host-runtime.ts and enforced by\n * scripts/check-classification-allowlist-parity.mjs — a key present on one side\n * only is silently dropped, reading as a zero that means \"never ingested\". Add a\n * new key to BOTH sets in the same change.\n */\nconst KNOWN_OCCUPANCY_QUALIFICATION_OUTCOMES = new Set([\n 'qualified',\n 'unqualified_dropped',\n 'unreadable',\n // ENG-8571: bucket kept (never dropped) because the bounded transcript tail did\n // not reach back far enough to judge it. MUST also be in the API-side set\n // (routes/host-runtime.ts) or the parity guard drops it to a silent zero.\n 'coverage_shortfall',\n // ENG-9178: bucket kept because it overlaps a COMPLETED tool-call bracket with\n // no qualifying turn in reach — the middle of a long tool call. Separate from\n // `qualified` so the flip can be sized against turn evidence and bracket\n // evidence independently. Mirror in the API set.\n 'qualified_by_bracket',\n // ENG-9178: bucket HELD (neither reported nor dropped) because it sits inside a\n // tool call that has not returned. Under `enforce` this is also the signal that\n // stands the legacy cron down for a drain that reported no buckets at all —\n // see the ENG-8625 note on hasOccupancyQualification. Mirror in the API set.\n 'bracket_deferred',\n]);\n\n/**\n * Build the wire classification record from a {@link QualifyBucketsResult}-shaped\n * count, keeping only known, positive-integer outcomes. Returns null when empty\n * so the caller omits the field entirely (absent = \"no signal\"). Keeping the\n * builder here — not in occupancy-gate.ts — keeps KNOWN_OCCUPANCY_QUALIFICATION_\n * OUTCOMES the single CLI-side allowlist the parity guard diffs against the API.\n */\nexport function occupancyQualificationClassifications(counts: {\n qualified: number;\n unqualifiedDropped: number;\n unreadable: number;\n coverageShortfall: number;\n qualifiedByBracket?: number;\n bracketDeferred?: number;\n}): Record<string, number> | null {\n const raw: Record<string, number> = {\n qualified: counts.qualified,\n unqualified_dropped: counts.unqualifiedDropped,\n unreadable: counts.unreadable,\n coverage_shortfall: counts.coverageShortfall,\n // ENG-9178. Optional on the input type so the manager's fail-open fallback\n // (which builds this record by hand when the qualifier throws) does not have\n // to know about outcomes it can never produce.\n qualified_by_bracket: counts.qualifiedByBracket ?? 0,\n bracket_deferred: counts.bracketDeferred ?? 0,\n };\n const out: Record<string, number> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (KNOWN_OCCUPANCY_QUALIFICATION_OUTCOMES.has(key) && Number.isSafeInteger(value) && value > 0) {\n out[key] = value;\n }\n }\n return Object.keys(out).length > 0 ? out : null;\n}\n\n/**\n * ENG-6327 — per-channel deflection counter file-layout contract. The channel\n * MCP servers increment `<channel>-deflections.json` in the agent home (via\n * recordChannelDeflection in packages/mcp/ack-reaction.ts); apps/cli does not\n * depend on @integrity-labs/mcp, so the manager re-implements the read against\n * this contract — the same read-only, no-IPC posture as the `*-pending-inbound`\n * scan above. Body is a JSON object keyed by cause with integer counts.\n */\nconst DEFLECTION_COUNTER_SUFFIX = '-deflections.json';\n// ENG-10054: this list used to be re-declared here. The comment it carried was\n// right about the danger - \"adding a cause in packages/mcp WITHOUT adding it\n// here produces a metric that is faithfully written and never observed\" - and a\n// parity test was built to enforce it. But the test only ever compared THIS copy\n// with the mcp one, and the drift happened in the THIRD copy nobody was\n// watching: the API ingest filter, missing `aged_out_unrecoverable` and\n// `marker_corrupt`. The warning was correct and the guard was pointed at the\n// wrong pair. There is now one list, in core, and no pair to keep in step.\n//\n// (The note above about apps/cli not depending on @integrity-labs/mcp still\n// holds - this imports @augmented/core, which apps/cli already depends on, so\n// the no-IPC read-only posture is unchanged.)\nconst KNOWN_DEFLECTION_CAUSES = DEFLECTION_CAUSES;\n\n/**\n * Read and RESET every `<channel>-deflections.json` in an agent home, returning\n * the summed counts by cause (or null when there are none). Consumes via atomic\n * rename-then-read-then-unlink so an increment racing the reset is carried into\n * the next window rather than lost. Only known causes are accumulated so a\n * corrupt file can't smuggle an unbounded dimension into CloudWatch.\n *\n * Reset-on-read is required: these are deltas-since-last-consume, so the metric\n * would over-count if they weren't cleared each cycle. The trade-off is that a\n * failed POST loses that cycle's counts (under-count) — acceptable for a\n * frequency metric and consistent with the other best-effort probe siblings;\n * unlike the in-memory give-up counter, file-based counts aren't re-credited\n * on POST failure.\n */\nexport function readAndResetChannelDeflections(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(DEFLECTION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming); // atomic; replaces any stale .consuming from a crashed cycle\n } catch {\n continue; // vanished / not a regular file — skip\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const cause of KNOWN_DEFLECTION_CAUSES) {\n const n = parsed[cause];\n if (typeof n === 'number' && Number.isFinite(n) && n > 0) {\n total[cause] = (total[cause] ?? 0) + Math.floor(n);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt — drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n/**\n * ENG-6408 / ADR-0024 Slice 1.5 - per-channel lane-classification counter\n * file-layout contract. The channel MCP servers increment\n * `<channel>-lane-classifications.json` in the agent home (via\n * recordLaneClassification in packages/mcp/inbound-lane-telemetry.ts); apps/cli\n * does not depend on @integrity-labs/mcp, so the manager re-implements the read\n * against this contract (same posture as the deflection scan above). Body is a\n * JSON object keyed by `<lane>|<expects_reply>|<source>` (plus the reserved\n * `suspected_misclassification`) with integer counts.\n */\nconst LANE_CLASSIFICATION_COUNTER_SUFFIX = '-lane-classifications.json';\nconst SUSPECTED_MISCLASSIFICATION_KEY = 'suspected_misclassification';\nconst KNOWN_LANES = new Set(['conversational', 'directive', 'liveness']);\nconst KNOWN_LANE_SOURCES = new Set(['slack', 'telegram', 'msteams', 'direct-chat']);\n\n/**\n * Only well-formed composite keys (and the misclassification canary) are\n * accumulated, so a corrupt counter file can't smuggle an unbounded dimension\n * into CloudWatch - the same bounding the deflection reader applies.\n */\nfunction isKnownLaneClassificationKey(key: string): boolean {\n if (key === SUSPECTED_MISCLASSIFICATION_KEY) return true;\n const parts = key.split('|');\n if (parts.length !== 3) return false;\n const [lane, expectsReply, source] = parts as [string, string, string];\n return (\n KNOWN_LANES.has(lane) &&\n (expectsReply === 'true' || expectsReply === 'false') &&\n KNOWN_LANE_SOURCES.has(source)\n );\n}\n\n/**\n * Read and RESET every `<channel>-lane-classifications.json` in an agent home,\n * returning the summed counts by key (or null when there are none). Mirrors\n * readAndResetChannelDeflections exactly: atomic rename-then-read-then-unlink so\n * an increment racing the reset carries into the next window, and only known\n * keys are accumulated. SHADOW telemetry - informs the Slice 2 gate, gates\n * nothing itself.\n */\nexport function readAndResetChannelLaneClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(LANE_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming); // atomic; replaces any stale .consuming from a crashed cycle\n } catch {\n continue; // vanished / not a regular file — skip\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownLaneClassificationKey(key)) continue;\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + Math.floor(raw);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt — drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n/**\n * WS0 (Slack cross-thread reply routing) - per-channel reply-target counter\n * file-layout contract. The channel MCP servers increment\n * `<channel>-reply-target-classifications.json` in the agent home (via\n * recordReplyTargetClassification in packages/mcp/slack-reply-target-telemetry.ts);\n * apps/cli does not depend on @integrity-labs/mcp, so the manager re-implements\n * the read against this contract (same posture as the lane-classification scan\n * above). Body is a JSON object keyed by `<result>|<pending_threads_bucket>`\n * with integer counts.\n */\nconst REPLY_TARGET_CLASSIFICATION_COUNTER_SUFFIX = '-reply-target-classifications.json';\nconst KNOWN_REPLY_TARGET_RESULTS = new Set([\n 'matched_pending',\n 'matched_recent',\n 'crossed_candidate',\n 'unknown_target',\n]);\nconst KNOWN_PENDING_THREAD_BUCKETS = new Set(['0', '1', '2', '3plus']);\n\n/**\n * Only well-formed `<result>|<bucket>` keys are accumulated, so a corrupt counter\n * file can't smuggle an unbounded dimension into CloudWatch - the same bounding\n * the lane-classification / deflection readers apply.\n */\nfunction isKnownReplyTargetKey(key: string): boolean {\n const parts = key.split('|');\n if (parts.length !== 2) return false;\n const [result, bucket] = parts as [string, string];\n return KNOWN_REPLY_TARGET_RESULTS.has(result) && KNOWN_PENDING_THREAD_BUCKETS.has(bucket);\n}\n\n/**\n * Read and RESET every `<channel>-reply-target-classifications.json` in an agent\n * home, returning the summed counts by key (or null when there are none). Mirrors\n * readAndResetChannelLaneClassifications exactly: atomic\n * rename-then-read-then-unlink so an increment racing the reset carries into the\n * next window, and only known keys are accumulated. SHADOW telemetry - informs\n * the WS2 binding gate, gates nothing itself.\n */\nexport function readAndResetSlackReplyTargetClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(REPLY_TARGET_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming); // atomic; replaces any stale .consuming from a crashed cycle\n } catch {\n continue; // vanished / not a regular file - skip\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownReplyTargetKey(key)) continue;\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + Math.floor(raw);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// WS2 (Slack cross-thread reply routing) - per-channel reply-BINDING counter\n// file-layout contract. The slack MCP increments\n// `<channel>-reply-binding-classifications.json` (via\n// recordReplyBindingClassification in packages/mcp/slack-reply-binding-telemetry.ts);\n// apps/cli re-implements the read against this contract, same posture as the\n// reply-target scan above. Body is a JSON object keyed by the binding\n// classification (plus the reserved unknown_inbound_id canary) with integer counts.\nconst REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX = '-reply-binding-classifications.json';\nconst KNOWN_REPLY_BINDING_KEYS = new Set([\n 'bound',\n 'coords_corrected',\n 'coords_match',\n 'proactive',\n 'unknown_target',\n 'legacy',\n 'unknown_inbound_id', // reserved canary\n // ENG-7716: the ENG-7542 DM-channel guard writes these into the same counter\n // file. They are additional reserved keys (channel_mistarget_corrected counts\n // every cross-channel rewrite; dm_channel_mistarget its DM subset), overlapping\n // the main classifications the same way unknown_inbound_id does. Allowlisted so\n // the DM->channel leak signal survives the read/reset fold and reaches CloudWatch.\n 'channel_mistarget_corrected',\n 'dm_channel_mistarget',\n // ENG-7806 (fm1): the inbound-delivery-ledger verdicts the Stop hook records for\n // each recover_*_for decision (self = this inbound already delivered; moved_on =\n // a different conversation was answered after this inbound; clear = ledger says\n // recover; fallback = no ledger data, used the transcript scan). Folded into the\n // same counter-file pipe so ledger coverage reaches CloudWatch as Classification\n // dimensions on the existing SlackReplyBindingClassifications metric.\n 'delivery_ledger_self',\n 'delivery_ledger_moved_on',\n 'delivery_ledger_clear',\n 'delivery_ledger_fallback',\n // ENG-8137: the scheduled-turn channel guard writes these into the same counter\n // file (recordScheduledChannelOverride). `_shadow` counts detections while the\n // flag observes without changing routing - the false-positive signal that gates\n // the enforce flip; `_blocked` counts sends actually refused under enforce.\n // Allowlisted so both reach CloudWatch: without them this guard would fire\n // fleet-wide with no observable rate, which is the blindness that let the\n // original misroute reach customers unnoticed.\n 'scheduled_channel_override_shadow',\n 'scheduled_channel_override_blocked',\n]);\n\n/**\n * Read and RESET every `<channel>-reply-binding-classifications.json` in an agent\n * home, returning the summed counts by classification (or null when there are\n * none). Mirrors readAndResetSlackReplyTargetClassifications; only known keys are\n * accumulated so a corrupt file can't add an unbounded CloudWatch dimension.\n * SHADOW telemetry - informs the enforce flip, gates nothing itself.\n */\nexport function readAndResetSlackReplyBindingClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming);\n } catch {\n continue;\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!KNOWN_REPLY_BINDING_KEYS.has(key)) continue;\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + Math.floor(raw);\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// ENG-7855 - per-channel hot-thread-guard (ENG-7462) counter file-layout contract.\n// The slack MCP increments `<channel>-hot-thread-classifications.json` (via\n// recordHotThreadClassification in packages/mcp/slack-hot-thread-telemetry.ts);\n// apps/cli re-implements the read against this contract, same posture as the\n// reply-target / reply-binding scans above. Body is a JSON object keyed by\n// `<mode>|<outcome>|<proactive>` with integer counts.\nconst HOT_THREAD_CLASSIFICATION_COUNTER_SUFFIX = '-hot-thread-classifications.json';\n// Mirrored by the API-side sets of the same names in routes/host-runtime.ts.\n// A value accepted by only one side is dropped silently and the metric reads a\n// clean zero meaning \"never ingested\" - the ENG-7716 / ENG-8214 failure. Any\n// new mode or outcome must land in BOTH, and in the mcp HotThreadGuardResult\n// union, in the same change.\nconst KNOWN_HOT_THREAD_MODES = new Set(['shadow', 'enforce']);\nconst KNOWN_HOT_THREAD_OUTCOMES = new Set([\n 'redirected_to_hot_thread',\n 'root_forced',\n 'root_no_hot_thread',\n 'not_applicable',\n]);\n\n/**\n * Only well-formed `<mode>|<outcome>|<proactive>` keys are accumulated, so a\n * corrupt counter file can't smuggle an unbounded dimension into CloudWatch - the\n * same bounding the reply-target / reply-binding readers apply.\n */\nfunction isKnownHotThreadKey(key: string): boolean {\n const parts = key.split('|');\n if (parts.length !== 3) return false;\n const [mode, outcome, proactive] = parts as [string, string, string];\n return (\n KNOWN_HOT_THREAD_MODES.has(mode) &&\n KNOWN_HOT_THREAD_OUTCOMES.has(outcome) &&\n (proactive === 'true' || proactive === 'false')\n );\n}\n\n/**\n * Read and RESET every `<channel>-hot-thread-classifications.json` in an agent\n * home, returning the summed counts by key (or null when there are none). Mirrors\n * readAndResetSlackReplyTargetClassifications exactly: atomic\n * rename-then-read-then-unlink so an increment racing the reset carries into the\n * next window, and only known keys are accumulated. SHADOW/enforce observability\n * for the hot-thread guard - gates nothing itself.\n */\nexport function readAndResetSlackHotThreadClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(HOT_THREAD_CLASSIFICATION_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming);\n } catch {\n continue;\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownHotThreadKey(key)) continue;\n // ENG-7855 (CodeRabbit): require a positive SAFE INTEGER. The writer only\n // ever emits integer counts, so a fractional/unsafe value is corruption -\n // flooring it would emit a spurious 0 datapoint (0.5) or silently alter a\n // count (1.9 → 1). Drop it instead.\n if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + raw;\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// ENG-8346 - direct-chat cursor-advance shortfall counter file-layout contract.\n// Two writers increment `<source>-cursor-advance-classifications.json` via\n// recordCursorAdvanceOutcome (@augmented/core/direct-chat/cursor-advance-\n// telemetry.js): the direct-chat channel MCP writes `direct-chat-...` and the\n// manager's ADR-0047 opencode arm writes `direct-chat-manager-...`. Separate\n// files because they are separate PROCESSES and read-modify-write on one file\n// loses increments. Both are summed here. Body is a JSON object keyed by\n// `<route>|<reason>|<partial>` with integer counts.\nconst CURSOR_SHORTFALL_COUNTER_SUFFIX = '-cursor-advance-classifications.json';\n// Mirrored by the API-side sets of the same names in routes/host-runtime.ts, and\n// sourced from CURSOR_SHORTFALL_REASONS in @augmented/core's direct-chat/\n// cursor-advance.ts (which the WRITER normalises against, so an unrecognised\n// server reason arrives here already folded into `other` rather than dropped).\n// A value accepted by only one side is filtered out with no error and no log,\n// and the metric then reads a clean zero meaning \"never ingested\" - the\n// ENG-7716 / ENG-8214 failure. Any new route or reason must land in BOTH.\n// ENG-8393: the last three are the NON-SHORTFALL outcomes. `advanced` and\n// `advanced_unreported` are benign; `failed` is not - it records an advance\n// that did not happen, and is counted because a fleet where every advance\n// failed would otherwise report zero shortfalls AND zero successes. The name of\n// this set now under-describes it - since that ticket the reason slot also\n// carries the non-shortfall verdicts, which is what gives the metric a\n// denominator and makes a zero on the shortfall reasons mean something. Kept\n// as-is because the name is load-bearing in the parity guard's REQUIRED_PAIRS\n// and mirrored on the API side.\nconst KNOWN_CURSOR_ADVANCE_ROUTES = new Set(['reply', 'consume']);\nconst KNOWN_CURSOR_SHORTFALL_REASONS = new Set([\n 'not_found',\n 'unclaimed',\n 'gave_up',\n 'already_delivered',\n 'error',\n 'undiagnosed',\n 'unknown',\n 'unreported',\n 'malformed_count',\n 'other',\n 'advanced',\n 'advanced_unreported',\n 'failed',\n]);\n\n/**\n * Only well-formed `<route>|<reason>|<partial>` keys are accumulated, so a\n * corrupt counter file cannot smuggle an unbounded dimension into CloudWatch -\n * the same bounding the reply-target / reply-binding / hot-thread readers apply.\n */\nfunction isKnownCursorShortfallKey(key: string): boolean {\n const parts = key.split('|');\n if (parts.length !== 3) return false;\n const [route, reason, partial] = parts as [string, string, string];\n return (\n KNOWN_CURSOR_ADVANCE_ROUTES.has(route) &&\n KNOWN_CURSOR_SHORTFALL_REASONS.has(reason) &&\n (partial === 'true' || partial === 'false')\n );\n}\n\n/**\n * Read and RESET every `<source>-cursor-advance-classifications.json` in an agent\n * home, returning the summed counts by key (or null when there are none).\n *\n * Mirrors readAndResetSlackHotThreadClassifications exactly: atomic\n * rename-then-read-then-unlink so an increment racing the reset carries into the\n * next window, and only known keys are accumulated.\n *\n * This is the numerator AND (since ENG-8393) the denominator for ENG-8347. A\n * direct-chat cursor that does not advance strands a message the agent has\n * ALREADY answered, and the only other trace is a stderr line in a log nobody\n * watches - which is how the ENG-8337 strand went unnoticed.\n *\n * ENG-8393 changed the cost profile deliberately: the writer used to record only\n * shortfalls, so a healthy agent wrote no file at all - and that was the defect,\n * not a saving. It made \"no shortfalls happened\" and \"this host never ingested\"\n * byte-identical, which for a metric that exists to gate a decision means the\n * decision is taken blind. A quiet agent still writes nothing (it advances no\n * cursors); an ACTIVE one now writes a small file per probe window either way.\n */\nexport function readAndResetDirectChatCursorShortfallClassifications(\n agentHomeDir: string,\n): Record<string, number> | null {\n let names: string[];\n try {\n names = readdirSync(agentHomeDir);\n } catch {\n return null;\n }\n const total: Record<string, number> = {};\n let any = false;\n for (const name of names) {\n if (!name.endsWith(CURSOR_SHORTFALL_COUNTER_SUFFIX)) continue;\n const full = join(agentHomeDir, name);\n const consuming = `${full}.consuming`;\n try {\n renameSync(full, consuming);\n } catch {\n continue;\n }\n try {\n const parsed = JSON.parse(readFileSync(consuming, 'utf8')) as Record<string, unknown>;\n if (parsed && typeof parsed === 'object') {\n for (const [key, raw] of Object.entries(parsed)) {\n if (!isKnownCursorShortfallKey(key)) continue;\n // Require a positive SAFE INTEGER, same as the hot-thread reader: the\n // writer only ever emits integer counts, so a fractional/unsafe value\n // is corruption - flooring it would emit a spurious 0 datapoint or\n // silently alter a count.\n if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0) {\n total[key] = (total[key] ?? 0) + raw;\n any = true;\n }\n }\n }\n } catch {\n /* corrupt - drop it */\n }\n try {\n unlinkSync(consuming);\n } catch {\n /* non-fatal */\n }\n }\n return any ? total : null;\n}\n\n// ENG-7550: 60s default (was 5 min). The probe post is what refreshes\n// agents.last_pane_activity_at, so its cadence is the resolution ceiling for\n// minutely busy sampling (sample_agent_activity at p_window_seconds=60).\n// Sized before lowering: the per-agent CloudWatch alarms already run\n// Period:60/EvaluationPeriods:1 (built for 1-min datapoints), CloudWatch\n// bills per metric not per datapoint, and the 5x Lambda/API-GW invocation\n// increase is trivial at fleet scale. The reset-on-read delta counters\n// (deflections, lane/reply classifications) are cadence-agnostic - smaller\n// batches, same totals.\nconst DEFAULT_INTERVAL_MS = 60 * 1000;\n\nexport function getResponsivenessIntervalMs(): number {\n const raw = process.env.AUGMENTED_RESPONSIVENESS_INTERVAL_MS;\n if (!raw) return DEFAULT_INTERVAL_MS;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_INTERVAL_MS;\n}\n\n/**\n * ENG-10048: the age basis for one marker — its TRUE arrival time where the\n * marker records one, falling back to the file's mtime where it does not.\n *\n * WHY NOT JUST mtime. `pending-inbound-stale` measured mtime, and every\n * in-place marker rewrite advances it: `seen_at`, `promised_at` (ENG-8265), a\n * `replay_count` bump. So reading a stuck thread reset the clock that reported\n * it, and the gauge said \"one hour\" about a three-hour-old undelivered message.\n * The standard diagnostic disarmed the alarm, and nothing said so. Measured on\n * sherlock's own markers: `received_at` 17:01:56, mtime 19:00:59, `seen_at`\n * 19:00:59 — 8ms apart, so the read is unambiguously what moved it.\n *\n * WHY NOT JUST received_at, WHICH IS THE TRAP. The ticket's own AC said a\n * missing `received_at` must fail closed and be treated as old. That is right\n * for a corrupt marker and wrong for a whole CHANNEL: msteams markers carry the\n * raw Bot Framework Activity and have no `received_at` at all, while\n * `msteams-inbound-puller.ts` deliberately stamps their mtime from `created_at`\n * so boot-drain preserves arrival order. For that channel mtime ALREADY IS the\n * arrival time, and failing closed would have fired this alarm on every Teams\n * agent on the first evaluation with nothing stranded anywhere.\n *\n * So: prefer the semantic field, fall back to the incidental one per marker.\n * A marker is never SKIPPED for lacking a clock — it always contributes an age,\n * which preserves the fail-closed intent (a corrupt marker cannot mask a wedge)\n * without inventing one for a channel that never had the field.\n *\n * A future-dated `received_at` (clock skew) falls back to mtime rather than\n * reporting a negative age, matching `isPendingMarkerStale`'s treatment.\n *\n * `nowMs` is REQUIRED and is the caller's sampled clock, not `Date.now()` read\n * here (CodeRabbit on #5691). The collector computes the reported age from a\n * `nowMs` it captured earlier in the tick; reading the clock again inside this\n * function opens a window where a `received_at` that is future relative to the\n * SAMPLE is already past by the time it is checked. It then passes the\n * future-date guard, yields a negative age, and `Math.max(0, ...)` in the\n * collector clamps that to 0 - reporting a stranded marker as brand new and\n * suppressing the very alarm this change exists to make honest. Taking the\n * clock as a parameter makes the two reads the same read, and making it\n * required stops a caller silently reintroducing the skew.\n */\nexport function markerArrivalMs(markerPath: string, mtimeMs: number, nowMs: number): number {\n let receivedAt: unknown;\n try {\n receivedAt = (JSON.parse(readFileSync(markerPath, 'utf8')) as { received_at?: unknown })\n ?.received_at;\n } catch {\n return mtimeMs; // unreadable/malformed — mtime is the only clock available\n }\n if (typeof receivedAt !== 'string') return mtimeMs;\n const parsed = Date.parse(receivedAt);\n if (!Number.isFinite(parsed)) return mtimeMs;\n // Future-dated: skew, not a real arrival. mtime is the safer basis.\n if (parsed > nowMs) return mtimeMs;\n return parsed;\n}\n\n/**\n * ENG-10048: which markers the `pending-inbound-stale` ALARM counts.\n *\n * Named and exported rather than inlined because \"which markers count\" is the\n * decision that went wrong when this alarm was first armed, and it deserves one\n * home and a test rather than a condition inside a loop.\n *\n * This filter is DELIBERATELY NOT the wedge detector's filter, because the two\n * ask different questions. `oldestLivePendingInboundMtimeMs` asks \"is THIS\n * SESSION failing to drain?\"; the alarm asks \"is a customer message stranded?\".\n *\n * - `discretionary: true` → EXCLUDED. Auto-followed participant chatter the\n * agent may legitimately skip. A deliberate skip is not a stranded ask, and\n * paging on one trains people to ignore the alarm.\n * - `undeliverable: true` → EXCLUDED. Already dead-lettered by the channel and\n * covered by its own `dead-lettered-inbound` alarm; counting it here pages\n * twice for one problem. NOTE the standing caveat: that sibling alarm is\n * itself reported to self-close while the message is still stranded, so\n * coverage of dead-lettered markers rests on that being fixed, not on this.\n * - pre-session markers → INCLUDED, unlike the wedge scan. A message that\n * outlived a respawn is MORE stranded, not less. The wedge detector excludes\n * them because a leftover cannot prove this session is stuck; the alarm does\n * not care which session was up when the customer wrote in.\n */\nexport function isAlarmRelevantMarker(flags: MarkerFlags): boolean {\n return !flags.undeliverable && !flags.discretionary;\n}\n\n/**\n * ENG-6017: oldest pending-inbound marker ARRIVAL time (ms epoch) for an\n * agent, or null when it has no counted markers / no pending-inbound dirs.\n *\n * ENG-10048 changed TWO things here, and both were load-bearing:\n * - the CLOCK is now `markerArrivalMs`, not the raw mtime, so an observer\n * reading a stuck thread can no longer reset the age this alarm reports;\n * - the marker SET is now filtered by `isAlarmRelevantMarker`. That is not\n * optional tidying: without it, moving to a clock nobody can reset makes\n * dead-lettered and discretionary markers - which never drain, by design -\n * report their true age and hold the alarm on forever. The old mtime clock\n * was accidentally masking them, so the filter has to arrive with it.\n *\n * The channel MCP servers (slack-channel, telegram-channel, …) write one\n * marker file per inbound into `~/.augmented/<codeName>/<channel>-pending-\n * inbound/` and clear it when the agent acknowledges the message. The\n * directory layout is the contract here — read-only, no IPC with the MCP\n * (the MCP and CLI release independently; file mtimes need no protocol).\n *\n * ENG-6072: only plain, non-hidden files count as markers. The msteams MCP\n * keeps `.markers/` and `.processed/` housekeeping SUBDIRECTORIES inside its\n * pending-inbound dir; their mtimes never advance, so statting every dirent\n * made the gauge climb forever and fired pending-inbound-stale on agents with\n * zero stranded messages (kylie ~3.4d / scout ~34h false ALARMs the moment\n * ENG-6023 activated the alarm). Dot-entries are skipped wholesale — the\n * hidden namespace is reserved for MCP bookkeeping, never for markers.\n */\nfunction oldestPendingInboundArrivalMs(agentHomeDir: string, nowMs: number): number | null {\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // agent home missing — nothing to report\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const full = join(dir, file.name);\n let mtimeMs: number;\n try {\n mtimeMs = statSync(full).mtimeMs;\n } catch {\n // Marker drained between readdir and stat — that's the happy path.\n continue;\n }\n const flags = readMarkerFlags(full);\n if (flags === null) continue; // vanished mid-scan — drained, exclude\n if (flags !== 'malformed' && !isAlarmRelevantMarker(flags)) continue;\n const arrivalMs = markerArrivalMs(full, mtimeMs, nowMs);\n if (oldest === null || arrivalMs < oldest) oldest = arrivalMs;\n }\n }\n return oldest;\n}\n\n/**\n * ENG-6160 / ENG-6319: read a marker's classification flags.\n * - `{...}` → parsed flags.\n * - `null` → vanished mid-scan (ENOENT) — drained between stat and\n * read, the happy path; callers exclude it.\n * - `'malformed'` → present but unreadable for another reason. The live\n * scan treats this as LIVE (a corrupt marker can never\n * mask a real wedge); park leaves it in place (a corrupt\n * marker must never become a dropped message).\n */\ninterface MarkerFlags {\n /** ENG-5846: dead-lettered by the channel (⏳-noticed, never drainable). */\n undeliverable: boolean;\n /**\n * ENG-6319: auto-followed participant-thread inbound the agent may\n * legitimately skip. Excluded from the wedge live-scan (a deliberate skip\n * must not read as \"failing to drain\" and force-respawn a healthy\n * session) but still parked across respawns — it may be a real\n * operator message awaiting a reply.\n */\n discretionary: boolean;\n}\n\nfunction readMarkerFlags(markerPath: string): MarkerFlags | null | 'malformed' {\n try {\n const parsed = JSON.parse(readFileSync(markerPath, 'utf8')) as {\n undeliverable?: unknown;\n discretionary?: unknown;\n };\n return {\n undeliverable: parsed?.undeliverable === true,\n discretionary: parsed?.discretionary === true,\n };\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'ENOENT' ? null : 'malformed';\n }\n}\n\n/**\n * ENG-6160: oldest *LIVE* pending-inbound marker mtime (ms epoch) for an agent,\n * or null when there is no live marker. \"Live\" excludes:\n *\n * - markers older than `sessionStartMs` — a marker written before the current\n * session started is a leftover from a PREVIOUS session and cannot mean\n * *this* session is failing to drain. This is the load-bearing exclusion:\n * without it, an orphan marker survives a fresh respawn and the wedge\n * detector re-fires forever on a healthy idle agent (the sherlock enforce\n * loop, 2026-06-08: `inboundAge=3389s` on a `● Ready.` session).\n * - markers flagged `undeliverable: true` — already dead-lettered by the channel.\n * - markers flagged `discretionary: true` (ENG-6319) — skippable auto-followed\n * participant-thread inbound; a deliberate skip must not force-respawn a\n * healthy session.\n *\n * Distinct from `oldestPendingInboundArrivalMs`, which feeds the ENG-6017\n * `pending-inbound-stale` CloudWatch alarm.\n *\n * ENG-10048: this note used to say that alarm's \"semantics must NOT change\",\n * and it was read as protecting the FILTER — which was fine — while the CLOCK\n * was resettable all along. The comment steering editors away pointed at the\n * half that was already correct, which is part of why the defect survived. The\n * two functions now differ deliberately on BOTH axes:\n * - clock: the alarm keys on arrival; this one still keys on mtime, because a\n * wedge is about THIS session's drain behaviour and an in-session rewrite is\n * genuine evidence of engagement.\n * - filter: the alarm INCLUDES pre-session markers (a message that outlived a\n * respawn is more stranded, not less); this one excludes them, because a\n * leftover cannot prove the current session is failing.\n */\nexport function oldestLivePendingInboundMtimeMs(\n agentHomeDir: string,\n opts: { sessionStartMs?: number | null } = {},\n): number | null {\n const sessionStartMs = opts.sessionStartMs ?? null;\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const full = join(dir, file.name);\n let mtimeMs: number;\n try {\n mtimeMs = statSync(full).mtimeMs;\n } catch {\n continue; // drained between readdir and stat — happy path\n }\n if (sessionStartMs !== null && mtimeMs < sessionStartMs) continue; // pre-session leftover\n const flags = readMarkerFlags(full);\n if (flags === null) continue; // vanished between stat and read — drained, exclude\n if (flags !== 'malformed' && (flags.undeliverable || flags.discretionary)) continue;\n if (oldest === null || mtimeMs < oldest) oldest = mtimeMs;\n }\n }\n return oldest;\n}\n\n/**\n * ENG-10045 — how many DEAD-LETTERED inbound markers are on this host for this\n * agent RIGHT NOW.\n *\n * ## Why this function exists at all\n *\n * `DEAD_LETTERED_INBOUND_HOURLY_METRIC` is a SUM of dead-letter EVENTS over a\n * trailing hour. An hour after the last event the datapoint is zero, CloudWatch\n * fires `OKActions`, the bridge closes the alert — and the undelivered customer\n * message is still sitting in `*-pending-inbound-stale/`, and will be forever.\n * The alarm never measured whether anything was still stranded. This is the\n * gauge that lets it: the count is the artifact's CURRENT population, so an OK\n * means the artifact is gone, which is what the ENG-10045 close contract in\n * `packages/api/src/lib/agent-heartbeat-alarms.ts` requires.\n *\n * ## The predicate, stated rather than inferred\n *\n * Sherlock's correction on ENG-10045 was that `mtime` looked like an oversight\n * and was a deliberate choice nobody had written down, so the next editor\n * \"fixed\" it into a known bug. Three choices here, each with its reason:\n *\n * 1. **`-stale` dirs only, never the live `*-pending-inbound/` dirs.** A live\n * marker is a message still queued for delivery — undelivered, but not\n * stranded. It has its own signal (`PENDING_INBOUND_AGE_METRIC`) and its own\n * alarm. Counting both here would make this gauge fire for the healthy case\n * where a message arrived four seconds ago.\n *\n * 2. **`msteams-pending-inbound-stale` IS counted**, even though\n * `parkPendingInbound` skips the LIVE msteams dir. Those are not the same\n * decision. The live skip exists because msteams top-level files are raw Bot\n * Framework activity payloads that the teams channel server re-drains on\n * boot, so moving them aside defeated the one channel with native respawn\n * recovery. Nothing re-drains a `-stale` dir: anything that reached it was\n * put there by a dead-letter, which is exactly the population this counts. A\n * skip here would make msteams the one channel whose lost messages are\n * invisible.\n *\n * 3. **A malformed marker IS counted.** It is the same rule this file already\n * applies in the other direction — `parkPendingInbound` parks a corrupt\n * marker rather than dead-lettering it, because \"corrupt must never become a\n * drop\". The mirror of that is that corrupt must never mask a drop either: a\n * file we cannot parse, sitting in a dead-letter dir, is a customer message\n * we cannot prove was delivered. It counts. This also means the count needs\n * no marker parse at all, which is why it is a `readdir` and not a `readFile`\n * per entry — it runs on the probe cadence for every agent on the host.\n *\n * ## A FAILED SCAN IS NOT AN EMPTY ONE (CodeRabbit, #5931 — and it was right)\n *\n * The first version returned 0 when `readdirSync` threw, matching every other\n * scan in this file. That is safe for the AGE scans, whose 0 means \"no marker so\n * no age\" and whose field is omitted anyway. It is NOT safe here, because a 0\n * from this function is published as an affirmative datapoint and an affirmative\n * zero is what CLEARS the alarm. A host that could not read its own marker\n * directory would have told CloudWatch \"nothing is stranded\" — the same false\n * all-clear as the trailing-hour SUM, arriving by a different route.\n *\n * So the return is `number | null`, and the rule is asymmetric on purpose:\n *\n * - a count we ESTABLISHED (every stale dir read cleanly) → that number,\n * including 0, which is the datapoint that legitimately clears the alarm;\n * - a count of ZERO we could NOT establish (the home or any stale dir threw) →\n * `null`, \"no signal\", and the caller omits the field entirely;\n * - a POSITIVE count with some dir unreadable → still that number. A partial\n * positive can only under-report, and under-reporting one stranded message\n * while reporting another still leaves the alarm open. Suppressing it to\n * `null` would throw away a true alarm to avoid an imprecise one.\n *\n * Paired with `TreatMissingData: 'ignore'` on the alarm (also CodeRabbit), the\n * `null` case means the alarm RETAINS its state rather than drifting to OK. The\n * two halves are one fix: this decides when to say nothing, and that decides\n * what silence means.\n */\nexport function deadLetteredInboundMarkerCount(agentHomeDir: string): number | null {\n let count = 0;\n let scanIncomplete = false;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // could not even list the home — we have established nothing\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound-stale')) continue;\n let files;\n try {\n files = readdirSync(join(agentHomeDir, entry.name), { withFileTypes: true });\n } catch {\n scanIncomplete = true;\n continue;\n }\n for (const file of files) {\n // Dotfiles are bookkeeping, never markers — the same exclusion every\n // other scan in this file makes.\n if (!file.isFile() || file.name.startsWith('.')) continue;\n count++;\n }\n }\n // Only a ZERO needs to have been established. A positive is reported however\n // partial it is — see the asymmetry note above.\n if (count === 0 && scanIncomplete) return null;\n return count;\n}\n\n/**\n * ENG-6160: age (s) of the oldest LIVE pending-inbound marker for an agent, or\n * null when none. The wedge detector uses this instead of the alarm-facing\n * `pending_inbound_oldest_age_seconds` so a stale/dead-letter marker can't\n * false-fire a respawn.\n */\nexport function livePendingInboundOldestAgeSeconds(\n codeName: string,\n sessionStartMs: number | null,\n now: Date = new Date(),\n): number | null {\n const oldest = oldestLivePendingInboundMtimeMs(dirname(paneLogPath(codeName)), { sessionStartMs });\n if (oldest === null) return null;\n return Math.max(0, Math.floor((now.getTime() - oldest) / 1000));\n}\n\n/**\n * ENG-8881: age of the most recent channel inbound ACTIVITY for an agent, in ms\n * since epoch — arrival *or* drain, across every channel.\n *\n * WHY NOT THE OBVIOUS SIGNALS. The incident (ENG-8881) is an agent that accepted\n * work on Telegram and was force-restarted three seconds later, before it could\n * write the kanban row. Neither existing signal sees that moment:\n *\n * - `lastInboundMs` / `inboundAgeSecondsFor` in manager-worker is stamped ONLY\n * from `processDirectChatMessage`. Slack and Telegram arrive through MCP\n * children, so for the channel this incident happened on it is never set.\n * - `oldestLivePendingInboundMtimeMs` counts UNANSWERED markers. The agent had\n * already replied, so its marker was drained and the signal was empty at the\n * instant of the kill. Correctly empty — and useless here.\n *\n * The window we must protect is exactly \"a message arrived and was answered just\n * now\", which is the moment a marker is created and then removed. Both mutate the\n * CONTAINING DIRECTORY's mtime, so the directory timestamp survives the drain\n * that erases the marker itself. That is the signal this returns.\n *\n * Deliberately reads directory mtimes and not file mtimes: the files are gone by\n * the time we need to know they existed.\n *\n * Returns null when the agent has no channel directories at all (never messaged).\n */\nexport function newestPendingInboundActivityMtimeMs(agentHomeDir: string): number | null {\n let newest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const entry of entries) {\n // Same predicate as oldestLivePendingInboundMtimeMs, so the two agree on\n // what counts as a channel dir. `-pending-inbound-stale` does not match.\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n try {\n const mtimeMs = statSync(join(agentHomeDir, entry.name)).mtimeMs;\n if (newest === null || mtimeMs > newest) newest = mtimeMs;\n } catch {\n continue;\n }\n }\n return newest;\n}\n\n/**\n * ENG-6160: move every pending-inbound marker for an agent aside into a sibling\n * `<channel>-pending-inbound-stale/` directory (NOT silently deleted — the\n * payload pointer is preserved for forensics), returning the count moved.\n *\n * ENG-6289: no longer called on wedge respawn — the wedge path parks instead\n * (see `parkPendingInbound` above; blanket dead-letter permanently dropped the\n * user's message). Kept as the explicit \"move everything aside\" seam for tests\n * and operator emergencies. The stale dir does not end in `-pending-inbound`,\n * so neither the probe nor this scan re-counts moved markers.\n */\n/**\n * ENG-6289: park-not-drop. On a force-fresh wedge respawn, KEEP undrained\n * pending-inbound markers in their live dirs instead of dead-lettering them —\n * the fresh session's orient hook surfaces them (\"N queued messages\" + details)\n * and ENG-5969 replay (when enabled) re-pushes their payloads. Markers are NOT\n * rewritten (no counter, no mtime bump): the ENG-6160 pre-session exclusion in\n * `oldestLivePendingInboundMtimeMs` already keeps an untouched parked marker\n * out of the fresh session's wedge signal, and re-delivery stays bounded by\n * the existing machinery — the channel-side `replay_count` cap (≤3) and the\n * orphan sweep's received_at TTL. A manager-side rewrite would be the first\n * cross-process writer into marker files, where a torn read in the channel\n * sweep DELETES the marker (`unlinkSync` on parse failure) — the exact drop\n * this function exists to prevent.\n *\n * Only markers already flagged `undeliverable: true` are dead-lettered (moved\n * to `-stale`) — the channel already gave the user the ⏳ notice for those, so\n * nothing can ever drain them. Malformed markers are LEFT IN PLACE, matching\n * the live-scan philosophy above (a corrupt marker must never mask — or\n * become — a dropped message).\n *\n * The msteams dir is skipped wholesale: its top-level files are raw Bot\n * Framework activity payloads (the transport queue, not bookkeeping — real\n * markers live in the hidden `.markers/` subdir this scan never touches), and\n * the teams channel server's boot drain redelivers them to the fresh session\n * on its own. Moving them aside (what the pre-ENG-6289 dead-letter did) was\n * actively defeating the one channel with native respawn recovery.\n */\nexport interface ParkPendingInboundResult {\n parked: number;\n deadLettered: number;\n}\n\n/**\n * Move one marker into the sibling `-stale` dead-letter dir (moved, not\n * deleted — preserved for forensics). Returns true on success; best-effort —\n * a marker that vanished or can't move is left as-is.\n */\nfunction moveMarkerToStale(dir: string, deadDir: string, name: string): boolean {\n try {\n mkdirSync(deadDir, { recursive: true });\n renameSync(join(dir, name), join(deadDir, name));\n return true;\n } catch {\n return false;\n }\n}\n\nexport function parkPendingInbound(codeName: string, _now: Date = new Date()): ParkPendingInboundResult {\n const home = dirname(paneLogPath(codeName));\n const result: ParkPendingInboundResult = { parked: 0, deadLettered: 0 };\n let entries;\n try {\n entries = readdirSync(home, { withFileTypes: true });\n } catch {\n return result;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n if (entry.name === 'msteams-pending-inbound') continue; // transport queue — boot drain owns recovery\n const dir = join(home, entry.name);\n const deadDir = join(home, `${entry.name}-stale`);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n const flags = readMarkerFlags(join(dir, file.name));\n if (flags === null) continue; // drained mid-scan — already gone\n // Only undeliverable markers dead-letter. Discretionary markers\n // (ENG-6319) PARK like engaged ones — they may be a real operator\n // message awaiting a reply (the live silent-loss class); malformed\n // markers park too (corrupt must never become a drop).\n if (flags !== 'malformed' && flags.undeliverable) {\n if (moveMarkerToStale(dir, deadDir, file.name)) result.deadLettered++;\n } else {\n result.parked++;\n }\n }\n }\n return result;\n}\n\nexport function deadLetterPendingInbound(codeName: string, _now: Date = new Date()): number {\n const home = dirname(paneLogPath(codeName));\n let moved = 0;\n let entries;\n try {\n entries = readdirSync(home, { withFileTypes: true });\n } catch {\n return 0;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(home, entry.name);\n const deadDir = join(home, `${entry.name}-stale`);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n if (moveMarkerToStale(dir, deadDir, file.name)) moved++;\n }\n }\n return moved;\n}\n\n/**\n * Compute the pane.log age for each agent. Missing or unreadable\n * pane.log returns null — the caller should drop those entries\n * rather than fabricate a \"fresh\" or \"ancient\" value. A missing\n * file means the agent has never spawned in this manager generation,\n * which is a separate problem covered by SessionAliveAgeSeconds.\n */\nexport function collectResponsivenessProbes(\n codeNames: string[],\n now: Date = new Date(),\n): ResponsivenessProbeResult[] {\n const nowMs = now.getTime();\n const results: ResponsivenessProbeResult[] = [];\n for (const codeName of codeNames) {\n try {\n const panePath = paneLogPath(codeName);\n const mtimeMs = statSync(panePath).mtimeMs;\n const ageSeconds = Math.max(0, Math.floor((nowMs - mtimeMs) / 1000));\n const result: ResponsivenessProbeResult = {\n code_name: codeName,\n pane_activity_age_seconds: ageSeconds,\n };\n attachSupplementalCounters(result, dirname(panePath), nowMs);\n results.push(result);\n } catch {\n // No pane.log yet (fresh agent, never spawned) — skip. The\n // session-alive monitor already covers the \"should be running\n // but isn't\" case.\n //\n // ENG-8090: an OPENCODE agent lands here on every cycle, permanently —\n // its serve writes `opencode-serve.log`, so this stat always throws. That\n // is what `collectPanelessActivityProbes` exists to cover; this function\n // stays pane-only because the wedge detector consumes its output and must\n // never act on an agent it cannot read a pane age for.\n }\n }\n return results;\n}\n\n/**\n * ENG-8090: read + RESET the per-agent counter files that ride the probe's\n * cadence, and attach the ones that produced anything.\n *\n * Extracted from the loop above because these reads were inside the same `try`\n * as the `pane.log` stat: an agent with no `pane.log` lost not just its pane age\n * but its deflection, lane-classification and reply-binding counters too. For\n * opencode that meant those telemetry streams — including the ADR-0024\n * misclassification canary — were silently empty for the whole fleet, a wider\n * blast radius than the liveness gap ENG-8090 was filed for.\n *\n * The reads are DESTRUCTIVE (read-and-reset), so exactly one collector may run\n * them per agent per cycle. That is guaranteed structurally, not by convention:\n * `collectResponsivenessProbes` handles agents whose `pane.log` stat succeeds\n * and `collectPanelessActivityProbes` handles those whose stat fails, and the\n * two sets are complements of each other.\n */\nfunction attachSupplementalCounters(\n result: ResponsivenessProbeResult | ResponsivenessProbePayloadEntry,\n agentHome: string,\n nowMs: number,\n): void {\n // ENG-6017: piggyback the pending-inbound drain-age scan on the same\n // cadence. Field omitted (not 0) when there are no markers.\n const oldestMarkerMs = oldestPendingInboundArrivalMs(agentHome, nowMs);\n if (oldestMarkerMs !== null) {\n result.pending_inbound_oldest_age_seconds = Math.max(\n 0,\n Math.floor((nowMs - oldestMarkerMs) / 1000),\n );\n }\n // ENG-10045: the stranded-marker gauge, set UNCONDITIONALLY — including 0.\n //\n // Every other field in this function is omitted when empty. This one is not,\n // and the asymmetry is the fix rather than an inconsistency: an omitted field\n // reaches CloudWatch as a missing datapoint, `TreatMissingData: 'notBreaching'`\n // reads that as fine, and the alarm's `OKActions` CLOSE the alert. So for this\n // one signal \"I have nothing to say\" would be indistinguishable from \"nothing\n // is stranded\" — which is the exact false all-clear ENG-10045 is about.\n //\n // The one case that IS omitted: a scan we could not complete that found\n // nothing. `null` means \"we established nothing\", and publishing a 0 there\n // would be the host asserting an all-clear it never checked.\n const stranded = deadLetteredInboundMarkerCount(agentHome);\n if (stranded !== null) result.dead_lettered_inbound_markers = stranded;\n // ENG-8265: the oldest OUTSTANDING interim promise — an inbound the agent\n // acked with `interim: true` and never answered substantively. Rides the same\n // cadence as the pending-inbound scan but is a DIFFERENT signal and must not be\n // conflated with it. ENG-8265 wrote this to explain why the promise clock had\n // to be separate: the pending-inbound age WAS measured from the marker's\n // mtime, which this very ack's in-place rewrite advances, so an ack-then-drop\n // reset that clock while leaving the user unanswered. ENG-10048 fixed that\n // clock for every channel that records `received_at`. It is NOT a blanket\n // immunity: msteams markers carry no `received_at`, so they fall back to\n // mtime and an in-place rewrite still resets their reported age (CodeRabbit\n // on #5691 - the PR body claimed the residual gap, this comment did not).\n // The two remain different signals regardless and must not be conflated.\n // This one is measured\n // from the marker's `promised_at`, is non-destructive (read-only, no reset), and\n // survives the mid-turn respawn that destroyed the work in the motivating\n // incident. Omitted (never 0) when no promise is outstanding.\n const promiseAge = outstandingPromiseOldestAgeSeconds(agentHome, new Date(nowMs));\n if (promiseAge !== null) {\n result.promise_outstanding_oldest_age_seconds = promiseAge;\n }\n // ENG-6327: read + RESET the per-channel deflection counters on the same\n // cadence so each cause's frequency reaches CloudWatch (ChannelDeflections,\n // Cause dimension). Omitted when there were none this window.\n const deflections = readAndResetChannelDeflections(agentHome);\n if (deflections) {\n result.deflections = deflections;\n }\n // ENG-6408 / ADR-0024 Slice 1.5: read + RESET the shadow lane-classification\n // counters on the same cadence so the classification mix (and the\n // misclassification canary) reaches CloudWatch. Omitted when none this window.\n const laneClassifications = readAndResetChannelLaneClassifications(agentHome);\n if (laneClassifications) {\n result.lane_classifications = laneClassifications;\n }\n // WS0: read + RESET the shadow reply-target counters on the same cadence so\n // the cross-thread classification mix reaches CloudWatch. Omitted when none.\n const replyTargetClassifications = readAndResetSlackReplyTargetClassifications(agentHome);\n if (replyTargetClassifications) {\n result.slack_reply_target_classifications = replyTargetClassifications;\n }\n // WS2: read + RESET the shadow reply-binding counters on the same cadence\n // so the bound / coords_corrected / unknown_target mix (the enforce-flip\n // gate) reaches CloudWatch. Omitted when none this window.\n const replyBindingClassifications = readAndResetSlackReplyBindingClassifications(agentHome);\n if (replyBindingClassifications) {\n result.slack_reply_binding_classifications = replyBindingClassifications;\n }\n // ENG-7855: read + RESET the hot-thread-guard outcome counters on the same\n // cadence so the redirect / root / not-applicable mix (the fire rate that\n // tells \"correctly quiet\" from \"never had the chance\") reaches CloudWatch.\n // Omitted when none this window (off-mode agents never write these).\n const hotThreadClassifications = readAndResetSlackHotThreadClassifications(agentHome);\n if (hotThreadClassifications) {\n result.slack_hot_thread_classifications = hotThreadClassifications;\n }\n // ENG-8346: read + RESET the direct-chat cursor-advance shortfall counters, so\n // \"the reply POST succeeded but the cursor did not move\" reaches CloudWatch\n // instead of only a per-host stderr line. Omitted when none this window.\n //\n // It matters that this lives HERE rather than in either collector: an opencode\n // agent has no pane.log, and site 9 (the manager's ADR-0047 arm, which writes\n // the `direct-chat-manager-` file) is the opencode path SPECIFICALLY. Putting\n // the read in `collectResponsivenessProbes` would have collected every agent\n // except the one whose writer this is.\n const cursorShortfalls = readAndResetDirectChatCursorShortfallClassifications(agentHome);\n if (cursorShortfalls) {\n result.direct_chat_cursor_shortfall_classifications = cursorShortfalls;\n }\n}\n\n/**\n * ENG-8090: activity probes for agents that have NO readable `pane.log`.\n *\n * The opencode runtime is the whole reason this exists. Its serve is a headless\n * HTTP server, not a tmux TUI, so there is no pane whose mtime tracks agent\n * work — and `opencode-serve.log`, the obvious substitute, is the wrong signal\n * twice over: it advances for HTTP-server reasons unrelated to any turn (so an\n * idle agent would bill as permanently busy), and it kept advancing all through\n * ENG-8058's deadlock (so a wedged agent would read alive). Both consumers are\n * fed from the turn lifecycle instead, via the two `*AgeSeconds` resolvers.\n *\n * Agents whose `pane.log` IS readable are skipped here and reported by\n * `collectResponsivenessProbes` — see the destructive-read note on\n * `attachSupplementalCounters`. A Claude Code agent that has simply never\n * spawned also has no `pane.log`; it lands here, both resolvers return null for\n * it, and it emits an entry carrying only whatever counters exist. That is a\n * strict improvement on today (those counters were dropped), and it writes no\n * activity timestamp, so it cannot make an unspawned agent look alive or busy.\n */\nexport function collectPanelessActivityProbes(\n codeNames: string[],\n resolvers: {\n /** Seconds since the agent was last occupied; 0 while a turn is in flight. */\n busyActivityAgeSeconds: (codeName: string) => number | null;\n /** Seconds since a turn last produced real reply text. */\n turnCompletionAgeSeconds: (codeName: string) => number | null;\n },\n now: Date = new Date(),\n): ResponsivenessProbePayloadEntry[] {\n const nowMs = now.getTime();\n const results: ResponsivenessProbePayloadEntry[] = [];\n for (const codeName of codeNames) {\n const panePath = paneLogPath(codeName);\n try {\n statSync(panePath);\n // Has a pane.log — `collectResponsivenessProbes` owns this agent, and\n // running the destructive counter reads here too would halve its counts.\n continue;\n } catch {\n // No pane.log — this collector owns it. Fall through.\n }\n const entry: ResponsivenessProbePayloadEntry = { code_name: codeName };\n attachSupplementalCounters(entry, dirname(panePath), nowMs);\n\n // ENG-10045: hold the stranded-marker gauge back until we know whether this\n // entry is going out at all, then re-attach it under the rule stated below.\n //\n // It is the one counter attached UNCONDITIONALLY (an affirmative zero is\n // what clears its alarm), so on a paneless agent with no other signal it was\n // the only field besides the name — and every never-spawned agent started\n // emitting a payload entry every cycle, which is the per-cycle iteration\n // ENG-8090 removed.\n const strandedMarkers = entry.dead_lettered_inbound_markers;\n delete entry.dead_lettered_inbound_markers;\n\n const busy = resolvers.busyActivityAgeSeconds(codeName);\n if (busy !== null && Number.isFinite(busy)) {\n entry.busy_activity_age_seconds = Math.max(0, Math.floor(busy));\n }\n const turn = resolvers.turnCompletionAgeSeconds(codeName);\n if (turn !== null && Number.isFinite(turn)) {\n entry.turn_completion_age_seconds = Math.max(0, Math.floor(turn));\n }\n // ENG-10045 — THE RULE, stated rather than counted (Koda on #5931).\n //\n // The gauge RIDES ALONG with an entry that exists on its own merits.\n // A POSITIVE gauge is itself a reason for the entry to exist.\n // A ZERO gauge is never a reason.\n //\n // The first draft of this was `Object.keys(entry).length === 2`, which is the\n // same rule expressed as arithmetic over the object's shape — and it is\n // wrong the moment anyone adds another field to this collector, because the\n // magic number silently stops matching and every never-spawned agent starts\n // emitting again. Nothing would fail; the entries would just come back. The\n // form below cannot drift that way: it asks the question directly.\n //\n // Why a zero is safe to drop: there is nothing to clear. An agent this quiet\n // has no alarm state to move, and `TreatMissingData: 'ignore'` means a\n // missing datapoint leaves the alarm exactly where it is. A POSITIVE count\n // must always get through — a stranded message is a signal by definition, and\n // losing it here would be this whole ticket again.\n const hasSignalOfItsOwn = Object.keys(entry).length > 1;\n if (strandedMarkers !== undefined && (strandedMarkers > 0 || hasSignalOfItsOwn)) {\n entry.dead_lettered_inbound_markers = strandedMarkers;\n }\n // Nothing but a name. Sending it would have the API iterate a payload entry\n // that can produce no metric and no write — on every cycle, for every agent\n // that has never spawned. Absent is already the API's \"no signal\", so say\n // nothing rather than say nothing at length.\n if (Object.keys(entry).length === 1) continue;\n results.push(entry);\n }\n return results;\n}\n","/**\n * ENG-8265 — scan an agent's pending-inbound marker dirs for the oldest\n * OUTSTANDING interim promise, and report its age on the responsiveness probe.\n *\n * The predicate itself (what counts as a promise, and why `seen_at` does not) is\n * in `@augmented/core/channels/interim-promise.js` — read that file first; it\n * carries the design rationale and the full list of shapes that must never\n * count. This module is only the filesystem half: walk `<agentHome>/*-pending-\n * inbound/`, parse each marker, and take the earliest `promised_at`.\n *\n * Deliberate differences from the two sibling scans in `responsiveness-probe.ts`:\n *\n * - **No `sessionStartMs` exclusion.** `oldestLivePendingInboundMtimeMs` skips\n * markers older than the current session because a leftover marker cannot\n * prove *this* session is failing to drain. Applying that here would have made\n * ENG-8265 a no-op in the incident that motivated it: stirling's promise was\n * made before two mid-turn respawns, so a session-relative measure would\n * discard the only evidence. A promise survives a restart precisely because\n * the user's expectation does.\n * - **Age from the marker's `promised_at`, not its mtime.** Every in-place\n * rewrite (a reaction, a `read_thread`, a replay-payload attach) bumps mtime;\n * none of them discharge a promise.\n * - **A malformed marker is excluded, not included.** The live wedge scan treats\n * an unreadable marker as LIVE so a corrupt file can never mask a real wedge.\n * Here the asymmetry is reversed: this signal opens an alert, and a false alert\n * about a healthy conversation is the expensive outcome, so an unparseable\n * marker fails quiet.\n */\nimport { readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nimport {\n interimPromiseAgeSeconds,\n outstandingPromiseAtMs,\n type InterimPromiseMarkerFields,\n} from '@augmented/core/channels/interim-promise.js';\n\n/**\n * Earliest `promised_at` (ms epoch) across every marker under `agentHomeDir` that\n * carries an outstanding promise, or `null` when the agent owes none.\n *\n * `null` is the healthy steady state and the reason the probe field is OMITTED\n * rather than sent as 0 — the absent-vs-zero contract every other probe counter\n * follows (a 0 would read as \"promise made 0s ago\", i.e. permanently healthy, and\n * would re-mask the failure on mixed-version fleets).\n */\nexport function oldestOutstandingPromiseAtMs(agentHomeDir: string): number | null {\n let oldest: number | null = null;\n let entries;\n try {\n entries = readdirSync(agentHomeDir, { withFileTypes: true });\n } catch {\n return null; // agent home missing — nothing to report\n }\n for (const entry of entries) {\n // Same directory contract as the sibling scans: only `<channel>-pending-\n // inbound/`. The `-stale` / `-dead-letter` siblings deliberately do not match\n // (a parked-aside marker is no longer an outstanding promise), and hidden\n // entries are MCP bookkeeping (`.markers/`, `.processed/`), never markers.\n if (!entry.isDirectory() || !entry.name.endsWith('-pending-inbound')) continue;\n const dir = join(agentHomeDir, entry.name);\n let files;\n try {\n files = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || file.name.startsWith('.')) continue;\n let marker: InterimPromiseMarkerFields;\n try {\n marker = JSON.parse(readFileSync(join(dir, file.name), 'utf8')) as InterimPromiseMarkerFields;\n } catch {\n // Vanished between readdir and read (a final reply landed — the happy\n // path), or unparseable. Either way: not a promise. See the header note\n // on why this fails quiet where the wedge scan fails loud.\n continue;\n }\n const promisedAt = outstandingPromiseAtMs(marker);\n if (promisedAt === null) continue;\n if (oldest === null || promisedAt < oldest) oldest = promisedAt;\n }\n }\n return oldest;\n}\n\n/**\n * Age (seconds) of the oldest outstanding promise under `agentHomeDir`, or `null`\n * when there is none. This is the value the responsiveness probe reports as\n * `promise_outstanding_oldest_age_seconds`.\n */\nexport function outstandingPromiseOldestAgeSeconds(\n agentHomeDir: string,\n now: Date = new Date(),\n): number | null {\n const promisedAt = oldestOutstandingPromiseAtMs(agentHomeDir);\n if (promisedAt === null) return null;\n return interimPromiseAgeSeconds(promisedAt, now.getTime());\n}\n\n/**\n * Convenience form keyed on an agent code name, for callers that only have the\n * pane-log path convention to hand. `paneLogPathFn` is injected rather than\n * imported so this module stays free of `responsiveness-probe.ts`'s import graph\n * (and testable without a real agent home).\n */\nexport function outstandingPromiseOldestAgeSecondsForAgent(\n codeName: string,\n paneLogPathFn: (codeName: string) => string,\n now: Date = new Date(),\n): number | null {\n return outstandingPromiseOldestAgeSeconds(dirname(paneLogPathFn(codeName)), now);\n}\n","/**\n * ENG-8265 — the outstanding-interim-promise predicate.\n *\n * ## The failure this closes\n *\n * An agent that replies *\"On it — pulling your v10 first…\"* and then never\n * delivers trips **nothing**. `agent_stall` only selects cards with\n * `status = 'in_progress'` and the agent had none; `agent_pending_inbound`\n * measures marker age from the marker's mtime, which the interim ack's own\n * in-place rewrite advances; and `agent_channel_silent_loss` never measured\n * \"did a reply go out\" at all (it counts `ChannelDeflections Cause=replay_orphaned`\n * — a recoverable inbound GC'd before the replay scanner re-pushed it, which an\n * interim ack neither causes nor suppresses). So the one failure a customer\n * noticed by hand was the one the platform could not see.\n *\n * ## Why `seen_at` is NOT the signal\n *\n * ENG-6567 already stopped an interim ack from DELETING the pending marker: it\n * downgrades it in place by stamping `seen_at`, so the Stop-hook ghost-reply\n * recovery stays armed for the eventual answer. It is tempting to read a\n * lingering `seen_at` marker as an outstanding promise. **It is not.**\n * `seen_at` is stamped by every form of engagement — an interim ack, a\n * `slack.read_thread`, an emoji reaction, `noteThreadActivityByMessageTs` on a\n * reaction to any message in the thread. An agent that reads a thread, decides\n * the message is not for it and correctly says nothing owes no follow-up\n * whatsoever, and alerting on that would page a human about a conversation that\n * was fine — repeatedly, on every skipped auto-followed thread, which is exactly\n * how an alert gets trained into background noise.\n *\n * So this module keys off a DISTINCT field, `promised_at`, written by exactly one\n * caller: the channel reply tool on the `interim: true` path. `interim: true` is\n * the agent explicitly declaring \"the real answer is still coming\" (it is what\n * the tool's own schema says), so `promised_at` present + the marker still on\n * disk is a promise the agent made and has not discharged. A final reply\n * (`interim` omitted/false) DELETES the marker via the existing settle path —\n * including the thread fan-out, so any substantive reply anywhere in the thread\n * discharges the promise, not just one keyed to the original message_ts.\n *\n * ## Why the age is measured from `promised_at`, not from mtime\n *\n * Two reasons, both load-bearing:\n *\n * 1. **mtime is resettable by unrelated engagement.** A reaction or a\n * `read_thread` rewrites the marker (to stamp `seen_at`) and bumps its\n * mtime. Measuring the promise from mtime would let an agent that keeps\n * touching the thread hold a broken promise open forever without the age\n * ever climbing.\n * 2. **mtime does not survive the incident that motivated the ticket.**\n * Stirling's session was killed twice mid-turn by token rotations\n * (ENG-8264). `promised_at` is a value inside the file, so the clock keeps\n * running across a respawn; an mtime- or session-relative measure would not.\n *\n * For the same reason this predicate deliberately does **NOT** reuse the\n * wedge-detector's `sessionStartMs` exclusion (`oldestLivePendingInboundMtimeMs`\n * skips markers older than the current session). That exclusion is correct for\n * \"is THIS session failing to drain\" and would have made this fix a no-op in the\n * exact incident it was filed for — stirling's promise predates both respawns.\n *\n * ## Shapes that must never count as an outstanding promise\n *\n * - no `promised_at` (write-once marker, `seen_at`-only engagement, a\n * read_thread, a reaction, an unrelated participant's new message)\n * - `discretionary: true` — a skippable auto-followed participant-thread\n * inbound the agent may legitimately never answer\n * - `undeliverable: true` — already dead-lettered by the channel; the user got\n * a ⏳ notice, so no promise is outstanding\n * - a marker that no longer exists — a final reply deleted it, which is the\n * normal ack-then-deliver path and the single most important negative case\n * - a malformed / unparseable `promised_at` — a corrupt value must not\n * manufacture a promise (contrast the wedge live-scan, which treats a corrupt\n * marker as LIVE because there a corrupt marker must never MASK a real wedge;\n * here the asymmetry runs the other way, so it fails quiet)\n * - `promised_at` in the future (clock skew) — clamped to age 0, never negative\n */\n\n/**\n * The subset of a channel pending-inbound marker this predicate reads. Every\n * field is optional: markers written before ENG-8265, and by channels with no\n * interim concept (direct-chat has no `interim` argument), carry none of them and\n * must parse cleanly to \"no promise\".\n */\nexport interface InterimPromiseMarkerFields {\n /**\n * ENG-8265: ISO timestamp stamped when the agent posted an INTERIM reply on\n * this inbound — an explicit \"the real answer is still coming\". Written only\n * by the reply tool's `interim: true` path, never by `read_thread` / reaction\n * engagement (those stamp `seen_at` only). First write wins: a second interim\n * ack on the same inbound must not restart the promise clock, or an agent that\n * keeps saying \"still working\" would never be visible as having dropped it.\n */\n promised_at?: unknown;\n /** ENG-6567 engagement stamp. Present for ALL engagement — not a promise. */\n seen_at?: unknown;\n /** ENG-5846: dead-lettered by the channel; the user was told, nothing owed. */\n undeliverable?: unknown;\n /** ENG-6319: skippable auto-followed inbound; a deliberate skip owes nothing. */\n discretionary?: unknown;\n}\n\n/**\n * Default promise window, in seconds. The ticket's guidance is \"start generous,\n * ~30 min, tune down\"; 30 min is also what the agent's own instructions imply\n * (\"I'll come back shortly\"), so it is the threshold that is easiest to defend.\n *\n * Shared between the CLI reporter (which needs it only for its own tests and\n * docs — CloudWatch does the comparing) and the API alarm, so the two can never\n * drift into a state where the reported age and the alarmed threshold disagree.\n */\nexport const DEFAULT_INTERIM_PROMISE_THRESHOLD_SECONDS = 1800;\n\n/**\n * `promised_at` as ms-epoch when this marker carries an OUTSTANDING promise,\n * else `null`. Pure: the caller supplies the parsed marker, so this is trivially\n * exhaustible over every shape above.\n */\nexport function outstandingPromiseAtMs(marker: InterimPromiseMarkerFields | null | undefined): number | null {\n if (!marker || typeof marker !== 'object') return null;\n // A skip and a dead-letter both mean \"nothing is owed here\", regardless of\n // whether an interim ack was posted earlier.\n if (marker.discretionary === true) return null;\n if (marker.undeliverable === true) return null;\n if (typeof marker.promised_at !== 'string' || marker.promised_at === '') return null;\n const ms = Date.parse(marker.promised_at);\n // Unparseable timestamp: fail quiet. A corrupt value must not invent a promise\n // — the cost of a false alert here is a human paged about a healthy thread.\n if (!Number.isFinite(ms)) return null;\n return ms;\n}\n\n/**\n * Age (whole seconds) of a promise made at `promisedAtMs`, clamped at 0 so clock\n * skew between the marker writer and the probe can never produce a negative age\n * (which would sail under any threshold and read as healthy).\n */\nexport function interimPromiseAgeSeconds(promisedAtMs: number, nowMs: number): number {\n return Math.max(0, Math.floor((nowMs - promisedAtMs) / 1000));\n}\n\n/**\n * Whether a reported promise age breaches the window. Mirrors the CloudWatch\n * alarm's `GreaterThanOrEqualToThreshold` **exactly**, so a test can assert \"an\n * alert by T+30min\" against the same comparison the alarm performs rather than\n * against a re-derived one. `>=` (not `>`) is deliberate: the acceptance\n * criterion is \"alert **by** T+30min\", and with `>` a promise sitting at exactly\n * 1800s would wait for the next publish cycle.\n */\nexport function interimPromiseBreaches(\n ageSeconds: number,\n thresholdSeconds: number = DEFAULT_INTERIM_PROMISE_THRESHOLD_SECONDS,\n): boolean {\n return Number.isFinite(ageSeconds) && ageSeconds >= thresholdSeconds;\n}\n\n/**\n * What kind of engagement is being recorded on a marker.\n *\n * - `interim_ack` — the agent replied with `interim: true`, explicitly promising\n * a substantive answer still to come. The ONLY kind that records a promise.\n * - `seen` — every other form of engagement: a `read_thread`, a reaction, a\n * deliberate skip. Proves the agent saw the inbound; promises nothing.\n */\nexport type MarkerEngagementKind = 'interim_ack' | 'seen';\n\nexport interface MarkerEngagementDecision {\n /** Fields to merge into the marker, or `null` when no rewrite is needed. */\n stamp: { seen_at?: string; promised_at?: string } | null;\n /**\n * Whether the marker carries an outstanding promise once this decision is\n * applied — `true` also when it was ALREADY promised and no rewrite is needed,\n * so a caller can treat \"already in the desired state\" as success rather than\n * as a failed write.\n */\n promised: boolean;\n}\n\n/**\n * Decide what an engagement stamps on a pending-inbound marker. Shared by the\n * Slack and Telegram channel servers so the two can never drift on the one\n * question that matters here — which engagements create a promise and which do\n * not. Pure; the caller owns reading and writing the file.\n *\n * Rules, each of which a negative test depends on:\n *\n * - `seen` never writes `promised_at`. A read_thread or a reaction must not make\n * a thread the agent correctly skipped look like a broken promise.\n * - `seen` on an already-seen marker rewrites NOTHING (keeps the first\n * engagement timestamp, and keeps a reaction storm from bumping mtime).\n * - `interim_ack` on an already-promised marker rewrites NOTHING and reports\n * `promised: true`. Keeping the FIRST promise timestamp is what makes the\n * elapsed-time signal measure the whole outstanding window; restarting it on\n * every \"still working\" ack would hide the drop indefinitely.\n * - `interim_ack` on a seen-but-unpromised marker adds `promised_at` while\n * leaving the earlier `seen_at` intact.\n */\nexport function decideMarkerEngagement(\n marker: InterimPromiseMarkerFields | null | undefined,\n kind: MarkerEngagementKind,\n nowIso: string,\n): MarkerEngagementDecision {\n const alreadySeen = typeof marker?.seen_at === 'string' && marker.seen_at !== '';\n const alreadyPromised = typeof marker?.promised_at === 'string' && marker.promised_at !== '';\n\n if (kind === 'seen') {\n return alreadySeen\n ? { stamp: null, promised: alreadyPromised }\n : { stamp: { seen_at: nowIso }, promised: alreadyPromised };\n }\n if (alreadyPromised) return { stamp: null, promised: true };\n return {\n stamp: alreadySeen ? { promised_at: nowIso } : { seen_at: nowIso, promised_at: nowIso },\n promised: true,\n };\n}\n\n/**\n * Per-settle tally of interim-promise stamp attempts, as each channel server\n * records it while fanning out over the markers a reply settles.\n */\nexport interface PromiseStampTally {\n /** Markers the stamp op was applied to. */\n attempted: number;\n /** Of those, how many now carry `promised_at` on disk. */\n stamped: number;\n}\n\n/**\n * The sentence a reply tool appends to its result after an `interim: true` send,\n * or `null` when nothing should be said.\n *\n * Shared by the Slack and Telegram servers for the same reason\n * {@link decideMarkerEngagement} is: an interim ack that the platform failed to\n * RECORD is the very failure this ticket closes, one layer down — nothing is\n * tracking the owed reply, and no alarm will ever fire because `promised_at` was\n * never written. A bare `sent` in that case tells the agent the obligation is\n * being watched when it is not. Slack surfaced this from day one and Telegram did\n * not (it only wrote the operator-facing stderr line), which is precisely the\n * cross-channel drift this module exists to prevent — so the text lives here and\n * both channels read it.\n *\n * Three outcomes:\n *\n * - `attempted > stamped` — a marker matched but the promise did not persist.\n * WARN: the agent must not rely on a reminder that cannot arrive, so tell it to\n * answer in this same turn.\n * - `stamped > 0` — recorded. Say so, so the agent knows the final reply is still\n * owed and is being tracked.\n * - `attempted === 0` — no marker matched this settle (a proactive interim post,\n * or the inbound was already discharged). Not an error, and not worth a line.\n */\nexport function describePromiseStampOutcome(\n tally: PromiseStampTally,\n /**\n * The channel's own reply tool (`slack.reply` / `telegram.reply`). Only the\n * success line names it — an instruction to \"send a separate reply\" is\n * actionable only if the agent is told which tool call discharges the promise,\n * and naming the wrong channel's tool would be worse than naming none.\n */\n replyToolName: string,\n): string | null {\n if (tally.attempted > tally.stamped) {\n return 'WARNING: this was recorded as an interim ack but the outstanding-promise marker could NOT be written, so nothing is tracking that you still owe a final reply. Do not rely on a reminder — post your substantive answer in this same turn.';\n }\n if (tally.stamped > 0) {\n return `Recorded as an interim ack — your final answer is still owed and is being tracked until you send it as a separate ${replyToolName} with interim omitted.`;\n }\n return null;\n}\n","/**\n * Deflection causes — the ONE definition (ENG-10054).\n *\n * WHY THIS LIVES IN CORE. This list had three copies: the writer\n * (`packages/mcp/src/ack-reaction.ts`), the probe reader\n * (`apps/cli/src/lib/responsiveness-probe.ts`), and the API ingest filter\n * (`packages/api/src/routes/host-runtime.ts`). The third had drifted to TEN of\n * the twelve, missing `aged_out_unrecoverable` and `marker_corrupt`, and the\n * ingest drops an unlisted cause with a bare `continue` — no log, no counter —\n * before `PutMetricData`.\n *\n * So those two CloudWatch series read zero, and the zero meant \"never\n * ingested\", not \"never happened\". They are ENG-9414's message-DESTRUCTION\n * counters, added precisely because \"four destroyed customer inbounds in one\n * hour produced zero alerts\". The counters shipped; the metric never arrived;\n * the silence they were built to end continued. A drifted copy of a list is\n * how a fix gets quietly undone.\n *\n * The ingest filter itself was right to be an allowlist — its comment explains\n * that an unknown key from a corrupt counter file would otherwise add an\n * unbounded CloudWatch dimension value. That reasoning is preserved: this is\n * still a closed allowlist, it is simply the SAME allowlist everywhere.\n *\n * RULE FOR WHOEVER ADDS THE NEXT CAUSE: add it here and nowhere else. If you\n * find yourself typing a cause string into a second file, that is the defect\n * this module exists to prevent.\n */\n\n/**\n * Why an inbound could not be delivered at all. The three hard-failure classes\n * `classifyUndeliverableCause` attributes; everything else is a healthy path.\n *\n * Moved here with `DeflectionCause` (ENG-10054) because the latter is a union\n * over it, and splitting a type across packages to keep a constant in one place\n * would just trade one drift surface for another. `ack-reaction.ts` re-exports\n * both, so existing importers are unaffected.\n */\nexport type UndeliverableCause = 'integration_down' | 'session_dead' | 'wedged';\n\n/**\n * The attributed trigger for a deflection. The undeliverable subset comes\n * straight from classifyUndeliverableCause; 'busy' is the busy-ack path;\n * 'duplicate' is a redundant re-delivery dropped at fresh ingress (ENG-6270);\n * 'replay_orphaned' is a recoverable inbound (a marker carrying a replay\n * payload) the orphan sweep GC'd WITHOUT it ever being replayed (ENG-6355 —\n * the candidate-silent-loss canary); 'replay_exhausted' (ENG-7736) is its\n * sibling for a marker GC'd AFTER one or more replay pushes that still never\n * cleared - under respawn churn a forced push can land in a session that is\n * killed before the turn starts, and without this cause those losses would be\n * invisible (replay_orphaned requires replay_count === 0, so bumping the\n * count would otherwise silence the alarm that caught the Sherlock loss); and\n * 'unknown' is the fail-safe when the cause genuinely can't be determined\n * (un-probeable host whose liveness reads 'unknown') — honest\n * under-attribution rather than a confident wrong label.\n *\n * ENG-6270/6355: 'duplicate' and 'replay_orphaned' share this cause-dimensioned\n * pipeline (rather than sibling metrics) so the ChannelDeflections{Cause=…}\n * series give the pre-flip baselines the ENG-6354 replay soak watches — dup-rate\n * (Cause=duplicate) must stay ~0 and candidate-silent-loss (Cause=replay_orphaned)\n * must fall when replay is enabled. No new counter-file kind, probe field, or\n * PutMetricData path to maintain.\n */\nexport type DeflectionCause =\n | UndeliverableCause\n | 'busy'\n | 'duplicate'\n | 'duplicate_mention'\n // ENG-8387 AC3: an echo we dropped whose app_mention twin NEVER arrived -\n // a mention thrown away and never delivered. Distinct from duplicate_mention,\n // which is a mention that did arrive and was dropped as a re-delivery.\n | 'mention_echo_orphan'\n | 'replay_orphaned'\n | 'replay_exhausted'\n // ENG-9414: a genuinely-aged, non-discretionary marker GC'd with NO replay\n // payload. `replay_orphaned` cannot see this class, by construction: it\n // requires `marker.payload`, and this is precisely the marker that has none.\n //\n // It is not the mild case. The payload is attached ~180 lines and three\n // awaits AFTER the marker is written (slack-channel.ts:9519 vs :9701), and\n // the in-session push to the agent happens after the attach - so a throw in\n // that window leaves a marker with no payload whose message THE AGENT WAS\n // NEVER NOTIFIED OF AT ALL. Unreplayable and undelivered. Under the old gate\n // it was deleted at 30 minutes with no record anywhere that it had existed.\n | 'aged_out_unrecoverable'\n // ENG-9414: a marker deleted WITHOUT being aged - structurally broken\n // (missing channel/thread/message ts), unparseable, or carrying a\n // future-dated `received_at`. `isPendingMarkerStale` deletes all three;\n // `isMarkerGenuinelyAged` returns false for all three, so none was counted.\n //\n // The clock-skew case is the one to watch: an NTP step backwards\n // future-dates every marker written in that window, so a single skew event\n // deletes an agent's entire live queue silently. A non-zero rate here means\n // messages are being destroyed for a reason that has nothing to do with age.\n | 'marker_corrupt'\n | 'unknown';\n\nexport const DEFLECTION_CAUSES: readonly DeflectionCause[] = [\n 'integration_down',\n 'session_dead',\n 'wedged',\n 'busy',\n 'duplicate',\n // ENG-8387: an `app_mention` — the CANONICAL delivery of an @mention — dropped\n // by the fresh-ingress dedup. Split out of the generic `duplicate` bucket\n // because the two mean very different things: `duplicate` is dominated by the\n // deliberate, high-volume ENG-6378 echo drop and by routine reconnect\n // redelivery, both benign, while a dropped app_mention has the exact shape of\n // a lost mention. Conflated, the second is invisible inside the first — which\n // is how ENG-6378's silent loss ran unobserved.\n 'duplicate_mention',\n 'mention_echo_orphan',\n 'replay_orphaned',\n 'replay_exhausted',\n // ENG-9414 — see the type union above for why these two exist. Both are\n // message-loss classes the pre-existing causes were structurally unable to\n // observe, which is why four destroyed customer inbounds in one hour\n // produced zero alerts.\n 'aged_out_unrecoverable',\n 'marker_corrupt',\n 'unknown',\n];\n\n/**\n * The same causes as a Set, for the membership tests the readers do.\n *\n * Typed `ReadonlySet<string>` rather than `ReadonlySet<DeflectionCause>` on\n * purpose: every caller is checking an UNTRUSTED string off the wire (a probe\n * payload, a counter file) against the allowlist. A `Set<DeflectionCause>`\n * would force each of them to cast the untrusted value to the very type the\n * check exists to establish, which defeats the check.\n */\nexport const DEFLECTION_CAUSE_SET: ReadonlySet<string> = new Set<string>(DEFLECTION_CAUSES);\n\n/** Narrowing membership test over the one allowlist. */\nexport function isDeflectionCause(value: string): value is DeflectionCause {\n return DEFLECTION_CAUSE_SET.has(value);\n}\n"],"mappings":"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","names":["readdirSync","readFileSync","dirname","join","readdirSync","join","readFileSync","dirname"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.946",
3
+ "version": "0.28.948",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {