toolroll 0.9.76 → 0.9.77
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 +192 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +28 -8
- package/dist/assignment.js +3 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/builder.d.ts +14 -0
- package/dist/builder.js +140 -41
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +267 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +52 -6
- package/dist/check-report.js +193 -8
- package/dist/checkout-cleanup.js +2 -2
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.js +5 -0
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +6 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.js +4 -1
- package/dist/exec.d.ts +7 -0
- package/dist/exec.js +7 -1
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/grant.js +5 -1
- package/dist/guides.js +4 -3
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +773 -319
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +2 -2
- package/dist/proof.js +6 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +48 -1
- package/dist/release-adapters.js +343 -10
- package/dist/release-authority.js +6 -2
- package/dist/release-auto.d.ts +38 -7
- package/dist/release-auto.js +131 -15
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.js +21 -12
- package/dist/release-coverage.js +5 -2
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +6 -1
- package/dist/release.d.ts +108 -11
- package/dist/release.js +294 -39
- package/dist/review-context.js +8 -6
- package/dist/runner.js +2 -1
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.js +7 -1
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +101 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/task-views.js +7 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +10 -0
- package/dist/store.js +25 -1
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +52 -3
- package/dist/teardown-git.js +143 -31
- package/dist/test-infra.js +1 -1
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +77 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/package.json +2 -1
|
@@ -63,6 +63,14 @@ export declare const SEATBELT_NO_OTHER_TERMINAL: string;
|
|
|
63
63
|
* any program read them (anthropics/sandbox-runtime#755). Build agents keep it (Claude Code signs in from the keychain).
|
|
64
64
|
*/
|
|
65
65
|
export declare const SEATBELT_NO_KEYCHAIN: string;
|
|
66
|
+
/**
|
|
67
|
+
* First after the profile's default deny: a mach service lookup the profile doesn't allow is still denied, unreported.
|
|
68
|
+
* On macOS 26.5 a reported mach-lookup denial now and then never answers: bootstrap_look_up blocks in launchd for good
|
|
69
|
+
* (seen as npm stuck in Node's DNS setup, dns_configuration_copy → com.apple.SystemConfiguration.DNSConfiguration, about
|
|
70
|
+
* 1 launch in 70 under load; never with the report off). Every allow after it still wins, so what is reachable is the
|
|
71
|
+
* same; only the report goes.
|
|
72
|
+
*/
|
|
73
|
+
export declare const SEATBELT_QUIET_MACH_DENY = "(deny mach-lookup (with no-report))";
|
|
66
74
|
/**
|
|
67
75
|
* Chromium, a project check's launch only: Playwright's default browser (its Chromium headless shell) registers its
|
|
68
76
|
* port rendezvous service as org.chromium.Chromium.MachPortRendezvousServer.<its pid> at start, and its helpers look
|
package/dist/sandbox-runtime.js
CHANGED
|
@@ -122,8 +122,16 @@ export const SEATBELT_NO_OTHER_TERMINAL = [
|
|
|
122
122
|
*/
|
|
123
123
|
export const SEATBELT_NO_KEYCHAIN = [
|
|
124
124
|
"; Toolroll: no keychain",
|
|
125
|
-
"(deny mach-lookup (global-name \"com.apple.SecurityServer\") (global-name \"com.apple.securityd.xpc\"))",
|
|
125
|
+
"(deny mach-lookup (with no-report) (global-name \"com.apple.SecurityServer\") (global-name \"com.apple.securityd.xpc\"))",
|
|
126
126
|
].join("\n");
|
|
127
|
+
/**
|
|
128
|
+
* First after the profile's default deny: a mach service lookup the profile doesn't allow is still denied, unreported.
|
|
129
|
+
* On macOS 26.5 a reported mach-lookup denial now and then never answers: bootstrap_look_up blocks in launchd for good
|
|
130
|
+
* (seen as npm stuck in Node's DNS setup, dns_configuration_copy → com.apple.SystemConfiguration.DNSConfiguration, about
|
|
131
|
+
* 1 launch in 70 under load; never with the report off). Every allow after it still wins, so what is reachable is the
|
|
132
|
+
* same; only the report goes.
|
|
133
|
+
*/
|
|
134
|
+
export const SEATBELT_QUIET_MACH_DENY = "(deny mach-lookup (with no-report))";
|
|
127
135
|
/**
|
|
128
136
|
* Chromium, a project check's launch only: Playwright's default browser (its Chromium headless shell) registers its
|
|
129
137
|
* port rendezvous service as org.chromium.Chromium.MachPortRendezvousServer.<its pid> at start, and its helpers look
|
|
@@ -226,11 +234,15 @@ export function withSeatbeltRules(command, rules, expected) {
|
|
|
226
234
|
const profile = words[exec + 2];
|
|
227
235
|
if (!profile.startsWith("(version 1)\n(deny default"))
|
|
228
236
|
throw shape();
|
|
237
|
+
// Where the default deny's line ends (the profile's end when it is the last line).
|
|
238
|
+
const lineEnd = profile.indexOf("\n", "(version 1)\n".length);
|
|
239
|
+
const opening = lineEnd < 0 ? profile.length : lineEnd;
|
|
229
240
|
if (!/^\/[A-Za-z0-9_./-]+$/.test(words[exec + 3]))
|
|
230
241
|
throw shape();
|
|
231
242
|
if (expected !== undefined && words[exec + 5] !== expected)
|
|
232
243
|
throw shape();
|
|
233
|
-
|
|
244
|
+
const quiet = `${profile.slice(0, opening)}\n${SEATBELT_QUIET_MACH_DENY}${profile.slice(opening)}`;
|
|
245
|
+
return srtQuote([...words.slice(0, exec + 2), `${quiet}\n${rules}\n`, ...words.slice(exec + 3)]);
|
|
234
246
|
}
|
|
235
247
|
/** The runtime's macOS invocation for `file args` run from `cwd`: a command line for /bin/sh. */
|
|
236
248
|
export function srtMacosCommand(file, args, terms, cwd = process.cwd(), temp) {
|
package/dist/scope.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The rubric's vocabulary loads before anything that reaches the plan contract (see contracts/acceptance-terms.ts).
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
|
+
import { mintSecret } from "./secret-mint.js";
|
|
3
4
|
import { passwordlessOn } from "./passwordless.js";
|
|
4
5
|
import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
|
|
5
6
|
import { validateScopeText } from "./task-text.js";
|
|
@@ -47,10 +48,11 @@ import { z } from "zod";
|
|
|
47
48
|
import {} from "./contracts/acceptance-terms.js";
|
|
48
49
|
import { parseContract } from "./contracts/contract.js";
|
|
49
50
|
import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
|
|
51
|
+
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
50
52
|
import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
|
|
51
53
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
52
54
|
function mintToken() {
|
|
53
|
-
return
|
|
55
|
+
return mintSecret(32);
|
|
54
56
|
}
|
|
55
57
|
export function hashToken(token) {
|
|
56
58
|
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
@@ -1098,6 +1100,10 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1098
1100
|
const releaseProblem = scope.releaseTerms == null ? null : releaseSealProblem(store, scope.releaseTerms);
|
|
1099
1101
|
if (releaseProblem !== null)
|
|
1100
1102
|
return { ok: false, reason: "policy", message: releaseProblem };
|
|
1103
|
+
// Filed while CI still ran: nobody approves it before the plane read exact-head CI passing (round 1, finding 4).
|
|
1104
|
+
const ciHold = releaseCiHoldOf(store, taskId);
|
|
1105
|
+
if (ciHold !== null)
|
|
1106
|
+
return { ok: false, reason: "policy", message: releaseCiHoldWords(ciHold) };
|
|
1101
1107
|
const gate = store.approvalGate(taskId, by, "person");
|
|
1102
1108
|
if (gate.verdict === "refuse")
|
|
1103
1109
|
return { ok: false, reason: gate.reason };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mintSecret(bytes: number, draw?: (size: number) => Buffer): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets a person types or pastes on a command line (passwords, invite and approver tokens, runner credentials):
|
|
3
|
+
* random base64url, never starting with "-". A value starting with "--" reads as a flag to many parsers (`--token
|
|
4
|
+
* --abc…` was refused as a missing value: about 1 in 4096 minted passwords), so it is drawn again. The CLI accepts one
|
|
5
|
+
* anyway (an older secret), but a new one never needs that.
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
export function mintSecret(bytes, draw = randomBytes) {
|
|
9
|
+
for (;;) {
|
|
10
|
+
const secret = draw(bytes).toString("base64url");
|
|
11
|
+
if (!secret.startsWith("-"))
|
|
12
|
+
return secret;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -14,19 +14,15 @@ import { signInsUnavailableWords } from "./check-access.js";
|
|
|
14
14
|
import { LIVE_VIEW_CONSENT, liveCommandsShown, liveViewConsentDigest } from "./live-detail.js";
|
|
15
15
|
import { html, htmlString, jsonScript, textHtml } from "./html.js";
|
|
16
16
|
import { browserAssetsAvailable, browserWorkspaceDocument } from './browser-shell.js';
|
|
17
|
-
import {
|
|
17
|
+
import { accessKey, countsKey, indexKey, projectSummaryOf, RevisionCache, summaryKey } from './console-chrome.js';
|
|
18
18
|
import { usageChipOf } from './limits-ui.js';
|
|
19
|
-
/** Tasks the rail lists under a project; the rest are a count that links to the project's tasks. */
|
|
20
|
-
const RAIL_TASKS = 6;
|
|
21
19
|
/** Any header a proxy adds: a request carrying one is never "from this computer" (password freshness, one-time links). */
|
|
22
20
|
const PROXY_HEADER = /^(forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-original-.+|x-cluster-client-ip|true-client-ip|cf-connecting-ip|fastly-client-ip|x-proxyuser-ip)$/i;
|
|
23
|
-
/** Asks a project names for the chime (its count is complete beyond them). */
|
|
24
|
-
const ASKS_PER_PROJECT = 100;
|
|
25
21
|
/** How often the background checks look at what is due (docs/plans/get-side-effects.md), and how often this
|
|
26
22
|
* computer's agent sign-ins are asked again. */
|
|
27
23
|
const BACKGROUND_CHECKS_MS = 30_000;
|
|
28
24
|
const LOCAL_AGENTS_EVERY_MS = 2 * 60_000;
|
|
29
|
-
import {
|
|
25
|
+
import { browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
|
|
30
26
|
import { sharedActionPayload } from './chat-actions.js';
|
|
31
27
|
import { prepareCodingContext } from './coding-context.js';
|
|
32
28
|
import { CodingWorkspace } from './coding-workspace.js';
|
|
@@ -255,6 +251,9 @@ export function createDecisionServer(options) {
|
|
|
255
251
|
};
|
|
256
252
|
};
|
|
257
253
|
const workspaceRevision = prepareWorkspaceRevision(store);
|
|
254
|
+
// The sidebar, the Tasks count and the crew: read at most once per workspace revision per reader's exact access
|
|
255
|
+
// (console-chrome.ts), and filled from a read worker before a page renders when one is attached.
|
|
256
|
+
const chromeReads = new RevisionCache(workspaceRevision, clock);
|
|
258
257
|
const workspaceIncarnation = randomBytes(16).toString('hex');
|
|
259
258
|
const workspaceValidators = new WorkspaceValidatorCache();
|
|
260
259
|
const providerHome = options.connectionHome ?? homedir();
|
|
@@ -881,6 +880,8 @@ export function createDecisionServer(options) {
|
|
|
881
880
|
get fresh() { return freshFacts.getStore() != null && ssoNowAvailable(who.name); } } : undefined,
|
|
882
881
|
refusal: (answer, status, body) => sendScreen(answer, status, screen(status === 404 ? "Not found" : "Request refused", body, { chrome: chromeFor(who.via === "cookie" ? who.session.project : null, "work") })),
|
|
883
882
|
workspaceRead,
|
|
883
|
+
freshWork: ["request", "task", "result"].some(name => url.searchParams.has(name)),
|
|
884
|
+
...(workspaceRead && typeof request.headers['if-none-match'] === 'string' ? { workspaceIfNoneMatch: request.headers['if-none-match'] } : {}),
|
|
884
885
|
workspaceRequest: url.searchParams.get('request'),
|
|
885
886
|
};
|
|
886
887
|
if (method === "GET" || method === "POST")
|
|
@@ -935,6 +936,16 @@ export function createDecisionServer(options) {
|
|
|
935
936
|
facts.workspaceValidator = { key, revision, expiresAt,
|
|
936
937
|
etag: '"' + createHash('sha256').update(`${key}:${revision}:${expiresAt}`).digest('hex') + '"' };
|
|
937
938
|
}
|
|
939
|
+
await prefetchChrome(route, url, who);
|
|
940
|
+
// The worker wait lets a sign-out, credential/access change or console ceiling change land. Prove the
|
|
941
|
+
// cookie and route again before rendering protected content or answering with a content validator.
|
|
942
|
+
if (who.via === 'cookie') {
|
|
943
|
+
const current = identify(request, false);
|
|
944
|
+
if (current?.via !== 'cookie' || current.session !== who.session)
|
|
945
|
+
return redirect(response, loginHref(url.pathname + url.search));
|
|
946
|
+
if (!projectRequestAllowed(route, route.project, url, who, request, response, null))
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
938
949
|
return handleGet(route, url, who, request, response);
|
|
939
950
|
}
|
|
940
951
|
return handlePost(route, url, who, request, response, body);
|
|
@@ -1480,7 +1491,10 @@ export function createDecisionServer(options) {
|
|
|
1480
1491
|
const project = s.chrome.active === 'chat' ? null : s.chrome.project;
|
|
1481
1492
|
crew = extras.team?.tasks ? browserCrewFromIndex(extras.team.tasks, extras.team.selected?.id) : requestFacts.workCrew?.project === project ? browserCrewFromIndex(requestFacts.workCrew.page)
|
|
1482
1493
|
// The reader's own lead's claims read "<name> is on it" here too.
|
|
1483
|
-
:
|
|
1494
|
+
: (() => {
|
|
1495
|
+
const access = { principal: 'operator', repos: managedRepos(), includeUnplaced: false, viewer: requestFacts.actor ?? null };
|
|
1496
|
+
return chromeRead(`crew\u0000${accessKey(access)}\u0000${project ?? ''}`, () => browserCrewOf(store, clock(), access, { evidenceRoot, project }));
|
|
1497
|
+
})();
|
|
1484
1498
|
}
|
|
1485
1499
|
catch {
|
|
1486
1500
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
@@ -1586,7 +1600,17 @@ export function createDecisionServer(options) {
|
|
|
1586
1600
|
workspaceValidators.set(validator.key, validator);
|
|
1587
1601
|
response.setHeader('ETag', validator.etag);
|
|
1588
1602
|
}
|
|
1589
|
-
|
|
1603
|
+
const body = JSON.stringify(workspace);
|
|
1604
|
+
// Every other page's re-read after a live `change` (chat keeps its own validator, and its receipt, task and
|
|
1605
|
+
// result reads none): when what it renders now is exactly what the page holds, it hears 304 and keeps its view
|
|
1606
|
+
// (no transfer, no re-render).
|
|
1607
|
+
if (status === 200 && validator === undefined && path.pathname !== '/chat') {
|
|
1608
|
+
const etag = '"w' + createHash('sha256').update(body).digest('base64url') + '"';
|
|
1609
|
+
response.setHeader('ETag', etag);
|
|
1610
|
+
if (requestFacts.workspaceIfNoneMatch === etag)
|
|
1611
|
+
return respond(response, 304, 'application/json; charset=utf-8', '');
|
|
1612
|
+
}
|
|
1613
|
+
return respond(response, status, 'application/json; charset=utf-8', body);
|
|
1590
1614
|
}
|
|
1591
1615
|
const nonce = randomBytes(16).toString('base64');
|
|
1592
1616
|
// React owns the conversation and its refresh/draft lifecycle. Native
|
|
@@ -1623,8 +1647,9 @@ export function createDecisionServer(options) {
|
|
|
1623
1647
|
return page(response, status, document, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
|
|
1624
1648
|
}
|
|
1625
1649
|
/** The needs-you count every surface wears, read from this request's own
|
|
1626
|
-
* Work counts (one
|
|
1627
|
-
* live page's badge and its Needs you tab
|
|
1650
|
+
* Work counts (one read per request, kept only while the workspace revision
|
|
1651
|
+
* it was read at stands), so a live page's badge and its Needs you tab
|
|
1652
|
+
* always agree. */
|
|
1628
1653
|
function needsYouBadge(project) {
|
|
1629
1654
|
return needsYouCount(project);
|
|
1630
1655
|
}
|
|
@@ -1653,44 +1678,8 @@ export function createDecisionServer(options) {
|
|
|
1653
1678
|
function projectSummary() {
|
|
1654
1679
|
const projects = sidebarProjects();
|
|
1655
1680
|
const actor = requestContext.getStore()?.actor ?? '';
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
// what its agent did last; a finished one says Done.
|
|
1659
|
-
const asking = [...states.values()].flatMap(one => one.families.slice(0, RAIL_TASKS)).filter(one => one.state === 'needs-you' || one.state === 'failed').map(one => one.rootId);
|
|
1660
|
-
const needWords = new Map();
|
|
1661
|
-
if (asking.length > 0) {
|
|
1662
|
-
try {
|
|
1663
|
-
// Every row the rail may list, in pages the index allows.
|
|
1664
|
-
for (let at = 0; at < asking.length; at += WORK_INDEX_MAX_LIMIT)
|
|
1665
|
-
for (const item of workIndexPage(store, clock(), workAccess(), { roots: asking.slice(at, at + WORK_INDEX_MAX_LIMIT), limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot }).items)
|
|
1666
|
-
needWords.set(item.rootId, item.status.detail !== '' ? item.status.detail : item.status.label);
|
|
1667
|
-
}
|
|
1668
|
-
catch { /* the rows keep their state's words */ }
|
|
1669
|
-
}
|
|
1670
|
-
// The chime's subjects: an opaque key per obligation and version, scoped to this reader (no words, no ids in it).
|
|
1671
|
-
const keyOf = (...parts) => createHash('sha256').update([actor, ...parts].join('\u0000')).digest('base64url').slice(0, 22);
|
|
1672
|
-
// Newest first, so a new ask always has a key however many older ones wait (those were heard when they were new).
|
|
1673
|
-
const asksOf = (state) => [
|
|
1674
|
-
...(state?.families ?? []).flatMap(one => one.ask === undefined ? [] : [{ at: one.ask.at, ask: { key: keyOf('task', one.rootId, one.ask.version), task: one.rootId, run: one.ask.run, at: one.ask.at } }]),
|
|
1675
|
-
...(state?.flowAsks ?? []).map(one => ({ at: one.at, ask: { key: keyOf('flow', one.card, one.stage, one.entry), task: null, run: null, at: one.at } })),
|
|
1676
|
-
].sort((a, b) => b.at.localeCompare(a.at)).slice(0, ASKS_PER_PROJECT).map(one => one.ask);
|
|
1677
|
-
const railOf = (state) => {
|
|
1678
|
-
const families = state?.families ?? [];
|
|
1679
|
-
const tasks = families.slice(0, RAIL_TASKS).flatMap((one) => {
|
|
1680
|
-
const task = store.getTask(one.rootId);
|
|
1681
|
-
const line = one.state === 'needs-you' || one.state === 'failed' ? needWords.get(one.rootId) ?? null : one.line;
|
|
1682
|
-
return task === null ? [] : [{ id: one.rootId, title: task.title, href: `/t/${encodeURIComponent(one.rootId)}`, state: one.state, line: line ?? STATE_WORDS[one.state] }];
|
|
1683
|
-
});
|
|
1684
|
-
return { tasks, more: Math.max(0, families.length - RAIL_TASKS), asks: asksOf(state) };
|
|
1685
|
-
};
|
|
1686
|
-
const rows = projects.map(one => {
|
|
1687
|
-
const state = states.get(one.path);
|
|
1688
|
-
return { path: one.path, name: one.name, href: `/work?project=${encodeURIComponent(one.path)}`, state: state?.state ?? 'idle', counts: state?.counts ?? { ...NO_COUNTS }, ...railOf(state) };
|
|
1689
|
-
});
|
|
1690
|
-
const unplaced = states.get(null);
|
|
1691
|
-
if (unplaced !== undefined && unplaced.state !== 'idle')
|
|
1692
|
-
rows.push({ path: null, name: 'No project', href: '/work', state: unplaced.state, counts: unplaced.counts, ...railOf(unplaced) });
|
|
1693
|
-
return boundProjectSummary(rows);
|
|
1681
|
+
const access = workAccess();
|
|
1682
|
+
return chromeRead(summaryKey(actor, access, projects), () => projectSummaryOf(store, clock(), access, actor, projects, evidenceRoot));
|
|
1694
1683
|
}
|
|
1695
1684
|
/** What one account may see now, as the summary room keys it: a narrowed ceiling or grant is a different key, so a
|
|
1696
1685
|
* snapshot read for the wider scope never reaches it. Null: no such account. */
|
|
@@ -1777,13 +1766,15 @@ export function createDecisionServer(options) {
|
|
|
1777
1766
|
...(scope === undefined ? {} : { scope }),
|
|
1778
1767
|
};
|
|
1779
1768
|
}
|
|
1780
|
-
// All project counts are read together
|
|
1781
|
-
//
|
|
1769
|
+
// All project counts are read together, at most once per workspace revision
|
|
1770
|
+
// for this reader's access. No task artifacts or processes are inspected to
|
|
1771
|
+
// paint a badge or a project switcher.
|
|
1782
1772
|
function projectCounts(now) {
|
|
1783
1773
|
const facts = requestContext.getStore();
|
|
1784
1774
|
if (facts?.workCounts !== undefined)
|
|
1785
1775
|
return facts.workCounts;
|
|
1786
|
-
const
|
|
1776
|
+
const access = workAccess();
|
|
1777
|
+
const counts = chromeRead(countsKey(access), () => workCountsByProject(store, now, access));
|
|
1787
1778
|
if (facts !== undefined)
|
|
1788
1779
|
facts.workCounts = counts;
|
|
1789
1780
|
return counts;
|
|
@@ -1793,6 +1784,64 @@ export function createDecisionServer(options) {
|
|
|
1793
1784
|
.reduce((sum, one) => sum + one.totals['needs-you'], 0);
|
|
1794
1785
|
return { count, saturated: false };
|
|
1795
1786
|
}
|
|
1787
|
+
/** One work index page for an already-admitted reader, through the chrome cache: a whole view the chat landing reads
|
|
1788
|
+
* on every refresh. */
|
|
1789
|
+
function sharedIndexPage(access, options) {
|
|
1790
|
+
// Receipt reconciliation, task and result reads always read work afresh (disk state included).
|
|
1791
|
+
if (requestContext.getStore()?.freshWork === true)
|
|
1792
|
+
return workIndexPage(store, clock(), access, options);
|
|
1793
|
+
return chromeRead(indexKey(access, options), () => workIndexPage(store, clock(), access, options));
|
|
1794
|
+
}
|
|
1795
|
+
/** A chrome read for this request: what it already read (its prefetch included) stands for the whole request, else
|
|
1796
|
+
* the revision cache's value, else `compute()` now. */
|
|
1797
|
+
function chromeRead(key, compute) {
|
|
1798
|
+
const facts = requestContext.getStore();
|
|
1799
|
+
const held = facts?.chromeReads?.get(key);
|
|
1800
|
+
if (held !== undefined)
|
|
1801
|
+
return held;
|
|
1802
|
+
const value = chromeReads.read(key, compute);
|
|
1803
|
+
if (facts !== undefined)
|
|
1804
|
+
(facts.chromeReads ??= new Map()).set(key, value);
|
|
1805
|
+
return value;
|
|
1806
|
+
}
|
|
1807
|
+
/** The chat landing's three whole views (chat.ts chatHomeOf), as it asks for them. */
|
|
1808
|
+
const chatHomeViews = () => ["all", "needs-you", "completed"]
|
|
1809
|
+
.map(view => ({ view, limit: WORK_INDEX_MAX_LIMIT, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) }));
|
|
1810
|
+
/** Pages that wear the chrome (the sidebar's states and the Tasks count). */
|
|
1811
|
+
const CHROME_PAGES = new Set(["home", "work", "board", "agents", "agents.live", "task.page", "run.page", "chat.page", "review", "tasks", "inbox",
|
|
1812
|
+
"next", "done", "projects.page", "flows.page", "flow.page", "settings.page", "decision.page", "menu"]);
|
|
1813
|
+
/** Before a chromed page renders: its chrome reads (and the chat landing's views) run on a read worker, so the
|
|
1814
|
+
* request thread finds them cached instead of computing them. The reader is the one this request admitted; the keys
|
|
1815
|
+
* carry its exact access (console-chrome.ts). Without a worker, or when one fails, the page computes them as before. */
|
|
1816
|
+
async function prefetchChrome(route, url, who) {
|
|
1817
|
+
if (reads === null || who.via !== "cookie" || !CHROME_PAGES.has(route.id))
|
|
1818
|
+
return;
|
|
1819
|
+
try {
|
|
1820
|
+
const actor = requestContext.getStore()?.actor ?? "";
|
|
1821
|
+
const access = workAccess(), projects = sidebarProjects(), now = clock();
|
|
1822
|
+
// Populate only the revision cache. Each answer must still pass its revision/expiry check when rendering:
|
|
1823
|
+
// another job can remain pending while an earlier answer's approval or Needs-you state changes.
|
|
1824
|
+
const fill = async (key, read) => { await chromeReads.fill(key, read); };
|
|
1825
|
+
const jobs = [
|
|
1826
|
+
fill(countsKey(access), () => reads.workCounts(now, access)),
|
|
1827
|
+
fill(summaryKey(actor, access, projects), () => reads.projectSummary(now, access, actor, projects, evidenceRoot)),
|
|
1828
|
+
];
|
|
1829
|
+
if (route.id === "chat.page") {
|
|
1830
|
+
if (!["project", "room", "team"].some(name => url.searchParams.has(name)) && requestContext.getStore()?.freshWork !== true)
|
|
1831
|
+
for (const options of chatHomeViews())
|
|
1832
|
+
jobs.push(fill(indexKey(access, options), () => reads.workIndexPage(now, access, options)));
|
|
1833
|
+
// The conversation's briefing (chat.ts: the fleet snapshot's states), read as chat reads it.
|
|
1834
|
+
const repos = managedRepos();
|
|
1835
|
+
if (repos.length > 0 && requestContext.getStore()?.freshWork !== true) {
|
|
1836
|
+
const fleet = { principal: "operator", repos, includeUnplaced: false, viewer: who.name };
|
|
1837
|
+
const options = { limit: 100, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) };
|
|
1838
|
+
jobs.push(fill(indexKey(fleet, options), () => reads.workIndexPage(now, fleet, options)));
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
await Promise.all(jobs);
|
|
1842
|
+
}
|
|
1843
|
+
catch { /* the page computes what is missing */ }
|
|
1844
|
+
}
|
|
1796
1845
|
/** A live page's own beat without a docked chat: 10 s while any admitted task is building, else 30 s. */
|
|
1797
1846
|
function liveRefreshSeconds() {
|
|
1798
1847
|
return projectCounts(clock()).some(one => one.totals.running > 0) ? 10 : 30;
|
|
@@ -2073,6 +2122,7 @@ export function createDecisionServer(options) {
|
|
|
2073
2122
|
get projectFamilyPeek() { return projectFamilyPeek; },
|
|
2074
2123
|
get needsYouBadge() { return needsYouBadge; },
|
|
2075
2124
|
get liveRefreshSeconds() { return liveRefreshSeconds; },
|
|
2125
|
+
get sharedIndexPage() { return sharedIndexPage; },
|
|
2076
2126
|
get chatKeyFor() { return chatKeyFor; },
|
|
2077
2127
|
get chatCatalog() { return chatCatalog; },
|
|
2078
2128
|
get chatCatalogKnown() { return chatCatalogKnown; },
|
package/dist/server/chat.js
CHANGED
|
@@ -44,7 +44,7 @@ import {} from "./render-pages.js";
|
|
|
44
44
|
import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
|
|
45
45
|
import { browserCaller, oneLineUa } from "./session.js";
|
|
46
46
|
export function createChatHandlers(runtime) {
|
|
47
|
-
const { firstRunStepsNow, managedRepos, leadWords, store, options, phoneSetup, visible, liveTurns, leadSaid, evidenceRoot, clock, sessions, CHAT_CANDIDATE_TTL_MS, chatFetcher, CHAT_CANDIDATES_PER_APPROVER, chatCeilingDigest, workAccess, familyOf, firstTasks, leadPrincipal, taskChatFocus, chatScopeOf, leadConversationRows, demoLeadHere, sendScreen, chromeFor, teamBrowserReply, team, teamChatProvider, runVisible, runIsLive, resultDetailOf, pullRequestTargetOf, chatEnablement, startLeadConversation, projectFamilyPeek, needsYouBadge, liveRefreshSeconds, chatKeyFor, chatCatalog, chatCatalogKnown, providerHome, mintApprovalNonce, checkLocalAgents, agentSignInCommand, bustBadge, authenticateApprover, revisionDestination, armTaskResume, consumeApprovalNonce } = runtime;
|
|
47
|
+
const { firstRunStepsNow, managedRepos, leadWords, store, options, phoneSetup, visible, liveTurns, leadSaid, evidenceRoot, clock, sessions, CHAT_CANDIDATE_TTL_MS, chatFetcher, CHAT_CANDIDATES_PER_APPROVER, chatCeilingDigest, workAccess, familyOf, firstTasks, leadPrincipal, taskChatFocus, chatScopeOf, leadConversationRows, demoLeadHere, sendScreen, chromeFor, teamBrowserReply, team, teamChatProvider, runVisible, runIsLive, resultDetailOf, pullRequestTargetOf, chatEnablement, sharedIndexPage, startLeadConversation, projectFamilyPeek, needsYouBadge, liveRefreshSeconds, chatKeyFor, chatCatalog, chatCatalogKnown, providerHome, mintApprovalNonce, checkLocalAgents, agentSignInCommand, bustBadge, authenticateApprover, revisionDestination, armTaskResume, consumeApprovalNonce } = runtime;
|
|
48
48
|
// The read-only refresh (package 2): the same JSON status as before —
|
|
49
49
|
// session binding, the send receipt, the live turn — plus a version
|
|
50
50
|
// of the DISPLAYED facts and, only when the caller's version differs,
|
|
@@ -273,7 +273,7 @@ export function createChatHandlers(runtime) {
|
|
|
273
273
|
if (repos.length > 0) {
|
|
274
274
|
try {
|
|
275
275
|
fleetSnapshot = store.chatSnapshot(repos, now);
|
|
276
|
-
const summaries =
|
|
276
|
+
const summaries = sharedIndexPage({ principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) }).items;
|
|
277
277
|
fleetSnapshot.assignmentStates = Object.fromEntries(summaries.flatMap(value => [value.rootId, value.activeTaskId].map(id => [id, { state: value.assignmentState, label: value.status.label, detail: value.status.detail }])));
|
|
278
278
|
fleetSnapshot.attentionCount = needsYouBadge(null);
|
|
279
279
|
}
|
|
@@ -1452,9 +1452,9 @@ export function createChatHandlers(runtime) {
|
|
|
1452
1452
|
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
|
|
1453
1453
|
activity: runActivityOf(store, run, now) };
|
|
1454
1454
|
});
|
|
1455
|
-
|
|
1456
|
-
const
|
|
1457
|
-
const
|
|
1455
|
+
// The three whole views, read at most once per workspace revision (and on a read worker first when one is attached).
|
|
1456
|
+
const view = (name) => sharedIndexPage(access, { view: name, limit: WORK_INDEX_MAX_LIMIT, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) });
|
|
1457
|
+
const all = view("all"), needs = view("needs-you"), done = view("completed");
|
|
1458
1458
|
const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
|
|
1459
1459
|
const counts = [
|
|
1460
1460
|
{ key: "working", label: "Working now", value: agents.length, href: "/work?view=running" },
|
|
@@ -533,6 +533,8 @@ export declare function taskBodyParts(data: {
|
|
|
533
533
|
kind: "checking" | "approved";
|
|
534
534
|
candidate: string;
|
|
535
535
|
} | null;
|
|
536
|
+
/** A release task filed while its pull request's CI still runs (a person may approve now; nothing deploys before CI passes). */
|
|
537
|
+
releaseCiRunning?: boolean;
|
|
536
538
|
/** One approval per release (release-once.md): the release this task belongs to, and whether this reader may stop it. */
|
|
537
539
|
release?: ReleaseView | null;
|
|
538
540
|
canStopRelease?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Task pages: approval, agents, plans, controls, decisions and the task body. */
|
|
2
2
|
import { networkWords } from "../egress.js";
|
|
3
3
|
import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
4
|
+
import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
|
|
4
5
|
import { writesWords } from "../writes-mode.js";
|
|
5
6
|
import { checkAccessWords } from "../check-access.js";
|
|
6
7
|
import { sandboxTermsWords } from "../project-sandbox.js";
|
|
@@ -1501,7 +1502,7 @@ export function taskBodyParts(data) {
|
|
|
1501
1502
|
scope.touches.length === 0 ? "" : html `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.join(", ")}</p>`,
|
|
1502
1503
|
html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
|
|
1503
1504
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
1504
|
-
scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
|
|
1505
|
+
scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${data.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : ""}${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
|
|
1505
1506
|
approval.approved && data.autoRelease != null
|
|
1506
1507
|
? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)} · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1507
1508
|
: approval.approved && data.release?.kind === "carried"
|
|
@@ -20,12 +20,18 @@ export type RequestFacts = {
|
|
|
20
20
|
createdTask?: string;
|
|
21
21
|
browser?: boolean;
|
|
22
22
|
workspaceRead?: boolean;
|
|
23
|
+
/** Receipt reconciliation, task and result reads (`request`, `task`, `result`): work is read afresh, never cached. */
|
|
24
|
+
freshWork?: boolean;
|
|
25
|
+
/** A workspace re-read's If-None-Match: the content validator it holds from its last read. */
|
|
26
|
+
workspaceIfNoneMatch?: string;
|
|
23
27
|
workspaceRequest?: string | null;
|
|
24
28
|
workCounts?: ReturnType<typeof workCountsByProject>;
|
|
25
29
|
workCrew?: {
|
|
26
30
|
project: string | null;
|
|
27
31
|
page: WorkIndexPage;
|
|
28
32
|
};
|
|
33
|
+
/** The chrome this request already read (console-chrome.ts keys): one snapshot per request, however often it asks. */
|
|
34
|
+
chromeReads?: Map<string, unknown>;
|
|
29
35
|
lens?: string | null;
|
|
30
36
|
workspaceValidator?: {
|
|
31
37
|
key: string;
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface ServerRuntime {
|
|
|
153
153
|
saturated: boolean;
|
|
154
154
|
};
|
|
155
155
|
liveRefreshSeconds: () => number;
|
|
156
|
+
/** One work index page for an already-admitted reader, read at most once per workspace revision (console-chrome.ts). */
|
|
157
|
+
sharedIndexPage: (access: import("../work-summary.js").WorkSummaryAccess, options: import("../work-index.js").WorkIndexOptions) => import("../work-index.js").WorkIndexPage;
|
|
156
158
|
chatKeyFor: (provider: DirectChatProviderId) => {
|
|
157
159
|
key: string;
|
|
158
160
|
source: "environment" | "stored";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { releaseViewOf } from "../release-view.js";
|
|
2
|
+
import { releaseCiRunningOf } from "../release-ci-hold.js";
|
|
2
3
|
import { releaseObligationsOf } from "../release-recheck.js";
|
|
3
4
|
import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
|
|
4
5
|
import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
|
|
5
6
|
import { originOf } from "../task-source.js";
|
|
6
|
-
import { taskBaseOf } from "../task-base.js";
|
|
7
|
+
import { recentGitReads, taskBaseOf } from "../task-base.js";
|
|
7
8
|
import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
|
|
8
9
|
import { html, joinHtml } from "../html.js";
|
|
9
10
|
import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
|
|
@@ -53,6 +54,9 @@ import {} from "../store.js";
|
|
|
53
54
|
import { taskControlOf } from "../task-control.js";
|
|
54
55
|
import { lastErrorLineOf, missedRequirementOf, workStatusOf } from "../workspace-ui.js";
|
|
55
56
|
export function createTaskViews(runtime) {
|
|
57
|
+
// The task page's base line reads Git through a frozen view of the project (~0.2-0.5 s on a large repository) and
|
|
58
|
+
// the page renders it twice per view and again on every live refresh: its answers are kept for a moment.
|
|
59
|
+
const baseGit = recentGitReads();
|
|
56
60
|
/** The first task's way to Ready shows until the first Ready result, and stays on the installation's first task. */
|
|
57
61
|
function guided(rootId, now) {
|
|
58
62
|
if (runtime.store.isDemo())
|
|
@@ -491,12 +495,13 @@ export function createTaskViews(runtime) {
|
|
|
491
495
|
return { rows, decisions: decisionsFor(runtime.store, rows), now };
|
|
492
496
|
})(),
|
|
493
497
|
steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
|
|
494
|
-
base: taskBaseOf(runtime.store, taskId),
|
|
498
|
+
base: taskBaseOf(runtime.store, taskId, baseGit),
|
|
495
499
|
steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
|
|
496
500
|
steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
|
|
497
501
|
release: ref === null ? null : releaseViewOf(runtime.store, ref.externalId, now, authority => releaseObligationsOf(runtime.store, runtime.evidenceRoot, authority)),
|
|
498
502
|
canStopRelease: who.role === "approver" && who.via === "cookie",
|
|
499
503
|
autoRelease: ref === null ? null : autoReleaseOf(runtime.store.getScope(ref.externalId)),
|
|
504
|
+
releaseCiRunning: ref !== null && releaseCiRunningOf(runtime.store, ref.externalId) !== null,
|
|
500
505
|
approvalProof: (() => {
|
|
501
506
|
// The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
|
|
502
507
|
const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
-
content?: never;
|
|
19
18
|
ok: false;
|
|
20
19
|
message: string;
|
|
21
20
|
plan?: never;
|
|
21
|
+
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
-
content?: never;
|
|
34
33
|
ok: false;
|
|
35
34
|
message: string;
|
|
36
35
|
plan?: never;
|
|
36
|
+
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/storage.d.ts
CHANGED
|
@@ -36,3 +36,8 @@ export declare function storageReport(store: Store, databaseFile: string, checko
|
|
|
36
36
|
lines: StorageLine[];
|
|
37
37
|
};
|
|
38
38
|
export declare function bytesWords(bytes: number): string;
|
|
39
|
+
/** diskBytes without holding the event loop: the same one `du` per 200 paths and the same bounds, awaited. A console
|
|
40
|
+
* measuring its checkouts (tens of GB) used to stall every page for as long as `du` took. */
|
|
41
|
+
export declare function diskBytesAsync(paths: readonly string[], run?: typeof duOnce): Promise<Map<string, number>>;
|
|
42
|
+
/** `du -sk` over a batch, its output whatever it managed (a slow disk or a failure answers what it has, or nothing). */
|
|
43
|
+
declare function duOnce(batch: readonly string[]): Promise<string>;
|
package/dist/storage.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* what slips through: stale test temp folders, leftover processes, extra
|
|
8
8
|
* staged runtimes and dependency installs unused for a week.
|
|
9
9
|
*/
|
|
10
|
-
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
11
11
|
import { lstatSync, readdirSync } from "node:fs";
|
|
12
12
|
import { basename, dirname, join } from "node:path";
|
|
13
13
|
import { DEPS_FOLDER, sharedCopies } from "./shared-deps.js";
|
|
@@ -190,3 +190,30 @@ export function bytesWords(bytes) {
|
|
|
190
190
|
return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
191
191
|
return `${Math.round(bytes / 1024)} KB`;
|
|
192
192
|
}
|
|
193
|
+
/** diskBytes without holding the event loop: the same one `du` per 200 paths and the same bounds, awaited. A console
|
|
194
|
+
* measuring its checkouts (tens of GB) used to stall every page for as long as `du` took. */
|
|
195
|
+
export async function diskBytesAsync(paths, run = duOnce) {
|
|
196
|
+
const sizes = new Map();
|
|
197
|
+
if (paths.length === 0)
|
|
198
|
+
return sizes;
|
|
199
|
+
if (process.platform !== "win32") {
|
|
200
|
+
for (let at = 0; at < paths.length; at += 200) {
|
|
201
|
+
const batch = paths.slice(at, at + 200);
|
|
202
|
+
for (const line of (await run(batch)).split("\n")) {
|
|
203
|
+
const match = /^(\d+)\t(.+)$/.exec(line);
|
|
204
|
+
if (match !== null && batch.includes(match[2]))
|
|
205
|
+
sizes.set(match[2], Number(match[1]) * 1024);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const path of paths)
|
|
210
|
+
if (!sizes.has(path))
|
|
211
|
+
sizes.set(path, treeBytes(path));
|
|
212
|
+
return sizes;
|
|
213
|
+
}
|
|
214
|
+
/** `du -sk` over a batch, its output whatever it managed (a slow disk or a failure answers what it has, or nothing). */
|
|
215
|
+
function duOnce(batch) {
|
|
216
|
+
return new Promise(resolve => {
|
|
217
|
+
execFile("du", ["-sk", ...batch], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 60_000 }, (_error, stdout) => resolve(typeof stdout === "string" ? stdout : ""));
|
|
218
|
+
});
|
|
219
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -7780,3 +7780,13 @@ export type AgentsLiveRun = Run & {
|
|
|
7780
7780
|
* Visibility is the caller's: it filters each by the console's ceiling and the viewer's access.
|
|
7781
7781
|
*/
|
|
7782
7782
|
export declare function agentsLiveRuns(store: Store, endedSince: Date): AgentsLiveRun[];
|
|
7783
|
+
/**
|
|
7784
|
+
* This runner's watches a predecessor left needing recovery (Codex review round 1, finding 2): a watch lease still
|
|
7785
|
+
* live, or one whose holder left a claim open (or a run open under a claim a reaper already released). A replacement
|
|
7786
|
+
* worker takes each over — recovering the holder's incarnation in the same transaction — before it admits new work,
|
|
7787
|
+
* whichever projects its start names (a project being removed when the predecessor died is in none of them).
|
|
7788
|
+
*/
|
|
7789
|
+
export declare function predecessorWatches(store: Store, runner: string, now: Date): {
|
|
7790
|
+
repo: string;
|
|
7791
|
+
owner: string;
|
|
7792
|
+
}[];
|