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/operate.js
CHANGED
|
@@ -60,6 +60,9 @@ import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
|
|
|
60
60
|
import { refreshConnections } from "./mcp-connect.js";
|
|
61
61
|
import { releaseCheckPurposeFor, releaseTermsFor } from "./release-carry.js";
|
|
62
62
|
import { refreshTrustedMains, settleReleaseChecks } from "./nightly-check.js";
|
|
63
|
+
import { githubNightlyRepos, readGithubNightlies } from "./nightly-github.js";
|
|
64
|
+
import { exec as releaseExec, githubReads } from "./release-adapters.js";
|
|
65
|
+
import { releaseTools } from "./release-git.js";
|
|
63
66
|
import { SECRETS_FIRST, startToolSecrets, toolSecretStorage } from "./project-tools.js";
|
|
64
67
|
import { followDiscord } from "./discord.js";
|
|
65
68
|
import { followTeams } from "./teams.js";
|
|
@@ -99,7 +102,7 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
99
102
|
* **Nothing ever prompts.** There is no terminal on the other end at 3am.
|
|
100
103
|
*/
|
|
101
104
|
import { homedir, hostname } from "node:os";
|
|
102
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
105
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
103
106
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
104
107
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
105
108
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
@@ -107,7 +110,7 @@ import { claimInstance, clearInstance, openInstanceSocket } from "./console-inst
|
|
|
107
110
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
108
111
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
109
112
|
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
110
|
-
import { addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
|
|
113
|
+
import { addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry, registryRemovals } from "./repos.js";
|
|
111
114
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
112
115
|
import { pushPass } from "./push.js";
|
|
113
116
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync } from "node:fs";
|
|
@@ -178,6 +181,8 @@ import { createDecisionServer } from "./serve.js";
|
|
|
178
181
|
import { readTailnetNames } from "./tailnet.js";
|
|
179
182
|
import { daemonLaunchCommand, daemonStatus, installReplacingLegacy, installedLegacyDaemon, planDaemon, uninstallDaemon, awaitFreshHeartbeat, } from "./daemon.js";
|
|
180
183
|
import { bridgePass, clearBotToken, createTransport, followBridge, hashPairingCode, loadBotToken, mintPairingCode, networkFence, networkFenceLine, redactToken, saveBotToken, PAIRING_TTL_MS, TOKEN_ENV, telegramHookSecret, telegramPushUrl, } from "./telegram.js";
|
|
184
|
+
import { projectIdentities } from "./project-identity.js";
|
|
185
|
+
import { lookIdentities } from "./folder-look.js";
|
|
181
186
|
import { scanRepo } from "./capscan.js";
|
|
182
187
|
import { computeGaps, describeCapability } from "./gaps.js";
|
|
183
188
|
import { ask as promptAsk, askHidden as promptHidden, confirm as promptConfirm, interactive as promptInteractive, underAgent } from "./prompt.js";
|
|
@@ -247,6 +252,7 @@ import { installationStatus, renderInstallationStatus, renderTaskWait, repairSta
|
|
|
247
252
|
import { latestRelease, newerRelease, notifySecurityRelease, notifyVersionLive, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
248
253
|
import { installMethod } from "./install-method.js";
|
|
249
254
|
import { PACKAGE_VERSION } from "./version.js";
|
|
255
|
+
import { machineActing } from "./scope.js";
|
|
250
256
|
/**
|
|
251
257
|
* 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
|
|
252
258
|
*
|
|
@@ -676,6 +682,9 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
676
682
|
"choose", "note", "max-open-decisions", "name", "days", "publication", "auto-approve", "review-auto", "port", "host", "allow-host",
|
|
677
683
|
"for", "tick-every", "bridge-every", "reconcile-every", "incarnation",
|
|
678
684
|
"say", "ceiling-usd", "cap-usd",
|
|
685
|
+
// evals: the cases a diagnostic campaign runs, its k, an attempt's world and what its builds may not read, and the
|
|
686
|
+
// stuck campaign a person ends (recover --abandon).
|
|
687
|
+
"case", "k", "world", "deny", "abandon",
|
|
679
688
|
"token-file", "bin", "poll", "github", "remote", "head-prefix", "password",
|
|
680
689
|
// v102: project rules.
|
|
681
690
|
"not-requester", "protect", "protect-paths",
|
|
@@ -714,6 +723,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
714
723
|
"release",
|
|
715
724
|
]);
|
|
716
725
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
726
|
+
// evals run: a report-only campaign; keep each attempt's world for a look afterwards.
|
|
727
|
+
"diagnostic", "keep",
|
|
717
728
|
// task scope --release ... --no-carry: every later commit asks again.
|
|
718
729
|
"no-carry",
|
|
719
730
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
|
|
@@ -1147,6 +1158,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1147
1158
|
switch (command) {
|
|
1148
1159
|
case "status":
|
|
1149
1160
|
return statusCommand(positional, flags, context);
|
|
1161
|
+
case "evals":
|
|
1162
|
+
return evalsCommand(positional, flags, context);
|
|
1150
1163
|
case "ready":
|
|
1151
1164
|
return readyCommand(flags, context);
|
|
1152
1165
|
case "task":
|
|
@@ -3928,7 +3941,13 @@ async function startConsole(options) {
|
|
|
3928
3941
|
const { context } = options;
|
|
3929
3942
|
// A git incident (teardown-git.ts) raises an attention notification in this console's outbox.
|
|
3930
3943
|
noticeGitIncidents(context.store, context.clock);
|
|
3944
|
+
// Every project folder this console starts with is resolved off its thread, under a deadline: one that doesn't
|
|
3945
|
+
// answer (a drive away, an iCloud folder macOS is holding) stays unresolved, kept restrictive, and is looked at again
|
|
3946
|
+
// by the background pass; the console starts and answers either way (project-identity.ts).
|
|
3947
|
+
const identities = projectIdentities();
|
|
3948
|
+
await identities.refresh([...options.repos ?? [], ...options.projectRoots ?? [], ...context.store.listProjects().map(one => one.path), ...context.store.knownRepos()]);
|
|
3931
3949
|
const server = createDecisionServer({
|
|
3950
|
+
identities,
|
|
3932
3951
|
store: context.store,
|
|
3933
3952
|
// Sign-in, integration and model checks, and the tidy-ups no page visit does (docs/plans/get-side-effects.md).
|
|
3934
3953
|
backgroundChecks: true,
|
|
@@ -6279,6 +6298,10 @@ async function runWatchLoop(args) {
|
|
|
6279
6298
|
const settledChecks = settleReleaseChecks(store, context.evidenceRoot, context.clock());
|
|
6280
6299
|
if (settledChecks > 0)
|
|
6281
6300
|
progress(`watch: settled ${settledChecks} release check(s)`);
|
|
6301
|
+
// GitHub's nightly as release debt (nightly-github.ts): each opted-in project at most every 30 minutes, 60 s in
|
|
6302
|
+
// all. A test's injected agent runner means a test: no real gh.
|
|
6303
|
+
if (context.agentRunner === undefined)
|
|
6304
|
+
await refreshGithubNightlies(store, context.clock);
|
|
6282
6305
|
// A finished check may send a build's card down its failure path: now, not on the next beat.
|
|
6283
6306
|
if (ran + batched > 0)
|
|
6284
6307
|
moveCards(store, repo, context.clock(), context.evidenceRoot);
|
|
@@ -7040,6 +7063,8 @@ async function upCommand(flags, context) {
|
|
|
7040
7063
|
// folders they may be added from are installation state, not arguments a
|
|
7041
7064
|
// person must repeat on every start.
|
|
7042
7065
|
const registryPath = registryPathOf(context);
|
|
7066
|
+
// Read before the list itself: a line removed after this (even if saved again) counts differently from now on.
|
|
7067
|
+
const removalsAtStart = registryRemovals(registryPath);
|
|
7043
7068
|
const loadedRegistry = await loadProjectRegistry(registryPath);
|
|
7044
7069
|
if ("error" in loadedRegistry)
|
|
7045
7070
|
return fail(write, json, "up", "registry", loadedRegistry.error, EXIT.refused);
|
|
@@ -7063,23 +7088,28 @@ async function upCommand(flags, context) {
|
|
|
7063
7088
|
// soon as a projects folder or saved repository exists.
|
|
7064
7089
|
const repos = [];
|
|
7065
7090
|
const proveRepo = async (input) => {
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
try {
|
|
7070
|
-
return realpathSync(top.stdout.trim());
|
|
7071
|
-
}
|
|
7072
|
-
catch {
|
|
7073
|
-
return null;
|
|
7074
|
-
}
|
|
7091
|
+
// The checkout's real path is read off the thread, under a deadline (folder-look.ts): a folder that doesn't answer
|
|
7092
|
+
// is unavailable, never a wait.
|
|
7093
|
+
return (await upProveSaved(gitRun, input))?.root ?? null;
|
|
7075
7094
|
};
|
|
7095
|
+
// A saved project that doesn't answer now (a drive not connected, a folder macOS is holding) is looked at again
|
|
7096
|
+
// while this start runs, and connects once it answers (upDeferredRetry).
|
|
7097
|
+
const deferredRepos = [];
|
|
7098
|
+
const deferredGenerations = {};
|
|
7099
|
+
// The saved line(s) each project was proved from, so its removal is judged by its own line alone.
|
|
7100
|
+
const repoLines = {};
|
|
7076
7101
|
for (const input of loadedRegistry.repos) {
|
|
7077
7102
|
const root = await proveRepo(input);
|
|
7078
7103
|
if (root === null) {
|
|
7079
|
-
progress(`saved project unavailable — skipped ${input}`);
|
|
7104
|
+
progress(`saved project unavailable — skipped ${input}; it connects when it's back`);
|
|
7105
|
+
deferredRepos.push(input);
|
|
7106
|
+
// An unreadable removal record leaves no count that could match later: the authority can't be used.
|
|
7107
|
+
deferredGenerations[input] = removalsAtStart === null ? -1 : removalsAtStart[input] ?? 0;
|
|
7080
7108
|
}
|
|
7081
|
-
else
|
|
7082
|
-
repos.
|
|
7109
|
+
else {
|
|
7110
|
+
if (!repos.includes(root))
|
|
7111
|
+
repos.push(root);
|
|
7112
|
+
(repoLines[root] ??= []).push(input);
|
|
7083
7113
|
}
|
|
7084
7114
|
}
|
|
7085
7115
|
const explicitRepos = context.repoList ?? [];
|
|
@@ -7103,7 +7133,9 @@ async function upCommand(flags, context) {
|
|
|
7103
7133
|
if (cwdRepo !== null)
|
|
7104
7134
|
pinnedRepos.add(cwdRepo);
|
|
7105
7135
|
}
|
|
7106
|
-
|
|
7136
|
+
// Saved projects that are only away for now still start the service: it serves an empty, restrictive set until they
|
|
7137
|
+
// answer (never the unscoped mode), and connects each as it comes back.
|
|
7138
|
+
if (repos.length === 0 && projectRoots.length === 0 && deferredRepos.length === 0)
|
|
7107
7139
|
return fail(write, json, "up", "no-projects", "choose where your projects live once: `toolroll up --project-root <dir>`", EXIT.refused);
|
|
7108
7140
|
// A person at a terminal reads three lines (onboarding); the folders are for --verbose, a service log, or an agent.
|
|
7109
7141
|
const quietStart = !json && !flags.has("verbose") && (context.upSeams?.terminal ?? process.stdout.isTTY === true) && !underAgent(context.upSeams?.env ?? process.env);
|
|
@@ -7361,6 +7393,9 @@ async function upCommand(flags, context) {
|
|
|
7361
7393
|
type: "start", role: "worker", databaseFile: context.databaseFile, containment: containmentFlag, build,
|
|
7362
7394
|
config: {
|
|
7363
7395
|
runner: runnerName, token: runnerToken, repos: generation === 1 ? [...repos] : [...currentRepos], pinnedRepos: [...pinnedRepos], projectRoots, registryPath, pool,
|
|
7396
|
+
// What this start still holds for its saved projects that were away: a restarted worker never gets back an
|
|
7397
|
+
// authority an earlier one gave up (connected, or removed from the saved list).
|
|
7398
|
+
deferredRepos: [...deferredRepos], deferredGenerations: { ...deferredGenerations }, repoLines,
|
|
7364
7399
|
forMs: forGiven === undefined ? null : Number(forGiven), publicUrl: publicUrlFlag ?? null, stopGraceMs, verbose: flags.has("verbose"),
|
|
7365
7400
|
terminal: context.upSeams?.terminal ?? process.stdout.isTTY === true,
|
|
7366
7401
|
// A restart waits out a crashed predecessor's lease (at most a minute), then recovers it: as a fresh `up` after a crash.
|
|
@@ -7370,6 +7405,12 @@ async function upCommand(flags, context) {
|
|
|
7370
7405
|
onMessage: message => {
|
|
7371
7406
|
if (message.type === "log")
|
|
7372
7407
|
roleLine("worker")(message.line);
|
|
7408
|
+
else if (message.type === "deferred") {
|
|
7409
|
+
// Only ever narrows: a worker's word can't give back an authority this start no longer holds.
|
|
7410
|
+
const narrowed = upNarrowDeferred(deferredRepos, message.repos);
|
|
7411
|
+
deferredRepos.length = 0;
|
|
7412
|
+
deferredRepos.push(...narrowed);
|
|
7413
|
+
}
|
|
7373
7414
|
else if (message.type === "repos") {
|
|
7374
7415
|
// The console's admitted projects widen only to what the builder is bound to in the store (the worker binds
|
|
7375
7416
|
// through the authenticated door before it reports): a worker's word alone admits nothing.
|
|
@@ -12078,15 +12119,9 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12078
12119
|
const pinnedRepos = new Set(config.pinnedRepos);
|
|
12079
12120
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
12080
12121
|
const proveRepo = async (input) => {
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
try {
|
|
12085
|
-
return realpathSync(top.stdout.trim());
|
|
12086
|
-
}
|
|
12087
|
-
catch {
|
|
12088
|
-
return null;
|
|
12089
|
-
}
|
|
12122
|
+
// The checkout's real path is read off the thread, under a deadline (folder-look.ts): a folder that doesn't answer
|
|
12123
|
+
// is unavailable, never a wait.
|
|
12124
|
+
return (await upProveSaved(gitRun, input))?.root ?? null;
|
|
12090
12125
|
};
|
|
12091
12126
|
// The proved set this worker builds; `up` relays each change to the console.
|
|
12092
12127
|
const activeRepos = new Set(config.repos);
|
|
@@ -12271,8 +12306,17 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12271
12306
|
await pause(follower === null ? 2_000 : 5_000);
|
|
12272
12307
|
}
|
|
12273
12308
|
})();
|
|
12274
|
-
|
|
12309
|
+
// The start's own projects and folders were resolved by `up`: taken as they are, never read again on this thread.
|
|
12310
|
+
const dynamicCeiling = resolveCeiling(config.repos, config.projectRoots, path => resolve(path)).ceiling;
|
|
12275
12311
|
const rejected = new Set();
|
|
12312
|
+
// Saved lines' identities, resolved off the thread under a deadline (project-identity.ts): a line whose folder
|
|
12313
|
+
// doesn't answer is skipped this pass, and never holds up the others.
|
|
12314
|
+
const lineIdentities = projectIdentities();
|
|
12315
|
+
/** The saved line(s) each active project came from: `up`'s proof for the start's own, the line admitted for later ones. */
|
|
12316
|
+
const admittedLines = new Map(Object.entries(config.repoLines ?? {}));
|
|
12317
|
+
const deferred = upDeferredRetry(config.deferredRepos ?? [], input => upProveSaved(gitRun, input));
|
|
12318
|
+
const deferredGenerations = config.deferredGenerations ?? {};
|
|
12319
|
+
const reportDeferred = () => port.send({ type: "deferred", repos: deferred.list() });
|
|
12276
12320
|
const registrySupervisor = (async () => {
|
|
12277
12321
|
const sleep = (ms) => new Promise(resolveSleep => setTimeout(resolveSleep, ms));
|
|
12278
12322
|
while (!stopping) {
|
|
@@ -12282,11 +12326,19 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12282
12326
|
stop();
|
|
12283
12327
|
break;
|
|
12284
12328
|
}
|
|
12329
|
+
// A saved line removed while this start runs takes the start's authority for it with it, for good.
|
|
12330
|
+
if (deferred.keepOnly(loaded.repos))
|
|
12331
|
+
reportDeferred();
|
|
12332
|
+
const plain = loaded.repos.filter(one => !deferred.has(one));
|
|
12333
|
+
await lineIdentities.refresh(plain);
|
|
12285
12334
|
// Removed from the saved list: stop building it and take it out of the console. A pinned start keeps its own.
|
|
12286
|
-
|
|
12335
|
+
// Each project is judged by its own saved line (the one it was proved or admitted from, or itself), never by
|
|
12336
|
+
// whether some other folder answered: an unrelated folder that is away never keeps a removed project building.
|
|
12337
|
+
const saved = new Set(loaded.repos);
|
|
12287
12338
|
let removed = false;
|
|
12288
12339
|
for (const repo of [...activeRepos]) {
|
|
12289
|
-
|
|
12340
|
+
const lines = admittedLines.get(repo) ?? [repo];
|
|
12341
|
+
if (stopping || pinnedRepos.has(repo) || saved.has(repo) || lines.some(line => saved.has(line)))
|
|
12290
12342
|
continue;
|
|
12291
12343
|
activeRepos.delete(repo);
|
|
12292
12344
|
removed = true;
|
|
@@ -12299,12 +12351,23 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12299
12351
|
for (const candidate of loaded.repos) {
|
|
12300
12352
|
if (stopping)
|
|
12301
12353
|
break;
|
|
12302
|
-
|
|
12354
|
+
// Skipped at start because it didn't answer: connected on the start's own authority once it does.
|
|
12355
|
+
if (deferred.has(candidate)) {
|
|
12356
|
+
await connectDeferred(candidate);
|
|
12357
|
+
continue;
|
|
12358
|
+
}
|
|
12359
|
+
const canonical = lineIdentities.known(candidate);
|
|
12360
|
+
// Not answered yet: looked at again on the next pass.
|
|
12361
|
+
if (canonical === undefined)
|
|
12362
|
+
continue;
|
|
12303
12363
|
// Still letting go of an earlier removal: it rejoins on the next pass after its watch has stopped.
|
|
12304
12364
|
if (canonical !== null && (activeRepos.has(canonical) || releasedRepos.has(canonical)))
|
|
12305
12365
|
continue;
|
|
12306
12366
|
const approvedAdditions = context.additionalProjectRepos?.() ?? [];
|
|
12307
|
-
|
|
12367
|
+
// A start serving nothing yet (its saved projects all away) is an empty ceiling, never the unscoped mode: a
|
|
12368
|
+
// name that matches no path stands in, so a line saved later still needs its recorded admission.
|
|
12369
|
+
const admittedRepos = [...dynamicCeiling.repos, ...approvedAdditions];
|
|
12370
|
+
const admittedCeiling = { repos: admittedRepos.length === 0 && dynamicCeiling.roots.length === 0 ? ["/\0nothing-admitted"] : admittedRepos, roots: dynamicCeiling.roots, canon: (path) => lineIdentities.known(path) ?? null };
|
|
12308
12371
|
// A project added after this start (`repos add`, the lead, the console) carries its recorded admission; a
|
|
12309
12372
|
// registry line alone is not authority. Either way the folder is proved to be that Git checkout again.
|
|
12310
12373
|
const allowed = canonical !== null &&
|
|
@@ -12318,6 +12381,7 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12318
12381
|
continue;
|
|
12319
12382
|
}
|
|
12320
12383
|
rejected.delete(candidate);
|
|
12384
|
+
admittedLines.set(canonical, [...new Set([...admittedLines.get(canonical) ?? [], candidate])]);
|
|
12321
12385
|
await launchRepo(canonical, true);
|
|
12322
12386
|
}
|
|
12323
12387
|
if (!stopping)
|
|
@@ -12327,6 +12391,61 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12327
12391
|
fatal = `the project registry stopped (${describe(error)})`;
|
|
12328
12392
|
stop();
|
|
12329
12393
|
});
|
|
12394
|
+
/**
|
|
12395
|
+
* A saved project the start couldn't reach, back now: connected only as the very folder that was saved (a parent
|
|
12396
|
+
* checkout found because the saved one lost its .git is another project, needing its own admission), and only while
|
|
12397
|
+
* the line is still saved, checked under the saved list's lock with the binding made inside it, so a removal racing
|
|
12398
|
+
* the proof always wins.
|
|
12399
|
+
*/
|
|
12400
|
+
async function connectDeferred(candidate) {
|
|
12401
|
+
const proved = await deferred.retry(candidate);
|
|
12402
|
+
if (proved === null || stopping)
|
|
12403
|
+
return;
|
|
12404
|
+
// Only the very folder that was saved: the saved line must still resolve to itself, and git's checkout must be that
|
|
12405
|
+
// folder. A parent checkout, or the saved path now leading somewhere else, is another project needing admission.
|
|
12406
|
+
if (proved.line !== candidate || proved.root !== candidate) {
|
|
12407
|
+
deferred.revoke(candidate);
|
|
12408
|
+
reportDeferred();
|
|
12409
|
+
progress(proved.root !== proved.line
|
|
12410
|
+
? `saved project not connected — ${candidate} came back as part of ${proved.root}; add it again to connect it`
|
|
12411
|
+
: `saved project not connected — ${candidate} now leads to ${proved.line}; add it again to connect it`);
|
|
12412
|
+
return;
|
|
12413
|
+
}
|
|
12414
|
+
const root = candidate;
|
|
12415
|
+
if (activeRepos.has(root) || releasedRepos.has(root)) {
|
|
12416
|
+
deferred.revoke(candidate);
|
|
12417
|
+
reportDeferred();
|
|
12418
|
+
return;
|
|
12419
|
+
}
|
|
12420
|
+
// Bound under the saved list's lock: the line is still saved AND has not been removed since this start read it
|
|
12421
|
+
// (its removal count unchanged, so a removal and a save again during the proof still counts as removed).
|
|
12422
|
+
let launching = null;
|
|
12423
|
+
let removedSince = false;
|
|
12424
|
+
await updateProjectRegistry(registryPath, current => {
|
|
12425
|
+
if (stopping || !deferred.has(candidate))
|
|
12426
|
+
return current;
|
|
12427
|
+
const count = registryRemovals(registryPath)?.[candidate] ?? (registryRemovals(registryPath) === null ? -2 : 0);
|
|
12428
|
+
if (!current.repos.includes(candidate) || count !== deferredGenerations[candidate]) {
|
|
12429
|
+
deferred.revoke(candidate);
|
|
12430
|
+
removedSince = true;
|
|
12431
|
+
return current;
|
|
12432
|
+
}
|
|
12433
|
+
deferred.revoke(candidate);
|
|
12434
|
+
admittedLines.set(root, [candidate]);
|
|
12435
|
+
launching = launchRepo(root, true);
|
|
12436
|
+
return current;
|
|
12437
|
+
});
|
|
12438
|
+
if (removedSince) {
|
|
12439
|
+
reportDeferred();
|
|
12440
|
+
progress(`saved project not connected — ${candidate} was removed while it was away; add it again to connect it`);
|
|
12441
|
+
return;
|
|
12442
|
+
}
|
|
12443
|
+
if (launching === null)
|
|
12444
|
+
return;
|
|
12445
|
+
reportDeferred();
|
|
12446
|
+
progress(`saved project is back — connecting ${root}`);
|
|
12447
|
+
await launching;
|
|
12448
|
+
}
|
|
12330
12449
|
// Provider readiness (v47): this machine's non-spending observations, recorded under its own runner name ONCE at
|
|
12331
12450
|
// start — so every route projection can say ready / unavailable / unknown per provider, and the dispatch gate can
|
|
12332
12451
|
// halt a leg this machine cannot run. Production only: a test's injected agent runner is a fake, and its readiness
|
|
@@ -12373,3 +12492,199 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12373
12492
|
}
|
|
12374
12493
|
return EXIT.ok;
|
|
12375
12494
|
}
|
|
12495
|
+
/** GitHub's nightly as release debt (docs/plans/evals-nightly.md, part A): the watch loop's background read of each
|
|
12496
|
+
* opted-in project's nightly runs, at most every 30 minutes, within a 60 s overall deadline, through the plane's own gh
|
|
12497
|
+
* (release-adapters.ts githubReads). Nothing is read for a project that never opted in. */
|
|
12498
|
+
export async function refreshGithubNightlies(store, now, reads) {
|
|
12499
|
+
if (githubNightlyRepos(store).length === 0)
|
|
12500
|
+
return 0;
|
|
12501
|
+
return readGithubNightlies(store, { gh: reads ?? githubReads(releaseExec, releaseTools().gh, { retry: true }), now });
|
|
12502
|
+
}
|
|
12503
|
+
/**
|
|
12504
|
+
* `toolroll evals run | attempt | cases | recover` (docs/plans/evals-nightly.md, part C).
|
|
12505
|
+
*
|
|
12506
|
+
* - `run [--diagnostic] [--case a,b] [--k n] --as <you> --token <password>`: admit a campaign (a person's proof, the
|
|
12507
|
+
* no release busy) and run it to its end from this installed plane. With no flags it is canonical,
|
|
12508
|
+
* refused while any case is a draft. Each attempt is a child process: `evals attempt`.
|
|
12509
|
+
* - `attempt <campaign> <case> <n> --world <dir> --db <world db>`: one attempt's world (eval-world.ts). Only under its
|
|
12510
|
+
* runner's dispatch gate, and only on the world's own database.
|
|
12511
|
+
* - `cases`: each installed case, its version, its pin, and whether a person has validated it yet.
|
|
12512
|
+
* - `recover [--abandon <campaign>]`: settle campaigns whose runner stopped; with --abandon, a person ends a stuck one.
|
|
12513
|
+
*/
|
|
12514
|
+
export async function evalsCommand(positional, flags, context) {
|
|
12515
|
+
const { write, json } = context;
|
|
12516
|
+
// Loaded when an eval command runs, so no other command (nor the console's bundle) pays for the eval runner.
|
|
12517
|
+
const { admitEvalCampaign, campaignSpending, EVALS_ROOT, installedBuildDigest, loadEvalSet, planCampaign, recoverEvalCampaigns, runEvalCampaign } = await import("./evals.js");
|
|
12518
|
+
const { inEvalWorld } = await import("./eval-gate.js");
|
|
12519
|
+
const { evalCliEntry, runEvalWorld } = await import("./eval-world.js");
|
|
12520
|
+
const action = positional[0];
|
|
12521
|
+
const command = `evals${action === undefined ? "" : ` ${action}`}`;
|
|
12522
|
+
const usage = "Use: toolroll evals run [--diagnostic] [--case <ids>] [--k <n>] --as <you> --token <password> | toolroll evals cases | toolroll evals recover [--abandon <campaign> --as <you> --token <password>]";
|
|
12523
|
+
if (action === "recover" && flags.has("abandon"))
|
|
12524
|
+
return evalsAbandonCommand(flags, context);
|
|
12525
|
+
if (action === "recover") {
|
|
12526
|
+
// A campaign whose runner stopped: its unfinished attempts recorded as interrupted, then it ends incomplete.
|
|
12527
|
+
const recovered = recoverEvalCampaigns(context.store, context.clock());
|
|
12528
|
+
return succeed(write, json, command, { campaigns: recovered }, () => recovered.length === 0 ? ["No campaign is running."] : recovered.map(one => `${one.id}: ${one.recovered ? "recovered" : "left running"} (${one.why})`));
|
|
12529
|
+
}
|
|
12530
|
+
if (action === "cases") {
|
|
12531
|
+
const set = loadEvalSet();
|
|
12532
|
+
if (!set.ok)
|
|
12533
|
+
return fail(write, json, command, "unreadable", set.problem, EXIT.failed);
|
|
12534
|
+
const cases = set.cases.map(one => ({ id: one.id, kind: one.kind, version: one.version, pin: one.pin, validatedBy: one.validatedBy, draft: one.validatedBy === null }));
|
|
12535
|
+
return succeed(write, json, command, { manifestDigest: set.manifestDigest, cases }, () => cases.map(one => `${one.id} (${one.kind}) ${one.version.slice(0, 12)} on ${one.pin.model}: ${one.draft ? "draft, awaiting a person's validation" : `validated by ${one.validatedBy}`}`));
|
|
12536
|
+
}
|
|
12537
|
+
if (action === "attempt") {
|
|
12538
|
+
// Only its campaign's runner starts an attempt: under the runner's dispatch gate, on the world's own database.
|
|
12539
|
+
if (!inEvalWorld())
|
|
12540
|
+
return fail(write, json, command, "refused", "An eval attempt runs only from its campaign's runner.", EXIT.refused);
|
|
12541
|
+
const [, campaign, caseId, n] = positional;
|
|
12542
|
+
const world = text(flags, "world");
|
|
12543
|
+
if (campaign === undefined || caseId === undefined || n === undefined || !/^[1-9][0-9]?$/.test(n) || world === undefined)
|
|
12544
|
+
return fail(write, json, command, "usage", "Use: toolroll evals attempt <campaign> <case> <n> --world <dir> --db <world>/toolroll/orders.db", EXIT.usage);
|
|
12545
|
+
let worldDir;
|
|
12546
|
+
try {
|
|
12547
|
+
worldDir = realpathSync(world);
|
|
12548
|
+
}
|
|
12549
|
+
catch {
|
|
12550
|
+
return fail(write, json, command, "refused", "The world's folder can't be read.", EXIT.refused);
|
|
12551
|
+
}
|
|
12552
|
+
if (resolve(context.databaseFile) !== join(worldDir, "toolroll", "orders.db"))
|
|
12553
|
+
return fail(write, json, command, "refused", "An eval attempt runs only on its world's own database.", EXIT.refused);
|
|
12554
|
+
const set = loadEvalSet();
|
|
12555
|
+
const one = set.ok ? set.cases.find(item => item.id === caseId) : undefined;
|
|
12556
|
+
if (one === undefined)
|
|
12557
|
+
return fail(write, json, command, "refused", set.ok ? `No case is named ${caseId}.` : set.problem, EXIT.refused);
|
|
12558
|
+
const deny = (text(flags, "deny") ?? "").split(",").filter(path => path !== "" && isAbsolute(path));
|
|
12559
|
+
const report = await runEvalWorld(one, { worldDir, databaseFile: context.databaseFile, deny, timeoutMs: one.timeoutMinutes * 60_000 });
|
|
12560
|
+
return succeed(write, json, command, { ...report }, () => [JSON.stringify(report)]);
|
|
12561
|
+
}
|
|
12562
|
+
if (action !== "run" || positional.length !== 1)
|
|
12563
|
+
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
12564
|
+
if (inEvalWorld())
|
|
12565
|
+
return fail(write, json, command, "refused", "An eval world doesn't start campaigns of its own.", EXIT.refused);
|
|
12566
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined)
|
|
12567
|
+
return fail(write, json, command, "person-only", "Only a person starts an eval campaign: it spends money.", EXIT.refused);
|
|
12568
|
+
const k = text(flags, "k");
|
|
12569
|
+
const cases = text(flags, "case");
|
|
12570
|
+
const set = loadEvalSet();
|
|
12571
|
+
if (!set.ok)
|
|
12572
|
+
return fail(write, json, command, "unreadable", set.problem, EXIT.failed);
|
|
12573
|
+
const planned = planCampaign(set, { ...(cases === undefined ? {} : { cases: cases.split(",").filter(Boolean) }), ...(k === undefined ? {} : { k: Number(k) }), diagnostic: flag(flags, "diagnostic") });
|
|
12574
|
+
if (!planned.ok)
|
|
12575
|
+
return fail(write, json, command, "refused", planned.problem, EXIT.refused);
|
|
12576
|
+
const acting = await askCredentials(flags, context);
|
|
12577
|
+
if (acting === null)
|
|
12578
|
+
return fail(write, json, command, "usage", "An eval campaign is started with your password: add --as <you> --token <password>.", EXIT.usage);
|
|
12579
|
+
const { plan } = planned;
|
|
12580
|
+
const buildDigest = () => installedBuildDigest(PACKAGE_VERSION);
|
|
12581
|
+
const sealed = { cases: plan.cases.map(one => ({ id: one.id, version: one.version, pin: one.pin, maxDispatches: one.maxDispatches })), k: plan.k, baseline: null, evals: EVALS_ROOT };
|
|
12582
|
+
// A campaign whose runner stopped no longer holds its place.
|
|
12583
|
+
recoverEvalCampaigns(context.store, context.clock());
|
|
12584
|
+
const admitted = admitEvalCampaign(context.store, { kind: "eval", canonical: plan.canonical, manifestDigest: set.manifestDigest, buildDigest: buildDigest(), version: PACKAGE_VERSION, k: plan.k,
|
|
12585
|
+
sealed, as: acting.name, token: acting.token, now: context.clock() });
|
|
12586
|
+
if (!admitted.ok)
|
|
12587
|
+
return fail(write, json, command, admitted.reason, admitted.message, EXIT.refused);
|
|
12588
|
+
const stateDir = dirname(context.databaseFile);
|
|
12589
|
+
const entry = evalCliEntry();
|
|
12590
|
+
const keep = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "SHELL", "TMPDIR"];
|
|
12591
|
+
const ended = await runEvalCampaign(context.store, admitted.id, plan.cases, {
|
|
12592
|
+
// Beside the plane's state folder, never inside it: a world's builds read-deny the plane's state.
|
|
12593
|
+
workRoot: join(dirname(stateDir), "toolroll-eval-worlds", admitted.id),
|
|
12594
|
+
buildDigest,
|
|
12595
|
+
keepWorlds: flag(flags, "keep"),
|
|
12596
|
+
now: context.clock,
|
|
12597
|
+
command: input => ({ file: entry.file, args: [...entry.args, "evals", "attempt", input.campaign, input.caseId, String(input.n), "--world", input.worldDir, "--deny", stateDir, "--db", input.databaseFile, "--json"],
|
|
12598
|
+
env: { ...Object.fromEntries(keep.flatMap(name => process.env[name] === undefined ? [] : [[name, process.env[name]]])), XDG_CONFIG_HOME: join(input.worldDir, "config") } }),
|
|
12599
|
+
});
|
|
12600
|
+
const rows = context.store.handle.prepare("SELECT case_id, n, replaces, outcome, error_why, api_microusd, subscription_microusd, usage_unknown FROM eval_attempt WHERE campaign = ? ORDER BY case_id, n, replaces").all(admitted.id);
|
|
12601
|
+
const nullable = (value) => value === null || value === undefined ? null : Number(value);
|
|
12602
|
+
const attempts = rows.map(row => ({ case: String(row["case_id"]), n: Number(row["n"]), replaces: Number(row["replaces"]), outcome: row["outcome"] === null ? null : String(row["outcome"]),
|
|
12603
|
+
why: row["error_why"] === null ? null : String(row["error_why"]), apiMicrousd: nullable(row["api_microusd"]), subscriptionMicrousd: nullable(row["subscription_microusd"]), usageUnknown: Number(row["usage_unknown"]) === 1 }));
|
|
12604
|
+
// What was actually spent, as recorded: API dollars and subscription usage apart, unknown never shown as measured.
|
|
12605
|
+
const spent = campaignSpending(context.store, admitted.id);
|
|
12606
|
+
const dollars = (micro, unknown) => micro === null ? `unknown (${unknown} attempt${unknown === 1 ? "" : "s"} with unknown spending)` : `$${(micro / 1_000_000).toFixed(2)}`;
|
|
12607
|
+
return succeed(write, json, command, { campaign: admitted.id, canonical: plan.canonical, state: ended.state, why: ended.why, attempts, spending: spent }, () => [
|
|
12608
|
+
`Campaign ${admitted.id} (${plan.canonical ? "canonical" : "diagnostic"}) is ${ended.state}${ended.why === null ? "" : `: ${ended.why}`}.`,
|
|
12609
|
+
...attempts.map(one => `${one.case} #${one.n}${one.replaces === 0 ? "" : " (replacement)"}: ${one.outcome ?? "not finished"}${one.why === null ? "" : ` — ${one.why}`}`),
|
|
12610
|
+
`API dollars: ${dollars(spent.apiMicrousd, spent.unknownAttempts)}. Subscription usage, not billed: ${dollars(spent.subscriptionMicrousd, spent.unknownAttempts)}.`,
|
|
12611
|
+
]);
|
|
12612
|
+
}
|
|
12613
|
+
/**
|
|
12614
|
+
* `toolroll evals recover --abandon <campaign> --as <you> --token <password>` (PR #263 review): a person ends a campaign
|
|
12615
|
+
* recovery can't settle (its runner or a world's processes can't be proven gone). Its unfinished attempts are recorded
|
|
12616
|
+
* as errors and its spending as unknown, and it ends incomplete; nothing is signalled and no world is removed. A lead
|
|
12617
|
+
* token, a machine or an eval world is refused; the password is checked inside the write (evals.ts abandonEvalCampaign).
|
|
12618
|
+
*/
|
|
12619
|
+
async function evalsAbandonCommand(flags, context) {
|
|
12620
|
+
const { write, json } = context;
|
|
12621
|
+
const command = "evals recover";
|
|
12622
|
+
const { abandonEvalCampaign } = await import("./evals.js");
|
|
12623
|
+
const { inEvalWorld } = await import("./eval-gate.js");
|
|
12624
|
+
const id = text(flags, "abandon");
|
|
12625
|
+
if (id === undefined || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(id))
|
|
12626
|
+
return fail(write, json, command, "usage", "Use: toolroll evals recover --abandon <campaign> --as <you> --token <password>", EXIT.usage);
|
|
12627
|
+
if (inEvalWorld() || machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined)
|
|
12628
|
+
return fail(write, json, command, "person-only", "Only a person abandons an eval campaign.", EXIT.refused);
|
|
12629
|
+
const acting = await askCredentials(flags, context);
|
|
12630
|
+
if (acting === null)
|
|
12631
|
+
return fail(write, json, command, "usage", "A campaign is abandoned with your password: add --as <you> --token <password>.", EXIT.usage);
|
|
12632
|
+
const ended = abandonEvalCampaign(context.store, { id, as: acting.name, token: acting.token, now: context.clock() });
|
|
12633
|
+
if (!ended.ok)
|
|
12634
|
+
return fail(write, json, command, ended.reason, ended.message, EXIT.refused);
|
|
12635
|
+
return succeed(write, json, command, { campaign: id, state: "incomplete", attempts: ended.attempts, spending: "unknown" }, () => [`Campaign ${id} ended incomplete. ${ended.attempts} unfinished attempt${ended.attempts === 1 ? "" : "s"} recorded as errors; what it spent is unknown. Nothing was stopped: check for its processes yourself.`]);
|
|
12636
|
+
}
|
|
12637
|
+
/**
|
|
12638
|
+
* The saved projects an `up` start couldn't reach (upCommand): each is proved again every `everyMs` while the worker
|
|
12639
|
+
* runs, by `prove` (upProveSaved), so a folder that answers again connects without a restart. Only what the start itself
|
|
12640
|
+
* read from the saved list is here, and the set only shrinks: a line connected, removed from the saved list
|
|
12641
|
+
* (`keepOnly`) or come back as some other checkout (`revoke`) is gone for good, so a line saved again later needs its
|
|
12642
|
+
* recorded admission like any other addition. `retry` answers the proof when one is due and answered, else null.
|
|
12643
|
+
*/
|
|
12644
|
+
export function upDeferredRetry(inputs, prove, everyMs = 5_000, now = Date.now) {
|
|
12645
|
+
// Wall time, not the store's clock (a test's may stand still): this is only how often a folder is asked.
|
|
12646
|
+
const nextAt = new Map(inputs.map(input => [input, now() + everyMs]));
|
|
12647
|
+
return {
|
|
12648
|
+
has: input => nextAt.has(input),
|
|
12649
|
+
list: () => [...nextAt.keys()],
|
|
12650
|
+
retry: async (input) => {
|
|
12651
|
+
const due = nextAt.get(input);
|
|
12652
|
+
if (due === undefined || now() < due)
|
|
12653
|
+
return null;
|
|
12654
|
+
nextAt.set(input, now() + everyMs);
|
|
12655
|
+
return prove(input);
|
|
12656
|
+
},
|
|
12657
|
+
revoke: input => { nextAt.delete(input); },
|
|
12658
|
+
keepOnly: saved => {
|
|
12659
|
+
const keep = new Set(saved);
|
|
12660
|
+
let changed = false;
|
|
12661
|
+
for (const input of [...nextAt.keys()])
|
|
12662
|
+
if (!keep.has(input)) {
|
|
12663
|
+
nextAt.delete(input);
|
|
12664
|
+
changed = true;
|
|
12665
|
+
}
|
|
12666
|
+
return changed;
|
|
12667
|
+
},
|
|
12668
|
+
};
|
|
12669
|
+
}
|
|
12670
|
+
/**
|
|
12671
|
+
* Whether a saved project's folder is a Git checkout now, read without blocking this thread: `git rev-parse
|
|
12672
|
+
* --show-toplevel` (bounded, its own process), then the real paths of the saved line and of the checkout, resolved off
|
|
12673
|
+
* the thread under a deadline (folder-look.ts). Null when it isn't one, or didn't answer.
|
|
12674
|
+
*/
|
|
12675
|
+
export async function upProveSaved(gitRun, input, look = {}) {
|
|
12676
|
+
const top = await gitRun("git", ["rev-parse", "--show-toplevel"], { cwd: resolve(input) });
|
|
12677
|
+
if (top.code !== 0)
|
|
12678
|
+
return null;
|
|
12679
|
+
const answered = await lookIdentities([resolve(input), top.stdout.trim()], look);
|
|
12680
|
+
if (!answered.ok)
|
|
12681
|
+
return null;
|
|
12682
|
+
const line = answered.value.get(resolve(input)) ?? null, root = answered.value.get(top.stdout.trim()) ?? null;
|
|
12683
|
+
return line === null || root === null ? null : { line, root };
|
|
12684
|
+
}
|
|
12685
|
+
/** What `up` hands its next worker after the worker reports which saved projects it still holds start authority for:
|
|
12686
|
+
* only what both still hold. A worker's report can give up an authority, never add or give back one. */
|
|
12687
|
+
export function upNarrowDeferred(held, reported) {
|
|
12688
|
+
const kept = new Set(reported);
|
|
12689
|
+
return held.filter(one => kept.has(one));
|
|
12690
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project identities as the console uses them, without touching a person's folders on its thread.
|
|
3
|
+
*
|
|
4
|
+
* A project's identity is its real path (project.ts canonicalProject). Resolving one reads the folder, and a folder can
|
|
5
|
+
* stop answering (folder-look.ts), so the console never resolves one on a page or at startup on its own thread:
|
|
6
|
+
*
|
|
7
|
+
* - `hot` (display and grouping only): the last answer, else the path as saved. Nothing is started by asking.
|
|
8
|
+
* - `strict` (visibility and authorization): only a resolved identity; an unknown one fails closed until the next
|
|
9
|
+
* refresh resolves it, so a stored path that leads outside its root never passes on its spelling.
|
|
10
|
+
* - `vouched` (a path a request names): only a path whose identity is known, or one that is exactly a saved identity.
|
|
11
|
+
* Anything else is refused this time and resolved by the next background pass, never resolved inline.
|
|
12
|
+
* - `resolveNow` (an action that needs a path checked now, a POST): resolved off the thread under the look's deadline.
|
|
13
|
+
* - `refresh` (the console's start and its background pass): resolves what was asked about and what has gone stale.
|
|
14
|
+
*
|
|
15
|
+
* An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
|
|
16
|
+
*/
|
|
17
|
+
import { type FolderLookOptions } from "./folder-look.js";
|
|
18
|
+
export type ProjectIdentities = {
|
|
19
|
+
/** For display and grouping only: the last known identity, else the path as saved (resolved lexically). Reads
|
|
20
|
+
* nothing. Never an authorization answer: use `strict`. */
|
|
21
|
+
hot: (path: string) => string;
|
|
22
|
+
/** For authorization: the known real folder path, else null (unknown or not a folder: fail closed until resolved).
|
|
23
|
+
* Reads nothing; an unknown path is asked about on the next refresh. */
|
|
24
|
+
strict: (path: string) => string | null;
|
|
25
|
+
/** The known identity: a real folder path, null (not a folder), or undefined (never answered). Reads nothing. */
|
|
26
|
+
known: (path: string) => string | null | undefined;
|
|
27
|
+
/** A request's path: its known identity, or the path itself when it is exactly one of `saved` (projects the plane
|
|
28
|
+
* itself proved and admitted); else undefined. */
|
|
29
|
+
vouched: (path: string, saved: () => Iterable<string>) => string | undefined;
|
|
30
|
+
/** This path's identity now, off the thread: a real folder path, null, or "no-answer" past the deadline. */
|
|
31
|
+
resolveNow: (path: string) => Promise<string | null | "no-answer">;
|
|
32
|
+
/** Resolve `paths`, everything asked about since, and answers older than a minute. False when some didn't answer. */
|
|
33
|
+
refresh: (paths: Iterable<string>) => Promise<boolean>;
|
|
34
|
+
};
|
|
35
|
+
export declare function projectIdentities(look?: FolderLookOptions, now?: () => number): ProjectIdentities;
|
|
36
|
+
/** The synchronous reads, as before: for embedders and tests that pass no identities. */
|
|
37
|
+
export declare function syncProjectIdentities(): ProjectIdentities;
|