toolroll 0.9.46 → 0.9.48
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 +27 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/contracts/cli.d.ts +87 -0
- package/dist/contracts/cli.js +2 -0
- package/dist/contracts/health.d.ts +84 -0
- package/dist/contracts/health.js +11 -0
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +7 -15
- package/dist/flow-live.js +77 -115
- package/dist/flows-ui.js +15 -1
- package/dist/health-cli.d.ts +3 -0
- package/dist/health-cli.js +79 -0
- package/dist/live-bus.d.ts +89 -0
- package/dist/live-bus.js +321 -0
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +53 -1
- package/dist/operate.js +43 -25
- package/dist/planner.js +4 -3
- package/dist/read-executor-worker.d.ts +1 -0
- package/dist/read-executor-worker.js +26 -0
- package/dist/read-executor.d.ts +49 -0
- package/dist/read-executor.js +119 -0
- package/dist/request-budget.d.ts +1 -0
- package/dist/request-budget.js +6 -0
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +118 -42
- package/dist/server-telemetry.d.ts +86 -0
- package/dist/server-telemetry.js +263 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/sqlite-telemetry.d.ts +9 -0
- package/dist/sqlite-telemetry.js +98 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.js +59 -3
- package/dist/surface.js +2 -1
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +8 -0
- package/dist/task-live.js +47 -0
- package/dist/work-index.js +12 -1
- package/dist/workspace-revision.d.ts +2 -0
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { projectBatchChecks, setProjectBatchChecks } from './batch-policy.js';
|
|
|
8
8
|
import { CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, suggestQuickCommand } from './check-levels.js';
|
|
9
9
|
import { repositoryContextHtml } from './repository-context-ui.js';
|
|
10
10
|
import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
|
|
11
|
-
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
|
|
11
|
+
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf, needsYouLabelOf } from './browser-workspace.js';
|
|
12
12
|
import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
|
|
13
13
|
import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
|
|
14
14
|
import { startMaintenance } from './maintenance.js';
|
|
@@ -44,6 +44,9 @@ import { addRoutine, removeRoutine, runRoutine } from "./teammate-desk.js";
|
|
|
44
44
|
import { sendTeammateWeeklies, undoCall } from "./teammate-week.js";
|
|
45
45
|
import { answerTeammateQuestion } from "./teammate-work.js";
|
|
46
46
|
import { createFlowRooms, flowFingerprint } from "./flow-live.js";
|
|
47
|
+
import { createLiveBus, followWorkspace } from "./live-bus.js";
|
|
48
|
+
import { createTaskRooms, taskFingerprint } from "./task-live.js";
|
|
49
|
+
import { runActivityOf } from "./task-activity.js";
|
|
47
50
|
import { disconnectGoogle, finishGoogleConsent, GOOGLE_CALLBACK, googleConnected, googleConsent, readGoogleMail, saveGoogleClient } from "./google-mail.js";
|
|
48
51
|
import { mailboxAccess, readThroughImap } from "./mailbox.js";
|
|
49
52
|
import { readEmailSettings, saveEmailSettings, sendingAccount, sendThroughServer, setFlowSecret } from "./flow-actions.js";
|
|
@@ -240,6 +243,7 @@ import { createIntegrationMonitor } from "./integrations.js";
|
|
|
240
243
|
import { BACKUP_CSS, backupHtml } from "./backup-ui.js";
|
|
241
244
|
import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder } from "./backup.js";
|
|
242
245
|
import { prometheusMetrics } from "./metrics.js";
|
|
246
|
+
import { healthWords, instrumentRequest } from "./server-telemetry.js";
|
|
243
247
|
import { SPEND_CSS, spendCsv, spendHtml } from "./spend-ui.js";
|
|
244
248
|
import { RETENTION_CSS, retentionHtml } from "./retention-ui.js";
|
|
245
249
|
import { STORAGE_CSS, storageHtml } from "./storage-ui.js";
|
|
@@ -289,6 +293,7 @@ import { installMethod } from "./install-method.js";
|
|
|
289
293
|
import { PACKAGE_VERSION } from "./version.js";
|
|
290
294
|
import { updateNoticeWords } from "./update-notice.js";
|
|
291
295
|
import { whenHtml } from "./when-html.js";
|
|
296
|
+
import { ReadExecutor, attachReadExecutor, defaultReadWorkers } from "./read-executor.js";
|
|
292
297
|
const SESSION_COOKIE = "standing-orders_session";
|
|
293
298
|
/** Stands in for "no project" in a ceiling: no folder resolves to it, so it admits nothing. */
|
|
294
299
|
const NO_PROJECT = "/\0no-project";
|
|
@@ -458,6 +463,11 @@ export function createDecisionServer(options) {
|
|
|
458
463
|
}
|
|
459
464
|
}
|
|
460
465
|
const clock = options.clock ?? (() => new Date());
|
|
466
|
+
// Heavy reads (the work index behind `task list`) run on read-only worker connections, off the request loop.
|
|
467
|
+
const readFile = store.isDemo() ? null : store.databaseFile();
|
|
468
|
+
const readWorkers = options.readWorkers ?? (process.env["VITEST"] !== undefined ? 0 : defaultReadWorkers());
|
|
469
|
+
const reads = readFile === null || readWorkers < 1 ? null : new ReadExecutor(readFile, readWorkers);
|
|
470
|
+
const detachReads = reads === null ? () => { } : attachReadExecutor(store, reads);
|
|
461
471
|
/** Task checkouts, for Settings → Storage: the pool's own root, or the folder beside the database. */
|
|
462
472
|
const storagePool = (databaseFile) => new WorktreePool(store, { root: options.poolRoot ?? join(dirname(databaseFile), "worktrees") });
|
|
463
473
|
const install = installMethod(options.installBin);
|
|
@@ -748,7 +758,11 @@ export function createDecisionServer(options) {
|
|
|
748
758
|
/** One-click connections on their way: the service's sign-in page and back, 15 minutes at most. */
|
|
749
759
|
const connectVisits = new Map();
|
|
750
760
|
/** Flows open in a browser (v88): who's here, and a nudge when one changes. */
|
|
751
|
-
|
|
761
|
+
// Live views push on write (live-bus.ts): the revision triggers move, the rooms hear it.
|
|
762
|
+
const liveBus = createLiveBus();
|
|
763
|
+
const liveFollower = followWorkspace(store, () => workspaceRevision.current(), liveBus, { file: store.databaseFile() });
|
|
764
|
+
const flowRooms = createFlowRooms(flow => flowFingerprint(store, flow), { bus: liveBus });
|
|
765
|
+
const taskRooms = createTaskRooms(root => taskFingerprint(store, root, clock()), { bus: liveBus });
|
|
752
766
|
const teamChatProvider = () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; };
|
|
753
767
|
const team = createTeamRuntime({ store, repos: codingProjects, evidenceRoot, clock, workspaceRevision,
|
|
754
768
|
...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
|
|
@@ -838,6 +852,7 @@ export function createDecisionServer(options) {
|
|
|
838
852
|
/** The task behind a resource, for the ceiling check; null = no ref (visible). */
|
|
839
853
|
const taskRepoOf = (taskRef) => store.refForId(taskRef)?.repo ?? null;
|
|
840
854
|
const server = createServer((request, response) => {
|
|
855
|
+
instrumentRequest(store.telemetry, request, response);
|
|
841
856
|
void handle(request, response).catch(error => {
|
|
842
857
|
// Every unhandled error is logged (v99): the path and the message, never the request's body or query.
|
|
843
858
|
logEvent("error", "serve.error", { method: request.method, path: redactedPath(new URL(request.url ?? "/", "http://placeholder").pathname), error: error instanceof Error ? error.message : String(error) });
|
|
@@ -854,6 +869,8 @@ export function createDecisionServer(options) {
|
|
|
854
869
|
}
|
|
855
870
|
});
|
|
856
871
|
});
|
|
872
|
+
server.once('listening', () => store.telemetry.start());
|
|
873
|
+
server.once('close', () => store.telemetry.stop());
|
|
857
874
|
// --public-url (arc 3): validated to EXACTLY an https origin. Its host
|
|
858
875
|
// joins the allowed set, its origin authorizes POSTs, and cookies turn
|
|
859
876
|
// Secure. Anything malformed refuses at startup, loudly.
|
|
@@ -1831,9 +1848,9 @@ export function createDecisionServer(options) {
|
|
|
1831
1848
|
}
|
|
1832
1849
|
const read = new Set(["/settings/flows", "/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
|
|
1833
1850
|
const write = new Set(["/settings/flows/on", "/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
|
|
1834
|
-
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1851
|
+
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence|/live)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1835
1852
|
const resource = request.method === "GET"
|
|
1836
|
-
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
|
|
1853
|
+
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/live|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
|
|
1837
1854
|
: /^\/d\/[0-9]{1,15}\/answer$/.test(path) || /^\/routines\/[0-9]{1,15}\/(approve|refresh|pause|resume|run-now)$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path) || path === "/flows/import" || /^\/flows\/[0-9]{1,15}\/(save|cards|archive|scripts)$/.test(path) || /^\/flows\/[0-9]{1,15}\/cards\/[0-9]{1,15}\/(move|decide|cancel|comment|assign|watch)$/.test(path) || /^\/flows\/[0-9]{1,15}\/triggers(\/[0-9]{1,15}\/(pause|resume|remove|check|press|renew|secret|share|unshare))?$/.test(path) || /^\/r\/[0-9]{1,15}\/(note|comment|revise|draft-repair|checks|add-tests)$/.test(path);
|
|
1838
1855
|
if (!(request.method === "GET" ? read : write).has(path) && task === null && !resource) {
|
|
1839
1856
|
refuse(response, who, 403, "This area requires instance access. Your account operates within its assigned projects.", "/projects");
|
|
@@ -1891,6 +1908,10 @@ export function createDecisionServer(options) {
|
|
|
1891
1908
|
}
|
|
1892
1909
|
project = wanted;
|
|
1893
1910
|
}
|
|
1911
|
+
// Every page's Tasks count covers this one project view, whatever project the page itself shows.
|
|
1912
|
+
const facts = requestContext.getStore();
|
|
1913
|
+
if (facts !== undefined)
|
|
1914
|
+
facts.lens = project;
|
|
1894
1915
|
// Exact result links carry their own read context. Check the stored
|
|
1895
1916
|
// placement against both account and instance access before using it;
|
|
1896
1917
|
// viewing a result never changes the session's selected project, and
|
|
@@ -1975,7 +1996,7 @@ export function createDecisionServer(options) {
|
|
|
1975
1996
|
url.pathname !== "/menu" &&
|
|
1976
1997
|
url.pathname !== "/recipes" &&
|
|
1977
1998
|
// A flow names its own project; the list spans every project.
|
|
1978
|
-
url.pathname !== "/flows" && url.pathname !== "/flows/import" && !/^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1999
|
+
url.pathname !== "/flows" && url.pathname !== "/flows/import" && !/^\/flows\/[0-9]{1,15}(\/insights|\/export|\/live|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1979
2000
|
// The gallery (Flows → New) asks for its project on each template's page.
|
|
1980
2001
|
url.pathname !== "/flows/new" && !/^\/flows\/new\/[a-z-]{1,40}$/.test(url.pathname) &&
|
|
1981
2002
|
// Teammates (v92) name their own project, like flows.
|
|
@@ -1983,7 +2004,7 @@ export function createDecisionServer(options) {
|
|
|
1983
2004
|
// New work names its project in the form (a dropdown of known
|
|
1984
2005
|
// projects); /tasks/add still admits the posted repo on its own.
|
|
1985
2006
|
url.pathname !== "/tasks/new" && url.pathname !== "/tasks/add" &&
|
|
1986
|
-
!/^\/t\/[^/]+(\/evidence)?$/.test(url.pathname) &&
|
|
2007
|
+
!/^\/t\/[^/]+(\/evidence|\/live)?$/.test(url.pathname) &&
|
|
1987
2008
|
!/^\/r\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1988
2009
|
!url.pathname.startsWith("/d/") && !url.pathname.startsWith("/contest/") &&
|
|
1989
2010
|
url.pathname !== "/projects" &&
|
|
@@ -1993,7 +2014,7 @@ export function createDecisionServer(options) {
|
|
|
1993
2014
|
url.pathname !== "/lead/status" && url.pathname !== "/onboarding/phone/dismiss" &&
|
|
1994
2015
|
!url.pathname.startsWith("/chat/demo/") &&
|
|
1995
2016
|
!/^\/chat\/action\/[0-9]{1,15}$/.test(url.pathname) &&
|
|
1996
|
-
!url.pathname.startsWith("/settings") && url.pathname !== "/logout" && url.pathname !== "/people" && url.pathname !== "/ledger" && url.pathname !== "/ledger/export" && url.pathname !== "/metrics" && url.pathname !== "/spend" && url.pathname !== "/spend/budget" &&
|
|
2017
|
+
!url.pathname.startsWith("/settings") && url.pathname !== "/logout" && url.pathname !== "/people" && url.pathname !== "/ledger" && url.pathname !== "/ledger/export" && url.pathname !== "/metrics" && url.pathname !== "/health" && url.pathname !== "/spend" && url.pathname !== "/spend/budget" &&
|
|
1997
2018
|
!(url.pathname === "/board" && url.searchParams.get("scope") === "all");
|
|
1998
2019
|
if (needsProject)
|
|
1999
2020
|
return redirect(response, `/projects?return=${encodeURIComponent(safeReturn(url.pathname + url.search))}`);
|
|
@@ -2276,6 +2297,8 @@ export function createDecisionServer(options) {
|
|
|
2276
2297
|
const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
|
|
2277
2298
|
if (docked !== null)
|
|
2278
2299
|
page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
|
|
2300
|
+
// Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
|
|
2301
|
+
page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
|
|
2279
2302
|
return sendScreen(response, 200, page);
|
|
2280
2303
|
}
|
|
2281
2304
|
if (url.pathname === "/next") {
|
|
@@ -3057,6 +3080,26 @@ export function createDecisionServer(options) {
|
|
|
3057
3080
|
const chrome = chromeFor(project, "tasks");
|
|
3058
3081
|
return sendScreen(response, 200, newTaskPage(chrome, project, csrf, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
|
|
3059
3082
|
}
|
|
3083
|
+
const taskLive = matchTaskPath(url.pathname, "/live$");
|
|
3084
|
+
if (taskLive !== null) {
|
|
3085
|
+
// The live task page: a nudge the moment the task changes, and who else
|
|
3086
|
+
// has it open. Hints only — the page reads the task the usual way.
|
|
3087
|
+
if (who.via !== "cookie")
|
|
3088
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
3089
|
+
const family = familyOf(taskLive.taskId);
|
|
3090
|
+
if (family === null || family.problem !== null)
|
|
3091
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "task" }));
|
|
3092
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "x-accel-buffering": "no" });
|
|
3093
|
+
const name = who.name, repo = family.root.repo;
|
|
3094
|
+
taskRooms.join(family.root.id, {
|
|
3095
|
+
name, response,
|
|
3096
|
+
// Rechecked while open, from the cookie and the account (never a request's context): a sign-out or a narrowed account stops it.
|
|
3097
|
+
valid: () => { const again = identify(request, false); return again !== null && again.name === name && rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo); },
|
|
3098
|
+
});
|
|
3099
|
+
request.once("close", () => { if (!response.writableEnded)
|
|
3100
|
+
response.end(); });
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3060
3103
|
const task = matchTaskPath(url.pathname, "");
|
|
3061
3104
|
if (task !== null) {
|
|
3062
3105
|
const family = familyOf(task.taskId);
|
|
@@ -3841,7 +3884,8 @@ export function createDecisionServer(options) {
|
|
|
3841
3884
|
// Console v2: the landing's live view (who is working, four counts, Catch up in tabs).
|
|
3842
3885
|
const home = focusTask === null && chatProject === null && !roomId ? chatHomeOf(who, repos, now) : null;
|
|
3843
3886
|
const withFirstRun = (shown) => firstRun === undefined && phone === undefined && home === null ? shown
|
|
3844
|
-
: { ...shown,
|
|
3887
|
+
: { ...shown, ...(home === null ? {} : { refreshSeconds: liveRefreshSeconds() }),
|
|
3888
|
+
workspace: { ...shown.workspace, ...(firstRun === undefined ? {} : { firstRun }), ...(phone === undefined ? {} : { phone }), ...(home === null ? {} : { home }) } };
|
|
3845
3889
|
if (enabled.ok && mateSession !== null && principal !== null && !ceilingStale) {
|
|
3846
3890
|
{
|
|
3847
3891
|
const said = takeMateNote(who.session.csrf, mateSession.id);
|
|
@@ -4196,6 +4240,14 @@ export function createDecisionServer(options) {
|
|
|
4196
4240
|
return sendScreen(response, 200, screen("Data", `<p><a href="/settings">Settings</a></p><h1>Data</h1>${dataExportHtml(who.session.csrf, { problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
4197
4241
|
}
|
|
4198
4242
|
// v104: Prometheus metrics, for an instance operator (a scraper sends one's API token).
|
|
4243
|
+
if (url.pathname === "/health") {
|
|
4244
|
+
if (!store.isInstanceOperator(who.name))
|
|
4245
|
+
return refuse(response, who, 403, "An instance operator reads server health.", "/");
|
|
4246
|
+
const health = store.telemetry.snapshot();
|
|
4247
|
+
return request.headers.accept?.includes('application/json')
|
|
4248
|
+
? respond(response, 200, 'application/json; charset=utf-8', JSON.stringify(health))
|
|
4249
|
+
: respond(response, 200, 'text/plain; charset=utf-8', `${healthWords(health)}\n`);
|
|
4250
|
+
}
|
|
4199
4251
|
if (url.pathname === "/metrics") {
|
|
4200
4252
|
if (!store.isInstanceOperator(who.name))
|
|
4201
4253
|
return refuse(response, who, 403, "An instance operator reads metrics.", "/");
|
|
@@ -5173,17 +5225,24 @@ export function createDecisionServer(options) {
|
|
|
5173
5225
|
catch {
|
|
5174
5226
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
5175
5227
|
}
|
|
5176
|
-
//
|
|
5177
|
-
// waiting on a person behind its Tasks link — the open project's, or
|
|
5178
|
-
// everything this person may see — so it matches that page's Needs you tab.
|
|
5179
|
-
let needsYou = 0;
|
|
5228
|
+
// A running row says what its agent did last, and when (the task page's own line).
|
|
5180
5229
|
try {
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5230
|
+
const now = clock(), live = new Map();
|
|
5231
|
+
if (crew.crew.length > 0)
|
|
5232
|
+
for (const run of store.liveRuns(now)) {
|
|
5233
|
+
const root = familyOf(run.taskId)?.root.id;
|
|
5234
|
+
if (root !== undefined && !live.has(root))
|
|
5235
|
+
live.set(root, runActivityOf(store, run, now));
|
|
5236
|
+
}
|
|
5237
|
+
if (live.size > 0)
|
|
5238
|
+
crew = { ...crew, crew: crew.crew.map(item => { const activity = live.get(item.id); return activity === undefined ? item : { ...item, activity }; }) };
|
|
5239
|
+
}
|
|
5240
|
+
catch { /* the rows keep their status; the line returns on the next read */ }
|
|
5241
|
+
// "Wake me only for these": the navigation carries the sidebar's own
|
|
5242
|
+
// Tasks count (chromeFor) and links to the project view it covers, so
|
|
5243
|
+
// it matches that page's Needs you tab on every screen.
|
|
5244
|
+
const needsYou = s.chrome.inboxCount;
|
|
5245
|
+
const tasksProject = s.chrome.inboxProject === undefined ? s.chrome.project : s.chrome.inboxProject;
|
|
5187
5246
|
// The person's own project and task conversations (v77), for the
|
|
5188
5247
|
// sidebar's chat list; a thread whose project is out of view is left out.
|
|
5189
5248
|
let chats = [];
|
|
@@ -5219,7 +5278,7 @@ export function createDecisionServer(options) {
|
|
|
5219
5278
|
catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
|
|
5220
5279
|
...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
|
|
5221
5280
|
pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
|
|
5222
|
-
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
5281
|
+
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
5223
5282
|
chats,
|
|
5224
5283
|
...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
|
|
5225
5284
|
...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
|
|
@@ -5274,12 +5333,6 @@ export function createDecisionServer(options) {
|
|
|
5274
5333
|
// functional script polls.
|
|
5275
5334
|
return page(response, status, html, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
|
|
5276
5335
|
}
|
|
5277
|
-
/** The badge cache: five seconds per project — mutations invalidate it. */
|
|
5278
|
-
const badgeCache = new Map();
|
|
5279
|
-
/** The needs-you count every surface wears for a project (workspace
|
|
5280
|
-
* package 1): Work's own Needs-you membership, cached per viewer and
|
|
5281
|
-
* project, so the rail, the phone tab, the scope bar, the project cards,
|
|
5282
|
-
* the chat overview, and the Needs-you tab never disagree. */
|
|
5283
5336
|
/** The Chat landing's live view: each agent at work now with its task and
|
|
5284
5337
|
* phase, four counts, plan-window use (never dollars: subscriptions don't
|
|
5285
5338
|
* bill per run) and Catch up's items, each tagged with its tab. Reads only. */
|
|
@@ -5289,7 +5342,8 @@ export function createDecisionServer(options) {
|
|
|
5289
5342
|
const agents = store.liveRuns(now).filter(run => admitted(run.repo)).map(run => {
|
|
5290
5343
|
const root = familyOf(run.taskId)?.root ?? null;
|
|
5291
5344
|
return { runId: run.id, taskId: root?.id ?? run.taskId, title: root?.title ?? run.title, href: taskHref(root?.id ?? run.taskId),
|
|
5292
|
-
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt
|
|
5345
|
+
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
|
|
5346
|
+
activity: runActivityOf(store, run, now) };
|
|
5293
5347
|
});
|
|
5294
5348
|
const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT });
|
|
5295
5349
|
const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT });
|
|
@@ -5323,19 +5377,13 @@ export function createDecisionServer(options) {
|
|
|
5323
5377
|
planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
|
|
5324
5378
|
};
|
|
5325
5379
|
}
|
|
5380
|
+
/** The needs-you count every surface wears, read from this request's own
|
|
5381
|
+
* Work counts (one query per request, never a cached earlier one), so a
|
|
5382
|
+
* live page's badge and its Needs you tab always agree. */
|
|
5326
5383
|
function needsYouBadge(project) {
|
|
5327
|
-
|
|
5328
|
-
const key = `${actor ?? ""}:${actor === undefined ? "" : store.accountOf(actor)?.generation}:${project ?? ""}`;
|
|
5329
|
-
const cached = badgeCache.get(key);
|
|
5330
|
-
if (cached !== undefined && Date.now() - cached.at <= 5_000)
|
|
5331
|
-
return cached;
|
|
5332
|
-
const counted = needsYouCount(project);
|
|
5333
|
-
const badge = { at: Date.now(), count: counted.count, saturated: counted.saturated };
|
|
5334
|
-
badgeCache.set(key, badge);
|
|
5335
|
-
return badge;
|
|
5384
|
+
return needsYouCount(project);
|
|
5336
5385
|
}
|
|
5337
5386
|
const bustBadge = () => {
|
|
5338
|
-
badgeCache.clear();
|
|
5339
5387
|
paletteCache.clear();
|
|
5340
5388
|
};
|
|
5341
5389
|
/** The sidebar's facts for this request. */
|
|
@@ -5343,7 +5391,12 @@ export function createDecisionServer(options) {
|
|
|
5343
5391
|
if (restricted() && !visible(project))
|
|
5344
5392
|
project = null;
|
|
5345
5393
|
const actor = requestContext.getStore()?.actor;
|
|
5346
|
-
|
|
5394
|
+
// One Tasks count per request: Needs you in the request's project view
|
|
5395
|
+
// (the open project, else every admitted project), never the page's own
|
|
5396
|
+
// project — a flow or task in another project shows the same number.
|
|
5397
|
+
const lens = requestContext.getStore()?.lens;
|
|
5398
|
+
const inboxProject = lens !== undefined && (!restricted() || visible(lens)) ? lens : project;
|
|
5399
|
+
const badge = needsYouBadge(inboxProject);
|
|
5347
5400
|
const liveMode = project === null ? null : store.activeMode(project, clock());
|
|
5348
5401
|
const liveModeTerms = liveMode === null ? null : modeTermsFromJson(liveMode.termsJson);
|
|
5349
5402
|
let projectPeek = project === null ? null : undefined;
|
|
@@ -5383,6 +5436,9 @@ export function createDecisionServer(options) {
|
|
|
5383
5436
|
...(facts === undefined ? {} : { csrf: facts.csrf, returnTo: facts.returnTo }),
|
|
5384
5437
|
inboxCount: badge.count,
|
|
5385
5438
|
inboxSaturated: badge.saturated,
|
|
5439
|
+
inboxProject,
|
|
5440
|
+
inboxLabel: needsYouLabelOf(badge.count, inboxProject === null ? null
|
|
5441
|
+
: store.listProjects().find(one => one.path === inboxProject)?.name ?? projectName(inboxProject), badge.saturated),
|
|
5386
5442
|
settings: true,
|
|
5387
5443
|
...(store.isDemo() ? { demo: true } : {}),
|
|
5388
5444
|
...(() => { const signIn = signInNotices(store); return signIn.length === 0 ? {} : { signIn }; })(),
|
|
@@ -5436,6 +5492,10 @@ export function createDecisionServer(options) {
|
|
|
5436
5492
|
.reduce((sum, one) => sum + one.totals['needs-you'], 0);
|
|
5437
5493
|
return { count, saturated: false };
|
|
5438
5494
|
}
|
|
5495
|
+
/** A live page's own beat without a docked chat: 10 s while any admitted task is building, else 30 s. */
|
|
5496
|
+
function liveRefreshSeconds() {
|
|
5497
|
+
return projectCounts(clock()).some(one => one.totals.running > 0) ? 10 : 30;
|
|
5498
|
+
}
|
|
5439
5499
|
function projectFamilyPeek(repo, now) {
|
|
5440
5500
|
const row = projectCounts(now).find(one => one.repo === repo);
|
|
5441
5501
|
return { waiting: row?.totals['needs-you'] ?? 0, running: row?.totals.running ?? 0,
|
|
@@ -6239,6 +6299,9 @@ export function createDecisionServer(options) {
|
|
|
6239
6299
|
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
6240
6300
|
presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
|
|
6241
6301
|
presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
|
|
6302
|
+
const liveRun = data.liveRunId == null ? undefined : data.runs.find(one => one.id === data.liveRunId);
|
|
6303
|
+
if (liveRun !== undefined)
|
|
6304
|
+
presentedData.activity = runActivityOf(store, liveRun, clock());
|
|
6242
6305
|
presentedData.demo = store.isDemo();
|
|
6243
6306
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
6244
6307
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
@@ -6249,11 +6312,19 @@ export function createDecisionServer(options) {
|
|
|
6249
6312
|
const docked = focus === null ? null : dockedConversation(who, focus, null, clock(), taskHref(focus.id));
|
|
6250
6313
|
// The conversation is the page's own thread here, so the page drops its Ask tab;
|
|
6251
6314
|
// only an approver is offered a road to message the agent.
|
|
6252
|
-
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null })
|
|
6315
|
+
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null }),
|
|
6316
|
+
// A signed-in page hears the moment the task changes, and who else has it open.
|
|
6317
|
+
...(who.via === "cookie" ? { live: { href: `${taskHref(family?.root.id ?? taskId)}/live`, at: (() => { try {
|
|
6318
|
+
return taskFingerprint(store, family?.root.id ?? taskId, clock());
|
|
6319
|
+
}
|
|
6320
|
+
catch {
|
|
6321
|
+
return null;
|
|
6322
|
+
} })() } } : {}) } : page.workspace?.view;
|
|
6253
6323
|
if (docked !== null)
|
|
6254
6324
|
page.workspace = { ...page.workspace, ...(taskView === undefined ? {} : { view: taskView }), conversation: docked, pageHtml: page.body };
|
|
6255
6325
|
else if (taskView !== undefined && page.workspace !== undefined)
|
|
6256
6326
|
page.workspace = { ...page.workspace, view: taskView };
|
|
6327
|
+
page.refreshSeconds = liveRefreshSeconds();
|
|
6257
6328
|
return sendScreen(response, status, page);
|
|
6258
6329
|
}
|
|
6259
6330
|
/** The routine screen: the standing order restated, its verbs, its ledger. */
|
|
@@ -12509,7 +12580,10 @@ export function createDecisionServer(options) {
|
|
|
12509
12580
|
stream.end();
|
|
12510
12581
|
chatStreams.clear();
|
|
12511
12582
|
flowRooms.close();
|
|
12512
|
-
|
|
12583
|
+
taskRooms.close();
|
|
12584
|
+
detachReads();
|
|
12585
|
+
liveFollower.close();
|
|
12586
|
+
void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop()), bounded(reads?.close())]).then(() => closeServer(callback)).catch(error => {
|
|
12513
12587
|
if (callback)
|
|
12514
12588
|
callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
|
|
12515
12589
|
else
|
|
@@ -16781,9 +16855,9 @@ function shell(title, body, options = {}) {
|
|
|
16781
16855
|
// destination it now lives under. The count rides Work: it is the
|
|
16782
16856
|
// saturated needs-you count, exactly as the inbox row wore it.
|
|
16783
16857
|
const primary = chrome.active === "code" ? "work" : primaryDestinationOf(chrome.active);
|
|
16784
|
-
const primaryItem = (key, href, label, count) => `<a href="${href}" aria-label="${escape(label)}" title="${escape(label)}"${primary === key ? ' class="active" aria-current="page"' : ""}${key === "work" && count !== undefined ? ` data-waiting="${count}"` : ""}>` +
|
|
16858
|
+
const primaryItem = (key, href, label, count) => `<a href="${href}" aria-label="${escape(key === "work" && count !== undefined && count > 0 && chrome.inboxLabel !== undefined ? `${label}, ${chrome.inboxLabel}` : label)}" title="${escape(label)}"${primary === key ? ' class="active" aria-current="page"' : ""}${key === "work" && count !== undefined ? ` data-waiting="${count}"` : ""}>` +
|
|
16785
16859
|
`<span class="glyph">${NAV_ICONS[key] ?? ""}</span>${label}` +
|
|
16786
|
-
`${count !== undefined && count > 0 ? ` <span class="count badge badge-open">${count}${chrome.inboxSaturated ? "+" : ""}</span>` : ""}</a>`;
|
|
16860
|
+
`${count !== undefined && count > 0 ? ` <span${chrome.inboxLabel === undefined ? "" : ` aria-label="${escape(chrome.inboxLabel)}" title="${escape(chrome.inboxLabel)}"`} class="count badge badge-open">${count}${chrome.inboxSaturated ? "+" : ""}</span>` : ""}</a>`;
|
|
16787
16861
|
const side = [
|
|
16788
16862
|
`<aside class="side">`,
|
|
16789
16863
|
`<div class="side-head"><a class="brand" href="${chrome.chat === true ? "/chat" : "/work"}"><span class="brand-long">Toolroll</span><span class="brand-short">T</span></a>`,
|
|
@@ -16869,7 +16943,7 @@ function shell(title, body, options = {}) {
|
|
|
16869
16943
|
// A phone tab says THAT something waits, with a dot; the number is on
|
|
16870
16944
|
// the Work views themselves (Linear Mobile's rule — one tap away).
|
|
16871
16945
|
`<a href="${href}"${primary === key ? ' class="active" aria-current="page"' : ""}><span class="glyph">${TAB_ICONS[key]}</span>${label}` +
|
|
16872
|
-
`${count !== undefined && count > 0 ? `<span class="dot-badge" role="img" aria-label="${count} waiting"></span>` : ""}</a>`;
|
|
16946
|
+
`${count !== undefined && count > 0 ? `<span class="dot-badge" role="img" aria-label="${escape(chrome.inboxLabel ?? `${count} waiting`)}"></span>` : ""}</a>`;
|
|
16873
16947
|
// Three primary tabs, the same three as the rail: chat where allowed,
|
|
16874
16948
|
// work, projects. Tools and settings sit behind the header's menu action.
|
|
16875
16949
|
const tabbar = [
|
|
@@ -22037,6 +22111,8 @@ function taskBodyParts(data) {
|
|
|
22037
22111
|
retry,
|
|
22038
22112
|
runChecks,
|
|
22039
22113
|
progress,
|
|
22114
|
+
// One quiet line under the step: what the agent did last, and when.
|
|
22115
|
+
activity: liveRun === undefined ? null : data.activity ?? null,
|
|
22040
22116
|
stop,
|
|
22041
22117
|
record,
|
|
22042
22118
|
earlier,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { BudgetLimit } from './request-budget.js';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
export declare const TELEMETRY_WINDOW_MS = 300000;
|
|
4
|
+
export declare const LATENCY_BUCKETS: number[];
|
|
5
|
+
export declare const ROUTE_FAMILIES: readonly ['cli', 'mcp', 'team', 'team-stream', 'chat', 'chat-stream', 'flows', 'flow-stream', 'tasks', 'results', 'decisions', 'sessions', 'settings', 'projects', 'assets', 'auth', 'metrics', 'health', 'console', 'other'];
|
|
6
|
+
export type RouteFamily = typeof ROUTE_FAMILIES[number];
|
|
7
|
+
export type StreamFamily = 'team-stream' | 'chat-stream' | 'flow-stream' | 'other';
|
|
8
|
+
export declare const STREAM_FAMILIES: readonly StreamFamily[];
|
|
9
|
+
export declare const QUEUE_BUCKETS: number[];
|
|
10
|
+
/** Only literals from the allowlist can become labels, including for unknown or malformed paths. */
|
|
11
|
+
export declare function routeFamily(raw: string): RouteFamily;
|
|
12
|
+
type Counts = {
|
|
13
|
+
bins: number[];
|
|
14
|
+
count: number;
|
|
15
|
+
sum: number;
|
|
16
|
+
max: number;
|
|
17
|
+
};
|
|
18
|
+
export type LatencySummary = {
|
|
19
|
+
count: number;
|
|
20
|
+
p50Ms: number | null;
|
|
21
|
+
p99Ms: number | null;
|
|
22
|
+
maxMs: number | null;
|
|
23
|
+
totalMs: number;
|
|
24
|
+
};
|
|
25
|
+
/** Lifetime histogram plus a ring of ten-second slices. Cost and memory do not grow with traffic. */
|
|
26
|
+
export declare class TimingHistogram {
|
|
27
|
+
private readonly clock;
|
|
28
|
+
readonly total: Counts;
|
|
29
|
+
private readonly slices;
|
|
30
|
+
constructor(clock: () => number);
|
|
31
|
+
observe(seconds: number, weight?: number): void;
|
|
32
|
+
private prune;
|
|
33
|
+
recent(): LatencySummary;
|
|
34
|
+
}
|
|
35
|
+
export type HealthSnapshot = {
|
|
36
|
+
version: 1;
|
|
37
|
+
windowSeconds: number;
|
|
38
|
+
uptimeSeconds: number;
|
|
39
|
+
requests: LatencySummary;
|
|
40
|
+
routes: Partial<Record<RouteFamily, LatencySummary>>;
|
|
41
|
+
eventLoop: LatencySummary;
|
|
42
|
+
writes: {
|
|
43
|
+
wait: LatencySummary;
|
|
44
|
+
hold: LatencySummary;
|
|
45
|
+
statement: LatencySummary;
|
|
46
|
+
};
|
|
47
|
+
streams: {
|
|
48
|
+
open: number;
|
|
49
|
+
queuedBytes: number;
|
|
50
|
+
maxQueuedBytes: number;
|
|
51
|
+
};
|
|
52
|
+
budgetRefusals: number;
|
|
53
|
+
};
|
|
54
|
+
export declare class ServerTelemetry {
|
|
55
|
+
readonly clock: () => number;
|
|
56
|
+
readonly requests: TimingHistogram;
|
|
57
|
+
readonly routes: Map<"assets" | "auth" | "chat" | "chat-stream" | "cli" | "console" | "decisions" | "flow-stream" | "flows" | "health" | "mcp" | "metrics" | "other" | "projects" | "results" | "sessions" | "settings" | "tasks" | "team" | "team-stream", TimingHistogram>;
|
|
58
|
+
readonly eventLoop: TimingHistogram;
|
|
59
|
+
readonly writeWait: TimingHistogram;
|
|
60
|
+
readonly writeHold: TimingHistogram;
|
|
61
|
+
readonly writeStatement: TimingHistogram;
|
|
62
|
+
readonly refusals: Map<string, number>;
|
|
63
|
+
private readonly recentRefusals;
|
|
64
|
+
private readonly streams;
|
|
65
|
+
private monitor;
|
|
66
|
+
private timer;
|
|
67
|
+
private readonly started;
|
|
68
|
+
constructor(clock?: () => number);
|
|
69
|
+
start(): void;
|
|
70
|
+
stop(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Native nanosecond samples, folded into bounded buckets once a second, never one retained object per tick.
|
|
73
|
+
* Each raw sample includes the sampling timer's own interval, so it is subtracted: an idle loop reads near zero.
|
|
74
|
+
*/
|
|
75
|
+
sampleEventLoop(): void;
|
|
76
|
+
budgetRefused(route: 'api' | 'mcp', reason: BudgetLimit | 'unavailable'): void;
|
|
77
|
+
private pruneRefusals;
|
|
78
|
+
observeRequest(family: RouteFamily, milliseconds: number): void;
|
|
79
|
+
openStream(response: ServerResponse, family: StreamFamily): void;
|
|
80
|
+
streamQueues(family?: StreamFamily): number[];
|
|
81
|
+
snapshot(): HealthSnapshot;
|
|
82
|
+
}
|
|
83
|
+
/** SSE latency is time to opening headers; a live view's lifetime is not request work. */
|
|
84
|
+
export declare function instrumentRequest(telemetry: ServerTelemetry, request: IncomingMessage, response: ServerResponse): void;
|
|
85
|
+
export declare function healthWords(health: HealthSnapshot): string;
|
|
86
|
+
export {};
|