toolroll 0.9.81 → 0.9.83
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/CHANGELOG.md +35 -0
- package/dist/cli.js +1 -0
- package/dist/commit-extract.d.ts +18 -0
- package/dist/commit-extract.js +39 -1
- package/dist/converse.d.ts +12 -0
- package/dist/converse.js +18 -2
- package/dist/deploy-gate.js +8 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/eval-gate.d.ts +51 -0
- package/dist/eval-gate.js +199 -0
- package/dist/eval-hooks.d.ts +39 -0
- package/dist/eval-hooks.js +477 -0
- package/dist/eval-oracle-main.d.ts +1 -0
- package/dist/eval-oracle-main.js +31 -0
- package/dist/eval-oracle.d.ts +60 -0
- package/dist/eval-oracle.js +217 -0
- package/dist/eval-world.d.ts +17 -0
- package/dist/eval-world.js +97 -0
- package/dist/evals.d.ts +299 -0
- package/dist/evals.js +964 -0
- package/dist/exec.js +15 -1
- package/dist/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/flow-draft.js +4 -0
- package/dist/flow-sort.js +4 -0
- package/dist/folder-look.d.ts +107 -0
- package/dist/folder-look.js +302 -0
- package/dist/github-jobs.d.ts +25 -0
- package/dist/github-jobs.js +22 -0
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.js +14 -0
- package/dist/lead.js +4 -1
- package/dist/learning-child.d.ts +1 -0
- package/dist/learning-child.js +34 -0
- package/dist/learning-look.d.ts +25 -0
- package/dist/learning-look.js +72 -0
- package/dist/memory-pass.js +4 -0
- package/dist/nightly-check.d.ts +4 -1
- package/dist/nightly-check.js +5 -2
- package/dist/nightly-github.d.ts +316 -0
- package/dist/nightly-github.js +1022 -0
- package/dist/operate.d.ts +123 -1
- package/dist/operate.js +344 -29
- package/dist/project-identity.d.ts +37 -0
- package/dist/project-identity.js +106 -0
- package/dist/project-knowledge.d.ts +4 -4
- package/dist/project-tools.d.ts +4 -1
- package/dist/project-tools.js +7 -2
- package/dist/project.d.ts +4 -1
- package/dist/project.js +11 -4
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +9 -2
- package/dist/release-adapters.d.ts +21 -21
- package/dist/release-adapters.js +52 -19
- package/dist/release-auto.js +26 -0
- package/dist/release-card.js +3 -0
- package/dist/release-cli.js +59 -3
- package/dist/release-view.d.ts +3 -0
- package/dist/release-view.js +2 -0
- package/dist/release.d.ts +16 -0
- package/dist/release.js +16 -0
- package/dist/reported-usage.d.ts +69 -0
- package/dist/reported-usage.js +97 -0
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +35 -8
- package/dist/server/chat.js +4 -0
- package/dist/server/flows.js +9 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.js +3 -0
- package/dist/server/pages.js +62 -88
- package/dist/server/render-pages.d.ts +2 -16
- package/dist/server/render-pages.js +1 -55
- package/dist/server/render-tasks.js +1 -0
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.js +14 -3
- package/dist/server/tasks.js +15 -4
- package/dist/store.d.ts +109 -1
- package/dist/store.js +394 -4
- package/dist/subagents.js +4 -0
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +52 -13
- package/dist/task-sizing.js +4 -0
- package/dist/teardown-git.d.ts +9 -2
- package/dist/teardown-git.js +56 -33
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/evals/cases/catalog/case.json +25 -0
- package/evals/cases/catalog/fixture/README.md +3 -0
- package/evals/cases/catalog/fixture/package.json +8 -0
- package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
- package/evals/cases/catalog/oracle.mjs +23 -0
- package/evals/cases/catalog/probe.mjs +10 -0
- package/evals/cases/catalog/source.md +9 -0
- package/evals/cases/csv-quoting/case.json +23 -0
- package/evals/cases/csv-quoting/fixture/README.md +3 -0
- package/evals/cases/csv-quoting/fixture/csv.js +1 -0
- package/evals/cases/csv-quoting/fixture/package.json +8 -0
- package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
- package/evals/cases/csv-quoting/oracle.mjs +14 -0
- package/evals/cases/csv-quoting/probe.mjs +9 -0
- package/evals/cases/csv-quoting/source.md +9 -0
- package/evals/cases/dedupe/case.json +23 -0
- package/evals/cases/dedupe/fixture/README.md +3 -0
- package/evals/cases/dedupe/fixture/dedupe.js +1 -0
- package/evals/cases/dedupe/fixture/package.json +8 -0
- package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
- package/evals/cases/dedupe/oracle.mjs +16 -0
- package/evals/cases/dedupe/probe.mjs +13 -0
- package/evals/cases/dedupe/source.md +9 -0
- package/evals/cases/endpoint/case.json +25 -0
- package/evals/cases/endpoint/fixture/README.md +3 -0
- package/evals/cases/endpoint/fixture/package.json +8 -0
- package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
- package/evals/cases/endpoint/oracle.mjs +27 -0
- package/evals/cases/endpoint/probe.mjs +10 -0
- package/evals/cases/endpoint/source.md +11 -0
- package/evals/cases/math-divide/case.json +23 -0
- package/evals/cases/math-divide/fixture/README.md +3 -0
- package/evals/cases/math-divide/fixture/package.json +8 -0
- package/evals/cases/math-divide/fixture/src/math.js +1 -0
- package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
- package/evals/cases/math-divide/oracle.mjs +13 -0
- package/evals/cases/math-divide/probe.mjs +9 -0
- package/evals/cases/math-divide/source.md +9 -0
- package/evals/cases/math-multiply/case.json +23 -0
- package/evals/cases/math-multiply/fixture/README.md +3 -0
- package/evals/cases/math-multiply/fixture/package.json +8 -0
- package/evals/cases/math-multiply/fixture/src/math.js +2 -0
- package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
- package/evals/cases/math-multiply/oracle.mjs +12 -0
- package/evals/cases/math-multiply/probe.mjs +9 -0
- package/evals/cases/math-multiply/source.md +9 -0
- package/evals/cases/math-subtract/case.json +23 -0
- package/evals/cases/math-subtract/fixture/README.md +3 -0
- package/evals/cases/math-subtract/fixture/package.json +8 -0
- package/evals/cases/math-subtract/fixture/src/math.js +1 -0
- package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
- package/evals/cases/math-subtract/oracle.mjs +12 -0
- package/evals/cases/math-subtract/probe.mjs +9 -0
- package/evals/cases/math-subtract/source.md +9 -0
- package/evals/cases/merge-config/case.json +23 -0
- package/evals/cases/merge-config/fixture/README.md +3 -0
- package/evals/cases/merge-config/fixture/config.js +1 -0
- package/evals/cases/merge-config/fixture/package.json +8 -0
- package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
- package/evals/cases/merge-config/oracle.mjs +28 -0
- package/evals/cases/merge-config/probe.mjs +15 -0
- package/evals/cases/merge-config/source.md +9 -0
- package/evals/cases/paginate/case.json +23 -0
- package/evals/cases/paginate/fixture/README.md +3 -0
- package/evals/cases/paginate/fixture/package.json +8 -0
- package/evals/cases/paginate/fixture/paginate.js +1 -0
- package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
- package/evals/cases/paginate/oracle.mjs +18 -0
- package/evals/cases/paginate/probe.mjs +13 -0
- package/evals/cases/paginate/source.md +9 -0
- package/evals/cases/round2-asks/case.json +24 -0
- package/evals/cases/round2-asks/fixture/README.md +3 -0
- package/evals/cases/round2-asks/fixture/package.json +8 -0
- package/evals/cases/round2-asks/fixture/src/math.js +1 -0
- package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
- package/evals/cases/round2-asks/source.md +11 -0
- package/evals/manifest.json +15 -0
- package/package.json +2 -1
package/dist/exec.js
CHANGED
|
@@ -1847,6 +1847,11 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1847
1847
|
let initLine = null;
|
|
1848
1848
|
let initIdentityLine = null;
|
|
1849
1849
|
let conflictingInitLine = null;
|
|
1850
|
+
/** v140: the first init's model, and the first top-level init naming another (the parser reads them as a conflict:
|
|
1851
|
+
* unknown, never one of them). Kept even within one session id. */
|
|
1852
|
+
let initModel = null;
|
|
1853
|
+
let initModelLine = null;
|
|
1854
|
+
let modelConflictLine = null;
|
|
1850
1855
|
let initSessionId = null;
|
|
1851
1856
|
let resultLine = null;
|
|
1852
1857
|
let resultOverflowed = false;
|
|
@@ -1955,6 +1960,15 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1955
1960
|
if (type === "system" && String(event["subtype"] ?? "") === "init" && topLevelEvent(event)) {
|
|
1956
1961
|
if (initLine === null)
|
|
1957
1962
|
initLine = line;
|
|
1963
|
+
const model = typeof event["model"] === "string" ? event["model"] : null;
|
|
1964
|
+
if (model !== null) {
|
|
1965
|
+
if (initModel === null) {
|
|
1966
|
+
initModel = model;
|
|
1967
|
+
initModelLine = line;
|
|
1968
|
+
}
|
|
1969
|
+
else if (model !== initModel && modelConflictLine === null)
|
|
1970
|
+
modelConflictLine = line;
|
|
1971
|
+
}
|
|
1958
1972
|
const id = transportSessionId(event["session_id"]);
|
|
1959
1973
|
let identityWitness = false;
|
|
1960
1974
|
if (id !== null) {
|
|
@@ -2052,7 +2066,7 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
2052
2066
|
liveProviders.delete(child);
|
|
2053
2067
|
framing.finish();
|
|
2054
2068
|
live?.detach();
|
|
2055
|
-
const lines = [initLine, initIdentityLine, conflictingInitLine, accountedResultLine(), overflowLine]
|
|
2069
|
+
const lines = [initLine, initIdentityLine, conflictingInitLine, initModelLine, modelConflictLine, accountedResultLine(), overflowLine]
|
|
2056
2070
|
.filter((one) => one !== null)
|
|
2057
2071
|
.filter((line, index, all) => all.indexOf(line) === index);
|
|
2058
2072
|
const settlement = container === null ? Promise.resolve(null) : settleContainer(container, options);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Started only by learning-look.ts: one project's first-task suggestions (first-run.ts findFirstTasks), in a process
|
|
3
|
+
* of its own so the hardened git's synchronous reads of the project's folder never hold the console's thread. Its one
|
|
4
|
+
* argument is JSON naming the project; it prints the suggestions as JSON and ends.
|
|
5
|
+
*/
|
|
6
|
+
import { findFirstTasks } from "./first-run.js";
|
|
7
|
+
const input = JSON.parse(process.argv[2] ?? "null");
|
|
8
|
+
if (input === null || typeof input.repo !== "string" || input.repo === "" || input.repo.includes("\0"))
|
|
9
|
+
process.exit(2);
|
|
10
|
+
const found = await findFirstTasks(input.repo);
|
|
11
|
+
process.stdout.write(JSON.stringify(found), () => process.exit(0));
|
package/dist/flow-draft.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* nothing is sent: a later zone shows it to a person, and a later step posts
|
|
8
8
|
* it ({{stage.<id>}}).
|
|
9
9
|
*/
|
|
10
|
+
import { inEvalWorld } from "./eval-gate.js";
|
|
10
11
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
11
12
|
import { run } from "./exec.js";
|
|
12
13
|
import { CLAUDE_NO_TOOLS_ARGS, CLAUDE_SIGNED_OUT, claudeJsonBody, claudeModelArgs, inScratchDir } from "./claude-print.js";
|
|
@@ -59,6 +60,9 @@ export function keptDraft(text) {
|
|
|
59
60
|
/** The production runner: Claude through the sign-in on this computer, with no tools, no MCP servers and no repository. */
|
|
60
61
|
export function claudeDraftRunner(runner = run) {
|
|
61
62
|
return request => inScratchDir("standing-orders-draft-", async (dir) => {
|
|
63
|
+
// An eval world makes no provider call it can't cap and record (eval-gate.ts).
|
|
64
|
+
if (inEvalWorld())
|
|
65
|
+
return { ok: false, said: "Drafts aren't written in an eval world." };
|
|
62
66
|
const started = Date.now();
|
|
63
67
|
const result = await runner("claude", [
|
|
64
68
|
"-p", "--output-format", "json", ...CLAUDE_NO_TOOLS_ARGS, ...claudeModelArgs(request.model),
|
package/dist/flow-sort.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* second, with how sure it is. It never writes text, so nothing it returns
|
|
8
8
|
* is instructions: an answer is only ever one of the zone's own options.
|
|
9
9
|
*/
|
|
10
|
+
import { inEvalWorld } from "./eval-gate.js";
|
|
10
11
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
11
12
|
import { SORT_ROUTE_KEY, sortKeyOf, sortNoteKeyOf } from "./contracts/flow.js";
|
|
12
13
|
import { readJevReply } from "./contracts/sort-answer.js";
|
|
@@ -51,6 +52,9 @@ export function sortRequest(sort, state) {
|
|
|
51
52
|
}
|
|
52
53
|
/** Ask Jev through OpenRouter. Trouble reaching it (or a refused key, or no credit) is a reason to try again, in words. */
|
|
53
54
|
export async function askJev(fetcher, key, request) {
|
|
55
|
+
// An eval world makes no provider call it can't gate and record (eval-gate.ts), so nothing is sent.
|
|
56
|
+
if (inEvalWorld())
|
|
57
|
+
return { ok: false, said: "An eval world makes no Jev call." };
|
|
54
58
|
const started = Date.now();
|
|
55
59
|
let response;
|
|
56
60
|
try {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads of a person's own folders, kept off the console's thread.
|
|
3
|
+
*
|
|
4
|
+
* A folder can stop answering: a drive that disconnected without unmounting, a folder macOS keeps in iCloud (Desktop &
|
|
5
|
+
* Documents), or one behind a privacy prompt nobody has answered. Opening it then blocks until the system answers, and
|
|
6
|
+
* a synchronous read on the console's thread freezes every page with it (the 0.9.82 deploy, 2026-10-31: the console
|
|
7
|
+
* listened for twenty minutes without answering, its main thread inside a directory read). So every such read runs in a
|
|
8
|
+
* child process, the same read it always was, and the console waits for it only as long as a deadline allows: a folder
|
|
9
|
+
* that didn't answer degrades the one thing that needed it, in plain words, and the next look tries again.
|
|
10
|
+
*
|
|
11
|
+
* A look never goes into another mounted drive, /Volumes, or the protected folders under ~/Library, unless that is the
|
|
12
|
+
* folder the person chose. Its answers are filesystem data, read exactly as before (directory names, a repository's
|
|
13
|
+
* `.git/config` as a plain bounded file never followed through a link), and the caller still checks every path against
|
|
14
|
+
* its own ceiling.
|
|
15
|
+
*/
|
|
16
|
+
/** How long the console waits for a look before saying the folder didn't answer. */
|
|
17
|
+
export declare const FOLDER_LOOK_MS = 4000;
|
|
18
|
+
/** The `url` of `[remote "origin"]` in a git config's text. Plain JavaScript with no outside names: the look's child
|
|
19
|
+
* runs this same function (its source is sent along), so the parent and the child read a config one way. */
|
|
20
|
+
export declare function originUrlIn(config: string): string | null;
|
|
21
|
+
export type FolderLookRequest =
|
|
22
|
+
/** One folder's sub-folders, as the browse page shows them: at most 400 kept of 2,000 entries read, links resolved
|
|
23
|
+
* and kept only inside a root. */
|
|
24
|
+
{
|
|
25
|
+
op: "list";
|
|
26
|
+
path: string;
|
|
27
|
+
roots: readonly string[];
|
|
28
|
+
home: string;
|
|
29
|
+
}
|
|
30
|
+
/** The origin of each registered project, and of each repository directly in a root (at most 400 folders a root). */
|
|
31
|
+
| {
|
|
32
|
+
op: "origins";
|
|
33
|
+
registered: readonly string[];
|
|
34
|
+
roots: readonly string[];
|
|
35
|
+
home: string;
|
|
36
|
+
}
|
|
37
|
+
/** Where each path really is (its real path, when that is a folder): a project's identity, resolved off the thread. */
|
|
38
|
+
| {
|
|
39
|
+
op: "identities";
|
|
40
|
+
paths: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/** A project's own `.mcp.json`, read as a plain bounded file and parsed (null when absent or not a JSON object). */
|
|
43
|
+
| {
|
|
44
|
+
op: "projectMcp";
|
|
45
|
+
path: string;
|
|
46
|
+
};
|
|
47
|
+
/** One look under way: its answer, and when its process has ended (a stuck child can outlive its answer). */
|
|
48
|
+
export type FolderLookRun = {
|
|
49
|
+
answer: Promise<unknown>;
|
|
50
|
+
ended: Promise<void>;
|
|
51
|
+
};
|
|
52
|
+
/** Starts one look. Production starts a child process; tests pass their own. */
|
|
53
|
+
export type FolderLookRunner = (request: FolderLookRequest, deadlineMs: number) => FolderLookRun;
|
|
54
|
+
export type FolderLook<T> = {
|
|
55
|
+
ok: true;
|
|
56
|
+
value: T;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
why: "no-answer" | "failed";
|
|
60
|
+
};
|
|
61
|
+
/** At most this many look processes at once, stuck ones included: a folder that never answers costs one each. */
|
|
62
|
+
export declare const FOLDER_LOOK_CHILDREN = 8;
|
|
63
|
+
/** The production look: the script above in a child process of this same Node (or Electron as Node), killed at its
|
|
64
|
+
* deadline. Its environment carries nothing of this process's (no NODE_OPTIONS, no secrets). At the deadline its pipe
|
|
65
|
+
* is closed and nothing of it keeps this process alive; `ended` settles only when the process has really gone. */
|
|
66
|
+
export declare const childLook: FolderLookRunner;
|
|
67
|
+
/** How many look processes are running now (stuck ones included). */
|
|
68
|
+
export declare function folderLooksRunning(): number;
|
|
69
|
+
/**
|
|
70
|
+
* One look, answered within `deadlineMs` whatever the folder does: its value, or that the folder didn't answer (it ran
|
|
71
|
+
* past the deadline, the same look is still stuck, or every child slot is held) or the look failed. A look already
|
|
72
|
+
* under way for the same request is joined, never started twice.
|
|
73
|
+
*/
|
|
74
|
+
export declare function lookFolders(request: FolderLookRequest, options?: {
|
|
75
|
+
runner?: FolderLookRunner;
|
|
76
|
+
deadlineMs?: number;
|
|
77
|
+
}): Promise<FolderLook<unknown>>;
|
|
78
|
+
export type FolderEntry = {
|
|
79
|
+
name: string;
|
|
80
|
+
path: string;
|
|
81
|
+
git: boolean;
|
|
82
|
+
};
|
|
83
|
+
export type FolderLookOptions = {
|
|
84
|
+
runner?: FolderLookRunner;
|
|
85
|
+
deadlineMs?: number;
|
|
86
|
+
home?: string;
|
|
87
|
+
};
|
|
88
|
+
/** A folder's sub-folders for the browse page (null `at`: outside every root, or not a folder). */
|
|
89
|
+
export declare function listFolder(path: string, roots: readonly string[], options?: FolderLookOptions): Promise<FolderLook<{
|
|
90
|
+
at: string | null;
|
|
91
|
+
entries: FolderEntry[];
|
|
92
|
+
}>>;
|
|
93
|
+
/** The origin URL of each registered project, and the repositories directly in each root with theirs. */
|
|
94
|
+
export declare function localOrigins(registered: readonly string[], roots: readonly string[], options?: FolderLookOptions): Promise<FolderLook<{
|
|
95
|
+
registered: {
|
|
96
|
+
path: string;
|
|
97
|
+
url: string | null;
|
|
98
|
+
}[];
|
|
99
|
+
found: {
|
|
100
|
+
path: string;
|
|
101
|
+
url: string | null;
|
|
102
|
+
}[];
|
|
103
|
+
}>>;
|
|
104
|
+
/** Each path's identity (its real path when that is a folder, else null), resolved off the thread. */
|
|
105
|
+
export declare function lookIdentities(paths: readonly string[], options?: FolderLookOptions): Promise<FolderLook<Map<string, string | null>>>;
|
|
106
|
+
/** A project's own `.mcp.json` (parsed object, or null when absent or not one), read off the thread. */
|
|
107
|
+
export declare function lookProjectMcp(repo: string, options?: FolderLookOptions): Promise<FolderLook<Record<string, unknown> | null>>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads of a person's own folders, kept off the console's thread.
|
|
3
|
+
*
|
|
4
|
+
* A folder can stop answering: a drive that disconnected without unmounting, a folder macOS keeps in iCloud (Desktop &
|
|
5
|
+
* Documents), or one behind a privacy prompt nobody has answered. Opening it then blocks until the system answers, and
|
|
6
|
+
* a synchronous read on the console's thread freezes every page with it (the 0.9.82 deploy, 2026-10-31: the console
|
|
7
|
+
* listened for twenty minutes without answering, its main thread inside a directory read). So every such read runs in a
|
|
8
|
+
* child process, the same read it always was, and the console waits for it only as long as a deadline allows: a folder
|
|
9
|
+
* that didn't answer degrades the one thing that needed it, in plain words, and the next look tries again.
|
|
10
|
+
*
|
|
11
|
+
* A look never goes into another mounted drive, /Volumes, or the protected folders under ~/Library, unless that is the
|
|
12
|
+
* folder the person chose. Its answers are filesystem data, read exactly as before (directory names, a repository's
|
|
13
|
+
* `.git/config` as a plain bounded file never followed through a link), and the caller still checks every path against
|
|
14
|
+
* its own ceiling.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
/** How long the console waits for a look before saying the folder didn't answer. */
|
|
20
|
+
export const FOLDER_LOOK_MS = 4_000;
|
|
21
|
+
/** The `url` of `[remote "origin"]` in a git config's text. Plain JavaScript with no outside names: the look's child
|
|
22
|
+
* runs this same function (its source is sent along), so the parent and the child read a config one way. */
|
|
23
|
+
export function originUrlIn(config) {
|
|
24
|
+
let inOrigin = false;
|
|
25
|
+
for (const line of config.split("\n")) {
|
|
26
|
+
if (/^\s*\[/.test(line)) {
|
|
27
|
+
inOrigin = /^\s*\[remote "origin"\]\s*$/.test(line);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!inOrigin)
|
|
31
|
+
continue;
|
|
32
|
+
const url = /^\s*url\s*=\s*(.+)$/.exec(line)?.[1];
|
|
33
|
+
if (url !== undefined)
|
|
34
|
+
return url.trim();
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** At most this many look processes at once, stuck ones included: a folder that never answers costs one each. */
|
|
39
|
+
export const FOLDER_LOOK_CHILDREN = 8;
|
|
40
|
+
/**
|
|
41
|
+
* The child's script: plain CommonJS, given the request as its one argument, printing one JSON answer. Every file it
|
|
42
|
+
* reads is opened without following a link and without waiting on a FIFO, checked to be a regular file under its cap
|
|
43
|
+
* on the open descriptor, and read only up to its limit; every folder is read through a bounded iterator.
|
|
44
|
+
*/
|
|
45
|
+
const LOOK_SCRIPT = `"use strict";
|
|
46
|
+
const fs = require("fs"), path = require("path");
|
|
47
|
+
const originUrlIn = ${originUrlIn.toString()};
|
|
48
|
+
const request = JSON.parse(process.argv[1]);
|
|
49
|
+
const within = (p, root) => p === root || p.startsWith(root === "/" ? "/" : root + "/");
|
|
50
|
+
const home = request.home || "/nonexistent";
|
|
51
|
+
const fenced = [path.join(home, "Library"), "/Volumes", "/System/Volumes", "/Network", "/net", "/private/var/automount"];
|
|
52
|
+
// A place the look never enters, unless the path is inside a folder the person chose there (that root itself, not any
|
|
53
|
+
// root that happens to lie in the same protected tree).
|
|
54
|
+
const offLimits = (p, roots) => fenced.some(one => within(p, one) && !roots.some(root => within(root, one) && within(p, root)));
|
|
55
|
+
const rootOf = (p, roots) => roots.filter(root => within(p, root)).sort((a, b) => b.length - a.length)[0];
|
|
56
|
+
// A plain file read bounded by construction (ghlist review, finding 1): never through a link (O_NOFOLLOW), never
|
|
57
|
+
// waiting on a FIFO (O_NONBLOCK), a regular file under the cap by the opened descriptor, at most the limit read.
|
|
58
|
+
function readBounded(file, cap, limit) {
|
|
59
|
+
let fd = null;
|
|
60
|
+
try {
|
|
61
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
62
|
+
const stat = fs.fstatSync(fd);
|
|
63
|
+
if (!stat.isFile() || stat.size > cap) return null;
|
|
64
|
+
const buffer = Buffer.alloc(Math.min(limit, stat.size));
|
|
65
|
+
const read = buffer.length === 0 ? 0 : fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
66
|
+
return buffer.toString("utf8", 0, read);
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
finally { if (fd !== null) { try { fs.closeSync(fd); } catch {} } }
|
|
69
|
+
}
|
|
70
|
+
function origin(repo) {
|
|
71
|
+
const config = readBounded(path.join(repo, ".git", "config"), 1024 * 1024, 64 * 1024);
|
|
72
|
+
return config === null ? null : originUrlIn(config);
|
|
73
|
+
}
|
|
74
|
+
function children(folder, roots, links) {
|
|
75
|
+
const out = [];
|
|
76
|
+
const folderDev = fs.statSync(folder).dev;
|
|
77
|
+
const dir = fs.opendirSync(folder);
|
|
78
|
+
try {
|
|
79
|
+
for (let seen = 0; seen < 2000 && out.length < 400; seen++) {
|
|
80
|
+
const entry = dir.readSync();
|
|
81
|
+
if (entry === null) break;
|
|
82
|
+
if (!(entry.isDirectory() || (links && entry.isSymbolicLink()))) continue;
|
|
83
|
+
const spelled = path.join(folder, entry.name);
|
|
84
|
+
if (offLimits(spelled, roots)) continue;
|
|
85
|
+
let real, stat;
|
|
86
|
+
try { real = fs.realpathSync(spelled); } catch { continue; }
|
|
87
|
+
if (offLimits(real, roots) || !roots.some(root => within(real, root))) continue;
|
|
88
|
+
try { stat = fs.statSync(real); } catch { continue; }
|
|
89
|
+
if (!stat.isDirectory()) continue;
|
|
90
|
+
// Another drive mounted here: not entered unless it is itself a chosen folder.
|
|
91
|
+
if (stat.dev !== folderDev && !roots.includes(real)) continue;
|
|
92
|
+
out.push({ name: entry.name, path: real });
|
|
93
|
+
}
|
|
94
|
+
} finally { dir.closeSync(); }
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function look() {
|
|
98
|
+
if (request.op === "projectMcp") {
|
|
99
|
+
const text = readBounded(path.join(request.path, ".mcp.json"), 1024 * 1024, 1024 * 1024);
|
|
100
|
+
if (text === null) return { config: null };
|
|
101
|
+
try { const parsed = JSON.parse(text); return { config: parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null }; }
|
|
102
|
+
catch { return { config: null }; }
|
|
103
|
+
}
|
|
104
|
+
if (request.op === "identities") {
|
|
105
|
+
return { paths: request.paths.slice(0, 500).map(one => {
|
|
106
|
+
try { const real = fs.realpathSync(path.resolve(one)); return { path: one, real: fs.statSync(real).isDirectory() ? real : null }; }
|
|
107
|
+
catch { return { path: one, real: null }; }
|
|
108
|
+
}) };
|
|
109
|
+
}
|
|
110
|
+
const roots = request.roots;
|
|
111
|
+
if (request.op === "list") {
|
|
112
|
+
const at = fs.realpathSync(request.path);
|
|
113
|
+
const root = rootOf(at, roots);
|
|
114
|
+
if (root === undefined || offLimits(at, roots) || !fs.statSync(at).isDirectory()) return { at: null, entries: [] };
|
|
115
|
+
// A folder on another drive than the root that allows it is not entered (the root itself may be one).
|
|
116
|
+
if (at !== root && fs.statSync(at).dev !== fs.statSync(root).dev) return { at: null, entries: [] };
|
|
117
|
+
const entries = children(at, roots, true)
|
|
118
|
+
.filter(one => !one.name.startsWith(".") && one.name !== "node_modules")
|
|
119
|
+
.map(one => ({ name: one.name, path: one.path, git: fs.existsSync(path.join(one.path, ".git")) }));
|
|
120
|
+
return { at, entries };
|
|
121
|
+
}
|
|
122
|
+
const registered = request.registered.slice(0, 500).map(repo => ({ path: repo, url: origin(repo) }));
|
|
123
|
+
const found = [];
|
|
124
|
+
for (const root of roots) {
|
|
125
|
+
let listed = [];
|
|
126
|
+
try { listed = children(root, roots, false); } catch { continue; }
|
|
127
|
+
for (const child of listed) {
|
|
128
|
+
if (!fs.existsSync(path.join(child.path, ".git"))) continue;
|
|
129
|
+
found.push({ path: child.path, url: origin(child.path) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { registered, found };
|
|
133
|
+
}
|
|
134
|
+
let answer;
|
|
135
|
+
try { answer = { ok: true, value: look() }; } catch (error) { answer = { ok: false, code: error && error.code || null }; }
|
|
136
|
+
process.stdout.write(JSON.stringify(answer));
|
|
137
|
+
`;
|
|
138
|
+
/** The production look: the script above in a child process of this same Node (or Electron as Node), killed at its
|
|
139
|
+
* deadline. Its environment carries nothing of this process's (no NODE_OPTIONS, no secrets). At the deadline its pipe
|
|
140
|
+
* is closed and nothing of it keeps this process alive; `ended` settles only when the process has really gone. */
|
|
141
|
+
export const childLook = (request, deadlineMs) => {
|
|
142
|
+
const child = spawn(process.execPath, ["-e", LOOK_SCRIPT, JSON.stringify(request)], {
|
|
143
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
144
|
+
env: { PATH: process.env["PATH"] ?? "/usr/bin:/bin", ELECTRON_RUN_AS_NODE: "1" },
|
|
145
|
+
killSignal: "SIGKILL",
|
|
146
|
+
});
|
|
147
|
+
child.unref();
|
|
148
|
+
const ended = new Promise(resolveEnded => {
|
|
149
|
+
child.once("exit", () => resolveEnded());
|
|
150
|
+
child.once("error", () => resolveEnded());
|
|
151
|
+
});
|
|
152
|
+
const answer = new Promise((resolveAnswer, reject) => {
|
|
153
|
+
const chunks = [];
|
|
154
|
+
let size = 0;
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
child.kill("SIGKILL");
|
|
157
|
+
// A child stuck in the kernel can't be reaped yet: its pipe is closed now, so it holds nothing here.
|
|
158
|
+
child.stdout.destroy();
|
|
159
|
+
reject(new Error("the look didn't answer"));
|
|
160
|
+
}, deadlineMs);
|
|
161
|
+
timer.unref();
|
|
162
|
+
child.stdout.on("data", (chunk) => {
|
|
163
|
+
size += chunk.length;
|
|
164
|
+
if (size > 8 * 1024 * 1024) {
|
|
165
|
+
child.kill("SIGKILL");
|
|
166
|
+
child.stdout.destroy();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
chunks.push(chunk);
|
|
170
|
+
});
|
|
171
|
+
child.once("error", error => { clearTimeout(timer); reject(error); });
|
|
172
|
+
child.once("close", code => {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
if (code !== 0) {
|
|
175
|
+
reject(new Error(`the look ended with ${code}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
resolveAnswer(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
reject(error);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
answer.catch(() => { });
|
|
187
|
+
return { answer, ended };
|
|
188
|
+
};
|
|
189
|
+
/** Looks whose process hasn't ended, by request: callers asking the same thing share one; each holds a child slot
|
|
190
|
+
* until its process is gone. */
|
|
191
|
+
const inFlight = new Map();
|
|
192
|
+
/** The same request however its paths were spelled: resolved paths, roots in order. */
|
|
193
|
+
function lookKey(request) {
|
|
194
|
+
const sorted = (paths) => paths.map(one => resolve(one)).sort();
|
|
195
|
+
switch (request.op) {
|
|
196
|
+
case "list": return JSON.stringify([request.op, resolve(request.path), sorted(request.roots), request.home]);
|
|
197
|
+
case "origins": return JSON.stringify([request.op, sorted(request.registered), sorted(request.roots), request.home]);
|
|
198
|
+
case "identities": return JSON.stringify([request.op, sorted(request.paths)]);
|
|
199
|
+
case "projectMcp": return JSON.stringify([request.op, resolve(request.path)]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** How many look processes are running now (stuck ones included). */
|
|
203
|
+
export function folderLooksRunning() {
|
|
204
|
+
return inFlight.size;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* One look, answered within `deadlineMs` whatever the folder does: its value, or that the folder didn't answer (it ran
|
|
208
|
+
* past the deadline, the same look is still stuck, or every child slot is held) or the look failed. A look already
|
|
209
|
+
* under way for the same request is joined, never started twice.
|
|
210
|
+
*/
|
|
211
|
+
export async function lookFolders(request, options = {}) {
|
|
212
|
+
const deadlineMs = options.deadlineMs ?? FOLDER_LOOK_MS;
|
|
213
|
+
const key = lookKey(request);
|
|
214
|
+
let run = inFlight.get(key);
|
|
215
|
+
if (run === undefined) {
|
|
216
|
+
if (inFlight.size >= FOLDER_LOOK_CHILDREN)
|
|
217
|
+
return { ok: false, why: "no-answer" };
|
|
218
|
+
try {
|
|
219
|
+
run = (options.runner ?? childLook)(request, deadlineMs);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { ok: false, why: "failed" };
|
|
223
|
+
}
|
|
224
|
+
const mine = run;
|
|
225
|
+
inFlight.set(key, mine);
|
|
226
|
+
const forget = () => { if (inFlight.get(key) === mine)
|
|
227
|
+
inFlight.delete(key); };
|
|
228
|
+
mine.ended.then(forget, forget);
|
|
229
|
+
}
|
|
230
|
+
let timer;
|
|
231
|
+
const late = new Promise(resolveLate => { timer = setTimeout(() => resolveLate("late"), deadlineMs); });
|
|
232
|
+
try {
|
|
233
|
+
const first = await Promise.race([run.answer.then(value => ({ value }), () => "failed"), late]);
|
|
234
|
+
if (first === "late")
|
|
235
|
+
return { ok: false, why: "no-answer" };
|
|
236
|
+
if (first === "failed")
|
|
237
|
+
return { ok: false, why: "failed" };
|
|
238
|
+
const answer = first.value;
|
|
239
|
+
if (answer === null || typeof answer !== "object" || answer.ok !== true)
|
|
240
|
+
return { ok: false, why: "failed" };
|
|
241
|
+
return { ok: true, value: answer.value };
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
|
|
248
|
+
/** A folder's sub-folders for the browse page (null `at`: outside every root, or not a folder). */
|
|
249
|
+
export async function listFolder(path, roots, options = {}) {
|
|
250
|
+
const looked = await lookFolders({ op: "list", path, roots: [...roots], home: options.home ?? homedir() }, options);
|
|
251
|
+
if (!looked.ok)
|
|
252
|
+
return looked;
|
|
253
|
+
const value = looked.value;
|
|
254
|
+
if (value === null || typeof value !== "object" || !(value.at === null || text(value.at)) || !Array.isArray(value.entries))
|
|
255
|
+
return { ok: false, why: "failed" };
|
|
256
|
+
const entries = value.entries.flatMap((one) => {
|
|
257
|
+
const row = one;
|
|
258
|
+
return row !== null && typeof row === "object" && text(row.name) && text(row.path) && typeof row.git === "boolean" ? [{ name: row.name, path: row.path, git: row.git }] : [];
|
|
259
|
+
});
|
|
260
|
+
return { ok: true, value: { at: value.at, entries } };
|
|
261
|
+
}
|
|
262
|
+
/** The origin URL of each registered project, and the repositories directly in each root with theirs. */
|
|
263
|
+
export async function localOrigins(registered, roots, options = {}) {
|
|
264
|
+
const looked = await lookFolders({ op: "origins", registered: [...registered], roots: [...roots], home: options.home ?? homedir() }, options);
|
|
265
|
+
if (!looked.ok)
|
|
266
|
+
return looked;
|
|
267
|
+
const value = looked.value;
|
|
268
|
+
if (value === null || typeof value !== "object" || !Array.isArray(value.registered) || !Array.isArray(value.found))
|
|
269
|
+
return { ok: false, why: "failed" };
|
|
270
|
+
const rows = (list) => list.flatMap((one) => {
|
|
271
|
+
const row = one;
|
|
272
|
+
return row !== null && typeof row === "object" && text(row.path) && (row.url === null || text(row.url)) ? [{ path: row.path, url: row.url }] : [];
|
|
273
|
+
});
|
|
274
|
+
return { ok: true, value: { registered: rows(value.registered), found: rows(value.found) } };
|
|
275
|
+
}
|
|
276
|
+
/** Each path's identity (its real path when that is a folder, else null), resolved off the thread. */
|
|
277
|
+
export async function lookIdentities(paths, options = {}) {
|
|
278
|
+
const looked = await lookFolders({ op: "identities", paths: [...paths] }, options);
|
|
279
|
+
if (!looked.ok)
|
|
280
|
+
return looked;
|
|
281
|
+
const value = looked.value;
|
|
282
|
+
if (value === null || typeof value !== "object" || !Array.isArray(value.paths))
|
|
283
|
+
return { ok: false, why: "failed" };
|
|
284
|
+
const out = new Map();
|
|
285
|
+
for (const one of value.paths) {
|
|
286
|
+
const row = one;
|
|
287
|
+
if (row !== null && typeof row === "object" && text(row.path) && (row.real === null || text(row.real)))
|
|
288
|
+
out.set(row.path, row.real);
|
|
289
|
+
}
|
|
290
|
+
return { ok: true, value: out };
|
|
291
|
+
}
|
|
292
|
+
/** A project's own `.mcp.json` (parsed object, or null when absent or not one), read off the thread. */
|
|
293
|
+
export async function lookProjectMcp(repo, options = {}) {
|
|
294
|
+
const looked = await lookFolders({ op: "projectMcp", path: repo }, options);
|
|
295
|
+
if (!looked.ok)
|
|
296
|
+
return looked;
|
|
297
|
+
const value = looked.value;
|
|
298
|
+
if (value === null || typeof value !== "object")
|
|
299
|
+
return { ok: false, why: "failed" };
|
|
300
|
+
const config = value.config;
|
|
301
|
+
return { ok: true, value: config !== null && typeof config === "object" && !Array.isArray(config) ? config : null };
|
|
302
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub's own job records, read by the plane's gh (release-adapters.ts for a release's checks, nightly-github.ts for
|
|
3
|
+
* GitHub's nightly): what each step of a finished job did. Step numbers and outcomes are the runner's records; nothing
|
|
4
|
+
* the tested code prints is read.
|
|
5
|
+
*/
|
|
6
|
+
/** One job's step outcomes as GitHub's Actions API reports them. */
|
|
7
|
+
export type JobRecord = {
|
|
8
|
+
id: number;
|
|
9
|
+
status: string;
|
|
10
|
+
conclusion: string | null;
|
|
11
|
+
steps: {
|
|
12
|
+
number: number;
|
|
13
|
+
name: string;
|
|
14
|
+
status: string;
|
|
15
|
+
conclusion: string | null;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Whether a finished, unsuccessful job ended before any of the workflow's own steps ran: every step after GitHub's own
|
|
20
|
+
* first one ("Set up job", always number 1) never ran (no conclusion, or skipped) — leaving out only GitHub's own
|
|
21
|
+
* closing "Complete job" when it is the last step. Step numbers and outcomes are the runner's records; nothing the
|
|
22
|
+
* candidate prints is read. A job with no steps never reached one. Anything else (a step that ran, even the checkout)
|
|
23
|
+
* is not confirmed infrastructure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function failedBeforeTests(job: JobRecord): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub's own job records, read by the plane's gh (release-adapters.ts for a release's checks, nightly-github.ts for
|
|
3
|
+
* GitHub's nightly): what each step of a finished job did. Step numbers and outcomes are the runner's records; nothing
|
|
4
|
+
* the tested code prints is read.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Whether a finished, unsuccessful job ended before any of the workflow's own steps ran: every step after GitHub's own
|
|
8
|
+
* first one ("Set up job", always number 1) never ran (no conclusion, or skipped) — leaving out only GitHub's own
|
|
9
|
+
* closing "Complete job" when it is the last step. Step numbers and outcomes are the runner's records; nothing the
|
|
10
|
+
* candidate prints is read. A job with no steps never reached one. Anything else (a step that ran, even the checkout)
|
|
11
|
+
* is not confirmed infrastructure.
|
|
12
|
+
*/
|
|
13
|
+
export function failedBeforeTests(job) {
|
|
14
|
+
if (job.status !== "completed" || job.conclusion === "success" || job.conclusion === null)
|
|
15
|
+
return false;
|
|
16
|
+
const steps = [...job.steps].sort((a, b) => a.number - b.number);
|
|
17
|
+
if (new Set(steps.map(one => one.number)).size !== steps.length)
|
|
18
|
+
return false;
|
|
19
|
+
const last = steps.at(-1);
|
|
20
|
+
const own = steps.filter(one => one.number > 1 && !(one === last && one.name === "Complete job" && steps.length > 1));
|
|
21
|
+
return own.every(one => one.status !== "in_progress" && (one.conclusion === null || one.conclusion === "skipped"));
|
|
22
|
+
}
|
package/dist/invoke.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type AgentSpec, type Invocation, type ProviderRunner, type AgentEnding } from "./provider.js";
|
|
20
20
|
import { type VersionProbe } from "./attest.js";
|
|
21
|
-
import type
|
|
21
|
+
import { type Store } from "./store.js";
|
|
22
22
|
import type { RunOptions } from "./exec.js";
|
|
23
23
|
import type { Runner } from "./worktree.js";
|
|
24
24
|
import { type NetworkFence, type SignInGrants } from "./agent-fence.js";
|
package/dist/invoke.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* (`adapterFor`), and only builder, planner, reviewer, and scout import
|
|
17
17
|
* `invokeAgent`.
|
|
18
18
|
*/
|
|
19
|
+
import { takeEvalDispatch } from "./eval-gate.js";
|
|
19
20
|
import { adapterFor, auditOf, claudePluginEnv, isolationUnsupported, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
20
21
|
import { readProviderKey, readAuthModeStrict, PROVIDER_KEY_ENV, OWN_KEY_ENV } from "./keys.js";
|
|
21
22
|
import { ACP_AGENTS, isAcpProvider, isKeyedProvider } from "./acp-agents.js";
|
|
@@ -26,6 +27,7 @@ import { attestProvider } from "./attest.js";
|
|
|
26
27
|
import { run as runCommand, runOwnerTag, streamsInput } from "./exec.js";
|
|
27
28
|
import { spawnSync } from "node:child_process";
|
|
28
29
|
import { currentContainment } from "./containment.js";
|
|
30
|
+
import { recordRunBillingBasis, recordRunCostUnknown, recordRunModelReported } from "./store.js";
|
|
29
31
|
import { witnessedRunner } from "./process-custody.js";
|
|
30
32
|
import { underStopWatch } from "./task-control.js";
|
|
31
33
|
import { childDatabaseEnv, isolatedChildDatabase, removeChildDatabase as removeAgentDatabase } from "./child-database.js";
|
|
@@ -153,6 +155,12 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
153
155
|
if (recordedSessionId !== null && requestedResumeSessionId === null && startSessionId !== recordedSessionId) {
|
|
154
156
|
throw new Error(`run ${runId} already records session ${recordedSessionId} but this invocation neither resumes nor starts under it`);
|
|
155
157
|
}
|
|
158
|
+
// An eval world (eval-gate.ts, docs/plans/evals-nightly.md part C): every dispatch takes one of the attempt's slots,
|
|
159
|
+
// named for this run so the runner can match it to the run's record; past the case's limit nothing spawns. Outside
|
|
160
|
+
// an eval world this changes nothing.
|
|
161
|
+
const evalSlot = takeEvalDispatch(`run:${runId}`);
|
|
162
|
+
if (evalSlot.active && !evalSlot.ok)
|
|
163
|
+
return { kind: "refused", reason: "containment", providerVersion: null, diagnostic: evalSlot.why };
|
|
156
164
|
const adapter = adapterFor(spec.provider);
|
|
157
165
|
const { startSessionId: _untrustedStartSessionId, streamInput: _untrustedStreamInput, ...invocationWithoutStart } = invocation;
|
|
158
166
|
// Live input (live-input.ts): only Claude's builds, only through a transport that keeps stdin open. The brief is
|
|
@@ -715,6 +723,8 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
715
723
|
// An ACP run bills as its admission stamp says (store.acpBillingOfRun): its agent's own plan, or metered.
|
|
716
724
|
const acpBilling = isAcpProvider(spec.provider) ? (store.acpBillingOfRun(runId)?.kind === "plan" ? "subscription" : "api-key") : null;
|
|
717
725
|
store.fixRunBilling(runId, acpBilling ?? (authMode === "api-key" ? "api-key" : evidence ?? billingOf(spec.provider, store.handle)), new Date());
|
|
726
|
+
// v141: whether that billing is this run's own evidence or this computer's default (a guess: unknown money).
|
|
727
|
+
recordRunBillingBasis(store, runId, acpBilling !== null || authMode === "api-key" || evidence !== null ? "evidence" : "default");
|
|
718
728
|
const envelope = adapter.parse(result.stdout);
|
|
719
729
|
store.stampAgentReportedUsage(runId, envelope.agentReportedUsage ?? null);
|
|
720
730
|
// An installed CLI too old for a build's own setup refuses its flags
|
|
@@ -765,6 +775,10 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
765
775
|
// How many agent turns it took, where the provider reports it (docs/plans/run-efficiency.md); unknown stays NULL.
|
|
766
776
|
...(envelope.ending?.turns == null ? {} : { agentTurns: envelope.ending.turns + (options.accumulateUsage ? run.agentTurns ?? 0 : 0) }),
|
|
767
777
|
});
|
|
778
|
+
// v141: a turn whose cost wasn't reported leaves the run's money unknown for good (a sticky column, never the note).
|
|
779
|
+
recordRunCostUnknown(store, runId, envelope.costUnknown === true);
|
|
780
|
+
// v140: the exact model the provider's own events named (null: not reported); a resumed turn adds its own to the run's.
|
|
781
|
+
recordRunModelReported(store, runId, envelope.modelReported ?? null, options.accumulateUsage === true);
|
|
768
782
|
// How the attempt ended, classified HERE where the structural terminal
|
|
769
783
|
// still exists; a sign-in that no longer works pauses its provider.
|
|
770
784
|
// A run that failed within seconds having produced nothing is read for
|
package/dist/lead.js
CHANGED
|
@@ -509,7 +509,10 @@ export async function runLeadTurn(input) {
|
|
|
509
509
|
return fail("over-reservation", LEAD_FAILURE_COPY.overReserved, true);
|
|
510
510
|
}
|
|
511
511
|
finishStep({ state: "answered", tokensIn: answer.tokensIn, tokensOut: answer.tokensOut, settledMicrousd: stepSettled, replyBytes: Buffer.byteLength(answer.text, "utf8"),
|
|
512
|
-
cacheReadTokens: answer.cacheReadTokens ?? null, cacheWriteTokens: answer.cacheWriteTokens ?? null
|
|
512
|
+
cacheReadTokens: answer.cacheReadTokens ?? null, cacheWriteTokens: answer.cacheWriteTokens ?? null,
|
|
513
|
+
// v140: what the provider itself reported for this request (absent: unreported, kept NULL).
|
|
514
|
+
modelReported: answer.modelReported ?? null, providerTurns: answer.providerTurns ?? null, reportedCostUsd: answer.reportedCostUsd ?? null,
|
|
515
|
+
billingReported: answer.billingReported ?? null });
|
|
513
516
|
tokensIn += answer.tokensIn;
|
|
514
517
|
tokensOut += answer.tokensOut;
|
|
515
518
|
if (answer.cacheReadTokens === undefined)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|