tines 0.0.146 → 0.0.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +233 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3560,6 +3560,9 @@ var selfRef = () => ({ kind: "self-ref" });
3560
3560
  function str(v) {
3561
3561
  return v === null || v === void 0 ? "" : String(v);
3562
3562
  }
3563
+ function isoTime(v) {
3564
+ return typeof v === "number" && Number.isFinite(v) ? new Date(v).toISOString() : "an unknown time";
3565
+ }
3563
3566
  function joinChanged(v, sep2) {
3564
3567
  return Array.isArray(v) ? v.join(sep2) : "";
3565
3568
  }
@@ -3659,6 +3662,13 @@ var DESCRIBERS = {
3659
3662
  // runner dropped after launch. The cause is in the error text.
3660
3663
  text(`fail (${str(p.consecutive_failures)} consecutive): ${str(p.error)}`)
3661
3664
  ],
3665
+ "runner.rate_limited": (_ev, p) => [
3666
+ text("saw runner"),
3667
+ name(p.runner_name),
3668
+ // A provider condition, not a failure: the runner holds itself until the
3669
+ // reported reset and resumes with nobody in the loop.
3670
+ text(`hit its usage limit \u2014 resumes ${isoTime(p.resets_at)}: ${str(p.error)}`)
3671
+ ],
3662
3672
  "routing_rule.created": (ev, p) => [
3663
3673
  text(`${action(ev.type)} the ${str(p.scope_label)} routing rule`)
3664
3674
  ],
@@ -4039,6 +4049,8 @@ function createApiClient(options) {
4039
4049
  updateRoutingRule: (id, body) => request("PATCH", `/api/v1/routing-rules/${id}`, body),
4040
4050
  deleteRoutingRule: (id) => request("DELETE", `/api/v1/routing-rules/${id}`),
4041
4051
  // Supervisor settings
4052
+ getPreferences: () => get("/api/v1/preferences"),
4053
+ updatePreferences: (body) => request("PATCH", "/api/v1/preferences", body),
4042
4054
  getSupervisorSettings: () => get("/api/v1/supervisor/settings"),
4043
4055
  getSupervisorQueue: () => get("/api/v1/supervisor/queue"),
4044
4056
  updateSupervisorSettings: (body) => request("PUT", "/api/v1/supervisor/settings", body),
@@ -4227,6 +4239,8 @@ function scheduleRef(s) {
4227
4239
  }
4228
4240
  function runnerStatusLabel(runner) {
4229
4241
  if (runner.status === "paused") return "paused";
4242
+ if (runner.backoff_reason === "rate_limit" && (runner.backoff_until ?? 0) > Date.now())
4243
+ return "rate limited";
4230
4244
  return runner.online ? "online" : "offline";
4231
4245
  }
4232
4246
  function ruleTargetsLabel(rule) {
@@ -6725,15 +6739,27 @@ function renderStreamEvent(event) {
6725
6739
  if (event.is_error && event.result) lines.push(`[error] ${clip(event.result, 2e3)}`);
6726
6740
  return lines;
6727
6741
  }
6742
+ case "rate_limit_event": {
6743
+ const info = event.rate_limit_info;
6744
+ if (info?.status !== "rejected") return [];
6745
+ const resets = typeof info.resetsAt === "number" && Number.isFinite(info.resetsAt) ? new Date(info.resetsAt < 1e12 ? info.resetsAt * 1e3 : info.resetsAt).toISOString() : "unknown";
6746
+ return [`[session] rate limit: ${info.rateLimitType ?? "usage"} rejected \u2014 resets ${resets}`];
6747
+ }
6728
6748
  default:
6729
6749
  return [];
6730
6750
  }
6731
6751
  }
6732
6752
  var ClaudeStreamRenderer = class {
6733
- constructor(emit) {
6753
+ /**
6754
+ * @param onEvent every successfully parsed event, before rendering — so the
6755
+ * rate-limit detector can read the stream without parsing it a second time.
6756
+ */
6757
+ constructor(emit, onEvent) {
6734
6758
  this.emit = emit;
6759
+ this.onEvent = onEvent;
6735
6760
  }
6736
6761
  emit;
6762
+ onEvent;
6737
6763
  pending = "";
6738
6764
  write(chunk) {
6739
6765
  this.pending += chunk;
@@ -6767,11 +6793,172 @@ var ClaudeStreamRenderer = class {
6767
6793
  `);
6768
6794
  return;
6769
6795
  }
6796
+ this.onEvent?.(event);
6770
6797
  for (const line of renderStreamEvent(event)) this.emit(`${line}
6771
6798
  `);
6772
6799
  }
6773
6800
  };
6774
6801
 
6802
+ // src/daemon/rate-limit.ts
6803
+ var USAGE_LIMIT_PREFIXES = [
6804
+ "You've hit your",
6805
+ "You've reached your",
6806
+ "You're out of usage credits",
6807
+ "Your org is out of usage",
6808
+ "Your seat type doesn't include usage credits",
6809
+ "Your usage allocation has been disabled by your admin",
6810
+ "Your group's usage limit is set to $0",
6811
+ "You're out of extra usage"
6812
+ ];
6813
+ var USAGE_LIMIT_PATTERN = /^Fable(?: [^·\n]{1,40})? requires usage credits\./;
6814
+ var ANSI = /\x1b\[[0-9;]*m/g;
6815
+ function toEpochMs(value) {
6816
+ return value < 1e12 ? Math.round(value * 1e3) : Math.round(value);
6817
+ }
6818
+ function rateLimitFromStreamEvent(event) {
6819
+ if (typeof event !== "object" || event === null) return null;
6820
+ const ev = event;
6821
+ if (ev.type !== "rate_limit_event") return null;
6822
+ const info = ev.rate_limit_info;
6823
+ if (typeof info !== "object" || info === null) return null;
6824
+ const { status, resetsAt, rateLimitType } = info;
6825
+ if (status !== "rejected") return null;
6826
+ const limit = typeof rateLimitType === "string" && rateLimitType ? rateLimitType : null;
6827
+ const resumeAt = typeof resetsAt === "number" && Number.isFinite(resetsAt) && resetsAt > 0 ? toEpochMs(resetsAt) : null;
6828
+ return {
6829
+ resumeAt,
6830
+ source: "stream",
6831
+ limit,
6832
+ detail: `${limit ?? "usage"} limit rejected${resumeAt === null ? "" : ` \xB7 resets ${new Date(resumeAt).toISOString()}`}`
6833
+ };
6834
+ }
6835
+ function rateLimitFromStderrLine(line, now) {
6836
+ const clean = line.replace(ANSI, "").trim();
6837
+ if (!clean) return null;
6838
+ const matches = USAGE_LIMIT_PREFIXES.some((prefix) => clean.startsWith(prefix)) || USAGE_LIMIT_PATTERN.test(clean);
6839
+ if (!matches) return null;
6840
+ const [, reset] = clean.split("\xB7");
6841
+ const resumeAt = reset ? parseResetTime(reset.replace(/^\s*resets\s*/i, ""), now) : null;
6842
+ return { resumeAt, source: "stderr", limit: null, detail: clean };
6843
+ }
6844
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
6845
+ function zoneOffsetMs(zone, at) {
6846
+ const parts = new Intl.DateTimeFormat("en-US", {
6847
+ timeZone: zone,
6848
+ hour12: false,
6849
+ year: "numeric",
6850
+ month: "2-digit",
6851
+ day: "2-digit",
6852
+ hour: "2-digit",
6853
+ minute: "2-digit",
6854
+ second: "2-digit"
6855
+ }).formatToParts(new Date(at));
6856
+ const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
6857
+ const hour = get("hour") % 24;
6858
+ const asUtc = Date.UTC(
6859
+ get("year"),
6860
+ get("month") - 1,
6861
+ get("day"),
6862
+ hour,
6863
+ get("minute"),
6864
+ get("second")
6865
+ );
6866
+ return asUtc - Math.floor(at / 1e3) * 1e3;
6867
+ }
6868
+ function zonedTimeToEpoch(zone, y, m, d, hh, mm) {
6869
+ const naive = Date.UTC(y, m, d, hh, mm, 0);
6870
+ let guess = naive - zoneOffsetMs(zone, naive);
6871
+ guess = naive - zoneOffsetMs(zone, guess);
6872
+ return guess;
6873
+ }
6874
+ function zonedDate(zone, at) {
6875
+ const parts = new Intl.DateTimeFormat("en-US", {
6876
+ timeZone: zone,
6877
+ year: "numeric",
6878
+ month: "2-digit",
6879
+ day: "2-digit"
6880
+ }).formatToParts(new Date(at));
6881
+ const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
6882
+ return { y: get("year"), m: get("month") - 1, d: get("day") };
6883
+ }
6884
+ var DURATION = /(\d+)\s*([dhms])/g;
6885
+ function parseResetTime(text2, now) {
6886
+ const raw = text2.trim();
6887
+ if (!raw) return null;
6888
+ const relative = raw.match(/^in\s+(.+)$/i);
6889
+ if (relative) {
6890
+ let ms = 0;
6891
+ let matched = false;
6892
+ for (const [, amount, unit] of relative[1].matchAll(DURATION)) {
6893
+ const n = Number(amount);
6894
+ ms += n * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit];
6895
+ matched = true;
6896
+ }
6897
+ return matched ? now + ms : null;
6898
+ }
6899
+ const zoned = raw.match(/^(.*?)\s*\(([A-Za-z_]+\/[A-Za-z_+-]+|UTC)\)$/);
6900
+ if (!zoned) return null;
6901
+ const [, head, zone] = zoned;
6902
+ try {
6903
+ zoneOffsetMs(zone, now);
6904
+ } catch {
6905
+ return null;
6906
+ }
6907
+ const clock = head.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/i);
6908
+ if (!clock) return null;
6909
+ const meridiem = clock[3].toLowerCase();
6910
+ const hour12 = Number(clock[1]) % 12;
6911
+ const hh = meridiem === "pm" ? hour12 + 12 : hour12;
6912
+ const mm = Number(clock[2] ?? "0");
6913
+ const dated = head.match(/^([A-Za-z]{3})[a-z]*\s+(\d{1,2})(?:,\s*(\d{4}))?,/);
6914
+ if (dated) {
6915
+ const month = MONTHS.indexOf(dated[1].toLowerCase());
6916
+ if (month === -1) return null;
6917
+ const here = zonedDate(zone, now);
6918
+ const year = dated[3] ? Number(dated[3]) : here.y;
6919
+ let at2 = zonedTimeToEpoch(zone, year, month, Number(dated[2]), hh, mm);
6920
+ if (!dated[3] && at2 < now - 864e5)
6921
+ at2 = zonedTimeToEpoch(zone, year + 1, month, Number(dated[2]), hh, mm);
6922
+ return at2;
6923
+ }
6924
+ const today = zonedDate(zone, now);
6925
+ const at = zonedTimeToEpoch(zone, today.y, today.m, today.d, hh, mm);
6926
+ return at > now ? at : zonedTimeToEpoch(zone, today.y, today.m, today.d + 1, hh, mm);
6927
+ }
6928
+ var RateLimitDetector = class {
6929
+ stream = null;
6930
+ stderr = null;
6931
+ pending = "";
6932
+ noteStreamEvent(event) {
6933
+ const signal = rateLimitFromStreamEvent(event);
6934
+ if (signal) this.stream = signal;
6935
+ }
6936
+ noteStderr(chunk, now = Date.now()) {
6937
+ this.pending += chunk;
6938
+ let nl = this.pending.indexOf("\n");
6939
+ while (nl !== -1) {
6940
+ this.line(this.pending.slice(0, nl), now);
6941
+ this.pending = this.pending.slice(nl + 1);
6942
+ nl = this.pending.indexOf("\n");
6943
+ }
6944
+ }
6945
+ /** Flushes a trailing partial line. Call once the harness has exited. */
6946
+ finish(now = Date.now()) {
6947
+ if (this.pending) {
6948
+ this.line(this.pending, now);
6949
+ this.pending = "";
6950
+ }
6951
+ }
6952
+ /** The structured signal when there is one; the prose only as a fallback. */
6953
+ signal() {
6954
+ return this.stream ?? this.stderr;
6955
+ }
6956
+ line(raw, now) {
6957
+ const signal = rateLimitFromStderrLine(raw, now);
6958
+ if (signal) this.stderr = signal;
6959
+ }
6960
+ };
6961
+
6775
6962
  // src/daemon/cli-refresh.ts
6776
6963
  import { spawn } from "node:child_process";
6777
6964
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
@@ -6901,11 +7088,13 @@ var RunTable = class {
6901
7088
  * this still cleans up — the slot, workspace, and state entry must never
6902
7089
  * outlive the run — but reports nothing.
6903
7090
  *
6904
- * Pass `judgment: 'interrupted'` when the daemon itself ended the run
7091
+ * Pass `{ judgment: 'interrupted' }` when the daemon itself ended the run
6905
7092
  * (shutdown, restart) rather than the work failing: the supervisor then
6906
7093
  * spares the issue a strike. Everything a run can do wrong to itself —
6907
7094
  * a non-zero harness exit, a workspace that would not set up — must not
6908
- * set it.
7095
+ * set it. The one exception is `{ judgment: 'rate_limited' }`: a non-zero
7096
+ * exit whose cause was the provider refusing on a usage limit is the
7097
+ * runner's condition, not the work's.
6909
7098
  */
6910
7099
  async finishAndCleanup(run, status, error, judgment) {
6911
7100
  if (run.settled) return this.cleanup(run);
@@ -7442,7 +7631,7 @@ async function runDaemon(opts) {
7442
7631
  await client2.finishRun(run.runId, {
7443
7632
  status,
7444
7633
  ...error ? { error } : {},
7445
- ...judgment ? { judgment } : {}
7634
+ ...judgment
7446
7635
  });
7447
7636
  },
7448
7637
  release: (run, { keep, outcome }) => {
@@ -7615,7 +7804,12 @@ async function runDaemon(opts) {
7615
7804
  table2.persist();
7616
7805
  log(`run ${runId}: launched ${invocation.file} (pid ${child.pid})`);
7617
7806
  if (opts.harness === "claude_code") {
7618
- const renderer = new ClaudeStreamRenderer((line) => run.batcher.append(line));
7807
+ const limiter = new RateLimitDetector();
7808
+ run.limiter = limiter;
7809
+ const renderer = new ClaudeStreamRenderer(
7810
+ (line) => run.batcher.append(line),
7811
+ (event) => limiter.noteStreamEvent(event)
7812
+ );
7619
7813
  run.renderer = renderer;
7620
7814
  run.drain = () => renderer.finish();
7621
7815
  const spoolPath = join5(opts.configDir, "rawlogs", `${runId}.ndjson`);
@@ -7630,7 +7824,11 @@ async function runDaemon(opts) {
7630
7824
  } else {
7631
7825
  child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
7632
7826
  }
7633
- child.stderr?.on("data", (data) => run.batcher.append(data.toString("utf8")));
7827
+ child.stderr?.on("data", (data) => {
7828
+ const text2 = data.toString("utf8");
7829
+ run.batcher.append(text2);
7830
+ run.limiter?.noteStderr(text2);
7831
+ });
7634
7832
  run.timeout = setTimeout(() => {
7635
7833
  if (run.settled) return;
7636
7834
  log(`run ${runId} hit its ${assignment.timeout_minutes}m timeout; killing`);
@@ -7643,6 +7841,7 @@ async function runDaemon(opts) {
7643
7841
  });
7644
7842
  child.on("exit", (code, signal) => {
7645
7843
  run.drain?.();
7844
+ run.limiter?.finish();
7646
7845
  const closing = exitLineForRun(run, {
7647
7846
  code,
7648
7847
  signal,
@@ -7658,11 +7857,22 @@ async function runDaemon(opts) {
7658
7857
  } else if (code === 0) {
7659
7858
  void table2.finishAndCleanup(run, "completed");
7660
7859
  } else {
7661
- void table2.finishAndCleanup(
7662
- run,
7663
- "failed",
7664
- signal ? `harness killed by ${signal}` : `harness exited with code ${code}`
7665
- );
7860
+ const limited = signal ? null : run.limiter?.signal() ?? null;
7861
+ if (limited) {
7862
+ log(
7863
+ `run ${runId}: harness rate limited (${limited.detail}); reporting without a strike`
7864
+ );
7865
+ void table2.finishAndCleanup(run, "failed", `rate limited: ${limited.detail}`, {
7866
+ judgment: "rate_limited",
7867
+ ...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
7868
+ });
7869
+ } else {
7870
+ void table2.finishAndCleanup(
7871
+ run,
7872
+ "failed",
7873
+ signal ? `harness killed by ${signal}` : `harness exited with code ${code}`
7874
+ );
7875
+ }
7666
7876
  }
7667
7877
  });
7668
7878
  } catch (err) {
@@ -7676,7 +7886,9 @@ async function runDaemon(opts) {
7676
7886
  await Promise.all(
7677
7887
  table2.values().map(async (run) => {
7678
7888
  if (run.child?.pid) killTree(run.child.pid, "SIGTERM");
7679
- await table2.finishAndCleanup(run, "failed", "daemon shut down", "interrupted");
7889
+ await table2.finishAndCleanup(run, "failed", "daemon shut down", {
7890
+ judgment: "interrupted"
7891
+ });
7680
7892
  })
7681
7893
  );
7682
7894
  process.exit(0);
@@ -7815,6 +8027,11 @@ function register8(program3) {
7815
8027
  `active runs: ${runner.active_runs}/${runner.max_concurrent} timeout: ${runner.max_run_minutes}m default tier: ${runner.default_tier}`
7816
8028
  );
7817
8029
  if (runner.last_seen_at) console.log(`last seen: ${timestamp(runner.last_seen_at)}`);
8030
+ if (runner.backoff_reason === "rate_limit" && runner.backoff_until) {
8031
+ console.log(
8032
+ `rate limited: the harness account is out of usage; resumes ${timestamp(runner.backoff_until)}`
8033
+ );
8034
+ }
7818
8035
  if (runner.launch_failures > 0) {
7819
8036
  console.log(
7820
8037
  `consecutive failures: ${runner.launch_failures}${runner.backoff_until ? ` (backing off until ${timestamp(runner.backoff_until)})` : ""}`
@@ -8416,6 +8633,8 @@ function queueHeadline(block) {
8416
8633
  return `${runner} draining`;
8417
8634
  case "backing_off":
8418
8635
  return `${runner} backing off`;
8636
+ case "rate_limited":
8637
+ return `${runner} rate limited`;
8419
8638
  case "no_rule":
8420
8639
  return "no matching routing rule";
8421
8640
  case "no_targets":
@@ -8452,6 +8671,8 @@ function queueFix(block) {
8452
8671
  return "tines supervisor enable";
8453
8672
  case "backing_off":
8454
8673
  return "retries automatically; check the daemon log if it keeps failing";
8674
+ case "rate_limited":
8675
+ return "resumes automatically when the usage window resets; nothing to do";
8455
8676
  default:
8456
8677
  return null;
8457
8678
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.146",
3
+ "version": "0.0.148",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",