@integrity-labs/agt-cli 0.28.441 → 0.28.443

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.
@@ -18867,7 +18867,7 @@ var require_filters = __commonJS({
18867
18867
  return r.copySafeness(str, res);
18868
18868
  }
18869
18869
  _exports.indent = indent;
18870
- function join13(arr, del, attr) {
18870
+ function join14(arr, del, attr) {
18871
18871
  del = del || "";
18872
18872
  if (attr) {
18873
18873
  arr = lib.map(arr, function(v) {
@@ -18876,7 +18876,7 @@ var require_filters = __commonJS({
18876
18876
  }
18877
18877
  return arr.join(del);
18878
18878
  }
18879
- _exports.join = join13;
18879
+ _exports.join = join14;
18880
18880
  function last(arr) {
18881
18881
  return arr[arr.length - 1];
18882
18882
  }
@@ -30087,6 +30087,40 @@ function resolveHostBooleanFlag(opts) {
30087
30087
  if (cached2 !== void 0) return cached2;
30088
30088
  return opts.defaultValue;
30089
30089
  }
30090
+ var USAGE_LIMIT_REACTIVE_FLAG_KEY = "usage-limit-reactive-notice";
30091
+ function parseUsageLimitReactiveMode(raw) {
30092
+ if (raw === void 0) return void 0;
30093
+ const v = raw.trim().toLowerCase();
30094
+ if (v === "") return void 0;
30095
+ if (v === "off" || v === "shadow" || v === "enforce") return v;
30096
+ const b = envBoolean(raw);
30097
+ if (b === true) return "enforce";
30098
+ if (b === false) return "off";
30099
+ return void 0;
30100
+ }
30101
+ function cachedUsageLimitReactiveMode(path) {
30102
+ try {
30103
+ if (!existsSync4(path)) return void 0;
30104
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
30105
+ if (!parsed || typeof parsed !== "object") return void 0;
30106
+ const flags = parsed.flags;
30107
+ if (!flags || typeof flags !== "object") return void 0;
30108
+ const value = flags[USAGE_LIMIT_REACTIVE_FLAG_KEY];
30109
+ if (typeof value === "string") return parseUsageLimitReactiveMode(value);
30110
+ if (typeof value === "boolean") return value ? "enforce" : "off";
30111
+ return void 0;
30112
+ } catch {
30113
+ return void 0;
30114
+ }
30115
+ }
30116
+ function resolveUsageLimitReactiveMode(opts) {
30117
+ const env2 = opts?.env ?? process.env;
30118
+ const modeOverride = parseUsageLimitReactiveMode(env2["AGT_USAGE_LIMIT_REACTIVE_MODE"]);
30119
+ if (modeOverride !== void 0) return modeOverride;
30120
+ const cached2 = cachedUsageLimitReactiveMode(opts?.cachePath ?? defaultFlagsCachePath());
30121
+ if (cached2 !== void 0) return cached2;
30122
+ return "off";
30123
+ }
30090
30124
 
30091
30125
  // src/ack-reaction.ts
30092
30126
  var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
@@ -30399,19 +30433,19 @@ function readLockHolder(path) {
30399
30433
  }
30400
30434
 
30401
30435
  // src/direct-chat-channel.ts
30402
- import { homedir as homedir4 } from "os";
30403
- import { join as join12 } from "path";
30436
+ import { homedir as homedir5 } from "os";
30437
+ import { join as join13 } from "path";
30404
30438
  import { randomUUID } from "crypto";
30405
30439
  import {
30406
30440
  watch,
30407
30441
  mkdirSync as mkdirSync5,
30408
30442
  writeFileSync as writeFileSync6,
30409
- readFileSync as readFileSync11,
30410
- readdirSync as readdirSync4,
30443
+ readFileSync as readFileSync12,
30444
+ readdirSync as readdirSync5,
30411
30445
  existsSync as existsSync6,
30412
30446
  renameSync as renameSync5,
30413
30447
  unlinkSync as unlinkSync4,
30414
- statSync as statSync2,
30448
+ statSync as statSync3,
30415
30449
  createWriteStream
30416
30450
  } from "fs";
30417
30451
 
@@ -31989,8 +32023,31 @@ var DEFAULT_SCOPES = [
31989
32023
  // links an inbound Slack sender to their organization_people record (inbound
31990
32024
  // identity reconcile). Existing installs without it degrade gracefully - the
31991
32025
  // reconcile no-ops until the bot is re-authorised with the wider scope set.
31992
- "users:read.email",
31993
- "users.profile:write"
32026
+ "users:read.email"
32027
+ // ENG-8203: 'users.profile:write' is deliberately NOT here.
32028
+ //
32029
+ // It is registered with `token_type: 'user'` (ENG-4812 — Slack rejects it
32030
+ // under oauth_config.scopes.bot with `illegal_bot_scopes`). The MANIFEST
32031
+ // generator partitions on that flag; the OAuth install-URL builder does not —
32032
+ // `buildSlackAuthorizeUrl` puts every stored scope into `scope=` — so a
32033
+ // default-scoped OAuth install sent a user-only scope as a bot scope and
32034
+ // Slack rejected the whole authorization with "Invalid permissions
32035
+ // requested". That blocked EVERY new install on the default set.
32036
+ //
32037
+ // Removing it rather than routing it to `user_scope=` is deliberate: the
32038
+ // callback destructures `authed_user?: { id?: string }` and persists only the
32039
+ // BOT token (oauth-callback/route.ts:217,246). `authed_user.access_token` is
32040
+ // never read, so asking for the scope would put an extra consent screen in
32041
+ // front of the installing admin and then discard the token it returned.
32042
+ //
32043
+ // Nothing is lost. `users.profile.set` needs an `xoxp-` user token, which no
32044
+ // OAuth-installed agent has ever had — the bot-status/live-presence indicator
32045
+ // only ever worked where an operator pasted a user token into the wizard by
32046
+ // hand, and that path is untouched. The registry definition stays so existing
32047
+ // configs still render and the `token_type: 'user'` knowledge is preserved.
32048
+ //
32049
+ // Wiring the indicator for OAuth installs means persisting
32050
+ // `authed_user.access_token` first, THEN adding `user_scope=`. Separate work.
31994
32051
  ];
31995
32052
  var SLACK_SCOPE_PRESETS = {
31996
32053
  minimal: [
@@ -34142,7 +34199,113 @@ var BANNER_PATTERNS = [
34142
34199
  // pct=100.
34143
34200
  new RegExp(`${SUBJECT}hit\\s+your\\s+(?:[a-z0-9-]+\\s+)?limit${SEP}resets\\s+(${RESET_DATE})`, "i")
34144
34201
  ];
34202
+ function parseUsageBanner(text, now = /* @__PURE__ */ new Date()) {
34203
+ let bestIndex = -1;
34204
+ let best = null;
34205
+ for (let i = 0; i < BANNER_PATTERNS.length; i++) {
34206
+ const pattern = new RegExp(BANNER_PATTERNS[i].source, "gi");
34207
+ let match;
34208
+ while ((match = pattern.exec(text)) !== null) {
34209
+ if (match.index === pattern.lastIndex)
34210
+ pattern.lastIndex++;
34211
+ let pct;
34212
+ let resetStr;
34213
+ if (i === 0) {
34214
+ pct = Number.parseInt(match[1], 10);
34215
+ resetStr = match[2];
34216
+ } else {
34217
+ pct = 100;
34218
+ resetStr = match[1];
34219
+ }
34220
+ if (!Number.isFinite(pct) || pct < 0 || pct > 100)
34221
+ continue;
34222
+ const nextChar = text[match.index + match[0].length];
34223
+ if (nextChar !== void 0 && /[A-Za-z0-9]/.test(nextChar))
34224
+ continue;
34225
+ const weekResetsAt = parseResetDateTime(resetStr, now);
34226
+ if (!weekResetsAt)
34227
+ continue;
34228
+ if (match.index >= bestIndex) {
34229
+ bestIndex = match.index;
34230
+ best = { pct, weekResetsAt };
34231
+ }
34232
+ }
34233
+ }
34234
+ return best;
34235
+ }
34236
+ var MONTHS = [
34237
+ "jan",
34238
+ "feb",
34239
+ "mar",
34240
+ "apr",
34241
+ "may",
34242
+ "jun",
34243
+ "jul",
34244
+ "aug",
34245
+ "sep",
34246
+ "oct",
34247
+ "nov",
34248
+ "dec"
34249
+ ];
34250
+ var TIME_TAIL = /,\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s*\(?UTC\)?)?\s*$/i;
34251
+ var TIME_ONLY = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s*\(?UTC\)?)?$/i;
34145
34252
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
34253
+ function parseAmPm(hourStr, minStr, ampm) {
34254
+ const rawHour = Number.parseInt(hourStr, 10);
34255
+ if (!Number.isFinite(rawHour) || rawHour < 1 || rawHour > 12)
34256
+ return null;
34257
+ let minute = 0;
34258
+ if (minStr) {
34259
+ minute = Number.parseInt(minStr, 10);
34260
+ if (!Number.isFinite(minute) || minute < 0 || minute > 59)
34261
+ return null;
34262
+ }
34263
+ const isPm = ampm.toLowerCase() === "pm";
34264
+ return { hour: rawHour % 12 + (isPm ? 12 : 0), minute };
34265
+ }
34266
+ function parseResetDateTime(humanDate, now) {
34267
+ const trimmed = humanDate.trim();
34268
+ const timeOnly = trimmed.match(TIME_ONLY);
34269
+ if (timeOnly) {
34270
+ const hm = parseAmPm(timeOnly[1], timeOnly[2], timeOnly[3]);
34271
+ if (!hm)
34272
+ return null;
34273
+ const todayAt = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hm.hour, hm.minute);
34274
+ return new Date(todayAt <= now.getTime() ? todayAt + MS_PER_DAY : todayAt);
34275
+ }
34276
+ const timeMatch = trimmed.match(TIME_TAIL);
34277
+ const dateOnly = timeMatch ? trimmed.slice(0, timeMatch.index).trim() : trimmed;
34278
+ const parts = dateOnly.split(/\s+/);
34279
+ if (parts.length !== 2)
34280
+ return null;
34281
+ const month = MONTHS.indexOf(parts[0].slice(0, 3).toLowerCase());
34282
+ if (month < 0)
34283
+ return null;
34284
+ const day = Number.parseInt(parts[1], 10);
34285
+ if (!Number.isFinite(day) || day < 1 || day > 31)
34286
+ return null;
34287
+ let hour = 0;
34288
+ let minute = 0;
34289
+ if (timeMatch) {
34290
+ const hm = parseAmPm(timeMatch[1], timeMatch[2], timeMatch[3]);
34291
+ if (!hm)
34292
+ return null;
34293
+ hour = hm.hour;
34294
+ minute = hm.minute;
34295
+ }
34296
+ const baseYear = now.getUTCFullYear();
34297
+ let resolved = null;
34298
+ let bestDelta = Number.POSITIVE_INFINITY;
34299
+ for (const y of [baseYear - 1, baseYear, baseYear + 1]) {
34300
+ const candidate = new Date(Date.UTC(y, month, day, hour, minute));
34301
+ const delta = Math.abs(candidate.getTime() - now.getTime());
34302
+ if (delta < bestDelta) {
34303
+ bestDelta = delta;
34304
+ resolved = candidate;
34305
+ }
34306
+ }
34307
+ return resolved;
34308
+ }
34146
34309
 
34147
34310
  // ../core/dist/claude-code-usage/usage-limit-marker.js
34148
34311
  var USAGE_LIMIT_MARKER_FILENAME = "claude-usage-limit.json";
@@ -34180,6 +34343,109 @@ function buildUsageLimitReplyText(limitedUntil) {
34180
34343
  return `Your agent has hit its Claude Code usage limit until ${formatUtcClock(limitedUntil)}. It'll pick back up once the limit resets \u2014 please try again after then.`;
34181
34344
  }
34182
34345
 
34346
+ // ../core/dist/claude-code-usage/rate-limit-classifier.js
34347
+ var UNKNOWN_RATE_LIMIT = Object.freeze({
34348
+ verdict: "unknown",
34349
+ atMs: null,
34350
+ resetsAt: null,
34351
+ text: null
34352
+ });
34353
+ function contentText(record2) {
34354
+ const candidates = [record2.content];
34355
+ const message = record2.message;
34356
+ if (typeof message === "object" && message !== null) {
34357
+ candidates.push(message.content);
34358
+ }
34359
+ const parts = [];
34360
+ for (const candidate of candidates) {
34361
+ if (typeof candidate === "string") {
34362
+ if (candidate)
34363
+ parts.push(candidate);
34364
+ continue;
34365
+ }
34366
+ if (!Array.isArray(candidate))
34367
+ continue;
34368
+ for (const block of candidate) {
34369
+ if (typeof block === "string") {
34370
+ if (block)
34371
+ parts.push(block);
34372
+ continue;
34373
+ }
34374
+ if (typeof block !== "object" || block === null)
34375
+ continue;
34376
+ const text = block.text;
34377
+ if (typeof text === "string" && text)
34378
+ parts.push(text);
34379
+ }
34380
+ }
34381
+ const joined = parts.join("\n").trim();
34382
+ return joined ? joined : null;
34383
+ }
34384
+ function classifyTranscriptLine(line, startMs, endMs, now) {
34385
+ const trimmed = line.trim();
34386
+ if (!trimmed)
34387
+ return null;
34388
+ let obj;
34389
+ try {
34390
+ obj = JSON.parse(trimmed);
34391
+ } catch {
34392
+ return null;
34393
+ }
34394
+ if (typeof obj !== "object" || obj === null)
34395
+ return null;
34396
+ const record2 = obj;
34397
+ if (record2.type !== "assistant")
34398
+ return null;
34399
+ const ts = record2.timestamp;
34400
+ if (typeof ts !== "string" || !ts)
34401
+ return null;
34402
+ const tsMs = new Date(ts).getTime();
34403
+ if (!Number.isFinite(tsMs) || tsMs < startMs || tsMs > endMs)
34404
+ return null;
34405
+ if (record2.error === "rate_limit" || record2.apiErrorStatus === 429) {
34406
+ const text = contentText(record2);
34407
+ const observation = text ? parseUsageBanner(text, now ?? new Date(endMs)) : null;
34408
+ return { verdict: "capped", atMs: tsMs, resetsAt: observation?.weekResetsAt ?? null, text };
34409
+ }
34410
+ if (record2.isApiErrorMessage === true)
34411
+ return null;
34412
+ const message = record2.message;
34413
+ if (typeof message !== "object" || message === null)
34414
+ return null;
34415
+ const msg = message;
34416
+ if (msg.model === "<synthetic>")
34417
+ return null;
34418
+ const usage = msg.usage;
34419
+ if (typeof usage !== "object" || usage === null)
34420
+ return null;
34421
+ const u = usage;
34422
+ const spent = Number(u.input_tokens ?? 0) + Number(u.output_tokens ?? 0) + Number(u.cache_creation_input_tokens ?? 0) + Number(u.cache_read_input_tokens ?? 0);
34423
+ if (!Number.isFinite(spent) || spent <= 0)
34424
+ return null;
34425
+ return { verdict: "serving", atMs: tsMs, resetsAt: null, text: null };
34426
+ }
34427
+ function pickNewerClassification(current, next) {
34428
+ if (next.verdict === "unknown")
34429
+ return current;
34430
+ if (current.verdict === "unknown")
34431
+ return next;
34432
+ return next.atMs >= current.atMs ? next : current;
34433
+ }
34434
+ function classifyTranscriptRateLimit(jsonl, startMs, endMs, now) {
34435
+ let newest = UNKNOWN_RATE_LIMIT;
34436
+ for (const line of jsonl.split("\n")) {
34437
+ const classified = classifyTranscriptLine(line, startMs, endMs, now);
34438
+ if (classified)
34439
+ newest = pickNewerClassification(newest, classified);
34440
+ }
34441
+ return newest;
34442
+ }
34443
+
34444
+ // ../core/dist/claude-code-usage/transcript-location.js
34445
+ function encodeClaudeProjectPath(projectDir) {
34446
+ return "-" + projectDir.replace(/^\//, "").replace(/[/.]/g, "-");
34447
+ }
34448
+
34183
34449
  // ../core/dist/account-enforcement/marker.js
34184
34450
  var ACCOUNT_ENFORCEMENT_MARKER_FILENAME = "account-enforcement.json";
34185
34451
  var ACCOUNT_ENFORCEMENT_MARKER_VERSION = 1;
@@ -34824,6 +35090,33 @@ var FLAG_REGISTRY = [
34824
35090
  // projectDefinition, so setting it does not roll FLAGS_SCHEMA_VERSION.
34825
35091
  since: "0.28.421"
34826
35092
  },
35093
+ {
35094
+ key: "usage-limit-reactive-notice",
35095
+ description: "Report a Claude Code usage cap REACTIVELY instead of predicting it (ENG-8201). Today the manager guesses from the agent transcript that the next turn will be refused, writes a marker, and every channel MCP refuses to dispatch on the strength of that guess - so a wrong guess is a swallowed message, and because the notice is throttled per (channel, sender) while the DROP is not, the usual symptom is total silence rather than a wrong reply. A refused turn actually costs nothing (rejected in under a second, zero tokens) and Claude Code records it with the reset time in it, so there is no need to guess: dispatch, and report the refusal if one comes back. off = today behaviour (pre-dispatch marker gate, no watcher). shadow = still gate on the marker, but ALSO watch dispatched messages and log the refusal that would have been reported - measures the true-positive rate with no user-visible change. enforce = stop reading the marker before dispatch; every admitted human message reaches the agent and a refusal is answered in-thread with the reset time from the error itself. Read live from the heartbeat flags-cache (or the env override).",
35096
+ flagType: "enum",
35097
+ allowedValues: ["off", "shadow", "enforce"],
35098
+ // Ships dark: off preserves today's gate byte-for-byte. shadow is a free
35099
+ // local log line (the watch is a transcript read, no model spend), so it is a
35100
+ // cheap soak; enforce changes what reaches the agent, so it is the audited
35101
+ // per-org flip.
35102
+ defaultValue: "off",
35103
+ // Enum override AGT_USAGE_LIMIT_REACTIVE_MODE (off|shadow|enforce), resolved
35104
+ // by resolveUsageLimitReactiveMode in the channel-server bundle.
35105
+ envVar: "AGT_USAGE_LIMIT_REACTIVE_MODE",
35106
+ // enforce sends a message to an agent the host currently believes is capped -
35107
+ // a deliberate availability trade (the refusal is free, but it is still a
35108
+ // dispatch the operator previously suppressed), so flipping toward it is an
35109
+ // audited change (ADR-0022 sensitive-flag confirm).
35110
+ sensitive: true
35111
+ // ENG-8149: `since` is the agt-cli version that first carries
35112
+ // resolveUsageLimitReactiveMode + the MCP watcher. It cannot be known before
35113
+ // this lands (the auto-publish patch-bumps on merge), and an undefined
35114
+ // `since` makes the flip-reach modal claim FULL reach - true for flags that
35115
+ // predate the reach work, wrong for a NEW host-read flag whose reader older
35116
+ // hosts simply do not have. Backfilled by a follow-up commit once the
35117
+ // publishing run reports the version. Excluded from projectDefinition, so
35118
+ // setting it does not roll FLAGS_SCHEMA_VERSION.
35119
+ },
34827
35120
  {
34828
35121
  key: "slack-hot-thread-guard",
34829
35122
  description: "Server-side hot-thread guard on the slack.reply surface (ENG-7462). Prevents an agent posting a NEW top-level Slack message when it meant to reply inside the thread it is already working in - a prompt/memory rule proved insufficient (the agent had the rule and still slipped). When a reply would otherwise post to channel ROOT (no thread_ts / message_ts / inbound_id, no active kanban card) and the agent has a recent active thread in that channel (its last bot-posted thread, from the persisted trackedThreads cache, within a freshness window), the reply is redirected into that thread. proactive:true no longer implies channel root; posting at root becomes a deliberate action (the to_channel_root flag, or the thread_ts:null sentinel). off = guard never runs, replies with no coords root exactly as today (ships dark). shadow = compute + log the would-redirect but STILL post to root (measure the fire rate before acting). enforce = apply the redirect (a soft-block: redirect + inform, never a hard rejection). Read live from the heartbeat flags-cache (or the env override); enforce is a deliberate per-org flip after a shadow soak.",
@@ -35547,6 +35840,75 @@ function shouldSendAccountWarn(opts) {
35547
35840
  }).reply;
35548
35841
  }
35549
35842
 
35843
+ // src/rate-limit-watch.ts
35844
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
35845
+ import { homedir as homedir4 } from "os";
35846
+ import { join as join12 } from "path";
35847
+ var DEFAULT_WATCH_MS = 5e3;
35848
+ var DEFAULT_POLL_MS = 400;
35849
+ function agentTranscriptDir(opts) {
35850
+ const cwd = opts?.cwd ?? process.cwd();
35851
+ const home = opts?.home ?? homedir4();
35852
+ return join12(home, ".claude", "projects", encodeClaudeProjectPath(cwd));
35853
+ }
35854
+ function classifyTranscriptSince(opts) {
35855
+ const dir = opts.transcriptDir ?? agentTranscriptDir({ cwd: opts.cwd, home: opts.home });
35856
+ let entries;
35857
+ try {
35858
+ entries = readdirSync4(dir);
35859
+ } catch {
35860
+ return UNKNOWN_RATE_LIMIT;
35861
+ }
35862
+ let newest = UNKNOWN_RATE_LIMIT;
35863
+ for (const name of entries) {
35864
+ if (!name.endsWith(".jsonl")) continue;
35865
+ const path = join12(dir, name);
35866
+ try {
35867
+ const st = statSync2(path);
35868
+ if (!st.isFile() || st.mtimeMs < opts.sinceMs) continue;
35869
+ } catch {
35870
+ continue;
35871
+ }
35872
+ let content;
35873
+ try {
35874
+ content = readFileSync11(path, "utf-8");
35875
+ } catch {
35876
+ continue;
35877
+ }
35878
+ newest = pickNewerClassification(
35879
+ newest,
35880
+ classifyTranscriptRateLimit(content, opts.sinceMs, opts.nowMs, new Date(opts.nowMs))
35881
+ );
35882
+ }
35883
+ return newest;
35884
+ }
35885
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
35886
+ async function watchForRateLimitRefusal(opts) {
35887
+ const now = opts.now ?? (() => Date.now());
35888
+ const wait = opts.wait ?? sleep;
35889
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_WATCH_MS;
35890
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
35891
+ const deadline = now() + timeoutMs;
35892
+ for (; ; ) {
35893
+ let result;
35894
+ try {
35895
+ result = classifyTranscriptSince({
35896
+ sinceMs: opts.sinceMs,
35897
+ nowMs: now(),
35898
+ transcriptDir: opts.transcriptDir,
35899
+ cwd: opts.cwd,
35900
+ home: opts.home
35901
+ });
35902
+ } catch {
35903
+ return null;
35904
+ }
35905
+ if (result.verdict === "capped") return result;
35906
+ if (result.verdict === "serving") return null;
35907
+ if (now() >= deadline) return null;
35908
+ await wait(pollMs);
35909
+ }
35910
+ }
35911
+
35550
35912
  // src/direct-chat-channel.ts
35551
35913
  var DIRECT_CHAT_MAINTENANCE_CACHE = /* @__PURE__ */ new Map();
35552
35914
  var DIRECT_CHAT_MAINTENANCE_COOLDOWN_MS = (() => {
@@ -35565,21 +35927,21 @@ var DIRECT_CHAT_ACCOUNT_WARN_COOLDOWN_MS = (() => {
35565
35927
  var AGT_HOST = process.env.AGT_HOST;
35566
35928
  var AGT_API_KEY = process.env.AGT_API_KEY;
35567
35929
  var AGT_AGENT_ID = process.env.AGT_AGENT_ID;
35568
- var DIRECT_CHAT_AGENT_DIR = AGT_AGENT_ID ? join12(homedir4(), ".augmented", AGT_AGENT_ID) : null;
35930
+ var DIRECT_CHAT_AGENT_DIR = AGT_AGENT_ID ? join13(homedir5(), ".augmented", AGT_AGENT_ID) : null;
35569
35931
  var INBOUND_ATTACHMENTS_DIR = resolveInboundAttachmentsDir({
35570
35932
  codeName: process.env.AGT_AGENT_CODE_NAME,
35571
35933
  turnInitiatorFile: process.env.AGT_TURN_INITIATOR_FILE,
35572
35934
  agentId: AGT_AGENT_ID,
35573
- homeDir: homedir4()
35935
+ homeDir: homedir5()
35574
35936
  });
35575
35937
  var AGT_AGENT_CODE_NAME = process.env.AGT_AGENT_CODE_NAME;
35576
35938
  var directChatStderrLogStream = null;
35577
35939
  if (AGT_AGENT_CODE_NAME) {
35578
35940
  try {
35579
- const logDir = join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME);
35941
+ const logDir = join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME);
35580
35942
  mkdirSync5(logDir, { recursive: true });
35581
35943
  directChatStderrLogStream = createWriteStream(
35582
- join12(logDir, "direct-chat-channel-stderr.log"),
35944
+ join13(logDir, "direct-chat-channel-stderr.log"),
35583
35945
  { flags: "a", mode: 384 }
35584
35946
  );
35585
35947
  directChatStderrLogStream.on("error", () => {
@@ -35598,10 +35960,10 @@ if (AGT_AGENT_CODE_NAME) {
35598
35960
  } catch {
35599
35961
  }
35600
35962
  }
35601
- var PROGRESS_HEARTBEAT_PATH = AGT_AGENT_CODE_NAME ? join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME, "channel-progress-heartbeat.json") : null;
35602
- var DIRECT_CHAT_PENDING_INBOUND_DIR = AGT_AGENT_CODE_NAME ? join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME, "direct-chat-pending-inbound") : null;
35603
- var DIRECT_CHAT_DELIVERY_LEDGER_DIR = AGT_AGENT_CODE_NAME ? join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME, ".agt-inbound-delivery-ledger") : null;
35604
- var DIRECT_CHAT_CODE_NAME_AGENT_DIR = AGT_AGENT_CODE_NAME ? join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME) : null;
35963
+ var PROGRESS_HEARTBEAT_PATH = AGT_AGENT_CODE_NAME ? join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME, "channel-progress-heartbeat.json") : null;
35964
+ var DIRECT_CHAT_PENDING_INBOUND_DIR = AGT_AGENT_CODE_NAME ? join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME, "direct-chat-pending-inbound") : null;
35965
+ var DIRECT_CHAT_DELIVERY_LEDGER_DIR = AGT_AGENT_CODE_NAME ? join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME, ".agt-inbound-delivery-ledger") : null;
35966
+ var DIRECT_CHAT_CODE_NAME_AGENT_DIR = AGT_AGENT_CODE_NAME ? join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME) : null;
35605
35967
  var DIRECT_CHAT_STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
35606
35968
  function recordDirectChatDelivery(sessionId, messageIds) {
35607
35969
  if (!sessionId) return;
@@ -35612,8 +35974,8 @@ function recordDirectChatDelivery(sessionId, messageIds) {
35612
35974
  delivered_at: (/* @__PURE__ */ new Date()).toISOString()
35613
35975
  });
35614
35976
  }
35615
- var DIRECT_CHAT_RECOVERY_OUTBOX_DIR = AGT_AGENT_CODE_NAME ? join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME, "direct-chat-recovery-outbox") : null;
35616
- var DIRECT_CHAT_RECOVERY_LEDGER_DIR = AGT_AGENT_CODE_NAME ? join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME, ".agt-direct-chat-recovery-ledger") : null;
35977
+ var DIRECT_CHAT_RECOVERY_OUTBOX_DIR = AGT_AGENT_CODE_NAME ? join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME, "direct-chat-recovery-outbox") : null;
35978
+ var DIRECT_CHAT_RECOVERY_LEDGER_DIR = AGT_AGENT_CODE_NAME ? join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME, ".agt-direct-chat-recovery-ledger") : null;
35617
35979
  var progressReceivedAt = /* @__PURE__ */ new Map();
35618
35980
  var directChatProgressState = { tracked: null };
35619
35981
  var directChatProgressTickRunning = false;
@@ -35632,7 +35994,7 @@ var directChatKanbanCardClient = createKanbanCardActiveClient({
35632
35994
  function readProgressHeartbeat() {
35633
35995
  if (!PROGRESS_HEARTBEAT_PATH || !existsSync6(PROGRESS_HEARTBEAT_PATH)) return null;
35634
35996
  try {
35635
- return parseProgressHeartbeat(readFileSync11(PROGRESS_HEARTBEAT_PATH, "utf-8"));
35997
+ return parseProgressHeartbeat(readFileSync12(PROGRESS_HEARTBEAT_PATH, "utf-8"));
35636
35998
  } catch {
35637
35999
  return null;
35638
36000
  }
@@ -35641,7 +36003,7 @@ function seedProgressHeartbeat() {
35641
36003
  if (!PROGRESS_HEARTBEAT_PATH) return;
35642
36004
  const tmp = `${PROGRESS_HEARTBEAT_PATH}.${process.pid}.tmp`;
35643
36005
  try {
35644
- mkdirSync5(join12(homedir4(), ".augmented", AGT_AGENT_CODE_NAME), { recursive: true });
36006
+ mkdirSync5(join13(homedir5(), ".augmented", AGT_AGENT_CODE_NAME), { recursive: true });
35645
36007
  writeFileSync6(tmp, serializeProgressHeartbeat(SEED_PROGRESS_STEP, Date.now()), { mode: 384 });
35646
36008
  renameSync5(tmp, PROGRESS_HEARTBEAT_PATH);
35647
36009
  } catch {
@@ -35762,7 +36124,7 @@ var STREAM_REPLY_FLUSH_MS = (() => {
35762
36124
  const n = raw ? Number(raw) : NaN;
35763
36125
  return Number.isFinite(n) && n >= 0 ? n : 60;
35764
36126
  })();
35765
- var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
36127
+ var sleep2 = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
35766
36128
  async function apiPost(path, body) {
35767
36129
  const token = await getAuthToken();
35768
36130
  const res = await fetchWithTimeout(`${AGT_HOST}${path}`, {
@@ -35833,7 +36195,7 @@ var inboundAttachmentDeps = {
35833
36195
  },
35834
36196
  ensureDir: (dir) => mkdirSync5(dir, { recursive: true }),
35835
36197
  writeFile: (path, bytes) => writeFileSync6(path, bytes, { mode: 384 }),
35836
- joinPath: (...parts) => join12(...parts),
36198
+ joinPath: (...parts) => join13(...parts),
35837
36199
  warn: (msg) => process.stderr.write(`${msg}
35838
36200
  `)
35839
36201
  };
@@ -36012,11 +36374,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
36012
36374
  recordDirectChatDelivery(session_id, message_ids);
36013
36375
  const messageId = data.message_id;
36014
36376
  for (let i = 1; i < slices.length; i++) {
36015
- await sleep(STREAM_REPLY_FLUSH_MS);
36377
+ await sleep2(STREAM_REPLY_FLUSH_MS);
36016
36378
  const isFinal = i === slices.length - 1;
36017
36379
  let ok = await postDirectChatUpdate(messageId, session_id, slices[i]);
36018
36380
  if (!ok && isFinal) {
36019
- await sleep(STREAM_REPLY_FLUSH_MS);
36381
+ await sleep2(STREAM_REPLY_FLUSH_MS);
36020
36382
  ok = await postDirectChatUpdate(messageId, session_id, slices[i]);
36021
36383
  if (!ok) {
36022
36384
  return {
@@ -36165,7 +36527,7 @@ function scheduleDirectChatBusyAck(sessionId, messageId, arrivedWhileBusy) {
36165
36527
  let paneLogFreshAgeMs = null;
36166
36528
  if (DIRECT_CHAT_CODE_NAME_AGENT_DIR) {
36167
36529
  try {
36168
- const paneMtimeMs = statSync2(join12(DIRECT_CHAT_CODE_NAME_AGENT_DIR, "pane.log")).mtimeMs;
36530
+ const paneMtimeMs = statSync3(join13(DIRECT_CHAT_CODE_NAME_AGENT_DIR, "pane.log")).mtimeMs;
36169
36531
  paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
36170
36532
  } catch {
36171
36533
  }
@@ -36213,6 +36575,63 @@ function scheduleDirectChatBusyAck(sessionId, messageId, arrivedWhileBusy) {
36213
36575
  }, thresholdMs);
36214
36576
  timer.unref();
36215
36577
  }
36578
+ function armUsageLimitWatch(args) {
36579
+ void (async () => {
36580
+ try {
36581
+ const refusal = await watchForRateLimitRefusal({ sinceMs: args.sinceMs });
36582
+ if (!refusal) return;
36583
+ if (!refusal.resetsAt) {
36584
+ process.stderr.write(
36585
+ `direct-chat-channel: [usage-limit-reactive] mode=${args.mode} refusal detected for session=${args.sessionId} but no reset time in it (${refusal.text ?? "no text"}) - no notice
36586
+ `
36587
+ );
36588
+ return;
36589
+ }
36590
+ if (args.mode === "shadow") {
36591
+ process.stderr.write(
36592
+ `direct-chat-channel: [usage-limit-reactive] mode=shadow WOULD notify session=${args.sessionId} message=${args.messageId} resets=${refusal.resetsAt.toISOString()}
36593
+ `
36594
+ );
36595
+ return;
36596
+ }
36597
+ const throttleKey = `${args.sessionId}|usage_limit`;
36598
+ const throttle = decideDeclineReply({
36599
+ cache: DIRECT_CHAT_USAGE_LIMIT_CACHE,
36600
+ key: throttleKey,
36601
+ cooldownMs: DIRECT_CHAT_MAINTENANCE_COOLDOWN_MS,
36602
+ now: Date.now()
36603
+ });
36604
+ const route = throttle.reply ? "/host/direct-chat/reply" : "/host/direct-chat/consume";
36605
+ const body = throttle.reply ? {
36606
+ agent_id: AGT_AGENT_ID,
36607
+ session_id: args.sessionId,
36608
+ content: buildUsageLimitReplyText(refusal.resetsAt),
36609
+ message_ids: [args.messageId]
36610
+ } : { agent_id: AGT_AGENT_ID, session_id: args.sessionId, message_ids: [args.messageId] };
36611
+ const res = await apiPost(route, body);
36612
+ const data = await res.json().catch(() => ({}));
36613
+ if (res.ok && data.error == null) {
36614
+ claimTracker.clear(args.sessionId, [args.messageId]);
36615
+ clearDirectChatPendingMarkersForSession(DIRECT_CHAT_PENDING_INBOUND_DIR, args.sessionId);
36616
+ process.stderr.write(
36617
+ `direct-chat-channel: [usage-limit-reactive] ${throttle.reply ? "reported" : "consumed (notice throttled)"} refusal for session=${args.sessionId} message=${args.messageId} resets=${refusal.resetsAt.toISOString()}
36618
+ `
36619
+ );
36620
+ } else {
36621
+ if (throttle.reply) DIRECT_CHAT_USAGE_LIMIT_CACHE.delete(throttleKey);
36622
+ process.stderr.write(
36623
+ `direct-chat-channel: [usage-limit-reactive] ${throttle.reply ? "notice" : "consume"} failed for session=${args.sessionId}: ${data.error ?? `HTTP ${res.status}`}
36624
+ `
36625
+ );
36626
+ }
36627
+ } catch (err) {
36628
+ process.stderr.write(
36629
+ `direct-chat-channel: [usage-limit-reactive] watch failed for session=${args.sessionId}: ${err.message}
36630
+ `
36631
+ );
36632
+ }
36633
+ })();
36634
+ }
36216
36635
  async function pollForMessages(sinceMs) {
36217
36636
  try {
36218
36637
  const res = await apiPost("/host/direct-chat/poll", {
@@ -36239,7 +36658,8 @@ async function pollForMessages(sinceMs) {
36239
36658
  const isNotice = msg.kind === "notice";
36240
36659
  const maintenanceActive = !isNotice && isMaintenanceModeActive();
36241
36660
  const accountMuted = !isNotice && !maintenanceActive ? readAccountEnforcementLevel({ codeName: AGT_AGENT_CODE_NAME ?? null }) === "mute" : false;
36242
- const usageLimitUntil = !isNotice && !maintenanceActive && !accountMuted ? readUsageLimitUntil({ codeName: AGT_AGENT_CODE_NAME ?? null }) : null;
36661
+ const reactiveMode = resolveUsageLimitReactiveMode();
36662
+ const usageLimitUntil = !isNotice && !maintenanceActive && !accountMuted && reactiveMode !== "enforce" ? readUsageLimitUntil({ codeName: AGT_AGENT_CODE_NAME ?? null }) : null;
36243
36663
  if (!isNotice && !maintenanceActive && !accountMuted && !usageLimitUntil) {
36244
36664
  markProcessed(msg.id);
36245
36665
  claimTracker.track(msg.session_id, msg.id);
@@ -36357,6 +36777,7 @@ async function pollForMessages(sinceMs) {
36357
36777
  if (!isNotice && msg.sender_id) {
36358
36778
  clearScheduledTurnMarker(DIRECT_CHAT_CODE_NAME_AGENT_DIR);
36359
36779
  }
36780
+ const dispatchedAtMs = Date.now();
36360
36781
  await mcp.notification({
36361
36782
  method: "notifications/claude/channel",
36362
36783
  params: {
@@ -36417,6 +36838,14 @@ async function pollForMessages(sinceMs) {
36417
36838
  `direct-chat-channel: Injected message ${msg.id} (session=${msg.session_id})
36418
36839
  `
36419
36840
  );
36841
+ if (!isNotice && reactiveMode !== "off") {
36842
+ armUsageLimitWatch({
36843
+ mode: reactiveMode,
36844
+ sessionId: msg.session_id,
36845
+ messageId: msg.id,
36846
+ sinceMs: dispatchedAtMs
36847
+ });
36848
+ }
36420
36849
  }
36421
36850
  } catch (err) {
36422
36851
  process.stderr.write(
@@ -36522,8 +36951,8 @@ function sweepAgedDirectChatMarkersNow(thresholdMs) {
36522
36951
  if (!DIRECT_CHAT_PENDING_INBOUND_DIR) return;
36523
36952
  const now = Date.now();
36524
36953
  const res = sweepAgedDirectChatMarkers(DIRECT_CHAT_PENDING_INBOUND_DIR, {
36525
- readdir: (dir) => readdirSync4(dir),
36526
- readFile: (p2) => readFileSync11(p2, "utf8"),
36954
+ readdir: (dir) => readdirSync5(dir),
36955
+ readFile: (p2) => readFileSync12(p2, "utf8"),
36527
36956
  unlink: (p2) => {
36528
36957
  if (existsSync6(p2)) unlinkSync4(p2);
36529
36958
  },
@@ -36559,7 +36988,7 @@ function sanitizeRecoveryText(text) {
36559
36988
  }
36560
36989
  function directChatRecoveryDeps() {
36561
36990
  return {
36562
- readFile: (p2) => readFileSync11(p2, "utf-8"),
36991
+ readFile: (p2) => readFileSync12(p2, "utf-8"),
36563
36992
  renameFile: (from, to) => renameSync5(from, to),
36564
36993
  unlinkFile: (p2) => {
36565
36994
  if (existsSync6(p2)) unlinkSync4(p2);
@@ -36572,7 +37001,7 @@ function directChatRecoveryDeps() {
36572
37001
  if (!DIRECT_CHAT_RECOVERY_LEDGER_DIR) return;
36573
37002
  if (markerName.includes("/") || markerName.includes("\\") || markerName.includes("..")) return;
36574
37003
  try {
36575
- const p2 = join12(DIRECT_CHAT_RECOVERY_LEDGER_DIR, markerName);
37004
+ const p2 = join13(DIRECT_CHAT_RECOVERY_LEDGER_DIR, markerName);
36576
37005
  if (existsSync6(p2)) unlinkSync4(p2);
36577
37006
  } catch {
36578
37007
  }
@@ -36608,7 +37037,7 @@ async function processDirectChatRecoveryOutboxFile(filename) {
36608
37037
  if (!enabled) return;
36609
37038
  directChatRecoveryInFlight.add(filename);
36610
37039
  try {
36611
- const fullPath = join12(DIRECT_CHAT_RECOVERY_OUTBOX_DIR, filename);
37040
+ const fullPath = join13(DIRECT_CHAT_RECOVERY_OUTBOX_DIR, filename);
36612
37041
  await consumeDirectChatRecoveryFile(fullPath, filename, directChatRecoveryDeps());
36613
37042
  } catch (err) {
36614
37043
  process.stderr.write(
@@ -36626,7 +37055,7 @@ if (DIRECT_CHAT_RECOVERY_OUTBOX_DIR) {
36626
37055
  } catch {
36627
37056
  }
36628
37057
  try {
36629
- for (const f of readdirSync4(DIRECT_CHAT_RECOVERY_OUTBOX_DIR)) {
37058
+ for (const f of readdirSync5(DIRECT_CHAT_RECOVERY_OUTBOX_DIR)) {
36630
37059
  if (f.endsWith(".json")) void processDirectChatRecoveryOutboxFile(f);
36631
37060
  }
36632
37061
  } catch {
@@ -36636,7 +37065,7 @@ if (DIRECT_CHAT_RECOVERY_OUTBOX_DIR) {
36636
37065
  if (!filename) return;
36637
37066
  const name = filename.toString();
36638
37067
  if (!name.endsWith(".json")) return;
36639
- if (existsSync6(join12(DIRECT_CHAT_RECOVERY_OUTBOX_DIR, name))) {
37068
+ if (existsSync6(join13(DIRECT_CHAT_RECOVERY_OUTBOX_DIR, name))) {
36640
37069
  void processDirectChatRecoveryOutboxFile(name);
36641
37070
  }
36642
37071
  });