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