toolroll 0.9.73 → 0.9.75
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 +63 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +7 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-quiet.js +3 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +16 -4
- package/dist/contracts/console-api.d.ts +5 -1
- package/dist/contracts/console-api.js +10 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/dispose.js +35 -2
- package/dist/exec.js +13 -16
- package/dist/first-run.d.ts +20 -1
- package/dist/first-run.js +71 -0
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +5 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-learning.js +44 -6
- package/dist/project-tools.js +19 -1
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.js +4 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +3 -0
- package/dist/repository-context-ui.js +2 -1
- package/dist/repository-context.js +6 -1
- package/dist/result-follow-ups.js +20 -1
- package/dist/result-review.js +5 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +196 -29
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +91 -0
- package/dist/server/chat.js +115 -53
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +98 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +10 -5
- package/dist/server/render-pages.js +83 -74
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.js +13 -3
- package/dist/server/route-table.js +19 -2
- package/dist/server/runtime.d.ts +17 -2
- package/dist/server/settings.js +85 -32
- package/dist/server/task-views.js +1 -2
- package/dist/server/tasks.js +70 -43
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +34 -1
- package/dist/store.js +108 -11
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-work.js +3 -0
- package/dist/subscription-chat.js +5 -3
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +27 -1
- package/dist/teardown-git.js +185 -39
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/package.json +1 -1
package/dist/server/tasks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { releaseAuthorityOfTask, revokeRelease } from "../store.js";
|
|
1
|
+
import { releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
|
|
2
2
|
import { steerCapabilityOf } from "../steer-capabilities.js";
|
|
3
3
|
import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
|
|
4
4
|
/** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
|
|
@@ -12,7 +12,7 @@ import { handlersOf } from './handler-registry.js';
|
|
|
12
12
|
import { closedQuestionWords } from "../decision.js";
|
|
13
13
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
14
14
|
/** tasks handlers, moved without changing their route bodies. */
|
|
15
|
-
import { html, htmlString, joinHtml, textHtml } from "../html.js";
|
|
15
|
+
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
16
16
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
17
17
|
import { lstatSync, realpathSync } from "node:fs";
|
|
18
18
|
import {} from "node:http";
|
|
@@ -64,7 +64,7 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
|
64
64
|
import { requestContext } from "./request-context.js";
|
|
65
65
|
import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
66
66
|
import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
67
|
-
import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES,
|
|
67
|
+
import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
|
|
68
68
|
import { runEfficiencyOf } from "../run-efficiency.js";
|
|
69
69
|
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
|
|
70
70
|
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
|
|
@@ -75,7 +75,9 @@ export function createTasksHandlers(runtime) {
|
|
|
75
75
|
function noPage(who, response) {
|
|
76
76
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// `posted`: the session page's own POST (form token). A GET of changes or the shipping preview runs no git in the
|
|
79
|
+
// agent's checkout: changes go back to the session page, the preview asks for one click.
|
|
80
|
+
const showCode = (part, posted = false) => async (ctx) => {
|
|
79
81
|
const { url, who, response, project } = ctx;
|
|
80
82
|
if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
|
|
81
83
|
return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
|
|
@@ -86,6 +88,11 @@ export function createTasksHandlers(runtime) {
|
|
|
86
88
|
const selected = sessionId !== null && runtime.coding ? runtime.coding.get(sessionId, actor) : null;
|
|
87
89
|
if (selected && !codingProjectAllowed(selected.repo))
|
|
88
90
|
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
91
|
+
if (!posted && part === 'changes' && selected)
|
|
92
|
+
return redirect(response, `/code/${selected.id}`);
|
|
93
|
+
if (!posted && part === 'ship' && selected) {
|
|
94
|
+
return sendScreen(response, 200, screen('Review for shipping', html `<section class="coding-workspace coding-handoff"><h1>Review for shipping</h1><p>${selected.title}</p>${postForm(`/code/${selected.id}/ship/preview`, html `<button type="submit">Review the committed change</button>`)}<p><a href="/code/${selected.id}">Back to coding</a></p></section>`, { chrome: chromeFor(selected.repo, 'code') }));
|
|
95
|
+
}
|
|
89
96
|
if (part === 'ship' && selected) {
|
|
90
97
|
const preview = codingHandoffPreview(store, { sessionId: selected.id, actor: who.name });
|
|
91
98
|
return sendScreen(response, 200, screen('Review for shipping', codingShippingHtml(preview), { chrome: chromeFor(selected.repo, 'code') }));
|
|
@@ -207,12 +214,18 @@ export function createTasksHandlers(runtime) {
|
|
|
207
214
|
// up like the inbox: admission binds the query and every row's repo
|
|
208
215
|
// is re-proved here.
|
|
209
216
|
const view = parseWorkView(url.searchParams.get("view"));
|
|
217
|
+
// The stored-state filter the old task list had (`?state=`): an unknown state is refused, as it was there.
|
|
218
|
+
const wantedState = url.searchParams.get("state");
|
|
219
|
+
if (wantedState !== null && !TASK_STATES.includes(wantedState))
|
|
220
|
+
return refuse(response, who, 400, "no such state", "/work");
|
|
221
|
+
const state = wantedState;
|
|
210
222
|
const rollup = project === null && !unscopedMode;
|
|
211
223
|
let work;
|
|
212
224
|
try {
|
|
213
|
-
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project, root: evidenceRoot });
|
|
225
|
+
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project, root: evidenceRoot, ...(state === null ? {} : { state }) });
|
|
214
226
|
const facts = requestContext.getStore();
|
|
215
|
-
|
|
227
|
+
// A filtered page never stands in for the counts or the Crew: those read the unfiltered work.
|
|
228
|
+
if (facts !== undefined && state === null) {
|
|
216
229
|
// These counts cover the exact admitted project lens, including
|
|
217
230
|
// unplaced tasks where permitted. Crew excludes unplaced rows.
|
|
218
231
|
if (project === null)
|
|
@@ -259,6 +272,7 @@ export function createTasksHandlers(runtime) {
|
|
|
259
272
|
previous: url.searchParams.has('cursor'),
|
|
260
273
|
multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
|
|
261
274
|
now,
|
|
275
|
+
state,
|
|
262
276
|
// v105: subscription windows and monthly budgets, for whoever runs the installation.
|
|
263
277
|
limits: store.isInstanceOperator(who.name)
|
|
264
278
|
? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, subagent: id => subagentNamesOf(store.handle).get(id) ?? `Subagent ${id}` }, now)
|
|
@@ -565,24 +579,18 @@ export function createTasksHandlers(runtime) {
|
|
|
565
579
|
const { response, project } = ctx;
|
|
566
580
|
return sendScreen(response, 200, donePage(chromeFor(project, "runs"), store.listCompletedWorkScoped(project, 50), pr => store.ciFailureObserved(pr)));
|
|
567
581
|
}
|
|
582
|
+
/** The old task list, retired into /work (docs/plans/reui-polish.md slice 7): its address keeps answering, as a
|
|
583
|
+
* redirect, so links and bookmarks still land. `state` carries over (an unknown one is still refused); `template`
|
|
584
|
+
* goes to the add page, which now pre-fills it. Anything else is dropped. */
|
|
568
585
|
async function showTasks(ctx) {
|
|
569
|
-
const { url, who, response
|
|
586
|
+
const { url, who, response } = ctx;
|
|
570
587
|
const wanted = url.searchParams.get("state");
|
|
571
|
-
if (wanted !== null && !TASK_STATES.includes(wanted))
|
|
572
|
-
return refuse(response, who, 400, "no such state", "/
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const fromTemplate = url.searchParams.get("template");
|
|
578
|
-
const picked = fromTemplate === null ? null : templateByName(fromTemplate);
|
|
579
|
-
const prefill = picked !== null && picked.kind === "task"
|
|
580
|
-
? {
|
|
581
|
-
title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "),
|
|
582
|
-
acceptance: acceptanceToLines(picked.acceptance).join("\n"),
|
|
583
|
-
}
|
|
584
|
-
: null;
|
|
585
|
-
return sendScreen(response, 200, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project, wanted === null ? undefined : wanted).slice(0, 200), wanted, null, project, prefill, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
588
|
+
if (wanted !== null && !TASK_STATES.includes(wanted))
|
|
589
|
+
return refuse(response, who, 400, "no such state", "/work");
|
|
590
|
+
const template = url.searchParams.get("template");
|
|
591
|
+
if (template !== null)
|
|
592
|
+
return redirect(response, `/tasks/new?template=${encodeURIComponent(template)}`);
|
|
593
|
+
return redirect(response, wanted === null ? "/work" : `/work?state=${encodeURIComponent(wanted)}`);
|
|
586
594
|
}
|
|
587
595
|
async function showQueue(ctx) {
|
|
588
596
|
const { url, who, response, project } = ctx;
|
|
@@ -599,14 +607,11 @@ export function createTasksHandlers(runtime) {
|
|
|
599
607
|
return redirect(response, QUEUE_VIEW);
|
|
600
608
|
}
|
|
601
609
|
async function showNewTask(ctx) {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
.map(one => ({ id: one.id, title: one.title }));
|
|
608
|
-
const chrome = chromeFor(project, "tasks");
|
|
609
|
-
return sendScreen(response, 200, newTaskPage(chrome, project, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
|
|
610
|
+
const picked = (() => { const name = ctx.url.searchParams.get("template"); return name === null ? null : templateByName(name); })();
|
|
611
|
+
const prefill = picked !== null && picked.kind === "task"
|
|
612
|
+
? { title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "), acceptance: acceptanceToLines(picked.acceptance).join("\n") }
|
|
613
|
+
: null;
|
|
614
|
+
return newTaskScreen(ctx, 200, null, prefill);
|
|
610
615
|
}
|
|
611
616
|
async function showTask(ctx) {
|
|
612
617
|
const { url, who, response } = ctx;
|
|
@@ -615,7 +620,7 @@ export function createTasksHandlers(runtime) {
|
|
|
615
620
|
return noPage(who, response);
|
|
616
621
|
const family = familyOf(task.taskId);
|
|
617
622
|
if (family === null)
|
|
618
|
-
return refuse(response, who, 404, "no such task", "/
|
|
623
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
619
624
|
const version = url.searchParams.get("version");
|
|
620
625
|
if (version !== null && !family.versions.some(one => one.id === version))
|
|
621
626
|
return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
|
|
@@ -666,12 +671,12 @@ export function createTasksHandlers(runtime) {
|
|
|
666
671
|
const access = workAccess();
|
|
667
672
|
const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
|
|
668
673
|
if (family === null || !visible(family.root.repo))
|
|
669
|
-
return refuse(response, who, 404, "no such task", "/
|
|
674
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
670
675
|
if (evidence.taskId !== family.root.id)
|
|
671
676
|
return redirect(response, `${taskHref(family.root.id)}/evidence${url.search}`);
|
|
672
677
|
const pack = evidencePack(store, family.root.id, access, who.name, now, evidenceRoot);
|
|
673
678
|
if (pack === null)
|
|
674
|
-
return refuse(response, who, 404, "no such task", "/
|
|
679
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
675
680
|
if (url.searchParams.get("format") === "json") {
|
|
676
681
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
|
|
677
682
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
@@ -782,11 +787,13 @@ export function createTasksHandlers(runtime) {
|
|
|
782
787
|
const tailRaw = url.searchParams.get("ktail") ?? "";
|
|
783
788
|
const keys = { inKey: url.searchParams.get("inkey") === "1", tail: validKeyTail(tailRaw) ? tailRaw : "" };
|
|
784
789
|
if (records) {
|
|
785
|
-
|
|
790
|
+
// Command text and output only under this run's own captured answer, never the file's `open` record (v136).
|
|
791
|
+
const commands = runLiveCommands(store, found.id);
|
|
792
|
+
const shown = liveWindowRecords(window, commands, keys);
|
|
786
793
|
return respond(response, 200, "application/json", JSON.stringify({ records: shown, nextOffset: window.nextOffset, gen: window.gen, inKey: keys.inKey, keyTail: keys.tail ?? "", final, ...notesOf() }));
|
|
787
794
|
}
|
|
788
795
|
// The plain words, whatever the file's format; the offset moves by the bytes read even when nothing shows.
|
|
789
|
-
const lines = liveWindowLines(window, keys);
|
|
796
|
+
const lines = liveWindowLines(window, runLiveCommands(store, found.id), keys);
|
|
790
797
|
return respond(response, 200, "application/json", JSON.stringify({ text: lines.length === 0 ? "" : `${lines.join("\n")}\n`, nextOffset: window.nextOffset, gen: window.gen, inKey: keys.inKey, keyTail: keys.tail ?? "", final }));
|
|
791
798
|
}
|
|
792
799
|
// One result page (2026-10-02): a builder's finished result opens on
|
|
@@ -978,7 +985,7 @@ export function createTasksHandlers(runtime) {
|
|
|
978
985
|
if (who.via === "cookie") {
|
|
979
986
|
const seen = body.get("projectRevision");
|
|
980
987
|
if (seen !== null && seen !== String(who.session.projectRevision)) {
|
|
981
|
-
return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/tasks");
|
|
988
|
+
return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/tasks/new");
|
|
982
989
|
}
|
|
983
990
|
}
|
|
984
991
|
const id = (body.get("id") ?? "").trim();
|
|
@@ -998,8 +1005,10 @@ export function createTasksHandlers(runtime) {
|
|
|
998
1005
|
// finding 1): the form's `required` is a courtesy, not the guard — a
|
|
999
1006
|
// direct POST with no project open must not mint an unplaced task
|
|
1000
1007
|
// under a ceiling. Unscoped mode keeps its historic unplaced filings.
|
|
1008
|
+
// A refusal from here on is the add page again with the person's own words (the old task list kept them too).
|
|
1009
|
+
const draft = { title, goal: body.get("goal") ?? "", not: body.get("not") ?? "", touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent };
|
|
1001
1010
|
if (!unscopedMode && effective === "") {
|
|
1002
|
-
return
|
|
1011
|
+
return newTaskScreen(ctx, 400, "name a repository — no project is open, so the task must say where it belongs", draft);
|
|
1003
1012
|
}
|
|
1004
1013
|
let repo = effective;
|
|
1005
1014
|
let admitted = unscopedMode ? null : admissionList() ?? [];
|
|
@@ -1007,7 +1016,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1007
1016
|
if (rootMode && effective !== "") {
|
|
1008
1017
|
const canonical = (await authorizedProject(liveCeiling(), effective)) ? canonicalProject(effective) : null;
|
|
1009
1018
|
if (canonical === null || canonical === undefined) {
|
|
1010
|
-
return
|
|
1019
|
+
return newTaskScreen(ctx, 403, `${effective} is outside what this server was configured to show`, draft);
|
|
1011
1020
|
}
|
|
1012
1021
|
repo = canonical;
|
|
1013
1022
|
admitted = [canonical];
|
|
@@ -1063,7 +1072,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1063
1072
|
return filed;
|
|
1064
1073
|
});
|
|
1065
1074
|
if (!made.ok) {
|
|
1066
|
-
return
|
|
1075
|
+
return newTaskScreen(ctx, made.reason === "backlog-full" ? 429 : 400, made.message, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent });
|
|
1067
1076
|
}
|
|
1068
1077
|
const actionContext = requestContext.getStore();
|
|
1069
1078
|
if (actionContext !== undefined)
|
|
@@ -1248,7 +1257,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1248
1257
|
const back = taskHref(act.taskId);
|
|
1249
1258
|
const ref = store.lookupRef(act.taskId);
|
|
1250
1259
|
if (ref === null || !visible(ref.repo))
|
|
1251
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1260
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
1252
1261
|
const named = (body.get("run") ?? "").trim();
|
|
1253
1262
|
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
1254
1263
|
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
@@ -1283,7 +1292,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1283
1292
|
return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
|
|
1284
1293
|
const ref = store.lookupRef(act.taskId);
|
|
1285
1294
|
if (ref === null || !visible(ref.repo))
|
|
1286
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1295
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
1287
1296
|
const named = (body.get("run") ?? "").trim();
|
|
1288
1297
|
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
1289
1298
|
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
@@ -1733,10 +1742,10 @@ export function createTasksHandlers(runtime) {
|
|
|
1733
1742
|
function taskMutation(response, who, taskId, verb, posted, now) {
|
|
1734
1743
|
const ref = store.lookupRef(taskId);
|
|
1735
1744
|
if (ref === null || store.getTask(taskId) === null) {
|
|
1736
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1745
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
1737
1746
|
}
|
|
1738
1747
|
if (!visible(ref.repo)) {
|
|
1739
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1748
|
+
return refuse(response, who, 404, "no such task", "/work");
|
|
1740
1749
|
}
|
|
1741
1750
|
switch (verb) {
|
|
1742
1751
|
case "steer": {
|
|
@@ -2994,6 +3003,8 @@ export function createTasksHandlers(runtime) {
|
|
|
2994
3003
|
"code.state": showCode("state"),
|
|
2995
3004
|
"code.changes": showCode("changes"),
|
|
2996
3005
|
"code.ship": showCode("ship"),
|
|
3006
|
+
"code.changes-read": showCode("changes", true),
|
|
3007
|
+
"code.ship-preview": showCode("ship", true),
|
|
2997
3008
|
"code.other": showCode("unknown"),
|
|
2998
3009
|
"inbox": showInbox,
|
|
2999
3010
|
"work": showWork,
|
|
@@ -3065,5 +3076,21 @@ export function createTasksHandlers(runtime) {
|
|
|
3065
3076
|
"session.editor-links": sessionEditorLinks,
|
|
3066
3077
|
"incident.resolve": resolveIncident,
|
|
3067
3078
|
});
|
|
3079
|
+
/** The add page, once more: blank, from a shipped template (`?template=`, as the old task list did), or with a refused
|
|
3080
|
+
* filing's own words and status (the draft keeps the project revision it was written against). */
|
|
3081
|
+
function newTaskScreen(ctx, status, problem, draft) {
|
|
3082
|
+
const { who, response, project } = ctx;
|
|
3083
|
+
const chainable = store
|
|
3084
|
+
.listTasksScoped(project, undefined, 100, null)
|
|
3085
|
+
.filter(one => one.state !== "done" && one.state !== "cancelled" && visible(one.repo))
|
|
3086
|
+
.map(one => ({ id: one.id, title: one.title }));
|
|
3087
|
+
// A refused draft keeps the task it was to start after, even if the refreshed list no longer carries it.
|
|
3088
|
+
const after = draft?.values?.get("after") ?? "";
|
|
3089
|
+
if (after !== "" && !chainable.some(one => one.id === after))
|
|
3090
|
+
chainable.unshift({ id: after, title: "(as submitted)" });
|
|
3091
|
+
const chrome = chromeFor(project, "tasks");
|
|
3092
|
+
const revision = draft?.values !== undefined ? undefined : who.via === "cookie" ? who.session.projectRevision : 0;
|
|
3093
|
+
return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft));
|
|
3094
|
+
}
|
|
3068
3095
|
return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
|
|
3069
3096
|
}
|
package/dist/storage-ui.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type StorageView = {
|
|
|
13
13
|
schedule?: SweepSchedule;
|
|
14
14
|
};
|
|
15
15
|
landed?: LandedSummary;
|
|
16
|
+
/** When the checkouts were last measured, and its Check now (docs/plans/get-side-effects.md). */
|
|
17
|
+
measured?: Html;
|
|
16
18
|
};
|
|
17
19
|
export declare function storageHtml(view: StorageView, notice: {
|
|
18
20
|
said?: string | null;
|
package/dist/storage-ui.js
CHANGED
|
@@ -85,5 +85,5 @@ export function storageHtml(view, notice) {
|
|
|
85
85
|
const preview = html `<details class="clean-up" data-clean-up${notice.problem?.startsWith("Enter your Toolroll password to clean") ? html ` open` : ""}><summary>Clean up</summary><div class="preview">${plan.go.length === 0 ? html `<p>Nothing to remove now.</p>` :
|
|
86
86
|
html `<h3>Removes ${plural(plan.go.length, "checkout")}, about ${bytesWords(plan.freeBytes)}</h3><p class="meta">Their branches stay, so nothing committed is lost.</p><ul class="checkouts" data-goes>${plan.go.map(one => item(one))}</ul>`}${plan.stay.length === 0 ? "" : html `<h3>Stays (${plan.stay.length})</h3><ul class="checkouts" data-stays>${plan.stay.map(one => item(one))}</ul>`}${remove}</div></details>`;
|
|
87
87
|
const options = CLEANUP_CHOICES.map(one => html `<option value="${one.value}"${one.value === plan.cleanup ? html ` selected` : ""}>${one.label}</option>`);
|
|
88
|
-
return html `<section class="storage">${note}<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${bytesWords(plan.totalBytes)}</h2><p class="meta">${facts}</p
|
|
88
|
+
return html `<section class="storage">${note}<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${bytesWords(plan.totalBytes)}</h2><p class="meta">${facts}</p>${view.measured ?? ""}<p data-clean-bytes="${plan.freeBytes}">${frees}</p>${view.sweep === undefined ? "" : sweepHtml(view.sweep)}${view.landed === undefined ? "" : landedHtml(view.landed)}${preview}</div><h2>Automatic cleanup</h2>${postForm("/settings/storage", html `<label>Remove a finished task's clean checkout<select name="cleanup">${options}</select></label><p class="meta">Its branch stays. Checkouts with changes, commits on no branch, in use, or waiting for review are kept. When a task's work is already in main, Toolroll marks it complete and removes its checkout and local branches.</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Save</button>`, { attrs: { class: "setting" } })}</section>`;
|
|
89
89
|
}
|
package/dist/storage.js
CHANGED
|
@@ -101,7 +101,8 @@ export function diskBytes(paths) {
|
|
|
101
101
|
if (process.platform !== "win32") {
|
|
102
102
|
for (let at = 0; at < paths.length; at += 200) {
|
|
103
103
|
const batch = paths.slice(at, at + 200);
|
|
104
|
-
|
|
104
|
+
// Bounded: a slow disk leaves the rest unmeasured rather than holding everything up.
|
|
105
|
+
const du = spawnSync("du", ["-sk", ...batch], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 60_000 });
|
|
105
106
|
for (const line of (du.stdout ?? "").split("\n")) {
|
|
106
107
|
const match = /^(\d+)\t(.+)$/.exec(line);
|
|
107
108
|
if (match !== null && batch.includes(match[2]))
|
package/dist/store.d.ts
CHANGED
|
@@ -103,6 +103,9 @@ export declare const BRANCH_BASE_MOVE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS bra
|
|
|
103
103
|
* continues a paused build with a short continue frame and no note: one row per request, bound to the exact paused run;
|
|
104
104
|
* sent_at when the frame was written, ended_at when the run was no longer paused to take it. */
|
|
105
105
|
export declare const LIVE_STEER_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_pause (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,\n task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,\n request_id TEXT NOT NULL UNIQUE,\n requested_by TEXT NOT NULL,\n requested_via TEXT NOT NULL CHECK (requested_via IN ('cli','web','chat')),\n requested_at TEXT NOT NULL,\n note INTEGER REFERENCES task_steer(id) ON DELETE SET NULL,\n sent_at TEXT,\n paused_at TEXT,\n resumed_at TEXT,\n ended_at TEXT\n);\nCREATE INDEX IF NOT EXISTS run_pause_by_run ON run_pause (run, id);\nCREATE TABLE IF NOT EXISTS run_resume (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,\n task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,\n request_id TEXT NOT NULL UNIQUE,\n requested_by TEXT NOT NULL,\n requested_via TEXT NOT NULL CHECK (requested_via IN ('cli','web','chat')),\n requested_at TEXT NOT NULL,\n sent_at TEXT,\n ended_at TEXT\n);\nCREATE INDEX IF NOT EXISTS run_resume_by_run ON run_resume (run, id);\n";
|
|
106
|
+
/** v136: a run's own "Show commands in the live view" answer, captured once before its live file opens (live-detail.ts).
|
|
107
|
+
* Absent, unreadable or anything but 1 means off. */
|
|
108
|
+
export declare const RUN_LIVE_DETAIL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_live_detail (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n commands INTEGER NOT NULL CHECK (commands IN (0,1)),\n captured_at TEXT NOT NULL\n);\n-- The Agents view's Live grid (docs/plans/agents-view.md \u00A72.5): every role's running runs and its recently ended ones.\n-- (id > 0 keeps them to the grid's own query, which names it: no other query's plan is offered them.)\nCREATE INDEX IF NOT EXISTS run_live_open ON run (id) WHERE outcome IS NULL AND id > 0;\nCREATE INDEX IF NOT EXISTS run_live_recent ON run (finished_at) WHERE outcome IS NOT NULL AND id > 0;\n";
|
|
106
109
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
107
110
|
export declare const LIVE_DETAIL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS live_detail_setting (\n repo TEXT PRIMARY KEY,\n commands INTEGER NOT NULL CHECK (commands IN (0,1)),\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n-- Every run a steering note reached, kept as it happens (live-notes.ts): a\n-- note's own attached/mailed fields move to the next attempt, this does not.\nCREATE TABLE IF NOT EXISTS steer_note_run (\n note INTEGER NOT NULL REFERENCES task_steer(id) ON DELETE CASCADE,\n run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,\n how TEXT NOT NULL CHECK (how IN ('attached','mailed','read')),\n at TEXT NOT NULL,\n PRIMARY KEY (note, run, how)\n);\nCREATE INDEX IF NOT EXISTS steer_note_run_by_run ON steer_note_run (run, note);\n";
|
|
108
111
|
export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
|
|
@@ -183,7 +186,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
183
186
|
hash: string;
|
|
184
187
|
} | null;
|
|
185
188
|
};
|
|
186
|
-
export declare const SCHEMA_VERSION =
|
|
189
|
+
export declare const SCHEMA_VERSION = 136;
|
|
187
190
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
188
191
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
189
192
|
/**
|
|
@@ -2394,6 +2397,9 @@ export declare class Store {
|
|
|
2394
2397
|
signedBy: string;
|
|
2395
2398
|
absoluteExpiry: string;
|
|
2396
2399
|
} | null;
|
|
2400
|
+
/** Close every operating mode past its expiry (the background pass). Returns how many were closed. */
|
|
2401
|
+
closeExpiredModes(now: Date): number;
|
|
2402
|
+
private closeExpiredMode;
|
|
2397
2403
|
/** Sign (or renew) a mode: closes any predecessor with a typed event and
|
|
2398
2404
|
* ATOMICALLY reconciles the repo's nonterminal merge intents to the new
|
|
2399
2405
|
* posture (F2). The caller has already run the password ceremony. */
|
|
@@ -7135,6 +7141,11 @@ export declare class Store {
|
|
|
7135
7141
|
error: string;
|
|
7136
7142
|
}, now: Date): boolean;
|
|
7137
7143
|
private bumpPushFailures;
|
|
7144
|
+
/** Which conversation a page's first send started (quiet page visits): recorded with the start, in the same
|
|
7145
|
+
* transaction, so a retry of that send never starts another, even after a restart or once that one has ended. */
|
|
7146
|
+
leadStartReceipt(approver: string, request: string): {
|
|
7147
|
+
session: number;
|
|
7148
|
+
} | null;
|
|
7138
7149
|
/** A chat send receipt contains no message text. The session is part of
|
|
7139
7150
|
* its namespace; callers must prove that session before reading it. */
|
|
7140
7151
|
leadRequestReceipt(session: number, request: string): {
|
|
@@ -7747,3 +7758,25 @@ export declare function currentReleaseIssuer(): ReleaseIssuer;
|
|
|
7747
7758
|
* that can't be read — is not proof, and the operation stays for a person.
|
|
7748
7759
|
*/
|
|
7749
7760
|
export declare function releaseIssuerGone(op: ReleaseOperation): boolean;
|
|
7761
|
+
/**
|
|
7762
|
+
* Capture a run's live-view answer once, before its live file opens: the first capture stands (a second never changes
|
|
7763
|
+
* it). Returns the answer now saved for the run; any failure, or a saved row that isn't plainly 1, is commands off.
|
|
7764
|
+
*/
|
|
7765
|
+
export declare function captureRunLiveDetail(store: Store, runId: number, commands: boolean, now: Date): boolean;
|
|
7766
|
+
/** Whether a run's live view may show commands: only its own captured answer, and only when that is plainly 1. Absent
|
|
7767
|
+
* (a run from before v136, or one whose capture failed), unreadable or anything else is off. */
|
|
7768
|
+
export declare function runLiveCommands(store: Store, runId: number): boolean;
|
|
7769
|
+
/** One run as the Agents view's Live grid reads it (docs/plans/agents-view.md §2.5). */
|
|
7770
|
+
export type AgentsLiveRun = Run & {
|
|
7771
|
+
taskId: string;
|
|
7772
|
+
title: string;
|
|
7773
|
+
repo: string | null;
|
|
7774
|
+
builtIn: boolean;
|
|
7775
|
+
};
|
|
7776
|
+
/**
|
|
7777
|
+
* The Live grid's runs: every role's running run (no outcome yet) and every run that ended at or after `endedSince`,
|
|
7778
|
+
* newest first — all of them (no cutoff before the caller's visibility filter: a viewer's agent is never hidden behind
|
|
7779
|
+
* others'). Two indexed reads (run_live_open, run_live_recent), never a scan of all runs.
|
|
7780
|
+
* Visibility is the caller's: it filters each by the console's ceiling and the viewer's access.
|
|
7781
|
+
*/
|
|
7782
|
+
export declare function agentsLiveRuns(store: Store, endedSince: Date): AgentsLiveRun[];
|
package/dist/store.js
CHANGED
|
@@ -97,6 +97,7 @@ import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
|
97
97
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, subagentFilers, usd } from "./spend.js";
|
|
98
98
|
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
99
99
|
import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
100
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
100
101
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
101
102
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
102
103
|
import { configBase, envValue, namedPath } from "./names.js";
|
|
@@ -443,6 +444,19 @@ CREATE TABLE IF NOT EXISTS run_resume (
|
|
|
443
444
|
);
|
|
444
445
|
CREATE INDEX IF NOT EXISTS run_resume_by_run ON run_resume (run, id);
|
|
445
446
|
`;
|
|
447
|
+
/** v136: a run's own "Show commands in the live view" answer, captured once before its live file opens (live-detail.ts).
|
|
448
|
+
* Absent, unreadable or anything but 1 means off. */
|
|
449
|
+
export const RUN_LIVE_DETAIL_SCHEMA = `
|
|
450
|
+
CREATE TABLE IF NOT EXISTS run_live_detail (
|
|
451
|
+
run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
|
|
452
|
+
commands INTEGER NOT NULL CHECK (commands IN (0,1)),
|
|
453
|
+
captured_at TEXT NOT NULL
|
|
454
|
+
);
|
|
455
|
+
-- The Agents view's Live grid (docs/plans/agents-view.md §2.5): every role's running runs and its recently ended ones.
|
|
456
|
+
-- (id > 0 keeps them to the grid's own query, which names it: no other query's plan is offered them.)
|
|
457
|
+
CREATE INDEX IF NOT EXISTS run_live_open ON run (id) WHERE outcome IS NULL AND id > 0;
|
|
458
|
+
CREATE INDEX IF NOT EXISTS run_live_recent ON run (finished_at) WHERE outcome IS NOT NULL AND id > 0;
|
|
459
|
+
`;
|
|
446
460
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
447
461
|
export const LIVE_DETAIL_SCHEMA = `
|
|
448
462
|
CREATE TABLE IF NOT EXISTS live_detail_setting (
|
|
@@ -737,7 +751,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
737
751
|
// release_operation), each release check attempt's intended mode, sealed before it runs (release_check_attempt), the
|
|
738
752
|
// nightly full check on main and its open failures (nightly_check, main_check_failure), and a scope's release terms
|
|
739
753
|
// (task_scope.release_terms_json; saved rows read NULL: no release terms).
|
|
740
|
-
|
|
754
|
+
// v136 adds, and changes no saved row: each run's own "Show commands in the live view" answer, captured once before its
|
|
755
|
+
// live file opens (run_live_detail; docs/plans/agents-view.md §2.4). Saved runs have none, which every reader takes as
|
|
756
|
+
// commands off: permission is never recovered from a transcript or the project's current setting.
|
|
757
|
+
export const SCHEMA_VERSION = 136;
|
|
741
758
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
742
759
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
743
760
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -870,7 +887,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
870
887
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
871
888
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
872
889
|
*/
|
|
873
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135]);
|
|
890
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136]);
|
|
874
891
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
875
892
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
876
893
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -4219,6 +4236,8 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4219
4236
|
for (const [column, { definition }] of Object.entries(columns))
|
|
4220
4237
|
addColumn(db, table, column, definition);
|
|
4221
4238
|
}
|
|
4239
|
+
// v136: each run's own live-view answer (a new table only; saved runs have none: commands off).
|
|
4240
|
+
db.exec(RUN_LIVE_DETAIL_SCHEMA);
|
|
4222
4241
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4223
4242
|
// nothing to reclaim.
|
|
4224
4243
|
if (!fresh)
|
|
@@ -7145,15 +7164,10 @@ export class Store {
|
|
|
7145
7164
|
if (row === undefined)
|
|
7146
7165
|
return null;
|
|
7147
7166
|
if (Date.parse(String(row["absolute_expiry"])) <= now.getTime()) {
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
this.db
|
|
7153
|
-
.prepare("INSERT INTO operating_mode_event (mode, kind, actor, at) VALUES (?, 'expired-closed', 'the-clock', ?)")
|
|
7154
|
-
.run(Number(row["id"]), now.toISOString());
|
|
7155
|
-
this.reconcileIntentsForMode(String(row["repo"]), null, now);
|
|
7156
|
-
});
|
|
7167
|
+
// A page visit only reads (docs/plans/get-side-effects.md): it sees no mode, and the background pass or the
|
|
7168
|
+
// next path that acts closes it.
|
|
7169
|
+
if (!inReadOnlyRequest())
|
|
7170
|
+
this.closeExpiredMode(Number(row["id"]), String(row["repo"]), now);
|
|
7157
7171
|
return null;
|
|
7158
7172
|
}
|
|
7159
7173
|
return {
|
|
@@ -7165,6 +7179,34 @@ export class Store {
|
|
|
7165
7179
|
absoluteExpiry: String(row["absolute_expiry"]),
|
|
7166
7180
|
};
|
|
7167
7181
|
}
|
|
7182
|
+
/** Close every operating mode past its expiry (the background pass). Returns how many were closed. */
|
|
7183
|
+
closeExpiredModes(now) {
|
|
7184
|
+
// The same rows activeMode would close: unrevoked, signed by a standing approver, past their expiry.
|
|
7185
|
+
const rows = this.db.prepare(`SELECT operating_mode.id, operating_mode.repo, operating_mode.absolute_expiry FROM operating_mode
|
|
7186
|
+
JOIN approver ON approver.name = operating_mode.signed_by AND approver.revoked_at IS NULL AND approver.role = 'approver'
|
|
7187
|
+
WHERE operating_mode.revoked_at IS NULL`).all();
|
|
7188
|
+
let closed = 0;
|
|
7189
|
+
for (const row of rows) {
|
|
7190
|
+
if (Date.parse(String(row["absolute_expiry"])) > now.getTime())
|
|
7191
|
+
continue;
|
|
7192
|
+
this.closeExpiredMode(Number(row["id"]), String(row["repo"]), now);
|
|
7193
|
+
closed++;
|
|
7194
|
+
}
|
|
7195
|
+
return closed;
|
|
7196
|
+
}
|
|
7197
|
+
closeExpiredMode(id, repo, now) {
|
|
7198
|
+
this.transact(() => {
|
|
7199
|
+
const closed = this.db
|
|
7200
|
+
.prepare("UPDATE operating_mode SET revoked_at = ?, revoked_by = ?, revoke_reason = 'expired' WHERE id = ? AND revoked_at IS NULL")
|
|
7201
|
+
.run(now.toISOString(), "the-clock", id);
|
|
7202
|
+
if (Number(closed.changes) !== 1)
|
|
7203
|
+
return;
|
|
7204
|
+
this.db
|
|
7205
|
+
.prepare("INSERT INTO operating_mode_event (mode, kind, actor, at) VALUES (?, 'expired-closed', 'the-clock', ?)")
|
|
7206
|
+
.run(id, now.toISOString());
|
|
7207
|
+
this.reconcileIntentsForMode(repo, null, now);
|
|
7208
|
+
});
|
|
7209
|
+
}
|
|
7168
7210
|
/** Sign (or renew) a mode: closes any predecessor with a typed event and
|
|
7169
7211
|
* ATOMICALLY reconciles the repo's nonterminal merge intents to the new
|
|
7170
7212
|
* posture (F2). The caller has already run the password ceremony. */
|
|
@@ -19157,6 +19199,13 @@ export class Store {
|
|
|
19157
19199
|
.run(pairId);
|
|
19158
19200
|
}
|
|
19159
19201
|
// ---- idempotency --------------------------------------------------------
|
|
19202
|
+
/** Which conversation a page's first send started (quiet page visits): recorded with the start, in the same
|
|
19203
|
+
* transaction, so a retry of that send never starts another, even after a restart or once that one has ended. */
|
|
19204
|
+
leadStartReceipt(approver, request) {
|
|
19205
|
+
const row = this.db.prepare("SELECT result FROM mutation WHERE idempotency_key = ? AND operation = 'mate-start'")
|
|
19206
|
+
.get(`mate-start:${approver}:${request}`);
|
|
19207
|
+
return row === undefined ? null : parseStoreColumn("mutation.result", row["result"]);
|
|
19208
|
+
}
|
|
19160
19209
|
/** A chat send receipt contains no message text. The session is part of
|
|
19161
19210
|
* its namespace; callers must prove that session before reading it. */
|
|
19162
19211
|
leadRequestReceipt(session, request) {
|
|
@@ -20746,3 +20795,51 @@ function processStartedAt(pid) {
|
|
|
20746
20795
|
return null;
|
|
20747
20796
|
}
|
|
20748
20797
|
}
|
|
20798
|
+
// A run's own "Show commands in the live view" answer (schema v136, docs/plans/agents-view.md §2.4). Free functions over
|
|
20799
|
+
// the store, kept together at the end.
|
|
20800
|
+
/**
|
|
20801
|
+
* Capture a run's live-view answer once, before its live file opens: the first capture stands (a second never changes
|
|
20802
|
+
* it). Returns the answer now saved for the run; any failure, or a saved row that isn't plainly 1, is commands off.
|
|
20803
|
+
*/
|
|
20804
|
+
export function captureRunLiveDetail(store, runId, commands, now) {
|
|
20805
|
+
try {
|
|
20806
|
+
store.handle.prepare("INSERT OR IGNORE INTO run_live_detail (run, commands, captured_at) VALUES (?, ?, ?)").run(runId, commands ? 1 : 0, now.toISOString());
|
|
20807
|
+
}
|
|
20808
|
+
catch {
|
|
20809
|
+
return false;
|
|
20810
|
+
}
|
|
20811
|
+
return runLiveCommands(store, runId);
|
|
20812
|
+
}
|
|
20813
|
+
/** Whether a run's live view may show commands: only its own captured answer, and only when that is plainly 1. Absent
|
|
20814
|
+
* (a run from before v136, or one whose capture failed), unreadable or anything else is off. */
|
|
20815
|
+
export function runLiveCommands(store, runId) {
|
|
20816
|
+
try {
|
|
20817
|
+
const row = store.handle.prepare("SELECT commands FROM run_live_detail WHERE run = ?").get(runId);
|
|
20818
|
+
return row !== undefined && row["commands"] === 1;
|
|
20819
|
+
}
|
|
20820
|
+
catch {
|
|
20821
|
+
return false;
|
|
20822
|
+
}
|
|
20823
|
+
}
|
|
20824
|
+
/**
|
|
20825
|
+
* The Live grid's runs: every role's running run (no outcome yet) and every run that ended at or after `endedSince`,
|
|
20826
|
+
* newest first — all of them (no cutoff before the caller's visibility filter: a viewer's agent is never hidden behind
|
|
20827
|
+
* others'). Two indexed reads (run_live_open, run_live_recent), never a scan of all runs.
|
|
20828
|
+
* Visibility is the caller's: it filters each by the console's ceiling and the viewer's access.
|
|
20829
|
+
*/
|
|
20830
|
+
export function agentsLiveRuns(store, endedSince) {
|
|
20831
|
+
const rows = store.handle.prepare(`SELECT run.*, task_ref.external_id AS live_task_id, task_ref.repo AS live_task_repo, task.title AS live_task_title, task_ref.backend AS live_task_backend
|
|
20832
|
+
FROM (SELECT id FROM run INDEXED BY run_live_open WHERE outcome IS NULL AND id > 0
|
|
20833
|
+
UNION SELECT id FROM run INDEXED BY run_live_recent WHERE outcome IS NOT NULL AND id > 0 AND finished_at >= ?) AS picked
|
|
20834
|
+
JOIN run ON run.id = picked.id
|
|
20835
|
+
JOIN task_ref ON task_ref.id = run.task_ref
|
|
20836
|
+
LEFT JOIN task ON task_ref.backend = ? AND task.id = task_ref.external_id
|
|
20837
|
+
ORDER BY run.id DESC`).all(endedSince.toISOString(), BUILT_IN);
|
|
20838
|
+
return rows.map(row => ({
|
|
20839
|
+
...readRun(row),
|
|
20840
|
+
taskId: String(row["live_task_id"]),
|
|
20841
|
+
builtIn: row["live_task_backend"] === BUILT_IN,
|
|
20842
|
+
title: row["live_task_title"] === null || row["live_task_title"] === undefined ? String(row["live_task_id"]) : String(row["live_task_title"]),
|
|
20843
|
+
repo: row["live_task_repo"] === null || row["live_task_repo"] === undefined ? null : String(row["live_task_repo"]),
|
|
20844
|
+
}));
|
|
20845
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose event is this (docs/plans/agents-view.md §2.1): Claude's `stream-json` interleaves the session's own events
|
|
3
|
+
* with its subagents', and a subagent's carry `parent_tool_use_id` (the parent's Task/Agent call). Only the session's
|
|
4
|
+
* own events drive its lifecycle, identity and accounting: the receipt, the primary result, the saved session, live
|
|
5
|
+
* input, the turn and activity marks, and the billing observations. A subagent's init, words or result prove nothing
|
|
6
|
+
* about the main query.
|
|
7
|
+
*
|
|
8
|
+
* Unknown shapes fail toward "not the parent": any present, non-null `parent_tool_use_id` (an id, `""`, a number)
|
|
9
|
+
* makes the event a subagent's. Pure, no imports: every reader shares this one copy.
|
|
10
|
+
*/
|
|
11
|
+
/** The session's own event: `parent_tool_use_id` absent or null. */
|
|
12
|
+
export declare function topLevelEvent(event: Record<string, unknown>): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The main query's `result`, selected structurally (finding 10): top-level, and its `origin` absent, null or
|
|
15
|
+
* `{kind: "human"}`. An allowlist, so a background task's result, a subagent's result, or any future origin kind is
|
|
16
|
+
* never the run's envelope.
|
|
17
|
+
*/
|
|
18
|
+
export declare function primaryResultEvent(event: Record<string, unknown>): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose event is this (docs/plans/agents-view.md §2.1): Claude's `stream-json` interleaves the session's own events
|
|
3
|
+
* with its subagents', and a subagent's carry `parent_tool_use_id` (the parent's Task/Agent call). Only the session's
|
|
4
|
+
* own events drive its lifecycle, identity and accounting: the receipt, the primary result, the saved session, live
|
|
5
|
+
* input, the turn and activity marks, and the billing observations. A subagent's init, words or result prove nothing
|
|
6
|
+
* about the main query.
|
|
7
|
+
*
|
|
8
|
+
* Unknown shapes fail toward "not the parent": any present, non-null `parent_tool_use_id` (an id, `""`, a number)
|
|
9
|
+
* makes the event a subagent's. Pure, no imports: every reader shares this one copy.
|
|
10
|
+
*/
|
|
11
|
+
/** The session's own event: `parent_tool_use_id` absent or null. */
|
|
12
|
+
export function topLevelEvent(event) {
|
|
13
|
+
const parent = event["parent_tool_use_id"];
|
|
14
|
+
return parent === undefined || parent === null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The main query's `result`, selected structurally (finding 10): top-level, and its `origin` absent, null or
|
|
18
|
+
* `{kind: "human"}`. An allowlist, so a background task's result, a subagent's result, or any future origin kind is
|
|
19
|
+
* never the run's envelope.
|
|
20
|
+
*/
|
|
21
|
+
export function primaryResultEvent(event) {
|
|
22
|
+
if (event["type"] !== "result" || !topLevelEvent(event))
|
|
23
|
+
return false;
|
|
24
|
+
const origin = event["origin"];
|
|
25
|
+
if (origin === undefined || origin === null)
|
|
26
|
+
return true;
|
|
27
|
+
return typeof origin === "object" && String(origin["kind"] ?? "") === "human";
|
|
28
|
+
}
|
package/dist/subagent-work.js
CHANGED
|
@@ -235,6 +235,9 @@ function carryOut(store, flow, definition, stage, card, mate, context, answer, n
|
|
|
235
235
|
}
|
|
236
236
|
if (answer.action === "cant") {
|
|
237
237
|
event("handled", `Couldn't handle “${card.title}”: ${answer.note}`);
|
|
238
|
+
// A zone that answers whoever asked tells them it couldn't, in its own words when it gave some: they always hear back.
|
|
239
|
+
if (stage.reply === true)
|
|
240
|
+
replyToAsker(store, flow, card, mate, answer.note || (answer.reason === "" ? `${context.name} couldn't handle this.` : `${context.name} couldn't handle this: ${answer.reason}`), now);
|
|
238
241
|
if (stage.onFail !== null)
|
|
239
242
|
store.moveFlowCard(card.id, { to: stage.onFail, outcome: "fail", actor, note: answer.note === "" ? null : note(answer.note), expectEntry: card.entry }, now);
|
|
240
243
|
else
|