teapot-coding-agent 0.37.59 → 0.37.61
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/acp/adapter.js +4 -72
- package/dist/agent/agent.js +61 -2247
- package/dist/agent/compact-rules.js +0 -53
- package/dist/agent/editorconfig.js +4 -56
- package/dist/agent/goal-audit.js +0 -34
- package/dist/agent/key-pool.js +0 -52
- package/dist/agent/kill.js +1 -13
- package/dist/agent/llm.js +9 -310
- package/dist/agent/mcp-oauth-manager.js +2 -255
- package/dist/agent/mcp-registry.js +1 -188
- package/dist/agent/mcp.js +1 -85
- package/dist/agent/meta-tools.js +0 -26
- package/dist/agent/plugins.js +1 -81
- package/dist/agent/provider-headers.js +1 -28
- package/dist/agent/skills.js +8 -94
- package/dist/agent/tools.js +16 -809
- package/dist/bus.js +1 -2
- package/dist/config-schema.js +0 -40
- package/dist/home.js +1 -19
- package/dist/index.js +0 -136
- package/dist/log/events.js +3 -195
- package/dist/master.js +31 -1180
- package/dist/migrations/index.js +0 -55
- package/dist/migrations/providers.js +0 -13
- package/dist/migrations/registry.js +0 -9
- package/dist/migrations/session-titles.js +3 -43
- package/dist/migrations/sessions.js +2 -20
- package/dist/model-meta.js +166 -142
- package/dist/scheduler/cron.js +2 -17
- package/dist/server/api.js +12 -778
- package/dist/server/image-resize.js +1 -64
- package/dist/server/term-size.js +1 -120
- package/dist/server/terminals.js +0 -159
- package/dist/session-title.js +2 -35
- package/package.json +1 -1
- package/public/assets/{index-KzHw4Sqo.js → index-C1eqcSbW.js} +4 -4
- package/public/assets/index-C1eqcSbW.js.map +1 -0
- package/public/assets/{shiki-BwDjoZdu.js → shiki-DaLn_bCe.js} +2 -2
- package/public/assets/shiki-DaLn_bCe.js.map +1 -0
- package/public/index.html +1 -1
- package/public/assets/index-KzHw4Sqo.js.map +0 -1
- package/public/assets/shiki-BwDjoZdu.js.map +0 -1
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keep the operator's hard rules alive across a compaction.
|
|
3
|
-
*
|
|
4
|
-
* ## Why this is not just another summary section
|
|
5
|
-
*
|
|
6
|
-
* A compaction summary is generated text: everything in it was re-derived by a model from the
|
|
7
|
-
* conversation it was shown. That is fine for facts, because facts can be re-checked against the
|
|
8
|
-
* code. A RULE cannot — "never install this into Wine", "do not clone repositories to study them"
|
|
9
|
-
* were each stated once, obeyed silently for dozens of turns, and are not repeated anywhere the
|
|
10
|
-
* next pass could find them. When the summary drops one, nothing downstream notices; the agent
|
|
11
|
-
* just starts doing it.
|
|
12
|
-
*
|
|
13
|
-
* So the rules get a section with a SHAPE the code can check (`MUST` / `MUST NOT` / `SHOULD` at the
|
|
14
|
-
* start of each line), extracted on the way out and RE-INSERTED on the way in if a pass lost them.
|
|
15
|
-
* The summarizer is still the thing that decides what the rules ARE; this module only makes sure
|
|
16
|
-
* that once known they cannot disappear.
|
|
17
|
-
*
|
|
18
|
-
* ## Why the shape matters more than the wording
|
|
19
|
-
*
|
|
20
|
-
* A free-form section lets a model merge three rules into one sentence, which is how a `MUST NOT`
|
|
21
|
-
* turns into a `SHOULD avoid` and then into nothing. A fixed keyword per line keeps each rule a
|
|
22
|
-
* separate, checkable item. `should` is included deliberately — the operator asked for it — but it
|
|
23
|
-
* is ranked below the musts so a squeeze drops the weakest first.
|
|
24
|
-
*/
|
|
25
1
|
export const RULES_HEADING = "## Hard constraints";
|
|
26
|
-
/** the levels, strongest first — the order a squeeze must respect */
|
|
27
2
|
const LEVELS = ["MUST NOT", "MUST", "SHOULD"];
|
|
28
|
-
/**
|
|
29
|
-
* Parse the `## Hard constraints` section out of a summary.
|
|
30
|
-
*
|
|
31
|
-
* Tolerant on purpose: the summarizer is asked for a shape, not guaranteed to produce it. A line
|
|
32
|
-
* that does not start with a keyword is ignored rather than guessed at, because inventing a rule
|
|
33
|
-
* from prose is worse than losing one — a wrong rule is obeyed.
|
|
34
|
-
*/
|
|
35
3
|
export function extractRules(summary) {
|
|
36
4
|
const lines = summary.split("\n");
|
|
37
5
|
const start = lines.findIndex((l) => l.trim().toLowerCase() === RULES_HEADING.toLowerCase());
|
|
@@ -41,13 +9,11 @@ export function extractRules(summary) {
|
|
|
41
9
|
const seen = new Set();
|
|
42
10
|
for (let i = start + 1; i < lines.length; i++) {
|
|
43
11
|
const raw = lines[i];
|
|
44
|
-
// a new `##` heading ends the section
|
|
45
12
|
if (/^#{1,6}\s/.test(raw.trim()))
|
|
46
13
|
break;
|
|
47
14
|
const line = raw.replace(/^\s*[-*+]\s*/, "").trim();
|
|
48
15
|
if (!line)
|
|
49
16
|
continue;
|
|
50
|
-
// longest keyword first, or `MUST NOT` parses as `MUST` + "NOT …"
|
|
51
17
|
const m = /^(MUST NOT|MUST|SHOULD)\b[:\s]*(.*)$/i.exec(line);
|
|
52
18
|
if (!m)
|
|
53
19
|
continue;
|
|
@@ -55,7 +21,6 @@ export function extractRules(summary) {
|
|
|
55
21
|
const text = m[2].trim();
|
|
56
22
|
if (!text)
|
|
57
23
|
continue;
|
|
58
|
-
// de-duplicate on the normalised text: two paraphrases of one rule is one rule
|
|
59
24
|
const key = text.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/\s+/g, " ").trim();
|
|
60
25
|
if (seen.has(key))
|
|
61
26
|
continue;
|
|
@@ -64,7 +29,6 @@ export function extractRules(summary) {
|
|
|
64
29
|
}
|
|
65
30
|
return out;
|
|
66
31
|
}
|
|
67
|
-
/** the section body for a set of rules, in strongest-first order */
|
|
68
32
|
export function renderRules(rules) {
|
|
69
33
|
const rank = (r) => LEVELS.indexOf(r.level);
|
|
70
34
|
return [...rules]
|
|
@@ -72,16 +36,6 @@ export function renderRules(rules) {
|
|
|
72
36
|
.map((r) => `- ${r.level} ${r.text}`)
|
|
73
37
|
.join("\n");
|
|
74
38
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Re-insert rules a compaction pass dropped.
|
|
77
|
-
*
|
|
78
|
-
* `before` is what was known going in, `after` is the section the new summary produced. Anything in
|
|
79
|
-
* `before` whose text is not present in `after` is appended (strongest first) so a pass can add
|
|
80
|
-
* rules and reword them, but can never lose one. Returns the summary with the section rewritten.
|
|
81
|
-
*
|
|
82
|
-
* Rewriting rather than appending is what makes this idempotent: running it twice on the same
|
|
83
|
-
* summary produces the same summary, which matters because the update path compacts repeatedly.
|
|
84
|
-
*/
|
|
85
39
|
export function enforceRules(summary, before) {
|
|
86
40
|
const after = extractRules(summary);
|
|
87
41
|
const present = new Set(after.map((r) => r.text.toLowerCase().replace(/[^a-z0-9 ]+/g, "").replace(/\s+/g, " ").trim()));
|
|
@@ -95,7 +49,6 @@ export function enforceRules(summary, before) {
|
|
|
95
49
|
const lines = summary.split("\n");
|
|
96
50
|
const start = lines.findIndex((l) => l.trim().toLowerCase() === RULES_HEADING.toLowerCase());
|
|
97
51
|
if (start < 0) {
|
|
98
|
-
// no section at all — append one rather than silently keeping nothing
|
|
99
52
|
return { summary: `${summary.trimEnd()}\n\n${section}`, restored };
|
|
100
53
|
}
|
|
101
54
|
let end = lines.length;
|
|
@@ -110,12 +63,6 @@ export function enforceRules(summary, before) {
|
|
|
110
63
|
restored,
|
|
111
64
|
};
|
|
112
65
|
}
|
|
113
|
-
/**
|
|
114
|
-
* The rules as a line the summarizer is asked to keep, for the UPDATE path.
|
|
115
|
-
*
|
|
116
|
-
* Passed as part of the prior checkpoint so a pass that is told "preserve what is still true" can
|
|
117
|
-
* see the rules explicitly instead of having to find them in prose.
|
|
118
|
-
*/
|
|
119
66
|
export function rulesForPrompt(rules) {
|
|
120
67
|
if (!rules.length)
|
|
121
68
|
return "";
|
|
@@ -1,52 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `.editorconfig` support (#109) — "edit_file の改行コードを .editorconfig に合わせて
|
|
3
|
-
* 正規化するとか?".
|
|
4
|
-
*
|
|
5
|
-
* ## What this DOES and deliberately does NOT do
|
|
6
|
-
*
|
|
7
|
-
* An EXISTING file already keeps its own line ending (#84, `detectEol`): the file's
|
|
8
|
-
* own bytes are the strongest possible evidence, and a `.editorconfig` that
|
|
9
|
-
* disagrees with the file on disk is nearly always stale relative to it. So this
|
|
10
|
-
* only supplies an answer where there was NONE:
|
|
11
|
-
*
|
|
12
|
-
* - a NEW file (nothing to detect), and
|
|
13
|
-
* - a MIXED-EOL file, where `detectEol` returns null on purpose because choosing
|
|
14
|
-
* a winner would be a silent whole-file rewrite.
|
|
15
|
-
*
|
|
16
|
-
* That keeps one rule: **the file's own content outranks configuration, which
|
|
17
|
-
* outranks the LF default.** `end_of_line = crlf` in a repo whose files are LF does
|
|
18
|
-
* not silently convert every file the agent touches.
|
|
19
|
-
*
|
|
20
|
-
* ## Why this is not a general editorconfig library
|
|
21
|
-
*
|
|
22
|
-
* The format has glob sections, brace expansion and a precedence order. Only the
|
|
23
|
-
* facts that change how a WRITE behaves are read — today that is `end_of_line`
|
|
24
|
-
* (plus `insert_final_newline`, which decides whether a new file ends in a
|
|
25
|
-
* newline). Anything else (`indent_style`, `charset`, …) has no in-repo consumer,
|
|
26
|
-
* and guessing at it would be untested surface.
|
|
27
|
-
*/
|
|
28
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
29
2
|
import path from "node:path";
|
|
30
3
|
const NONE = { eol: null };
|
|
31
|
-
/**
|
|
32
|
-
* Read the `.editorconfig` files that apply to `targetPath`, nearest-wins.
|
|
33
|
-
*
|
|
34
|
-
* Walks from the file's own directory up to the filesystem root so a nested
|
|
35
|
-
* `.editorconfig` overrides the repo root, which is the documented precedence.
|
|
36
|
-
* Sections are matched against the path RELATIVE to the file that declared them.
|
|
37
|
-
* A `root = true` stops the walk, as the format requires.
|
|
38
|
-
*/
|
|
39
4
|
export function editorConfigFor(targetPath) {
|
|
40
5
|
const dirs = [];
|
|
41
6
|
let dir = path.dirname(path.resolve(targetPath));
|
|
42
7
|
for (;;) {
|
|
43
|
-
dirs.push(dir);
|
|
8
|
+
dirs.push(dir);
|
|
44
9
|
const parent = path.dirname(dir);
|
|
45
10
|
if (parent === dir)
|
|
46
11
|
break;
|
|
47
12
|
dir = parent;
|
|
48
13
|
}
|
|
49
|
-
// nearest-first, so the FIRST answer found wins; `root = true` truncates the walk
|
|
50
14
|
const applicable = [];
|
|
51
15
|
for (const d of dirs) {
|
|
52
16
|
const file = path.join(d, ".editorconfig");
|
|
@@ -61,9 +25,8 @@ export function editorConfigFor(targetPath) {
|
|
|
61
25
|
}
|
|
62
26
|
applicable.push({ dir: d, text });
|
|
63
27
|
if (/^\s*root\s*=\s*true\s*$/im.test(text))
|
|
64
|
-
break;
|
|
28
|
+
break;
|
|
65
29
|
}
|
|
66
|
-
// apply outermost -> innermost so the nearest file's value lands last
|
|
67
30
|
let out = NONE;
|
|
68
31
|
for (const { dir: declDir, text } of applicable.reverse()) {
|
|
69
32
|
const rel = path.relative(declDir, path.resolve(targetPath)).split(path.sep).join("/");
|
|
@@ -86,7 +49,6 @@ export function editorConfigFor(targetPath) {
|
|
|
86
49
|
}
|
|
87
50
|
return out;
|
|
88
51
|
}
|
|
89
|
-
/** `[section]` blocks with their `key = value` pairs. Comments and blanks ignored. */
|
|
90
52
|
function parseSections(text) {
|
|
91
53
|
const sections = [];
|
|
92
54
|
let current = null;
|
|
@@ -101,26 +63,17 @@ function parseSections(text) {
|
|
|
101
63
|
continue;
|
|
102
64
|
}
|
|
103
65
|
if (!current)
|
|
104
|
-
continue;
|
|
66
|
+
continue;
|
|
105
67
|
const eq = line.indexOf("=");
|
|
106
68
|
if (eq === -1)
|
|
107
69
|
continue;
|
|
108
70
|
const key = line.slice(0, eq).trim().toLowerCase();
|
|
109
71
|
const value = line.slice(eq + 1).trim().toLowerCase();
|
|
110
|
-
// the FIRST value wins within a section (the format's rule)
|
|
111
72
|
if (key && value && !current.props.has(key))
|
|
112
73
|
current.props.set(key, value);
|
|
113
74
|
}
|
|
114
75
|
return sections;
|
|
115
76
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Does an editorconfig glob match a POSIX-style relative path?
|
|
118
|
-
*
|
|
119
|
-
* Supports the parts that appear in real configs: `*` (within a segment), `**`
|
|
120
|
-
* (across segments), `?`, `{a,b}` alternation, and a `[abc]` class. A section with
|
|
121
|
-
* no `/` matches the BASENAME at any depth, which is the format's documented special
|
|
122
|
-
* case (and why `[*.md]` in a root file applies to `docs/x.md`).
|
|
123
|
-
*/
|
|
124
77
|
export function sectionMatches(pattern, relPath) {
|
|
125
78
|
const anchored = pattern.includes("/");
|
|
126
79
|
const names = anchored ? [relPath] : [relPath.split("/").pop() ?? ""];
|
|
@@ -128,14 +81,12 @@ export function sectionMatches(pattern, relPath) {
|
|
|
128
81
|
const re = globToRegExp(body, anchored);
|
|
129
82
|
return names.some((n) => re.test(n));
|
|
130
83
|
}
|
|
131
|
-
/** Translate an editorconfig glob into a RegExp. */
|
|
132
84
|
function globToRegExp(glob, anchored) {
|
|
133
85
|
let out = "";
|
|
134
86
|
for (let i = 0; i < glob.length; i++) {
|
|
135
87
|
const c = glob[i];
|
|
136
88
|
if (c === "*") {
|
|
137
89
|
if (glob[i + 1] === "*") {
|
|
138
|
-
// `**/` matches zero or more leading segments; a bare `**` is `.*`
|
|
139
90
|
if (glob[i + 2] === "/") {
|
|
140
91
|
out += "(?:.*/)?";
|
|
141
92
|
i += 2;
|
|
@@ -146,7 +97,7 @@ function globToRegExp(glob, anchored) {
|
|
|
146
97
|
}
|
|
147
98
|
}
|
|
148
99
|
else {
|
|
149
|
-
out += "[^/]*";
|
|
100
|
+
out += "[^/]*";
|
|
150
101
|
}
|
|
151
102
|
continue;
|
|
152
103
|
}
|
|
@@ -169,7 +120,6 @@ function globToRegExp(glob, anchored) {
|
|
|
169
120
|
if (c === "[") {
|
|
170
121
|
const close = glob.indexOf("]", i);
|
|
171
122
|
if (close !== -1) {
|
|
172
|
-
// a character class is passed through as-is, minus any escaping surprises
|
|
173
123
|
out += glob.slice(i, close + 1).replace(/[\\^]/g, "\\$&");
|
|
174
124
|
i = close;
|
|
175
125
|
continue;
|
|
@@ -177,7 +127,5 @@ function globToRegExp(glob, anchored) {
|
|
|
177
127
|
}
|
|
178
128
|
out += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
129
|
}
|
|
180
|
-
// anchored patterns are relative to the config's directory; a basename pattern
|
|
181
|
-
// already matched against the basename, so neither needs a prefix
|
|
182
130
|
return new RegExp(`^${out}$`, anchored ? "" : "");
|
|
183
131
|
}
|
package/dist/agent/goal-audit.js
CHANGED
|
@@ -1,26 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Completion-audit verdict parsing and the auditor prompt.
|
|
3
|
-
*
|
|
4
|
-
* The auditor is an INDEPENDENT reviewer: a separate LLM call that decides whether the worker's
|
|
5
|
-
* `finish(goalComplete=true)` is honest against the goal's verification contract. Its answer must
|
|
6
|
-
* be unambiguous, because the two readings are not symmetric — a missed approval costs one more
|
|
7
|
-
* round, while a misread rejection sends the worker to fix gaps that nobody named.
|
|
8
|
-
*/
|
|
9
|
-
/** provider fallbacks for a message with no usable text */
|
|
10
1
|
const PLACEHOLDER = /^\((tool call|no content|no output)\)$/i;
|
|
11
|
-
/**
|
|
12
|
-
* Read the auditor's verdict.
|
|
13
|
-
*
|
|
14
|
-
* The verdict must be the LAST non-empty line, and it must carry a reason. Upstream pi-goal-x made
|
|
15
|
-
* this exact correction: matching the marker ANYWHERE lets a prose mention of it be read as the
|
|
16
|
-
* verdict — "I would only approve if the tests passed" is a REJECTION that contains the word.
|
|
17
|
-
*
|
|
18
|
-
* MEASURED against the real failure: an auditor that answered `Independent verification against …`
|
|
19
|
-
* (prose, no marker) was scored as a verdict three times in a row, hit the unavailable cap, and the
|
|
20
|
-
* goal was abandoned with the audit never actually running. Requiring the marker on the last line
|
|
21
|
-
* turns that into an honest "no verdict", which the caller already handles by retrying or failing
|
|
22
|
-
* closed with a loud note.
|
|
23
|
-
*/
|
|
24
2
|
export function parseAuditVerdict(reply) {
|
|
25
3
|
const text = String(reply ?? "").trim();
|
|
26
4
|
if (!text || PLACEHOLDER.test(text))
|
|
@@ -29,7 +7,6 @@ export function parseAuditVerdict(reply) {
|
|
|
29
7
|
.split("\n")
|
|
30
8
|
.map((l) => l.trim())
|
|
31
9
|
.filter(Boolean);
|
|
32
|
-
// the verdict line is the LAST one; a reason may wrap onto earlier lines
|
|
33
10
|
const last = lines[lines.length - 1] ?? "";
|
|
34
11
|
const approved = /^APPROVED\b(:|\s-|$)(.*)$/i.exec(last);
|
|
35
12
|
if (approved) {
|
|
@@ -39,19 +16,8 @@ export function parseAuditVerdict(reply) {
|
|
|
39
16
|
if (rejected) {
|
|
40
17
|
return { kind: "changes-required", reason: (rejected[2] ?? "").trim() || "(no gaps named)" };
|
|
41
18
|
}
|
|
42
|
-
/**
|
|
43
|
-
* No marker on the last line. The reason still matters — it is shown to the worker when this
|
|
44
|
-
* happens repeatedly — so keep the tail rather than discarding the reply.
|
|
45
|
-
*/
|
|
46
19
|
return { kind: "none", detail: `auditor reply had no verdict on its last line: ${last.slice(0, 200)}` };
|
|
47
20
|
}
|
|
48
|
-
/**
|
|
49
|
-
* The auditor's instructions.
|
|
50
|
-
*
|
|
51
|
-
* Deliberately NOT the worker's conversation: this call is a review, and handing the auditor a
|
|
52
|
-
* 200k-token work log makes it continue the work instead of judging it. MEASURED: three audits in
|
|
53
|
-
* one session replied with prose that started re-verifying the goal rather than naming a verdict.
|
|
54
|
-
*/
|
|
55
21
|
export function auditPrompt(input) {
|
|
56
22
|
return [
|
|
57
23
|
"You are an independent completion AUDITOR, not the worker. Decide whether the goal is genuinely complete.",
|
package/dist/agent/key-pool.js
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multiple API keys per provider (#170) — "複数鍵登録しといて、規定のリトライ回数を超えたら
|
|
3
|
-
* 次の鍵にローテ / 全キーを回ってもまだレートリミットだったらエラーで停止".
|
|
4
|
-
*
|
|
5
|
-
* ## The algorithm, deliberately small
|
|
6
|
-
*
|
|
7
|
-
* A request is made with the current key. The HTTP client already retries a rate limit
|
|
8
|
-
* a fixed number of times on that key (its own backoff ladder). When those retries are
|
|
9
|
-
* spent and the failure is STILL a rate limit, the pool moves to the next key and the
|
|
10
|
-
* whole ladder runs again on it. When the last key is exhausted too, the error is
|
|
11
|
-
* raised and the turn stops.
|
|
12
|
-
*
|
|
13
|
-
* That is the entire policy:
|
|
14
|
-
*
|
|
15
|
-
* key1 [retry × N] -> key2 [retry × N] -> … -> raise
|
|
16
|
-
*
|
|
17
|
-
* ## What this intentionally does NOT do
|
|
18
|
-
*
|
|
19
|
-
* An earlier version of this file tracked per-key "recovered at" timestamps, parsed
|
|
20
|
-
* provider reset hints, classified a 429 as transient-vs-exhausted, and persisted spent
|
|
21
|
-
* accounts to disk. None of that was asked for, none of it was generic (the hint
|
|
22
|
-
* formats are provider-specific), and the operator's actual ask is the loop above. A
|
|
23
|
-
* simpler rule is also easier to reason about when it goes wrong: with this policy the
|
|
24
|
-
* only state is "which key am I on", and the only outcome is success or a raised error.
|
|
25
|
-
*
|
|
26
|
-
* ## Provider-agnostic
|
|
27
|
-
*
|
|
28
|
-
* Nothing here knows about any particular endpoint. "Rate limited" is the HTTP 429 that
|
|
29
|
-
* every OpenAI-compatible provider uses; the caller decides which statuses qualify.
|
|
30
|
-
*/
|
|
31
|
-
/** Build a pool from config order, dropping blanks and duplicates. */
|
|
32
1
|
export function makeKeyPool(keys) {
|
|
33
2
|
const seen = new Set();
|
|
34
3
|
const unique = [];
|
|
@@ -41,22 +10,11 @@ export function makeKeyPool(keys) {
|
|
|
41
10
|
}
|
|
42
11
|
return { keys: unique, current: 0 };
|
|
43
12
|
}
|
|
44
|
-
/** The key in use, or null for an empty pool (or no pool at all). */
|
|
45
13
|
export function currentKey(pool) {
|
|
46
|
-
// tolerate a missing pool: most callers simply have no `keyPool`, and making every
|
|
47
|
-
// one of them guard first is how a `undefined.keys` crash reaches a 400 (measured)
|
|
48
14
|
if (!pool)
|
|
49
15
|
return null;
|
|
50
16
|
return pool.keys[pool.current] ?? null;
|
|
51
17
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Move to the next key, or null when every key has already had its turn.
|
|
54
|
-
*
|
|
55
|
-
* Null is the "全キーを回ってもまだレートリミットだった" signal: the caller stops and raises
|
|
56
|
-
* the last error instead of cycling forever. No wraparound on purpose — wrapping would
|
|
57
|
-
* re-try keys whose retry ladder was just spent, turning a hard limit into an infinite
|
|
58
|
-
* loop.
|
|
59
|
-
*/
|
|
60
18
|
export function nextKey(pool) {
|
|
61
19
|
const next = pool.current + 1;
|
|
62
20
|
if (next >= pool.keys.length)
|
|
@@ -64,22 +22,12 @@ export function nextKey(pool) {
|
|
|
64
22
|
pool.current = next;
|
|
65
23
|
return pool.keys[next];
|
|
66
24
|
}
|
|
67
|
-
/** Start over from the first key (a fresh turn gets the full cycle again). */
|
|
68
25
|
export function resetPool(pool) {
|
|
69
26
|
pool.current = 0;
|
|
70
27
|
}
|
|
71
|
-
/** How many keys this pool holds. */
|
|
72
28
|
export function poolSize(pool) {
|
|
73
29
|
return pool.keys.length;
|
|
74
30
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Is this failure a rate limit — the only kind rotating can fix?
|
|
77
|
-
*
|
|
78
|
-
* 429 is the universal spelling. 402 ("payment required") and 403 are included because
|
|
79
|
-
* an exhausted subscription and a quota-blocked key answer that way, and a second
|
|
80
|
-
* account is exactly what resolves them. Everything else (5xx, 400, a network error) is
|
|
81
|
-
* NOT rotated: it would fail identically on every key, so rotating just burns the cycle.
|
|
82
|
-
*/
|
|
83
31
|
export function isRateLimitStatus(status) {
|
|
84
32
|
return status === 429 || status === 402 || status === 403;
|
|
85
33
|
}
|
package/dist/agent/kill.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
/** can this platform kill a whole process group? */
|
|
2
1
|
export function canKillProcessGroup(platform = process.platform) {
|
|
3
2
|
return platform !== "win32";
|
|
4
3
|
}
|
|
5
|
-
/**
|
|
6
|
-
* Kill a spawned child, and its whole group where the platform allows it.
|
|
7
|
-
*
|
|
8
|
-
* Deliberately does NOT throw: callers are timeouts and abort handlers, where a
|
|
9
|
-
* throw would replace the real message with a secondary failure.
|
|
10
|
-
*/
|
|
11
4
|
export function killTree(child, platform = process.platform) {
|
|
12
5
|
if (!child?.pid)
|
|
13
6
|
return { killed: false, wholeTree: false, reason: "no-pid" };
|
|
14
7
|
if (!canKillProcessGroup(platform)) {
|
|
15
|
-
// Windows: no group kill. Try the direct child so at least THAT stops, and
|
|
16
|
-
// report that the tree is not covered rather than implying it is.
|
|
17
8
|
try {
|
|
18
9
|
process.kill(child.pid, "SIGKILL");
|
|
19
10
|
return { killed: true, wholeTree: false };
|
|
@@ -28,13 +19,11 @@ export function killTree(child, platform = process.platform) {
|
|
|
28
19
|
}
|
|
29
20
|
}
|
|
30
21
|
try {
|
|
31
|
-
process.kill(-child.pid, "SIGKILL");
|
|
22
|
+
process.kill(-child.pid, "SIGKILL");
|
|
32
23
|
return { killed: true, wholeTree: true };
|
|
33
24
|
}
|
|
34
25
|
catch (err) {
|
|
35
26
|
const code = err.code;
|
|
36
|
-
// EPERM on the group can still mean the child is killable directly, and
|
|
37
|
-
// EPERM is what a partially-privileged tree produces.
|
|
38
27
|
if (code === "EPERM") {
|
|
39
28
|
try {
|
|
40
29
|
process.kill(child.pid, "SIGKILL");
|
|
@@ -49,7 +38,6 @@ export function killTree(child, platform = process.platform) {
|
|
|
49
38
|
return { killed: false, wholeTree: false, reason: "error" };
|
|
50
39
|
}
|
|
51
40
|
}
|
|
52
|
-
/** a sentence for the tool result, so a partial kill is never silent */
|
|
53
41
|
export function describeKill(outcome) {
|
|
54
42
|
if (outcome.killed && outcome.wholeTree)
|
|
55
43
|
return "killed (whole process group)";
|