toolroll 0.9.55 → 0.9.57
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 +18 -0
- package/README.md +24 -8
- package/dist/action-ledger.d.ts +2 -4
- package/dist/action-ledger.js +2 -10
- package/dist/approval-policy.d.ts +5 -5
- package/dist/approval-policy.js +4 -4
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-adapters.d.ts +5 -0
- package/dist/assignment.d.ts +24 -3
- package/dist/assignment.js +36 -10
- package/dist/browser/workspace.js +5 -5
- package/dist/browser-workspace.d.ts +12 -12
- package/dist/browser-workspace.js +1 -1
- package/dist/budget-alerts.js +3 -3
- package/dist/chat-actions.d.ts +23 -18
- package/dist/chat-actions.js +110 -88
- package/dist/chat-ask.d.ts +2 -2
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.d.ts +15 -15
- package/dist/chat-channel.js +20 -20
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.js +1 -1
- package/dist/chat-delivery-state.js +2 -2
- package/dist/chat-delivery.d.ts +2 -2
- package/dist/chat-delivery.js +30 -37
- package/dist/chat-flow.js +3 -3
- package/dist/chat-messages.d.ts +1 -1
- package/dist/chat-messages.js +4 -4
- package/dist/chat-migration.d.ts +1 -1
- package/dist/chat-migration.js +11 -11
- package/dist/chat-rooms.js +3 -3
- package/dist/chat-voice.js +1 -1
- package/dist/chat-warmth.d.ts +2 -2
- package/dist/check-levels.d.ts +3 -0
- package/dist/check-levels.js +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -7
- package/dist/contracts/chat-actions.d.ts +32 -27
- package/dist/contracts/chat-actions.js +29 -11
- package/dist/contracts/chat-content.d.ts +1 -1
- package/dist/contracts/chat-content.js +1 -1
- package/dist/contracts/chat-tables.js +4 -4
- package/dist/contracts/console-api.d.ts +6 -6
- package/dist/contracts/console-api.js +7 -7
- package/dist/contracts/flow-propose.d.ts +3 -3
- package/dist/contracts/flow.d.ts +132 -125
- package/dist/contracts/flow.js +27 -12
- package/dist/contracts/lead-context.d.ts +1 -1
- package/dist/contracts/lead-context.js +2 -2
- package/dist/contracts/lead-tools.d.ts +20 -18
- package/dist/contracts/lead-tools.js +15 -15
- package/dist/contracts/stage-output.d.ts +1 -1
- package/dist/contracts/stage-output.js +1 -1
- package/dist/contracts/store-json.d.ts +23 -22
- package/dist/contracts/store-json.js +17 -17
- package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
- package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
- package/dist/converse.d.ts +23 -23
- package/dist/converse.js +28 -28
- package/dist/coordinator-proposals.d.ts +2 -2
- package/dist/coordinator-proposals.js +1 -1
- package/dist/coordinator.js +1 -1
- package/dist/deprecations.d.ts +12 -0
- package/dist/deprecations.js +20 -0
- package/dist/desktop-update-gate.js +8 -4
- package/dist/desktop-update.js +6 -1
- package/dist/discord-chat.js +6 -6
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.js +0 -5
- package/dist/envelope.js +0 -1
- package/dist/export-ui.js +1 -1
- package/dist/export.js +7 -7
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +21 -21
- package/dist/flow-insights.d.ts +1 -1
- package/dist/flow-share.js +13 -8
- package/dist/flow-steps.d.ts +5 -5
- package/dist/flow-steps.js +19 -19
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/flows-cli.js +1 -1
- package/dist/flows-ui.js +20 -20
- package/dist/flows.js +19 -19
- package/dist/guides.js +4 -3
- package/dist/invoke.js +1 -1
- package/dist/kits-ui.js +5 -5
- package/dist/kits.d.ts +12 -12
- package/dist/kits.js +31 -31
- package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
- package/dist/{mate-cli.js → lead-cli.js} +38 -38
- package/dist/lead-commitments.js +4 -4
- package/dist/lead-context.d.ts +2 -2
- package/dist/lead-context.js +9 -9
- package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
- package/dist/{mate-contract.js → lead-contract.js} +7 -6
- package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
- package/dist/{mate-doors.js → lead-doors.js} +24 -24
- package/dist/lead-follow.d.ts +5 -5
- package/dist/lead-follow.js +9 -9
- package/dist/lead-people.d.ts +5 -5
- package/dist/lead-people.js +14 -14
- package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
- package/dist/{mate-progress.js → lead-progress.js} +3 -3
- package/dist/lead-status.d.ts +1 -1
- package/dist/lead-status.js +2 -2
- package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
- package/dist/{mate-tools.js → lead-tools.js} +109 -101
- package/dist/{mate.d.ts → lead.d.ts} +27 -27
- package/dist/{mate.js → lead.js} +102 -102
- package/dist/limits-ui.d.ts +1 -1
- package/dist/limits-ui.js +1 -1
- package/dist/mcp-connect.d.ts +1 -1
- package/dist/mcp-connect.js +3 -3
- package/dist/mcp-core.js +1 -1
- package/dist/memory-pass.d.ts +2 -2
- package/dist/memory-pass.js +7 -7
- package/dist/notification-rows.d.ts +2 -2
- package/dist/operate.d.ts +3 -3
- package/dist/operate.js +52 -42
- package/dist/policy.d.ts +1 -1
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +2 -2
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-delete.d.ts +1 -1
- package/dist/project-delete.js +26 -26
- package/dist/project-memory.js +1 -1
- package/dist/project-tools.d.ts +1 -1
- package/dist/project-tools.js +1 -1
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +12 -0
- package/dist/recipe-ui.js +1 -1
- package/dist/release-coverage.d.ts +16 -0
- package/dist/release-coverage.js +92 -0
- package/dist/retention.js +4 -4
- package/dist/scope.d.ts +2 -2
- package/dist/scout-report.d.ts +1 -1
- package/dist/scout-report.js +1 -1
- package/dist/serve.js +40 -45
- package/dist/server/chat.d.ts +4 -4
- package/dist/server/chat.js +105 -103
- package/dist/server/chrome.js +3 -3
- package/dist/server/flows.js +69 -62
- package/dist/server/guards.js +3 -3
- package/dist/server/http.d.ts +3 -3
- package/dist/server/http.js +1 -1
- package/dist/server/live.js +6 -6
- package/dist/server/people-tokens.js +1 -1
- package/dist/server/remote.js +12 -10
- package/dist/server/render-chat.d.ts +30 -30
- package/dist/server/render-chat.js +33 -33
- package/dist/server/render-pages.js +2 -2
- package/dist/server/render-results.d.ts +4 -1
- package/dist/server/render-results.js +23 -9
- package/dist/server/render-settings.d.ts +7 -0
- package/dist/server/render-settings.js +3 -0
- package/dist/server/route-table.js +9 -7
- package/dist/server/runtime.d.ts +13 -13
- package/dist/server/settings.js +14 -13
- package/dist/server/tasks.js +34 -30
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.d.ts +2 -2
- package/dist/slack-chat.js +6 -6
- package/dist/spend-ui.d.ts +3 -3
- package/dist/spend-ui.js +9 -9
- package/dist/spend.d.ts +14 -14
- package/dist/spend.js +31 -31
- package/dist/store-baseline.d.ts +19 -0
- package/dist/store-baseline.js +2873 -0
- package/dist/store.d.ts +200 -254
- package/dist/store.js +830 -3152
- package/dist/subagent-admin.d.ts +53 -0
- package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
- package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
- package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
- package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
- package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
- package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
- package/dist/{teammate-question.js → subagent-question.js} +17 -17
- package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
- package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
- package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
- package/dist/{teammate-week.js → subagent-week.js} +23 -23
- package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
- package/dist/{teammate-work.js → subagent-work.js} +50 -50
- package/dist/subagents-ui.d.ts +22 -0
- package/dist/{teammates-ui.js → subagents-ui.js} +45 -45
- package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
- package/dist/{teammates.js → subagents.js} +21 -21
- package/dist/subscription-chat.d.ts +13 -13
- package/dist/subscription-chat.js +19 -19
- package/dist/surface.d.ts +19 -2
- package/dist/surface.js +7 -31
- package/dist/task-outcome-cli.js +3 -1
- package/dist/task-status.d.ts +59 -7
- package/dist/task-status.js +132 -16
- package/dist/team-cli.d.ts +1 -0
- package/dist/team-cli.js +14 -2
- package/dist/team-leads.js +13 -13
- package/dist/team-runtime.d.ts +2 -2
- package/dist/team-runtime.js +9 -9
- package/dist/teams-chat.js +6 -6
- package/dist/telegram-flow.d.ts +3 -3
- package/dist/telegram-flow.js +3 -3
- package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
- package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
- package/dist/telegram-status.d.ts +11 -4
- package/dist/telegram-status.js +18 -11
- package/dist/telegram-team.d.ts +1 -1
- package/dist/telegram-team.js +5 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +13 -34
- package/dist/text-limits.d.ts +18 -18
- package/dist/text-limits.js +18 -18
- package/dist/toolroll-update.d.ts +8 -0
- package/dist/toolroll-update.js +45 -8
- package/dist/work-index.d.ts +6 -1
- package/dist/work-index.js +50 -8
- package/dist/workspace-ui.js +2 -1
- package/package.json +1 -2
- package/dist/teammate-admin.d.ts +0 -53
- package/dist/teammates-ui.d.ts +0 -21
package/dist/server/tasks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { handlersOf } from './handler-registry.js';
|
|
2
|
+
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
2
3
|
/** tasks handlers, moved without changing their route bodies. */
|
|
3
4
|
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
4
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -38,7 +39,7 @@ import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js
|
|
|
38
39
|
import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
|
|
39
40
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
40
41
|
import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, hasFreshIdentitySignIn, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
41
|
-
import {
|
|
42
|
+
import { subagentNames as subagentNamesOf } from "../spend.js";
|
|
42
43
|
import { verifiedAuthor } from "../store.js";
|
|
43
44
|
import { runCostWords } from "../summary.js";
|
|
44
45
|
import { requestTaskStop, resumeTaskStop } from "../task-control.js";
|
|
@@ -54,7 +55,7 @@ import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReview
|
|
|
54
55
|
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
|
|
55
56
|
import {} from "./session.js";
|
|
56
57
|
export function createTasksHandlers(runtime) {
|
|
57
|
-
const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult,
|
|
58
|
+
const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, taskRooms, identify, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
|
|
58
59
|
/** A row admitted the address but its path parameter does not parse: the console's unknown page. */
|
|
59
60
|
function noPage(who, response) {
|
|
60
61
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
@@ -92,7 +93,7 @@ export function createTasksHandlers(runtime) {
|
|
|
92
93
|
const codeProject = selected?.repo ?? requestedProject ?? project;
|
|
93
94
|
const chrome = chromeFor(codeProject, 'code');
|
|
94
95
|
const content = codingWorkspaceHtml({ owner: who.name, projects: chrome.projects ?? [], sessions: runtime.coding?.list(actor).filter(s => codingProjectAllowed(s.repo)) ?? [], selected: selected && runtime.coding ? runtime.coding.snapshot(selected.id, actor) : null, csrf: who.session.csrf, project: codeProject, available: runtime.coding !== null, ...(runtime.codingProblem ? { error: runtime.codingProblem } : {}) });
|
|
95
|
-
return sendScreen(response, 200, screen('Code', content
|
|
96
|
+
return sendScreen(response, 200, screen('Code', html `<p class="problem" role="status" data-deprecated="session">${DEPRECATED_PAGE.session}</p>${content}`, { chrome, functional: { script: codingWorkspaceScript(), fetches: true } }));
|
|
96
97
|
}
|
|
97
98
|
catch (error) {
|
|
98
99
|
if (!codingActorAllowed(actor))
|
|
@@ -164,8 +165,8 @@ export function createTasksHandlers(runtime) {
|
|
|
164
165
|
// Console v2: results ready to review and work running now, from the same admitted work index.
|
|
165
166
|
...(() => {
|
|
166
167
|
const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
|
|
167
|
-
const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project });
|
|
168
|
-
const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project });
|
|
168
|
+
const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project, root: evidenceRoot });
|
|
169
|
+
const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project, root: evidenceRoot });
|
|
169
170
|
return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
|
|
170
171
|
})(),
|
|
171
172
|
};
|
|
@@ -194,7 +195,7 @@ export function createTasksHandlers(runtime) {
|
|
|
194
195
|
const rollup = project === null && !unscopedMode;
|
|
195
196
|
let work;
|
|
196
197
|
try {
|
|
197
|
-
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project });
|
|
198
|
+
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project, root: evidenceRoot });
|
|
198
199
|
const facts = requestContext.getStore();
|
|
199
200
|
if (facts !== undefined) {
|
|
200
201
|
// These counts cover the exact admitted project lens, including
|
|
@@ -245,7 +246,7 @@ export function createTasksHandlers(runtime) {
|
|
|
245
246
|
now,
|
|
246
247
|
// v105: subscription windows and monthly budgets, for whoever runs the installation.
|
|
247
248
|
limits: store.isInstanceOperator(who.name)
|
|
248
|
-
? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName,
|
|
249
|
+
? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, subagent: id => subagentNamesOf(store.handle).get(id) ?? `Subagent ${id}` }, now)
|
|
249
250
|
: null,
|
|
250
251
|
});
|
|
251
252
|
// One project's Tasks dock that project's own conversation (v77).
|
|
@@ -1381,7 +1382,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1381
1382
|
// CI repair, suggestion-first (M8.18): a red episode never spawns an
|
|
1382
1383
|
// agent by itself — it EARNS a button, and the button creates one
|
|
1383
1384
|
// unapproved task through the same revision machinery as review
|
|
1384
|
-
// comments.
|
|
1385
|
+
// comments. One draft per task/PR, ever: the merge blocker records it.
|
|
1385
1386
|
const id = Number(draftRepair[1]);
|
|
1386
1387
|
const found = store.getRun(id);
|
|
1387
1388
|
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
@@ -1400,10 +1401,6 @@ export function createTasksHandlers(runtime) {
|
|
|
1400
1401
|
// failure was SEEN on and when (audit C-2) — a PR that advanced since
|
|
1401
1402
|
// is a different failure, and the click time is not an observation.
|
|
1402
1403
|
const episode = store.latestOpenCiEpisode(publication.githubRepo, publication.prNumber);
|
|
1403
|
-
// Suffixes survive truncation (audit C-7): the prefix gives way, the
|
|
1404
|
-
// identity-bearing tail never does.
|
|
1405
|
-
const suffix = `-ci-${publication.prNumber}`;
|
|
1406
|
-
const draftId = `${sourceTaskId.slice(0, 64 - suffix.length)}${suffix}`;
|
|
1407
1404
|
const brief = {
|
|
1408
1405
|
schema: 1,
|
|
1409
1406
|
kind: "ci-repair",
|
|
@@ -1424,25 +1421,32 @@ export function createTasksHandlers(runtime) {
|
|
|
1424
1421
|
// repair use (contract handoff task 2): the inherited terms come from
|
|
1425
1422
|
// the source rows inside the seal; the scope read above only names
|
|
1426
1423
|
// the digest this draft was composed against.
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1424
|
+
// One draft per task/PR, ever: the PR's merge blocker names it, lifted or not, and is written in the seal's transaction.
|
|
1425
|
+
const sealed = store.transact(() => {
|
|
1426
|
+
const prior = store.raw().prepare("SELECT task_id FROM merge_blocker WHERE publication = ? AND task_id IS NOT NULL ORDER BY rowid LIMIT 1").get(publication.id);
|
|
1427
|
+
if (prior !== undefined)
|
|
1428
|
+
return { ok: false, reason: "duplicate", detail: String(prior["task_id"]) };
|
|
1429
|
+
const result = store.sealRevision({
|
|
1430
|
+
source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
|
|
1431
|
+
brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
|
|
1432
|
+
child: {
|
|
1433
|
+
title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
|
|
1434
|
+
repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
|
|
1435
|
+
`Read the failing checks on GitHub before approving; this draft carries no log content.`,
|
|
1436
|
+
},
|
|
1437
|
+
commentIds: null,
|
|
1438
|
+
requestedBy: who.name,
|
|
1439
|
+
}, now);
|
|
1440
|
+
// The merge blocker rides the SAME breath as the draft (merge grant,
|
|
1441
|
+
// findings 3/12/13): while this repair exists, the source PR merges
|
|
1442
|
+
// NOTHING — sticky until the operator's unblock act or the PR closes.
|
|
1443
|
+
if (result.ok)
|
|
1444
|
+
store.createMergeBlocker(publication.id, result.id, now);
|
|
1445
|
+
return result;
|
|
1446
|
+
});
|
|
1439
1447
|
if (!sealed.ok) {
|
|
1440
|
-
return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${
|
|
1448
|
+
return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${sealed.detail}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
|
|
1441
1449
|
}
|
|
1442
|
-
// The merge blocker rides the SAME breath as the draft (merge grant,
|
|
1443
|
-
// findings 3/12/13): while this repair exists, the source PR merges
|
|
1444
|
-
// NOTHING — sticky until the operator's unblock act or the PR closes.
|
|
1445
|
-
store.createMergeBlocker(publication.id, sealed.id, now);
|
|
1446
1450
|
return redirect(response, taskHref(sealed.id));
|
|
1447
1451
|
}
|
|
1448
1452
|
async function resolveIncident(ctx) {
|
|
@@ -2227,7 +2231,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2227
2231
|
case "complete": {
|
|
2228
2232
|
const body = readForm(posted, CONSOLE_FORMS.taskComplete);
|
|
2229
2233
|
// The row admits only an approver's browser session; the principal re-proves that session's standing.
|
|
2230
|
-
const principal = who.via === "cookie" ?
|
|
2234
|
+
const principal = who.via === "cookie" ? leadPrincipal(who) : null;
|
|
2231
2235
|
if (principal === null)
|
|
2232
2236
|
return refuse(response, who, 403, "Your access changed. Sign in again.");
|
|
2233
2237
|
const digest = body.get("receipt") ?? "";
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/slack-chat.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChatState, type ChatBinding, type ChatContent, type ChatEvent } from "./chat-delivery-state.js";
|
|
2
2
|
import { uploadSlackBytes, type SlackApi, type SlackIdentity } from "./slack-api.js";
|
|
3
3
|
import { type Store } from "./store.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { SubscriptionLeadRunner } from "./subscription-chat.js";
|
|
5
5
|
export type SlackChatOptions = {
|
|
6
6
|
store: Store;
|
|
7
7
|
identity: SlackIdentity;
|
|
@@ -11,7 +11,7 @@ export type SlackChatOptions = {
|
|
|
11
11
|
evidenceRoot: string;
|
|
12
12
|
current: () => boolean;
|
|
13
13
|
origin: () => string | null;
|
|
14
|
-
subscriptionRunner?:
|
|
14
|
+
subscriptionRunner?: SubscriptionLeadRunner;
|
|
15
15
|
canNotify?: () => boolean;
|
|
16
16
|
clock?: () => Date;
|
|
17
17
|
upload?: typeof uploadSlackBytes;
|
package/dist/slack-chat.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
2
2
|
import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
|
|
3
|
-
import { chatQuestionButtons } from "./
|
|
3
|
+
import { chatQuestionButtons } from "./subagent-question.js";
|
|
4
4
|
import { chatAskButtons } from "./chat-ask.js";
|
|
5
5
|
import { channelInbox } from "./chat-inbox.js";
|
|
6
6
|
import { roomCommand } from "./chat-rooms.js";
|
|
7
7
|
import { processChatEvent, applyChatAction, planChatNotifications, channelAccess, planRoomMessages, } from "./chat-delivery.js";
|
|
8
8
|
/** Slack is a transport for the same saved assistant and confirmation doors. */
|
|
9
9
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
10
|
-
import {
|
|
10
|
+
import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
11
11
|
import { renderReply } from "./reply-shape.js";
|
|
12
12
|
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
13
13
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
@@ -65,7 +65,7 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
65
65
|
payload = match
|
|
66
66
|
? { hash: chatHash(match[1]) }
|
|
67
67
|
: {
|
|
68
|
-
text: event.text.slice(0,
|
|
68
|
+
text: event.text.slice(0, LEAD_MESSAGE_MAX_CHARS + 1),
|
|
69
69
|
originalLength: event.text.length,
|
|
70
70
|
};
|
|
71
71
|
id = chatHash(`${identity.installation}:event:${body.event_id}`);
|
|
@@ -217,7 +217,7 @@ export async function deliverSlackPart(options) {
|
|
|
217
217
|
return true;
|
|
218
218
|
}
|
|
219
219
|
try {
|
|
220
|
-
const session = event.session === null ? null : store.
|
|
220
|
+
const session = event.session === null ? null : store.getLeadSession(event.session);
|
|
221
221
|
const repos = await access(options, binding, session?.ceilingDigest);
|
|
222
222
|
if (event.kind === "notice" && options.canNotify?.() === false)
|
|
223
223
|
return false;
|
|
@@ -300,7 +300,7 @@ export async function deliverSlackPart(options) {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
if (content.proposal) {
|
|
303
|
-
const proposal = store.
|
|
303
|
+
const proposal = store.getLeadProposal(content.proposal);
|
|
304
304
|
if (!proposal)
|
|
305
305
|
text = "This proposal is unavailable.";
|
|
306
306
|
else if (proposal.state !== "pending")
|
|
@@ -363,7 +363,7 @@ export async function deliverSlackPart(options) {
|
|
|
363
363
|
...(one.action === "approve" ? { style: "primary" } : {}),
|
|
364
364
|
}))
|
|
365
365
|
: []),
|
|
366
|
-
// A
|
|
366
|
+
// A subagent's question (v93): its options, then "Answer in words".
|
|
367
367
|
...(content.question
|
|
368
368
|
? chatQuestionButtons(state, row.id, now).map((one) => ({
|
|
369
369
|
type: "button",
|
package/dist/spend-ui.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ export type SpendView = {
|
|
|
16
16
|
targets: {
|
|
17
17
|
value: string;
|
|
18
18
|
label: string;
|
|
19
|
-
group: "Everything" | "Projects" | "People" | "
|
|
19
|
+
group: "Everything" | "Projects" | "People" | "Subagents";
|
|
20
20
|
}[];
|
|
21
|
-
|
|
21
|
+
subagentNames: Map<number, string>;
|
|
22
22
|
csrf: string;
|
|
23
23
|
};
|
|
24
24
|
export declare function spendHtml(view: SpendView, notice: {
|
|
@@ -26,4 +26,4 @@ export declare function spendHtml(view: SpendView, notice: {
|
|
|
26
26
|
problem?: string | null;
|
|
27
27
|
}): Html;
|
|
28
28
|
/** One row per piece of spend, for a spreadsheet. */
|
|
29
|
-
export declare function spendCsv(items: readonly SpendItem[],
|
|
29
|
+
export declare function spendCsv(items: readonly SpendItem[], subagentNames: Map<number, string>): string;
|
package/dist/spend-ui.js
CHANGED
|
@@ -41,32 +41,32 @@ export function spendHtml(view, notice) {
|
|
|
41
41
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
42
42
|
const total = view.items.reduce((sum, item) => sum + (item.microusd ?? 0), 0);
|
|
43
43
|
const unpriced = view.items.filter(item => item.microusd === null && (item.tokensIn !== null || item.kind !== "run")).length;
|
|
44
|
-
const counts = { run: view.items.filter(item => item.kind === "run").length,
|
|
45
|
-
const parts = [`${counts.run} ${counts.run === 1 ? "run" : "runs"}`, ...(counts.
|
|
46
|
-
const
|
|
44
|
+
const counts = { run: view.items.filter(item => item.kind === "run").length, subagent: view.items.filter(item => item.kind === "subagent").length, chat: view.items.filter(item => item.kind === "chat").length, sort: view.items.filter(item => item.kind === "sort").length };
|
|
45
|
+
const parts = [`${counts.run} ${counts.run === 1 ? "run" : "runs"}`, ...(counts.subagent > 0 ? [`${counts.subagent} subagent turns`] : []), ...(counts.chat > 0 ? [`${counts.chat} chat turns`] : []), ...(counts.sort > 0 ? [`${counts.sort} ${counts.sort === 1 ? "sort" : "sorts"}`] : [])];
|
|
46
|
+
const nameOfSubagent = (key) => view.subagentNames.get(Number(key)) ?? `Subagent ${key}`;
|
|
47
47
|
const budgets = view.budgets.map(budget => {
|
|
48
48
|
const width = Math.min(100, Math.max(0, budget.percent));
|
|
49
49
|
const over = budget.spentMicrousd >= budget.limitMicrousd;
|
|
50
|
-
const label = budgetLabel(budget, budget.scope === "
|
|
50
|
+
const label = budgetLabel(budget, budget.scope === "subagent" ? nameOfSubagent(budget.key) : undefined).replace(/'s$/, "");
|
|
51
51
|
return html `<div class="budget${over ? " over" : ""}" data-budget="${budget.id}"><span class="name">${label}</span><span class="figures">${usd(budget.spentMicrousd)} of ${usd(budget.limitMicrousd)} · ${budget.percent}%</span><span class="bar" aria-hidden="true"><span style="width:${width}%"></span></span><span class="meta">${over && budget.hardStop ? "Used up: new API work waits until next month or a higher budget." : budget.hardStop ? "Stops API work at 100%." : "Alerts only."}${budget.unpriced > 0 ? ` ${budget.unpriced} unpriced` : ""}</span><details><summary>Change</summary>${postForm("/spend/budget", html `<label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" value="${Math.round(budget.limitMicrousd / 1_000_000)}" required></label><label class="choice"><input type="checkbox" name="stop" value="1"${budget.hardStop ? html ` checked` : ""}> Stop new work at 100%</label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><button type="submit" name="action" value="save">Save</button> <button type="submit" name="action" value="remove">Remove</button>`, { attrs: { class: "budget-form" }, hidden: { target: `${budget.scope}:${budget.key}` } })}</details></div>`;
|
|
52
52
|
});
|
|
53
|
-
const groups = ["Everything", "Projects", "People", "
|
|
53
|
+
const groups = ["Everything", "Projects", "People", "Subagents"];
|
|
54
54
|
const options = groups.map(group => {
|
|
55
55
|
const members = view.targets.filter(one => one.group === group);
|
|
56
56
|
return members.length === 0 ? "" : html `<optgroup label="${group}">${members.map(one => html `<option value="${one.value}">${one.label}</option>`)}</optgroup>`;
|
|
57
57
|
});
|
|
58
58
|
const add = html `<details${view.budgets.length === 0 ? html ` open` : ""}><summary>Add a budget</summary>${postForm("/spend/budget", html `<label>For<select name="target" required>${options}</select></label><label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" required></label><label class="choice"><input type="checkbox" name="stop" value="1" checked> Stop new work at 100%</label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><button type="submit" name="action" value="save">Add budget</button>`, { attrs: { class: "budget-form" } })}</details>`;
|
|
59
|
-
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${view.previous}">← ${view.previous}</a><strong>${view.month}</strong>${view.next === null ? "" : html `<a href="/spend?month=${view.next}">${view.next} →</a>`}<a href="/spend?month=${view.month}&format=csv" download>CSV</a></p></div>${note}<p class="spend-total" data-spend-total="${total}">${usd(total)}</p><p class="meta">${parts.join(" · ")}${unpriced > 0 ? ` · ${unpriced} unpriced (no reported cost or catalogue price)` : ""} · UTC month</p><section><h2>Budgets</h2>${budgets.length === 0 ? html `<p class="meta">No budgets yet.</p>` : budgets}${add}</section><div class="spend-grid">${table("By project", breakdown(view.items, item => item.project), projectName)}${table("By person", breakdown(view.items, item => item.person), key => key)}${table("By
|
|
59
|
+
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${view.previous}">← ${view.previous}</a><strong>${view.month}</strong>${view.next === null ? "" : html `<a href="/spend?month=${view.next}">${view.next} →</a>`}<a href="/spend?month=${view.month}&format=csv" download>CSV</a></p></div>${note}<p class="spend-total" data-spend-total="${total}">${usd(total)}</p><p class="meta">${parts.join(" · ")}${unpriced > 0 ? ` · ${unpriced} unpriced (no reported cost or catalogue price)` : ""} · UTC month</p><section><h2>Budgets</h2>${budgets.length === 0 ? html `<p class="meta">No budgets yet.</p>` : budgets}${add}</section><div class="spend-grid">${table("By project", breakdown(view.items, item => item.project), projectName)}${table("By person", breakdown(view.items, item => item.person), key => key)}${table("By subagent", breakdown(view.items, item => item.subagent === null ? null : String(item.subagent)), nameOfSubagent)}${table("By model", breakdown(view.items, item => `${item.provider}${item.model === null ? "" : ` · ${item.model}`}`), key => key)}</div><p class="meta">Subscription work is $0; its limits are on Tasks. API work is what the provider reported, or its tokens at the prices in Settings → Models.</p></article>`;
|
|
60
60
|
}
|
|
61
61
|
/** One row per piece of spend, for a spreadsheet. */
|
|
62
|
-
export function spendCsv(items,
|
|
62
|
+
export function spendCsv(items, subagentNames) {
|
|
63
63
|
const cell = (value) => {
|
|
64
64
|
const text = value === null || value === undefined ? "" : String(value);
|
|
65
65
|
const safe = /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
|
|
66
66
|
return /[",\n\r]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe;
|
|
67
67
|
};
|
|
68
|
-
const head = ["time_utc", "kind", "project", "person", "
|
|
69
|
-
const rows = items.map(item => [item.at, item.kind, item.project, item.person, item.
|
|
68
|
+
const head = ["time_utc", "kind", "project", "person", "subagent", "task", "run", "provider", "model", "tokens_in", "tokens_out", "cost_usd", "priced_by", "billing"];
|
|
69
|
+
const rows = items.map(item => [item.at, item.kind, item.project, item.person, item.subagent === null ? null : subagentNames.get(item.subagent) ?? item.subagent,
|
|
70
70
|
item.taskId, item.runId, item.provider, item.model, item.tokensIn, item.tokensOut, item.microusd === null ? null : (item.microusd / 1_000_000).toFixed(6), item.source, item.authMode].map(cell).join(","));
|
|
71
71
|
return `${head.join(",")}\r\n${rows.map(row => `${row}\r\n`).join("")}`;
|
|
72
72
|
}
|
package/dist/spend.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Models), or at the provider's highest listed price when the model isn't
|
|
8
8
|
* listed; work with no tokens and no cost is "unpriced" and said so.
|
|
9
9
|
*
|
|
10
|
-
* Spend counts toward a project (a run's task, a
|
|
11
|
-
* a person (who filed the task, or had the chat), a
|
|
10
|
+
* Spend counts toward a project (a run's task, a subagent's, a project chat),
|
|
11
|
+
* a person (who filed the task, or had the chat), a subagent (its turns, and
|
|
12
12
|
* tasks it filed), and the whole installation. Budgets are per calendar
|
|
13
13
|
* month (UTC).
|
|
14
14
|
*/
|
|
@@ -30,7 +30,7 @@ export declare function claudeBillingFrom(seen: {
|
|
|
30
30
|
model: string | null;
|
|
31
31
|
planWindows: boolean;
|
|
32
32
|
}): Billing | null;
|
|
33
|
-
/** How Claude bills on this computer when Toolroll gives it no key (
|
|
33
|
+
/** How Claude bills on this computer when Toolroll gives it no key (subagent turns, drafts): as its keyless runs
|
|
34
34
|
* were last seen, and its plan until one has been. */
|
|
35
35
|
export declare function claudeMachineBilling(db: Database): Billing;
|
|
36
36
|
/** Whether API work on this provider can be priced: it reports its cost, or the catalogue has its prices. */
|
|
@@ -60,8 +60,8 @@ export declare function priceWork(db: Database, work: {
|
|
|
60
60
|
source: PriceSource;
|
|
61
61
|
price: Price | null;
|
|
62
62
|
};
|
|
63
|
-
export declare const SPEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_spend (\n run INTEGER PRIMARY KEY,\n microusd INTEGER,\n source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n -- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.\n billing_fixed INTEGER NOT NULL DEFAULT 0,\n price_model TEXT,\n input_usd REAL,\n output_usd REAL,\n priced_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS budget (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', '
|
|
64
|
-
export type BudgetScope = "installation" | "project" | "person" | "
|
|
63
|
+
export declare const SPEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_spend (\n run INTEGER PRIMARY KEY,\n microusd INTEGER,\n source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n -- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.\n billing_fixed INTEGER NOT NULL DEFAULT 0,\n price_model TEXT,\n input_usd REAL,\n output_usd REAL,\n priced_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS budget (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'subagent')),\n scope_key TEXT NOT NULL,\n limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),\n hard_stop INTEGER NOT NULL DEFAULT 1,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n removed_by TEXT,\n removed_at TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS budget_live ON budget (scope_kind, scope_key) WHERE removed_at IS NULL;\nCREATE TABLE IF NOT EXISTS provider_limit (\n provider TEXT NOT NULL,\n window TEXT NOT NULL,\n used_percent REAL NOT NULL,\n window_minutes INTEGER,\n resets_at TEXT,\n reached INTEGER NOT NULL DEFAULT 0,\n plan TEXT,\n observed_at TEXT NOT NULL,\n PRIMARY KEY (provider, window)\n);\n-- Spend that isn't a run, a subagent turn or a chat: a flow's Claude draft.\nCREATE TABLE IF NOT EXISTS side_spend (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL CHECK (kind IN ('draft')),\n repo TEXT,\n provider TEXT NOT NULL,\n model TEXT,\n cost_usd REAL,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS side_spend_at ON side_spend (at);\nCREATE TABLE IF NOT EXISTS provider_account (\n provider TEXT PRIMARY KEY,\n billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),\n observed_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);\nCREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);\nCREATE INDEX IF NOT EXISTS subagent_turn_at ON subagent_turn (at);\n";
|
|
64
|
+
export type BudgetScope = "installation" | "project" | "person" | "subagent";
|
|
65
65
|
export type Budget = {
|
|
66
66
|
id: number;
|
|
67
67
|
scope: BudgetScope;
|
|
@@ -74,12 +74,12 @@ export type Budget = {
|
|
|
74
74
|
/** One piece of spend, whatever made it, with what it counts toward. */
|
|
75
75
|
export type SpendItem = {
|
|
76
76
|
at: string;
|
|
77
|
-
kind: "run" | "
|
|
77
|
+
kind: "run" | "subagent" | "chat" | "sort" | "draft";
|
|
78
78
|
microusd: number | null;
|
|
79
79
|
source: PriceSource;
|
|
80
80
|
project: string | null;
|
|
81
81
|
person: string | null;
|
|
82
|
-
|
|
82
|
+
subagent: number | null;
|
|
83
83
|
provider: string;
|
|
84
84
|
model: string | null;
|
|
85
85
|
tokensIn: number | null;
|
|
@@ -100,9 +100,9 @@ export declare function monthNamed(name: string | null): {
|
|
|
100
100
|
to: string;
|
|
101
101
|
name: string;
|
|
102
102
|
} | null;
|
|
103
|
-
/** Every
|
|
104
|
-
export declare function
|
|
105
|
-
export declare function
|
|
103
|
+
/** Every subagent's name by id (its handle when its soul file names none). */
|
|
104
|
+
export declare function subagentNames(db: Database): Map<number, string>;
|
|
105
|
+
export declare function subagentFilers(db: Database): (repo: string | null, filedBy: string | null, kind: string | null) => number | null;
|
|
106
106
|
/** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
|
|
107
107
|
* filer of the task it revises, up its ancestry (bounded). */
|
|
108
108
|
export declare function filersOf(db: Database): (taskRef: number) => {
|
|
@@ -110,7 +110,7 @@ export declare function filersOf(db: Database): (taskRef: number) => {
|
|
|
110
110
|
filedBy: string | null;
|
|
111
111
|
kind: string | null;
|
|
112
112
|
};
|
|
113
|
-
/** Every piece of spend in [from, to): runs (by when they started),
|
|
113
|
+
/** Every piece of spend in [from, to): runs (by when they started), subagent turns and chat turns. `modeOf` says how
|
|
114
114
|
* a provider bills when a record doesn't (older rows): its current login or key. */
|
|
115
115
|
export declare function spendItems(db: Database, from: string, to: string, modeOf?: (provider: string) => Billing): SpendItem[];
|
|
116
116
|
/** Whether a piece of spend counts toward a budget. */
|
|
@@ -134,9 +134,9 @@ export type BudgetHold = {
|
|
|
134
134
|
remainingMicrousd: number | null;
|
|
135
135
|
};
|
|
136
136
|
/** Why work waits on a budget, in words. */
|
|
137
|
-
export declare function budgetHoldWords(hold: BudgetHold, month: string,
|
|
137
|
+
export declare function budgetHoldWords(hold: BudgetHold, month: string, subagentName?: string): string;
|
|
138
138
|
/** Each budget's month so far: what counted toward it, and how many pieces of work had no price. */
|
|
139
139
|
export declare function budgetStates(budgets: readonly Budget[], items: readonly SpendItem[]): BudgetState[];
|
|
140
|
-
/** A budget in words: "The shop project's", "alex's", "Maya's (
|
|
141
|
-
export declare function budgetLabel(budget: Pick<Budget, "scope" | "key">,
|
|
140
|
+
/** A budget in words: "The shop project's", "alex's", "Maya's (subagent)", "The whole installation's". */
|
|
141
|
+
export declare function budgetLabel(budget: Pick<Budget, "scope" | "key">, subagentName?: string): string;
|
|
142
142
|
export declare const usd: (microusd: number | null) => string;
|
package/dist/spend.js
CHANGED
|
@@ -30,7 +30,7 @@ export function claudeBillingFrom(seen) {
|
|
|
30
30
|
return "subscription";
|
|
31
31
|
return seen.keySource === "none" ? "api-key" : null;
|
|
32
32
|
}
|
|
33
|
-
/** How Claude bills on this computer when Toolroll gives it no key (
|
|
33
|
+
/** How Claude bills on this computer when Toolroll gives it no key (subagent turns, drafts): as its keyless runs
|
|
34
34
|
* were last seen, and its plan until one has been. */
|
|
35
35
|
export function claudeMachineBilling(db) {
|
|
36
36
|
return seenBilling(db, "claude") ?? "subscription";
|
|
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS run_spend (
|
|
|
100
100
|
);
|
|
101
101
|
CREATE TABLE IF NOT EXISTS budget (
|
|
102
102
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
-
scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', '
|
|
103
|
+
scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'subagent')),
|
|
104
104
|
scope_key TEXT NOT NULL,
|
|
105
105
|
limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),
|
|
106
106
|
hard_stop INTEGER NOT NULL DEFAULT 1,
|
|
@@ -123,7 +123,7 @@ CREATE TABLE IF NOT EXISTS provider_limit (
|
|
|
123
123
|
observed_at TEXT NOT NULL,
|
|
124
124
|
PRIMARY KEY (provider, window)
|
|
125
125
|
);
|
|
126
|
-
-- Spend that isn't a run, a
|
|
126
|
+
-- Spend that isn't a run, a subagent turn or a chat: a flow's Claude draft.
|
|
127
127
|
CREATE TABLE IF NOT EXISTS side_spend (
|
|
128
128
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
129
129
|
kind TEXT NOT NULL CHECK (kind IN ('draft')),
|
|
@@ -142,7 +142,7 @@ CREATE TABLE IF NOT EXISTS provider_account (
|
|
|
142
142
|
);
|
|
143
143
|
CREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);
|
|
144
144
|
CREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);
|
|
145
|
-
CREATE INDEX IF NOT EXISTS
|
|
145
|
+
CREATE INDEX IF NOT EXISTS subagent_turn_at ON subagent_turn (at);
|
|
146
146
|
`;
|
|
147
147
|
/** The calendar month (UTC) `at` falls in: [start, end) as ISO strings, and its name (YYYY-MM). */
|
|
148
148
|
export function monthOf(at) {
|
|
@@ -156,25 +156,25 @@ export function monthNamed(name) {
|
|
|
156
156
|
return null;
|
|
157
157
|
return monthOf(new Date(`${name}-01T00:00:00.000Z`));
|
|
158
158
|
}
|
|
159
|
-
/** The
|
|
160
|
-
* its project. A later
|
|
161
|
-
/** A
|
|
159
|
+
/** The subagent behind a filing: tasks a subagent files are filed as its name ("Maya (AI)", from its soul file), in
|
|
160
|
+
* its project. A later subagent with the same name and project wins, as it would read on the card. */
|
|
161
|
+
/** A subagent's name as its soul file gives it ("Maya"), or null. */
|
|
162
162
|
const soulName = (soul) => {
|
|
163
163
|
const front = /^---\s*\n([\s\S]*?)\n---/.exec(String(soul ?? ""));
|
|
164
164
|
return front === null ? null : /^name:\s*(.+?)\s*$/m.exec(front[1])?.[1] ?? null;
|
|
165
165
|
};
|
|
166
|
-
/** Every
|
|
167
|
-
export function
|
|
168
|
-
return new Map(db.prepare("SELECT id, handle, soul FROM
|
|
166
|
+
/** Every subagent's name by id (its handle when its soul file names none). */
|
|
167
|
+
export function subagentNames(db) {
|
|
168
|
+
return new Map(db.prepare("SELECT id, handle, soul FROM subagent").all().map(row => [Number(row["id"]), soulName(row["soul"]) ?? String(row["handle"])]));
|
|
169
169
|
}
|
|
170
|
-
export function
|
|
170
|
+
export function subagentFilers(db) {
|
|
171
171
|
const byName = new Map();
|
|
172
|
-
for (const row of db.prepare("SELECT id, repo, handle, soul FROM
|
|
172
|
+
for (const row of db.prepare("SELECT id, repo, handle, soul FROM subagent ORDER BY id").all()) {
|
|
173
173
|
const name = soulName(row["soul"]);
|
|
174
174
|
for (const one of [String(row["handle"]), ...(name === null ? [] : [name])])
|
|
175
175
|
byName.set(`${String(row["repo"])}\n${one} (AI)`, Number(row["id"]));
|
|
176
176
|
}
|
|
177
|
-
return (repo, filedBy, kind) => kind !== "
|
|
177
|
+
return (repo, filedBy, kind) => kind !== "subagent" || repo === null || filedBy === null ? null : byName.get(`${repo}\n${filedBy}`) ?? null;
|
|
178
178
|
}
|
|
179
179
|
/** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
|
|
180
180
|
* filer of the task it revises, up its ancestry (bounded). */
|
|
@@ -198,10 +198,10 @@ export function filersOf(db) {
|
|
|
198
198
|
return found;
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
-
/** Every piece of spend in [from, to): runs (by when they started),
|
|
201
|
+
/** Every piece of spend in [from, to): runs (by when they started), subagent turns and chat turns. `modeOf` says how
|
|
202
202
|
* a provider bills when a record doesn't (older rows): its current login or key. */
|
|
203
203
|
export function spendItems(db, from, to, modeOf = provider => billingOf(provider, db)) {
|
|
204
|
-
const
|
|
204
|
+
const subagentOf = subagentFilers(db);
|
|
205
205
|
const filerOf = filersOf(db);
|
|
206
206
|
const text = (value) => value == null ? null : String(value);
|
|
207
207
|
const count = (value) => value == null ? null : Number(value);
|
|
@@ -224,19 +224,19 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
|
|
|
224
224
|
source: settled ? String(row["source"]) : priced.source,
|
|
225
225
|
project: text(row["repo"]),
|
|
226
226
|
person: kind === "person" || kind === "coordinator" ? filer.filedBy : null,
|
|
227
|
-
|
|
227
|
+
subagent: subagentOf(filer.repo, filer.filedBy, kind), provider, model: text(row["model"]),
|
|
228
228
|
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]),
|
|
229
229
|
taskId: String(row["task"]), runId: Number(row["id"]), authMode: billing,
|
|
230
230
|
};
|
|
231
231
|
});
|
|
232
|
-
// A
|
|
232
|
+
// A subagent's own turns get no key from Toolroll (subagents.ts claudeTurnRunner): they bill as this computer's
|
|
233
233
|
// Claude sign-in was seen billing when the turn ran (a plan, or a Console login, key helper or gateway).
|
|
234
|
-
for (const row of db.prepare(`SELECT t.at, t.
|
|
234
|
+
for (const row of db.prepare(`SELECT t.at, t.subagent, t.model, t.cost_usd, t.tokens_in, t.tokens_out, t.billing, m.repo FROM subagent_turn t JOIN subagent m ON m.id = t.subagent
|
|
235
235
|
WHERE t.at >= ? AND t.at < ? ORDER BY t.id`).all(from, to)) {
|
|
236
236
|
const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
|
|
237
237
|
const priced = priceWork(db, { provider: "claude", model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), billing });
|
|
238
|
-
items.push({ at: String(row["at"]), kind: "
|
|
239
|
-
|
|
238
|
+
items.push({ at: String(row["at"]), kind: "subagent", microusd: priced.microusd, source: priced.source, project: String(row["repo"]), person: null,
|
|
239
|
+
subagent: Number(row["subagent"]), provider: "claude", model: text(row["model"]),
|
|
240
240
|
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: billing });
|
|
241
241
|
}
|
|
242
242
|
// A chat turn that mirrors a project chat's turn is that turn: counted once, as the project chat's. A task's chat
|
|
@@ -246,19 +246,19 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
|
|
|
246
246
|
UNION ALL SELECT t.created_at, t.approver, COALESCE((SELECT c.provider FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), 'lead'),
|
|
247
247
|
(SELECT c.model FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), t.settled_microusd, t.tokens_in, t.tokens_out,
|
|
248
248
|
CASE th.scope_kind WHEN 'project' THEN th.scope_key WHEN 'task' THEN (SELECT r.repo FROM task_ref r WHERE r.external_id = th.scope_key ORDER BY r.id DESC LIMIT 1) END
|
|
249
|
-
FROM mate_turn t LEFT JOIN
|
|
249
|
+
FROM mate_turn t LEFT JOIN lead_thread th ON th.id = t.thread WHERE t.created_at >= ? AND t.created_at < ?`).all(from, to, from, to)) {
|
|
250
250
|
const provider = String(row["provider"]);
|
|
251
251
|
const subscription = provider.endsWith("-subscription") && modeOf(provider) === "subscription";
|
|
252
252
|
const settled = count(row["settled_microusd"]);
|
|
253
253
|
items.push({ at: String(row["at"]), kind: "chat", microusd: subscription ? 0 : settled, source: subscription ? "subscription" : settled === null ? "unpriced" : "reported",
|
|
254
|
-
project: text(row["project"]), person: text(row["approver"]),
|
|
254
|
+
project: text(row["project"]), person: text(row["approver"]), subagent: null, provider, model: text(row["model"]),
|
|
255
255
|
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: subscription ? "subscription" : "api-key" });
|
|
256
256
|
}
|
|
257
|
-
// A flow's Claude drafts: like a
|
|
257
|
+
// A flow's Claude drafts: like a subagent's turns, billed as the sign-in was.
|
|
258
258
|
for (const row of db.prepare("SELECT at, kind, repo, provider, model, cost_usd, billing FROM side_spend WHERE at >= ? AND at < ? ORDER BY id").all(from, to)) {
|
|
259
259
|
const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
|
|
260
260
|
const priced = priceWork(db, { provider: String(row["provider"]), model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: null, tokensOut: null, billing });
|
|
261
|
-
items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null,
|
|
261
|
+
items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null, subagent: null,
|
|
262
262
|
provider: String(row["provider"]), model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: billing });
|
|
263
263
|
}
|
|
264
264
|
// A flow's Sort zone asks Jev on OpenRouter (the key's credit): what it cost is kept with its answer.
|
|
@@ -267,7 +267,7 @@ export function spendItems(db, from, to, modeOf = provider => billingOf(provider
|
|
|
267
267
|
WHERE fs.kind = 'sort' AND fs.decision_json IS NOT NULL AND fs.started_at >= ? AND fs.started_at < ? ORDER BY fs.started_at`).all(from, to)) {
|
|
268
268
|
const cost = typeof row["cost"] === "number" && Number.isFinite(row["cost"]) && row["cost"] >= 0 ? row["cost"] : null;
|
|
269
269
|
items.push({ at: String(row["at"]), kind: "sort", microusd: cost === null ? null : Math.round(cost * 1_000_000), source: cost === null ? "unpriced" : "reported",
|
|
270
|
-
project: text(row["repo"]), person: null,
|
|
270
|
+
project: text(row["repo"]), person: null, subagent: null, provider: "openrouter", model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: "api-key" });
|
|
271
271
|
}
|
|
272
272
|
return items;
|
|
273
273
|
}
|
|
@@ -279,14 +279,14 @@ export function countsToward(item, budget) {
|
|
|
279
279
|
return item.project === budget.key;
|
|
280
280
|
if (budget.scope === "person")
|
|
281
281
|
return item.person === budget.key;
|
|
282
|
-
return item.
|
|
282
|
+
return item.subagent !== null && String(item.subagent) === budget.key;
|
|
283
283
|
}
|
|
284
284
|
const PROVIDER_WORDS = { claude: "Claude", codex: "Codex", gemini: "Gemini", openrouter: "OpenRouter" };
|
|
285
285
|
/** Why work waits on a budget, in words. */
|
|
286
|
-
export function budgetHoldWords(hold, month,
|
|
286
|
+
export function budgetHoldWords(hold, month, subagentName) {
|
|
287
287
|
if (hold.over === null)
|
|
288
288
|
return "";
|
|
289
|
-
const label = budgetLabel(hold.over,
|
|
289
|
+
const label = budgetLabel(hold.over, subagentName);
|
|
290
290
|
return hold.why === "unpriced"
|
|
291
291
|
? `${label} budget can't price ${PROVIDER_WORDS[hold.unpricedProvider ?? ""] ?? hold.unpricedProvider} work on a key yet. Open Settings → Models to load prices`
|
|
292
292
|
: `${label} budget is used up for ${month}`;
|
|
@@ -300,14 +300,14 @@ export function budgetStates(budgets, items) {
|
|
|
300
300
|
percent: Math.floor((spent / budget.limitMicrousd) * 100) };
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
|
-
/** A budget in words: "The shop project's", "alex's", "Maya's (
|
|
304
|
-
export function budgetLabel(budget,
|
|
303
|
+
/** A budget in words: "The shop project's", "alex's", "Maya's (subagent)", "The whole installation's". */
|
|
304
|
+
export function budgetLabel(budget, subagentName) {
|
|
305
305
|
if (budget.scope === "installation")
|
|
306
306
|
return "The whole installation's";
|
|
307
307
|
if (budget.scope === "project")
|
|
308
308
|
return `The ${budget.key.split("/").filter(Boolean).pop() ?? budget.key} project's`;
|
|
309
309
|
if (budget.scope === "person")
|
|
310
310
|
return `${budget.key}'s`;
|
|
311
|
-
return `${
|
|
311
|
+
return `${subagentName ?? `Subagent ${budget.key}`}'s`;
|
|
312
312
|
}
|
|
313
313
|
export const usd = (microusd) => microusd === null ? "unpriced" : `$${(microusd / 1_000_000).toFixed(microusd !== 0 && Math.abs(microusd) < 10_000_000 ? 2 : 0)}`;
|