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.
- package/dist/index.js +233 -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
|
],
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
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",
|
|
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
|
}
|