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/serve.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { secretFolder } from "./tool-secret-store.js";
|
|
2
|
+
import { acceptedIdentities, recoverLearning } from "./project-learning.js";
|
|
3
|
+
import { loadOrCreateVapidKeys } from "./push.js";
|
|
4
|
+
import { checkoutPlan } from "./checkout-cleanup.js";
|
|
5
|
+
import { latestReleaseFor } from "./server/render-settings.js";
|
|
6
|
+
import { LastChecks } from "./server/last-checks.js";
|
|
7
|
+
import { readProviderKey } from "./keys.js";
|
|
8
|
+
import { closeExpiredUnattendedGrants } from "./flow-unattended.js";
|
|
9
|
+
import { isKeyedProvider } from "./acp-agents.js";
|
|
10
|
+
import { PROVIDER_IDS } from "./provider.js";
|
|
1
11
|
import { taskMovedSince } from "./chat-ask.js";
|
|
2
12
|
import { linuxFenceAvailable, macosFenceAvailable } from "./agent-fence.js";
|
|
3
13
|
import { signInsUnavailableWords } from "./check-access.js";
|
|
@@ -12,6 +22,10 @@ const RAIL_TASKS = 6;
|
|
|
12
22
|
const PROXY_HEADER = /^(forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-original-.+|x-cluster-client-ip|true-client-ip|cf-connecting-ip|fastly-client-ip|x-proxyuser-ip)$/i;
|
|
13
23
|
/** Asks a project names for the chime (its count is complete beyond them). */
|
|
14
24
|
const ASKS_PER_PROJECT = 100;
|
|
25
|
+
/** How often the background checks look at what is due (docs/plans/get-side-effects.md), and how often this
|
|
26
|
+
* computer's agent sign-ins are asked again. */
|
|
27
|
+
const BACKGROUND_CHECKS_MS = 30_000;
|
|
28
|
+
const LOCAL_AGENTS_EVERY_MS = 2 * 60_000;
|
|
15
29
|
import { boundProjectSummary, browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
|
|
16
30
|
import { sharedActionPayload } from './chat-actions.js';
|
|
17
31
|
import { prepareCodingContext } from './coding-context.js';
|
|
@@ -24,6 +38,7 @@ import { configureLeadFollow, recordLeadFollowFailure, runLeadFollowPass } from
|
|
|
24
38
|
import { leadNameOf } from "./lead-identity.js";
|
|
25
39
|
import { createLiveBus, followWorkspace } from "./live-bus.js";
|
|
26
40
|
import { startMaintenance } from './maintenance.js';
|
|
41
|
+
import { asReadOnlyRequest } from './read-only-request.js';
|
|
27
42
|
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
28
43
|
import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
29
44
|
import { projectAuthority } from "./project-access.js";
|
|
@@ -99,7 +114,7 @@ import { hasForbiddenControls } from "./decision.js";
|
|
|
99
114
|
import { UPDATE_PAUSED, updateAdmissionPaused } from "./desktop-update-gate.js";
|
|
100
115
|
import { scanForSecrets } from "./evidence.js";
|
|
101
116
|
import { run as execRun } from "./exec.js";
|
|
102
|
-
import { firstRunSteps, signInCommandFor } from "./first-run.js";
|
|
117
|
+
import { findFirstTasks, firstRunSteps, signInCommandFor } from "./first-run.js";
|
|
103
118
|
import { installMethod } from "./install-method.js";
|
|
104
119
|
import { createIntegrationMonitor } from "./integrations.js";
|
|
105
120
|
import { logEvent } from "./log.js";
|
|
@@ -139,7 +154,7 @@ import { createSettingsHandlers } from './server/settings.js';
|
|
|
139
154
|
import { requestContext } from "./server/request-context.js";
|
|
140
155
|
import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP, pinnedTheme, QUEUE_VIEW, refuse, screen, SENSITIVE_INPUT, shell, sidebarScript } from "./server/chrome.js";
|
|
141
156
|
import { BODY_CAP, form, loginHref, matchTaskPath, page, projectChatHref, redactedPath, redirect, respond, safeReturn, SHUTDOWN_WAIT_MS, taskChatHref } from "./server/http.js";
|
|
142
|
-
import { decisionsFor, LEAD_BY_DEFAULT_FACT, leadBrowserMessages, leadChatVersion, TASK_COMPOSER_MODES, teamProposalCardParts } from "./server/render-chat.js";
|
|
157
|
+
import { decisionsFor, LEAD_BY_DEFAULT_FACT, leadBrowserMessages, leadChatVersion, NOT_STARTED_SESSION, TASK_COMPOSER_MODES, teamProposalCardParts } from "./server/render-chat.js";
|
|
143
158
|
import {} from "./server/render-pages.js";
|
|
144
159
|
import { ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
145
160
|
import { NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, sessionWithinLifetime, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
|
|
@@ -245,8 +260,26 @@ export function createDecisionServer(options) {
|
|
|
245
260
|
const providerHome = options.connectionHome ?? homedir();
|
|
246
261
|
const checkConnection = createConnectionChecker({ home: providerHome, clock, ...(options.connectionProbe === undefined ? {} : { probe: options.connectionProbe }) });
|
|
247
262
|
// Every sign-in check the console makes also answers a sign-in pause.
|
|
263
|
+
// Whether this computer can fence agents: asked once as the console starts (a sandbox running /usr/bin/true), so
|
|
264
|
+
// Settings → Project never starts a process (docs/plans/get-side-effects.md).
|
|
265
|
+
const fenceHere = options.fenceAvailable === undefined ? macosFenceAvailable() || linuxFenceAvailable() : false;
|
|
266
|
+
// Phone notifications' signing pair, made once as the console starts so /push/key only reads it.
|
|
267
|
+
if (options.telegramTokenFile !== undefined) {
|
|
268
|
+
try {
|
|
269
|
+
loadOrCreateVapidKeys(dirname(options.telegramTokenFile));
|
|
270
|
+
}
|
|
271
|
+
catch { /* /push/key says push is not set up */ }
|
|
272
|
+
}
|
|
273
|
+
// The last answer of every other check a page shows (docs/plans/get-side-effects.md).
|
|
274
|
+
const lastChecks = new LastChecks();
|
|
275
|
+
/** The newest release, asked of npm (Settings → Updates shows the last answer). */
|
|
276
|
+
const checkRelease = () => lastChecks.run("release", () => latestReleaseFor(options.updates?.latest));
|
|
277
|
+
// What each sign-in check said last: what a page shows (docs/plans/get-side-effects.md). A page never checks.
|
|
278
|
+
const knownConnections = new Map();
|
|
279
|
+
const connectionKnown = (provider) => knownConnections.get(provider) ?? null;
|
|
248
280
|
const connectionCheck = async (provider, fresh) => {
|
|
249
281
|
const value = await checkConnection(provider, fresh);
|
|
282
|
+
knownConnections.set(provider, value);
|
|
250
283
|
try {
|
|
251
284
|
noteSignInProbe(store, provider, value.state, clock());
|
|
252
285
|
}
|
|
@@ -382,6 +415,17 @@ export function createDecisionServer(options) {
|
|
|
382
415
|
ssoProvider = { issuer, provider: found.provider, at: Date.now() };
|
|
383
416
|
return found;
|
|
384
417
|
}
|
|
418
|
+
/** The identity provider as last discovered: what starting a sign-in uses (a GET, so it fetches nothing,
|
|
419
|
+
* docs/plans/get-side-effects.md). Discovered as the console starts, by the background checks every ten minutes, and
|
|
420
|
+
* when sign-in settings are saved or tested. */
|
|
421
|
+
function providerKnown(issuer) {
|
|
422
|
+
return ssoProvider !== null && ssoProvider.issuer === issuer ? ssoProvider.provider : null;
|
|
423
|
+
}
|
|
424
|
+
{
|
|
425
|
+
const settings = ssoSettings();
|
|
426
|
+
if (settings !== null)
|
|
427
|
+
void providerFor(settings.issuer).catch(() => undefined);
|
|
428
|
+
}
|
|
385
429
|
/** Password sign-in is only for instance operators when the provider says so (a way in if it's down). */
|
|
386
430
|
const passwordAllowed = (name) => { const settings = ssoSettings(); return settings === null || settings.passwords === "everyone" || store.isInstanceOperator(name); };
|
|
387
431
|
const minutesWords = (ms) => { const minutes = Math.max(1, Math.ceil(ms / 60_000)); return minutes >= 120 ? `${Math.round(minutes / 60)} hours` : `${minutes} minute${minutes === 1 ? "" : "s"}`; };
|
|
@@ -456,7 +500,7 @@ export function createDecisionServer(options) {
|
|
|
456
500
|
setting: store.projectCheckAccess(repo), changed: store.projectCheckAccessChange(repo),
|
|
457
501
|
canNarrow: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo),
|
|
458
502
|
canWiden: who.via === "cookie" && !store.isDemo() && store.isInstanceOperator(who.name),
|
|
459
|
-
unconfinedHere: !(options.fenceAvailable?.() ??
|
|
503
|
+
unconfinedHere: !(options.fenceAvailable?.() ?? fenceHere) && store.unfencedAgentsAllowed(),
|
|
460
504
|
signInsUnavailable: signInsUnavailableWords(options.platform ?? process.platform),
|
|
461
505
|
},
|
|
462
506
|
liveView: (() => {
|
|
@@ -613,7 +657,9 @@ export function createDecisionServer(options) {
|
|
|
613
657
|
const server = createServer((request, response) => {
|
|
614
658
|
instrumentRequest(store.telemetry, request, response);
|
|
615
659
|
// Every password checked while answering counts against where this request came from (sign-in-guard.ts).
|
|
616
|
-
|
|
660
|
+
// A GET or HEAD only reads (docs/plans/get-side-effects.md): store reads that would tidy up as they go don't.
|
|
661
|
+
const answer = () => handle(request, response);
|
|
662
|
+
void withPasswordSource(joinSourceOf(request), () => request.method === "GET" || request.method === "HEAD" ? asReadOnlyRequest(answer) : answer()).catch(error => {
|
|
617
663
|
// Every unhandled error is logged (v99): the path and the message, never the request's body or query.
|
|
618
664
|
logEvent("error", "serve.error", { method: request.method, path: redactedPath(new URL(request.url ?? "/", "http://placeholder").pathname), error: error instanceof Error ? error.message : String(error) });
|
|
619
665
|
if (envValue(process.env, "SERVE_DEBUG") === "1")
|
|
@@ -775,7 +821,9 @@ export function createDecisionServer(options) {
|
|
|
775
821
|
// Passive polls authenticate without extending the browser session.
|
|
776
822
|
const fragmentName = url.searchParams.get("fragment");
|
|
777
823
|
const workspaceRead = method === "GET" && url.searchParams.get('format') === 'workspace';
|
|
778
|
-
const touch = !(method === "GET" && (edgeRoute?.domain === 'live' || workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname) || url.pathname === '/term/state'
|
|
824
|
+
const touch = !(method === "GET" && (edgeRoute?.domain === 'live' || workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname) || url.pathname === '/term/state'
|
|
825
|
+
// The Agents view's Live grid only watches: its polls never extend or write the session.
|
|
826
|
+
|| url.pathname === '/agents/live' || /^\/agents\/live\/[0-9]{1,15}$/.test(url.pathname) || (url.pathname === '/agents' && url.searchParams.get('view') === 'live')));
|
|
779
827
|
const refuseBudget = (admitted) => {
|
|
780
828
|
if (admitted.ok)
|
|
781
829
|
return false;
|
|
@@ -1071,9 +1119,9 @@ export function createDecisionServer(options) {
|
|
|
1071
1119
|
.finally(() => { localSignInChecking = null; });
|
|
1072
1120
|
return localSignInChecking;
|
|
1073
1121
|
}
|
|
1122
|
+
/** The last answer only (docs/plans/get-side-effects.md): this computer is asked again by the background checks or a
|
|
1123
|
+
* person's Check now, never because a page was opened. */
|
|
1074
1124
|
function localAgentSignedIn() {
|
|
1075
|
-
if (localSignInChecking === null && (localSignIn === null || Date.now() - localSignIn.at > 30_000))
|
|
1076
|
-
void checkLocalAgents();
|
|
1077
1125
|
return localSignIn?.signedIn ?? null;
|
|
1078
1126
|
}
|
|
1079
1127
|
/**
|
|
@@ -1139,12 +1187,15 @@ export function createDecisionServer(options) {
|
|
|
1139
1187
|
const allCoordinatorRows = store.listCoordinatorProposals({ repos: managedRepos(), states: ["pending", "confirmed", "refused"], limit: 30 });
|
|
1140
1188
|
const coordinatorProposals = focusTask !== null ? allCoordinatorRows.filter(one => focusTask.family.versions.some(version => version.id === one.payload["task"]))
|
|
1141
1189
|
: chatProject !== null ? allCoordinatorRows.filter(one => one.repo === chatProject) : allCoordinatorRows;
|
|
1142
|
-
|
|
1143
|
-
|
|
1190
|
+
// A visit only reads (quiet page visits): this opens no thread. The first message opens it (chatSend), and a
|
|
1191
|
+
// live thread kept under another ceiling is what that message replaces, so it reads here as the earlier one.
|
|
1192
|
+
const found = store.liveLeadThreadFor(who.name, chatScopeOf(focusTask, chatProject));
|
|
1193
|
+
const thread = found !== null && found.ceilingDigest === principal.ceilingDigest ? found : null;
|
|
1194
|
+
const proposals = thread === null ? [] : store.listLeadProposals(thread.id);
|
|
1144
1195
|
// One reply runs at a time per person; this thread shows it only when
|
|
1145
1196
|
// the reply is its own.
|
|
1146
1197
|
const live = store.liveLeadTurnFor(who.name);
|
|
1147
|
-
const messages = store.listLeadMessages(
|
|
1198
|
+
const messages = thread === null ? [] : store.listLeadMessages(thread.id, 40);
|
|
1148
1199
|
// The lead's question with buttons, while its reply is the last word in the thread.
|
|
1149
1200
|
const last = messages.at(-1);
|
|
1150
1201
|
const asking = last?.role === "assistant" && last.turn !== null && store.getLeadTurn(last.turn)?.approver === who.name ? store.leadAskOpen(last.turn, now) : null;
|
|
@@ -1155,9 +1206,10 @@ export function createDecisionServer(options) {
|
|
|
1155
1206
|
// Answered questions stay readable above the answer; only their buttons go.
|
|
1156
1207
|
const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.leadAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
|
|
1157
1208
|
// The person's queued messages in this thread: each shows waiting (with Remove) or why it was not sent.
|
|
1158
|
-
const queue = new Map(personalQueue.queue.forThread(
|
|
1159
|
-
// The thread a ceiling change replaced (ruling 9):
|
|
1160
|
-
const replaced = store.replacedLeadThread(
|
|
1209
|
+
const queue = new Map(thread === null ? [] : personalQueue.queue.forThread(thread.id).map(row => [row.message, row]));
|
|
1210
|
+
// The thread a ceiling change replaced (ruling 9), display only: closed already, or still live under the old ceiling.
|
|
1211
|
+
const replaced = thread !== null ? store.replacedLeadThread(thread)
|
|
1212
|
+
: found !== null && found.scope.kind === "lead" && store.listLeadMessages(found.id, 1).length > 0 ? found : null;
|
|
1161
1213
|
const previousProposals = replaced === null ? [] : store.listLeadProposals(replaced.id);
|
|
1162
1214
|
return {
|
|
1163
1215
|
previous: replaced === null ? null : { messages: store.listLeadMessages(replaced.id, 40), proposals: previousProposals, decisions: decisionsFor(store, previousProposals) },
|
|
@@ -1169,7 +1221,7 @@ export function createDecisionServer(options) {
|
|
|
1169
1221
|
proposals,
|
|
1170
1222
|
decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
|
|
1171
1223
|
coordinatorProposals,
|
|
1172
|
-
pending: live !== null && live.thread ===
|
|
1224
|
+
pending: live !== null && thread !== null && live.thread === thread.id ? live : null,
|
|
1173
1225
|
recent: store.recentLeadTurns(who.name, 5),
|
|
1174
1226
|
};
|
|
1175
1227
|
}
|
|
@@ -1182,7 +1234,9 @@ export function createDecisionServer(options) {
|
|
|
1182
1234
|
/** The Ask panel (v77): the conversation a task, result or project page
|
|
1183
1235
|
* docks beside itself — that page's own thread, the same session and
|
|
1184
1236
|
* cards as /chat. Null when this person has no lead chat here. A page
|
|
1185
|
-
* view
|
|
1237
|
+
* view starts nothing (quiet page visits): with no membership
|
|
1238
|
+
* conversation yet, the panel binds its sends to NOT_STARTED_SESSION and
|
|
1239
|
+
* the first message starts one (chatSend). */
|
|
1186
1240
|
function dockedConversation(who, focusTask, chatProject, now, back, resultRunId = null) {
|
|
1187
1241
|
if (who.via !== "cookie" || who.role !== "approver" || (focusTask === null && chatProject === null))
|
|
1188
1242
|
return null;
|
|
@@ -1190,16 +1244,14 @@ export function createDecisionServer(options) {
|
|
|
1190
1244
|
const principal = enabled.ok ? leadPrincipal(who) : null;
|
|
1191
1245
|
if (!enabled.ok || principal === null)
|
|
1192
1246
|
return null;
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
if (session === null || session.ceilingDigest !== principal.ceilingDigest || session.approverGeneration !== principal.generation)
|
|
1247
|
+
const found = store.activeLeadSession(who.name);
|
|
1248
|
+
const notStarted = (found === null || found.ceilingDigest !== principal.ceilingDigest) && enabled.billing === "subscription";
|
|
1249
|
+
const session = notStarted ? null : found;
|
|
1250
|
+
if (!notStarted && (session === null || session.ceilingDigest !== principal.ceilingDigest || session.approverGeneration !== principal.generation))
|
|
1199
1251
|
return null;
|
|
1200
1252
|
const rows = leadConversationRows(who, principal, focusTask, now, chatProject);
|
|
1201
1253
|
return {
|
|
1202
|
-
sessionId: session
|
|
1254
|
+
sessionId: session?.id ?? NOT_STARTED_SESSION, user: session?.approver ?? who.name, version: leadChatVersion({ ...rows, focusTask }),
|
|
1203
1255
|
messages: leadBrowserMessages(rows, back, { task: focusTask?.id ?? null, project: chatProject }),
|
|
1204
1256
|
pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: LEAD_MESSAGE_MAX_CHARS,
|
|
1205
1257
|
taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
|
|
@@ -1210,11 +1262,17 @@ export function createDecisionServer(options) {
|
|
|
1210
1262
|
/** Start the person's lead conversation: the session every later turn
|
|
1211
1263
|
* debits (a membership spends nothing; direct API gets a per-conversation
|
|
1212
1264
|
* ceiling) and its thread. Starting ends any older session. */
|
|
1213
|
-
function startLeadConversation(who, principal, enabled, ceilingUsd, follow, now) {
|
|
1265
|
+
function startLeadConversation(who, principal, enabled, ceilingUsd, follow, now, scope = LEAD_THREAD, request) {
|
|
1266
|
+
// A page's first send (quiet page visits) starts it under that send's request: the start and its record are one
|
|
1267
|
+
// write, so a retry of the same request finds the conversation it started and never starts another.
|
|
1268
|
+
if (request !== undefined) {
|
|
1269
|
+
return store.transact(() => store.replay({ idempotencyKey: `mate-start:${who.name}:${request}`, actor: who.name, at: now }, "mate-start", () => ({ session: startLeadConversation(who, principal, enabled, ceilingUsd, follow, now, scope) })).session);
|
|
1270
|
+
}
|
|
1214
1271
|
const ceilingMicrousd = enabled.billing === "subscription" ? 0 : Math.round(ceilingUsd * 1_000_000);
|
|
1215
1272
|
const termsDigest = createHash("sha256").update(`${ceilingMicrousd}\n${principal.ceilingDigest}`).digest("hex");
|
|
1216
1273
|
const sessionId = store.mintLeadSession({ approver: who.name, approverGeneration: principal.generation, credentialKey: enabled.credentialKey, ceilingMicrousd, ceilingDigest: principal.ceilingDigest, termsDigest }, now);
|
|
1217
|
-
|
|
1274
|
+
// The thread the first message goes to (quiet page visits: a task's first message opens only the task's thread).
|
|
1275
|
+
const thread = store.openLeadThread(who.name, principal.ceilingDigest, now, scope).thread;
|
|
1218
1276
|
if (follow)
|
|
1219
1277
|
configureLeadFollow(store, principal, store.getLeadSession(sessionId), thread, true, now);
|
|
1220
1278
|
leadSaid.delete(who.session.csrf);
|
|
@@ -1293,6 +1351,11 @@ export function createDecisionServer(options) {
|
|
|
1293
1351
|
* whole catalog, not a hand-pinned shortlist). null = no key or the
|
|
1294
1352
|
* catalog is unreachable; callers fall back to the compiled table. */
|
|
1295
1353
|
let catalogCache = null;
|
|
1354
|
+
/** The catalog as last fetched, for a page to show (docs/plans/get-side-effects.md): a page never fetches it. The
|
|
1355
|
+
* background checks keep it current while an OpenRouter key is saved; saving a choice fetches it (chatCatalog). */
|
|
1356
|
+
function chatCatalogKnown() {
|
|
1357
|
+
return chatKeyFor("openrouter-api") === null ? null : catalogCache?.models ?? null;
|
|
1358
|
+
}
|
|
1296
1359
|
async function chatCatalog() {
|
|
1297
1360
|
const key = chatKeyFor("openrouter-api")?.key;
|
|
1298
1361
|
if (key === undefined)
|
|
@@ -1323,7 +1386,7 @@ export function createDecisionServer(options) {
|
|
|
1323
1386
|
function paletteIndexTag(project) {
|
|
1324
1387
|
const admitted = project === null ? admissionList() : null;
|
|
1325
1388
|
const entries = [
|
|
1326
|
-
{ label: "
|
|
1389
|
+
{ label: "tasks", href: "/work" },
|
|
1327
1390
|
{ label: "needs you", href: "/work?view=needs-you" },
|
|
1328
1391
|
{ label: "inbox", href: "/" },
|
|
1329
1392
|
{ label: "board", href: "/board?scope=all" },
|
|
@@ -1332,7 +1395,6 @@ export function createDecisionServer(options) {
|
|
|
1332
1395
|
{ label: "flows", href: "/flows" },
|
|
1333
1396
|
{ label: "done", href: "/done" },
|
|
1334
1397
|
{ label: "review cockpit", href: "/review" },
|
|
1335
|
-
{ label: "task list", href: "/tasks" },
|
|
1336
1398
|
{ label: "fleet", href: "/fleet" },
|
|
1337
1399
|
{ label: "activity", href: "/activity" },
|
|
1338
1400
|
{ label: "action ledger", href: "/ledger" },
|
|
@@ -1347,8 +1409,8 @@ export function createDecisionServer(options) {
|
|
|
1347
1409
|
entries.push({ label: `${one.id} — ${one.title}`, href: `/t/${encodeURIComponent(one.id)}` });
|
|
1348
1410
|
}
|
|
1349
1411
|
const saturated = open.length > 200;
|
|
1350
|
-
const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/flows", "/done", "/review", "/
|
|
1351
|
-
return jsonScript(saturated ? [...safeEntries, { label: "… more in
|
|
1412
|
+
const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/flows", "/done", "/review", "/runs", "/projects", "/ledger"].includes(one.href) || one.href.startsWith("/t/")) : entries;
|
|
1413
|
+
return jsonScript(saturated ? [...safeEntries, { label: "… more in Tasks", href: "/work" }] : safeEntries, { id: "palette-index" });
|
|
1352
1414
|
}
|
|
1353
1415
|
/**
|
|
1354
1416
|
* The palette cache (arc 4, finding 6), and its honest contract: a
|
|
@@ -1809,6 +1871,106 @@ export function createDecisionServer(options) {
|
|
|
1809
1871
|
onError: error => recordLeadFollowFailure(store, error, clock()),
|
|
1810
1872
|
});
|
|
1811
1873
|
});
|
|
1874
|
+
// Background checks (docs/plans/get-side-effects.md): what pages show about this computer's agent sign-ins and the
|
|
1875
|
+
// services Toolroll uses is asked here, on a timer, and a page reads the last answer. A page visit never starts a
|
|
1876
|
+
// process or a network call, and never acts on what a check finds: lifting a sign-in pause and the lead-by-default
|
|
1877
|
+
// rule run from this pass (or a person's Check now), not from a GET.
|
|
1878
|
+
let backgroundChecks = null;
|
|
1879
|
+
let agentsCheckedAt = 0;
|
|
1880
|
+
if (options.backgroundChecks === true)
|
|
1881
|
+
server.once('listening', () => {
|
|
1882
|
+
// This Toolroll's folder for secrets, checked once now, so a page can read which are set without preparing it.
|
|
1883
|
+
try {
|
|
1884
|
+
secretFolder();
|
|
1885
|
+
}
|
|
1886
|
+
catch { /* a page says secrets can't be read; the next action reports why */ }
|
|
1887
|
+
backgroundChecks = startMaintenance({ intervalMs: options.backgroundChecksMs ?? BACKGROUND_CHECKS_MS, shouldStop: () => leadClosing,
|
|
1888
|
+
run: async () => {
|
|
1889
|
+
const now = Date.now();
|
|
1890
|
+
const waits = [];
|
|
1891
|
+
if (now - agentsCheckedAt >= (options.backgroundChecksMs ?? LOCAL_AGENTS_EVERY_MS)) {
|
|
1892
|
+
agentsCheckedAt = now;
|
|
1893
|
+
waits.push(checkLocalAgents());
|
|
1894
|
+
// The other agents Settings lists: their last answer is what the page shows.
|
|
1895
|
+
for (const provider of PROVIDER_IDS.filter(isKeyedProvider).filter(one => one !== "claude" && one !== "codex"))
|
|
1896
|
+
waits.push(connectionCheck(provider).catch(() => undefined));
|
|
1897
|
+
}
|
|
1898
|
+
integrations.refreshIfDue();
|
|
1899
|
+
const sso = ssoSettings();
|
|
1900
|
+
if (sso !== null && (ssoProvider === null || ssoProvider.issuer !== sso.issuer || Date.now() - ssoProvider.at >= 10 * 60_000))
|
|
1901
|
+
waits.push(providerFor(sso.issuer));
|
|
1902
|
+
if (chatKeyFor("openrouter-api") !== null)
|
|
1903
|
+
waits.push(chatCatalog());
|
|
1904
|
+
const openrouter = readProviderKey("openrouter", providerHome);
|
|
1905
|
+
// The cache answers until its list is due again (ten minutes, or 30 s after a failure), then fetches.
|
|
1906
|
+
if (openrouter !== null)
|
|
1907
|
+
waits.push(modelCatalog(openrouter));
|
|
1908
|
+
// What used to be closed by whichever page read it first: an operating mode or a flow's "runs on its own"
|
|
1909
|
+
// grant past its expiry, a stalled update, old coordinator proposals. (Lead turns past their deadline are the
|
|
1910
|
+
// follow pass's, which settles them even while an update drains.)
|
|
1911
|
+
const at = clock();
|
|
1912
|
+
store.closeExpiredModes(at);
|
|
1913
|
+
closeExpiredUnattendedGrants(store, at);
|
|
1914
|
+
store.sweepCoordinatorProposals(at);
|
|
1915
|
+
// The demo's scripted story, if a beat timer missed (its pages only read it).
|
|
1916
|
+
try {
|
|
1917
|
+
options.demoLead?.advance(at);
|
|
1918
|
+
}
|
|
1919
|
+
catch { /* the next pass tries again */ }
|
|
1920
|
+
const databaseFile = store.databaseFile();
|
|
1921
|
+
if (databaseFile !== null && !updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off) {
|
|
1922
|
+
const last = lastChecks.known("release");
|
|
1923
|
+
if (lastChecks.due("release", last !== null && "problem" in last.value ? 60_000 : 15 * 60_000))
|
|
1924
|
+
waits.push(checkRelease());
|
|
1925
|
+
}
|
|
1926
|
+
// Chat's first tasks (issues and TODOs) while the first run is open and nothing is filed yet.
|
|
1927
|
+
if (firstRunStepsNow(clock()) !== null && !store.hasAnyWork()) {
|
|
1928
|
+
for (const repo of managedRepos()) {
|
|
1929
|
+
const entry = firstTasks.get(repo);
|
|
1930
|
+
if (entry !== undefined && (entry.reading || entry.until > Date.now()))
|
|
1931
|
+
continue;
|
|
1932
|
+
const reading = { until: Date.now() + 10 * 60_000, found: entry?.found ?? null, reading: true };
|
|
1933
|
+
firstTasks.set(repo, reading);
|
|
1934
|
+
waits.push(findFirstTasks(repo, options.firstTaskRunner ?? execRun).then(found => { reading.found = found; }, () => { }).finally(() => { reading.reading = false; }));
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
// Learning recovered from reviewed runs (and a project's saved rows carried to its current identity), which
|
|
1938
|
+
// Settings → Learning and Knowledge used to do as they opened.
|
|
1939
|
+
if (lastChecks.due("learning", 10 * 60_000)) {
|
|
1940
|
+
waits.push(lastChecks.run("learning", async () => {
|
|
1941
|
+
for (const repo of managedRepos()) {
|
|
1942
|
+
try {
|
|
1943
|
+
acceptedIdentities(store, repo);
|
|
1944
|
+
}
|
|
1945
|
+
catch { /* the next pass tries again */ }
|
|
1946
|
+
try {
|
|
1947
|
+
recoverLearning(store, evidenceRoot, repo);
|
|
1948
|
+
}
|
|
1949
|
+
catch { /* the next pass tries again */ }
|
|
1950
|
+
}
|
|
1951
|
+
}));
|
|
1952
|
+
}
|
|
1953
|
+
// What Codex has configured, per project, for Settings → Tools to offer.
|
|
1954
|
+
for (const repo of managedRepos()) {
|
|
1955
|
+
if (lastChecks.due(`codex-tools:${repo}`, 10 * 60_000))
|
|
1956
|
+
waits.push(lastChecks.run(`codex-tools:${repo}`, () => codexServers(repo)));
|
|
1957
|
+
}
|
|
1958
|
+
if (databaseFile !== null && lastChecks.due("storage", 30 * 60_000)) {
|
|
1959
|
+
waits.push(lastChecks.run("storage", () => checkoutPlan(store, storagePool(databaseFile), clock(), { manual: true })));
|
|
1960
|
+
}
|
|
1961
|
+
// The same reading the chrome does, outside a page visit, so it releases a stalled `toolroll update` and a stalled
|
|
1962
|
+
// desktop update in every folder the chrome looks in.
|
|
1963
|
+
if (databaseFile !== null) {
|
|
1964
|
+
try {
|
|
1965
|
+
waitingUpdate(databaseFile, run => store.stopQuiescenceProblem(run) !== null, at);
|
|
1966
|
+
}
|
|
1967
|
+
catch { /* shown as last saved */ }
|
|
1968
|
+
}
|
|
1969
|
+
await Promise.all(waits);
|
|
1970
|
+
},
|
|
1971
|
+
onError: () => { }, });
|
|
1972
|
+
void backgroundChecks.runNow();
|
|
1973
|
+
});
|
|
1812
1974
|
// Keep native process custody until shutdown has verified agent/tool exit.
|
|
1813
1975
|
// Coding custody does not depend on subagents or the lead's follow pass, so
|
|
1814
1976
|
// it closes at once and never waits behind a model turn: a stop must release
|
|
@@ -1837,7 +1999,7 @@ export function createDecisionServer(options) {
|
|
|
1837
1999
|
liveHandlers.close();
|
|
1838
2000
|
detachReads();
|
|
1839
2001
|
liveFollower.close();
|
|
1840
|
-
void Promise.all([closeCoding(), terminalClosing.close?.().then(problem => { terminalClosing.problem = problem; }), bounded(team.close()), bounded(personalQueue.close()), bounded(leadMaintenance?.stop()), bounded(reads?.close())])
|
|
2002
|
+
void Promise.all([closeCoding(), terminalClosing.close?.().then(problem => { terminalClosing.problem = problem; }), bounded(team.close()), bounded(personalQueue.close()), bounded(leadMaintenance?.stop()), bounded(backgroundChecks?.stop()), bounded(reads?.close())])
|
|
1841
2003
|
// The server always closes; a terminal whose processes couldn't be proved stopped is the close's error.
|
|
1842
2004
|
.then(() => closeServer(error => callback?.(error ?? terminalClosing.problem ?? undefined))).catch(error => {
|
|
1843
2005
|
if (callback)
|
|
@@ -1913,6 +2075,7 @@ export function createDecisionServer(options) {
|
|
|
1913
2075
|
get liveRefreshSeconds() { return liveRefreshSeconds; },
|
|
1914
2076
|
get chatKeyFor() { return chatKeyFor; },
|
|
1915
2077
|
get chatCatalog() { return chatCatalog; },
|
|
2078
|
+
get chatCatalogKnown() { return chatCatalogKnown; },
|
|
1916
2079
|
get leadRecheckedAt() { return leadRecheckedAt; },
|
|
1917
2080
|
set leadRecheckedAt(value) { leadRecheckedAt = value; },
|
|
1918
2081
|
get checkLocalAgents() { return checkLocalAgents; },
|
|
@@ -1920,6 +2083,9 @@ export function createDecisionServer(options) {
|
|
|
1920
2083
|
set localSignIn(value) { localSignIn = value; },
|
|
1921
2084
|
get agentSignInCommand() { return agentSignInCommand; },
|
|
1922
2085
|
get connectionCheck() { return connectionCheck; },
|
|
2086
|
+
get connectionKnown() { return connectionKnown; },
|
|
2087
|
+
get lastChecks() { return lastChecks; },
|
|
2088
|
+
get checkRelease() { return checkRelease; },
|
|
1923
2089
|
get modelCatalog() { return modelCatalog; },
|
|
1924
2090
|
get modelSeams() { return modelSeams; },
|
|
1925
2091
|
get toolsViewOf() { return toolsViewOf; },
|
|
@@ -1949,6 +2115,7 @@ export function createDecisionServer(options) {
|
|
|
1949
2115
|
get ssoProvider() { return ssoProvider; },
|
|
1950
2116
|
set ssoProvider(value) { ssoProvider = value; },
|
|
1951
2117
|
get providerFor() { return providerFor; },
|
|
2118
|
+
get providerKnown() { return providerKnown; },
|
|
1952
2119
|
get connectVisits() { return connectVisits; },
|
|
1953
2120
|
get defaultProject() { return defaultProject; },
|
|
1954
2121
|
get googleVisits() { return googleVisits; },
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work a lead filed, nested under it on the Live grid (docs/plans/lead-dispatched-tiles.md): which of the viewer's
|
|
3
|
+
* tiles were filed through the viewer's OWN lead chat, and which chat. Proved from the store only, at request time: the
|
|
4
|
+
* task's family root was filed through the lead (`filed_via = 'mate'`) by the viewer as a person, and exactly one record
|
|
5
|
+
* links that root to one of the viewer's own personal lead threads — a confirmed task card the viewer confirmed (its
|
|
6
|
+
* door-written outcome, never the model's payload) or a sweep the viewer started from a card still kept. Attribution is
|
|
7
|
+
* to the chat that filed the family's ORIGINAL task: a revision asked for in another chat stays with the family. Anything
|
|
8
|
+
* short of that is no link:
|
|
9
|
+
* the tile stays a top-level tile, as it is today. A link only groups tiles the viewer already gets; it never adds one.
|
|
10
|
+
*/
|
|
11
|
+
import type { Store } from "../store.js";
|
|
12
|
+
export type FiledBy = {
|
|
13
|
+
thread: number;
|
|
14
|
+
chat: "lead" | "project" | "task";
|
|
15
|
+
};
|
|
16
|
+
/** For each run id given (the tiles already in this viewer's answer), the viewer's lead chat that filed its task. */
|
|
17
|
+
export declare function leadFiledLinks(store: Store, viewer: string, runIds: readonly number[]): Map<number, FiledBy>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const BUILT_IN = "built-in";
|
|
2
|
+
/** The family root of a built-in task, or null when its lineage can't be proved: the family's own proof (taskFamilyOf:
|
|
3
|
+
* every revision in the root's project, each bound by its brief to its parent's own run), and the task one of it. */
|
|
4
|
+
function rootOf(store, taskId) {
|
|
5
|
+
const family = store.taskFamilyOf(taskId, null, false);
|
|
6
|
+
return family === null || family.problem !== null || !family.versions.some(version => version.id === taskId) ? null : family.root.id;
|
|
7
|
+
}
|
|
8
|
+
const parse = (text) => {
|
|
9
|
+
if (typeof text !== "string")
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
const value = JSON.parse(text);
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** For each run id given (the tiles already in this viewer's answer), the viewer's lead chat that filed its task. */
|
|
20
|
+
export function leadFiledLinks(store, viewer, runIds) {
|
|
21
|
+
const links = new Map();
|
|
22
|
+
const db = store.handle;
|
|
23
|
+
// The viewer's own personal threads (never a team conversation's), by id.
|
|
24
|
+
const threads = new Map();
|
|
25
|
+
for (const row of db.prepare(`SELECT id, scope_kind FROM lead_thread WHERE approver = ? AND NOT EXISTS (SELECT 1 FROM team_conversation tc WHERE tc.thread = lead_thread.id)`).all(viewer)) {
|
|
26
|
+
const kind = row["scope_kind"];
|
|
27
|
+
threads.set(Number(row["id"]), kind === "project" ? "project" : kind === "task" ? "task" : "lead");
|
|
28
|
+
}
|
|
29
|
+
if (threads.size === 0)
|
|
30
|
+
return links;
|
|
31
|
+
// Each run's family root, proved once per task (attempts of one task share it), and only roots the viewer filed
|
|
32
|
+
// through the lead as a person go further.
|
|
33
|
+
const rootOfTask = new Map();
|
|
34
|
+
const runRoots = new Map();
|
|
35
|
+
const repos = new Map();
|
|
36
|
+
const refRow = db.prepare("SELECT repo, filed_via, filed_by, filed_by_kind FROM task_ref WHERE backend = ? AND external_id = ?");
|
|
37
|
+
for (const runId of runIds) {
|
|
38
|
+
const row = db.prepare("SELECT r.backend, r.external_id FROM run JOIN task_ref r ON r.id = run.task_ref WHERE run.id = ?").get(runId);
|
|
39
|
+
if (row === undefined || row["backend"] !== BUILT_IN)
|
|
40
|
+
continue;
|
|
41
|
+
const task = String(row["external_id"]);
|
|
42
|
+
if (!rootOfTask.has(task))
|
|
43
|
+
rootOfTask.set(task, rootOf(store, task));
|
|
44
|
+
const root = rootOfTask.get(task);
|
|
45
|
+
if (root === null || root === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
if (!repos.has(root)) {
|
|
48
|
+
const ref = refRow.get(BUILT_IN, root);
|
|
49
|
+
// Filed through the lead, by the viewer, as a person.
|
|
50
|
+
const filed = ref !== undefined && ref["filed_via"] === "mate" && ref["filed_by_kind"] === "person" && ref["filed_by"] === viewer && typeof ref["repo"] === "string";
|
|
51
|
+
repos.set(root, filed ? String(ref["repo"]) : "");
|
|
52
|
+
}
|
|
53
|
+
if (repos.get(root) !== "")
|
|
54
|
+
runRoots.set(runId, root);
|
|
55
|
+
}
|
|
56
|
+
const roots = [...new Set(runRoots.values())];
|
|
57
|
+
if (roots.length === 0)
|
|
58
|
+
return links;
|
|
59
|
+
// Every confirmed task card in the viewer's own threads naming one of these roots: ONE read per request, through the
|
|
60
|
+
// viewer's threads (lead_thread by approver, lead_proposal by thread and state), never once per family.
|
|
61
|
+
const found = new Map(roots.map(root => [root, []]));
|
|
62
|
+
const spoiled = new Set();
|
|
63
|
+
const cards = db.prepare(`SELECT p.thread, p.resolved_by, p.payload_json, p.outcome_json, json_extract(p.outcome_json, '$.taskId') AS task FROM lead_proposal p
|
|
64
|
+
JOIN lead_thread t ON t.id = p.thread WHERE t.approver = ? AND p.kind = 'task' AND p.state = 'confirmed' AND json_valid(p.outcome_json)
|
|
65
|
+
AND json_extract(p.outcome_json, '$.taskId') IN (${roots.map(() => "?").join(",")})`).all(viewer, ...roots);
|
|
66
|
+
for (const row of cards) {
|
|
67
|
+
const root = String(row["task"]);
|
|
68
|
+
const outcome = parse(row["outcome_json"]), payload = parse(row["payload_json"]);
|
|
69
|
+
// The door's own outcome (the model writes only the payload), confirmed by the viewer, for this project.
|
|
70
|
+
if (outcome?.["ok"] !== true || outcome["kind"] !== "task" || outcome["taskId"] !== root || row["resolved_by"] !== viewer || payload?.["repo"] !== repos.get(root))
|
|
71
|
+
spoiled.add(root);
|
|
72
|
+
else
|
|
73
|
+
found.get(root)?.push(Number(row["thread"]));
|
|
74
|
+
}
|
|
75
|
+
// A sweep links while the card that started it is kept, as a task card does: ending a chat (or revocation) erases its
|
|
76
|
+
// cards, and with them every link it made — one retention rule for both.
|
|
77
|
+
const sweeps = db.prepare(`SELECT k.root_task, s.owner, s.thread, s.repo, p.thread AS card_thread, p.state AS card_state, p.resolved_by AS card_by
|
|
78
|
+
FROM lead_sweep_task k JOIN lead_sweep s ON s.id = k.sweep LEFT JOIN lead_proposal p ON p.id = s.proposal
|
|
79
|
+
WHERE k.root_task IN (${roots.map(() => "?").join(",")})`).all(...roots);
|
|
80
|
+
for (const row of sweeps) {
|
|
81
|
+
const root = String(row["root_task"]);
|
|
82
|
+
if (row["owner"] !== viewer || row["repo"] !== repos.get(root) || row["thread"] === null || row["card_thread"] !== row["thread"] || row["card_state"] !== "confirmed" || row["card_by"] !== viewer)
|
|
83
|
+
spoiled.add(root);
|
|
84
|
+
else
|
|
85
|
+
found.get(root)?.push(Number(row["thread"]));
|
|
86
|
+
}
|
|
87
|
+
for (const [runId, root] of runRoots) {
|
|
88
|
+
// Exactly one record, nothing amiss beside it, naming one of the viewer's own personal threads.
|
|
89
|
+
const named = found.get(root) ?? [];
|
|
90
|
+
if (spoiled.has(root) || named.length !== 1)
|
|
91
|
+
continue;
|
|
92
|
+
const chat = threads.get(named[0]);
|
|
93
|
+
if (chat !== undefined)
|
|
94
|
+
links.set(runId, { thread: named[0], chat });
|
|
95
|
+
}
|
|
96
|
+
return links;
|
|
97
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lead tiles on the Live grid (docs/plans/agents-view.md slice 8): the viewer's OWN lead chat turns, and only through
|
|
3
|
+
* the chat room's own admission — an approver with a lead standing, the thread theirs, open, the newest for its scope,
|
|
4
|
+
* never a team conversation, under the ceiling it opened with, and its project or task still theirs to see. Team
|
|
5
|
+
* threads wait for their own admission. The words go through one stateful redactor per turn (one key state carried
|
|
6
|
+
* across every step and every line of the turn's current snapshot), then controls out, then a cap. Memory only: the
|
|
7
|
+
* tile is the live turn's, and leaves with it (60 s after the turn ends).
|
|
8
|
+
*/
|
|
9
|
+
import type { LeadLiveStep } from "../lead-progress.js";
|
|
10
|
+
import type { ServerRuntime } from "./runtime.js";
|
|
11
|
+
import type { Who } from "./session.js";
|
|
12
|
+
/** A lead tile keeps at most this many lines, and this many bytes of them (the newest). */
|
|
13
|
+
export declare const LEAD_LINES = 200, LEAD_BYTES: number;
|
|
14
|
+
export type LeadTile = {
|
|
15
|
+
thread: number;
|
|
16
|
+
/** The turn being answered (lead.ts's turn id), once the turn has said it. */
|
|
17
|
+
turn: number | null;
|
|
18
|
+
scope: "lead" | "project" | "task";
|
|
19
|
+
title: string;
|
|
20
|
+
project: string | null;
|
|
21
|
+
state: "working" | "done" | "failed";
|
|
22
|
+
startedAt: string;
|
|
23
|
+
endedAt: string | null;
|
|
24
|
+
/** When each step began. */
|
|
25
|
+
stepAt: string[];
|
|
26
|
+
/** What it is doing now, in fixed words; null once it ended. */
|
|
27
|
+
doing: string | null;
|
|
28
|
+
lines: string[];
|
|
29
|
+
/** Whether older lines were cut to fit. */
|
|
30
|
+
earlier: boolean;
|
|
31
|
+
chatHref: string;
|
|
32
|
+
taskHref: string | null;
|
|
33
|
+
};
|
|
34
|
+
/** One turn's lines, redacted as one stream: every step's tools and words in order through ONE key state (a key block
|
|
35
|
+
* opened in one step stays hidden into the next), each line cleaned and cut, then the newest kept within the caps.
|
|
36
|
+
* `confirmed[i]`: step i's text is its finished answer, secret-checked whole. */
|
|
37
|
+
export declare function leadLines(steps: readonly LeadLiveStep[], confirmed?: readonly boolean[]): {
|
|
38
|
+
lines: string[];
|
|
39
|
+
earlier: boolean;
|
|
40
|
+
};
|
|
41
|
+
type LeadDeps = Pick<ServerRuntime, "store" | "liveTurns" | "leadPrincipal" | "familyOf" | "visible" | "managedRepos">;
|
|
42
|
+
/** The viewer's own live lead turns, admitted as the chat room admits them, proved now. */
|
|
43
|
+
export declare function leadTilesFor(deps: LeadDeps, who: Who): LeadTile[];
|
|
44
|
+
/** The tiles projected earlier that are still this viewer's right now (the whole admission proved again, with the
|
|
45
|
+
* session as it is saved now): what is delivered is never more than what is admitted at the moment of delivery. */
|
|
46
|
+
export declare function leadsStillAdmitted(deps: LeadDeps, who: Who, tiles: readonly LeadTile[]): LeadTile[];
|
|
47
|
+
export {};
|