toolroll 0.9.63 → 0.9.65
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 +51 -0
- package/README.md +15 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +50 -2
- package/dist/agent-fence.js +157 -10
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +36 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +12 -4
- package/dist/contracts/console-api.js +12 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +217 -21
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +275 -17
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +170 -5
- package/dist/scope.js +251 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +247 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +4 -1
- package/dist/server/render-pages.js +5 -4
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +110 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +46 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +121 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +320 -74
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +430 -15
- package/dist/store.js +1041 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +2 -0
- package/dist/tool-broker.js +1 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/package.json +11 -2
package/dist/result-review.d.ts
CHANGED
|
@@ -149,7 +149,7 @@ export declare const RESULT_FACT_KEYS: readonly ["run", "head", "base", "head-so
|
|
|
149
149
|
export declare function resultFactsFromHtml(html: string): Record<string, string>[];
|
|
150
150
|
/** Where a result form may send its reader back: the review cockpit's
|
|
151
151
|
* own deep link, the chat's result detail, the page of a task named in
|
|
152
|
-
* `tasks` (the run's own), or the run page. Anything else lands on the run
|
|
152
|
+
* `tasks` (the run's own) or its Result tab at this run, or the run page. Anything else lands on the run
|
|
153
153
|
* page. Matched exactly — never a bare open redirect. */
|
|
154
154
|
export declare function resultReturnTarget(raw: string | null | undefined, runId: number, tasks?: readonly string[]): string;
|
|
155
155
|
/** A form's own request token, minted per render — the same shape the
|
package/dist/result-review.js
CHANGED
|
@@ -147,7 +147,7 @@ export function resultFactsFromHtml(html) {
|
|
|
147
147
|
}
|
|
148
148
|
/** Where a result form may send its reader back: the review cockpit's
|
|
149
149
|
* own deep link, the chat's result detail, the page of a task named in
|
|
150
|
-
* `tasks` (the run's own), or the run page. Anything else lands on the run
|
|
150
|
+
* `tasks` (the run's own) or its Result tab at this run, or the run page. Anything else lands on the run
|
|
151
151
|
* page. Matched exactly — never a bare open redirect. */
|
|
152
152
|
export function resultReturnTarget(raw, runId, tasks = []) {
|
|
153
153
|
if (raw !== null && raw !== undefined) {
|
|
@@ -160,6 +160,10 @@ export function resultReturnTarget(raw, runId, tasks = []) {
|
|
|
160
160
|
// The run's own task page, which offered Run checks in place; never another task's.
|
|
161
161
|
if (tasks.some(task => raw === `/t/${encodeURIComponent(task)}`))
|
|
162
162
|
return raw;
|
|
163
|
+
// That task page's Result tab at this run (docs/plans/one-page-task-and-agent.md), its version and view kept.
|
|
164
|
+
const tab = /^\/t\/([A-Za-z0-9._~%-]{1,200})\?(?:version=[A-Za-z0-9._~%-]{1,200}&)?tab=result&run=([0-9]{1,15})(?:§ion=(?:summary|changes|checks))?$/.exec(raw);
|
|
165
|
+
if (tab !== null && Number(tab[2]) === runId && tasks.some(task => tab[1] === encodeURIComponent(task)))
|
|
166
|
+
return raw;
|
|
163
167
|
}
|
|
164
168
|
return `/r/${runId}`;
|
|
165
169
|
}
|
|
@@ -240,6 +244,8 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
240
244
|
var user=panel?panel.getAttribute('data-result-user')||'':'';
|
|
241
245
|
var task=panel?panel.getAttribute('data-result-task')||'':'';
|
|
242
246
|
var run=panel?panel.getAttribute('data-result-run')||'':'';
|
|
247
|
+
// The address parameter that holds the result's own view: 'tab' on its page, 'section' on a task page's Result tab.
|
|
248
|
+
var viewParam=panel&&panel.getAttribute('data-result-param')==='section'?'section':'tab';
|
|
243
249
|
var draftPrefix='standing-orders:review-draft:',scrollPrefix='standing-orders:result-scroll:';
|
|
244
250
|
var day=86400000,storage=true,rememberPosition=null;
|
|
245
251
|
function read(key){try{return JSON.parse(sessionStorage.getItem(key)||'null');}catch(e){return null;}}
|
|
@@ -262,7 +268,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
262
268
|
tab.addEventListener('click',function(ev){
|
|
263
269
|
if(ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey||ev.button!==0)return;
|
|
264
270
|
ev.preventDefault();var name=tab.getAttribute('data-result-tab');show(name);
|
|
265
|
-
try{var url=new URL(location.href);url.searchParams.set(
|
|
271
|
+
try{var url=new URL(location.href);url.searchParams.set(viewParam,name);url.hash='';history.replaceState(history.state,'',url.toString());}catch(e){}
|
|
266
272
|
if(rememberPosition)rememberPosition();
|
|
267
273
|
});
|
|
268
274
|
tab.addEventListener('keydown',function(ev){
|
|
@@ -383,7 +389,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
383
389
|
// view the URL now names.
|
|
384
390
|
var returnKey=scrollPrefix+user+':'+task+':return';
|
|
385
391
|
function scrollKeyFor(){var url=null;try{url=new URL(location.href);}catch(e){}
|
|
386
|
-
return scrollPrefix+user+':'+task+':'+(url?(url.searchParams.get('result')||run)+':'+(url.searchParams.get(
|
|
392
|
+
return scrollPrefix+user+':'+task+':'+(url?(url.searchParams.get('result')||run)+':'+(url.searchParams.get(viewParam)||'summary'):run);}
|
|
387
393
|
// Once a navigation has begun (pageswap / beforeunload) the document's
|
|
388
394
|
// scroll is no longer the reader's — a cross-document transition can
|
|
389
395
|
// move it to the top while it is captured — so remembering stops.
|
package/dist/retention-ui.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the next daily sweep would remove. An instance operator's page; changes
|
|
4
4
|
* take a step-up.
|
|
5
5
|
*/
|
|
6
|
-
import { html, postForm } from "./html.js";
|
|
6
|
+
import { html, postForm, reauth } from "./html.js";
|
|
7
7
|
import { bytesWords } from "./storage.js";
|
|
8
8
|
import { RETENTION_KINDS, countWords, periodChoices, periodLabel } from "./retention.js";
|
|
9
9
|
export const RETENTION_CSS = `.retention{max-width:720px;min-width:0}.retention-next{margin:0 0 16px}` +
|
|
@@ -31,5 +31,5 @@ export function retentionHtml(view, notice) {
|
|
|
31
31
|
return html `<div class="kind" data-kind="${kind}"><label class="name" for="keep-${kind}">${label}</label><p class="meta">${detail}</p><select id="keep-${kind}" name="${kind}">${options}</select></div>`;
|
|
32
32
|
});
|
|
33
33
|
const last = view.lastSweep === null ? "" : ` Last sweep ${view.lastSweep.slice(0, 10)}.`;
|
|
34
|
-
return html `<article class="retention">${note}<p class="retention-next" data-retention-next>${nextWords(view)}${last}</p>${postForm("/settings/retention", html `${rows}
|
|
34
|
+
return html `<article class="retention">${note}<p class="retention-next" data-retention-next>${nextWords(view)}${last}</p>${postForm("/settings/retention", html `${rows}${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Save</button>`)}<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold).</p></article>`;
|
|
35
35
|
}
|
package/dist/retention.js
CHANGED
|
@@ -24,6 +24,10 @@ import { RETENTION_NOTE } from "./evidence.js";
|
|
|
24
24
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
25
25
|
import { bytesWords, treeBytes } from "./storage.js";
|
|
26
26
|
import { RETENTION_DAYS, retentionDaysSchema, retentionKindSchema } from "./contracts/retention.js";
|
|
27
|
+
/** A personal lead-queue row the sweep keeps (with its message): one still waiting, running or uncertain, and any of a
|
|
28
|
+
* conversation that is still open — its request id is the tombstone that stops a removed message being sent again. */
|
|
29
|
+
const KEPT_QUEUE_ROW = (message) => `lead_queue lq WHERE lq.message = ${message}
|
|
30
|
+
AND (lq.status IN ('queued', 'running', 'uncertain') OR EXISTS (SELECT 1 FROM lead_session ls WHERE ls.id = lq.session AND ls.ended_at IS NULL))`;
|
|
27
31
|
const RETENTION_TABLE = (name) => `
|
|
28
32
|
CREATE TABLE IF NOT EXISTS ${name} (
|
|
29
33
|
kind TEXT PRIMARY KEY CHECK (kind IN ('evidence', 'checkouts', 'chat', 'notifications')),
|
|
@@ -177,6 +181,7 @@ export function retentionPlan(store, evidenceRoot, now, periods = store.retentio
|
|
|
177
181
|
const rows = db.prepare(`SELECT m.id AS id, length(m.text) + COALESCE(length(m.activity), 0) + 64 AS bytes FROM lead_message m JOIN lead_thread th ON th.id = m.thread
|
|
178
182
|
WHERE m.created_at < ?
|
|
179
183
|
AND NOT EXISTS (SELECT 1 FROM team_message tm WHERE tm.message = m.id AND tm.status IN ('queued', 'running', 'uncertain'))
|
|
184
|
+
AND NOT EXISTS (SELECT 1 FROM ${KEPT_QUEUE_ROW("m.id")})
|
|
180
185
|
AND NOT EXISTS (SELECT 1 FROM mate_turn turn WHERE turn.id = m.turn AND turn.state IN ('queued', 'running'))
|
|
181
186
|
AND NOT (th.scope_kind = 'task' AND NOT EXISTS (SELECT 1 FROM task_ref r WHERE r.external_id = th.scope_key AND ${finishedTask("r")}))
|
|
182
187
|
ORDER BY m.id LIMIT ?`).all(cutoff(now, periods.chat), SWEEP_BATCH + 1);
|
|
@@ -263,14 +268,18 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
|
|
|
263
268
|
}
|
|
264
269
|
if (periods.chat !== null) {
|
|
265
270
|
const team = db.prepare("DELETE FROM team_message WHERE message = ? AND status NOT IN ('queued', 'running', 'uncertain')");
|
|
266
|
-
const message = db.prepare(
|
|
271
|
+
const message = db.prepare(`DELETE FROM lead_message WHERE id = ? AND NOT EXISTS (SELECT 1 FROM team_message WHERE message = ?)
|
|
272
|
+
AND NOT EXISTS (SELECT 1 FROM ${KEPT_QUEUE_ROW("lead_message.id")})`);
|
|
267
273
|
const asked = db.prepare("SELECT turn FROM lead_message WHERE id = ? AND role = 'assistant' AND turn IS NOT NULL");
|
|
274
|
+
const searchCopy = db.prepare("DELETE FROM memory_search WHERE kind = 'conversation' AND ref = ?");
|
|
268
275
|
// The lead's question goes with the reply that asked it; its buttons then say it expired.
|
|
269
276
|
const ask = db.prepare("DELETE FROM lead_ask WHERE turn = ? AND NOT EXISTS (SELECT 1 FROM lead_message WHERE turn = ? AND role = 'assistant')");
|
|
270
277
|
for (const one of plan.items.chat) {
|
|
271
278
|
team.run(one.id);
|
|
272
279
|
const turn = asked.get(one.id)?.["turn"];
|
|
273
280
|
if (Number(message.run(one.id, one.id).changes) === 1) {
|
|
281
|
+
// Its search copy goes with it (search never shows words the store no longer keeps).
|
|
282
|
+
searchCopy.run(`message:${one.id}`);
|
|
274
283
|
done.chat.count++;
|
|
275
284
|
done.chat.bytes += one.bytes;
|
|
276
285
|
if (turn !== undefined && turn !== null)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run went (docs/plans/run-efficiency.md): derived from the run's own records, display only. It never changes a
|
|
3
|
+
* completion, an approval, a charge or a check verdict, and it claims nothing its sources don't hold — an unreported
|
|
4
|
+
* number is unknown (null, shown as a dash), never zero.
|
|
5
|
+
*
|
|
6
|
+
* Sources, and what each can say: the run row (wall times, tokens, the provider's raw usage with its cache counts, the
|
|
7
|
+
* agent's turns where reported) is kept with the run; the live records (`<evidence>/live/<run>.jsonl`, display-only,
|
|
8
|
+
* kept 24 hours) are read once, bounded, through the descriptor-safe live reader, for step counts and observed step
|
|
9
|
+
* times. No command text is ever stored here: repeats are counted, never quoted.
|
|
10
|
+
*/
|
|
11
|
+
import type { Run, Store } from "./store.js";
|
|
12
|
+
import type { LiveActivityKind, LiveRecord } from "./live-records.js";
|
|
13
|
+
export declare const RUN_EFFICIENCY_VERSION = 3;
|
|
14
|
+
/** Budgets for one derivation: never more than the live file's own cap, and a bounded record count. */
|
|
15
|
+
export declare const EFFICIENCY_READ_BYTES: number;
|
|
16
|
+
export declare const EFFICIENCY_RECORDS = 20000;
|
|
17
|
+
export type StepsState = "complete" | "truncated" | "expired" | "absent";
|
|
18
|
+
export type RunEfficiency = {
|
|
19
|
+
version: number;
|
|
20
|
+
/** Waiting to start (claimed → provider started) and the agent (provider started → finished); null when unrecorded. */
|
|
21
|
+
time: {
|
|
22
|
+
waitMs: number | null;
|
|
23
|
+
agentMs: number | null;
|
|
24
|
+
};
|
|
25
|
+
turns: number | null;
|
|
26
|
+
/** Tokens in counts everything the model read (cached included); `cacheShare` is read-from-cache ÷ that, 0–1. */
|
|
27
|
+
usage: {
|
|
28
|
+
in: number | null;
|
|
29
|
+
out: number | null;
|
|
30
|
+
cacheRead: number | null;
|
|
31
|
+
cacheWrite: number | null;
|
|
32
|
+
cacheShare: number | null;
|
|
33
|
+
provider: string;
|
|
34
|
+
model: string | null;
|
|
35
|
+
};
|
|
36
|
+
steps: {
|
|
37
|
+
state: StepsState;
|
|
38
|
+
reason: string | null;
|
|
39
|
+
total: number | null;
|
|
40
|
+
failed: number | null;
|
|
41
|
+
unknownDuration: number | null;
|
|
42
|
+
byKind: Partial<Record<LiveActivityKind, number>>;
|
|
43
|
+
longest: {
|
|
44
|
+
kind: LiveActivityKind;
|
|
45
|
+
ms: number;
|
|
46
|
+
}[];
|
|
47
|
+
/** Repeated activity: the same step (kind and its full recorded input) at least three times. Null when the inputs
|
|
48
|
+
* can't be compared (command detail off, or a target hidden or shortened) — said, never guessed. */
|
|
49
|
+
repeats: {
|
|
50
|
+
kind: LiveActivityKind;
|
|
51
|
+
count: number;
|
|
52
|
+
}[] | null;
|
|
53
|
+
repeatsReason: string | null;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** Tokens and cache from the provider's own usage report, read per provider (their counts mean different things). */
|
|
57
|
+
export declare function usageOf(run: Pick<Run, "provider" | "model" | "tokensIn" | "tokensOut"> & {
|
|
58
|
+
usageJson?: string | null;
|
|
59
|
+
}): RunEfficiency["usage"];
|
|
60
|
+
/** The live records' steps, bounded: counts by kind, failures, observed times and repeats (never their words). */
|
|
61
|
+
/** `measured`: the recorder saw this provider's steps start and end as they happened (Claude's stream does); for the
|
|
62
|
+
* others their times are partly made up (Codex reports a step when it completes, ACP agents may report late), so no
|
|
63
|
+
* duration is claimed for them. */
|
|
64
|
+
export declare function stepsOf(records: Iterable<LiveRecord>, truncated: string | null, measured?: boolean): RunEfficiency["steps"];
|
|
65
|
+
/** Derive a finished run's How it ran from its records. */
|
|
66
|
+
export declare function deriveRunEfficiency(run: Run & {
|
|
67
|
+
usageJson?: string | null;
|
|
68
|
+
}, evidenceRoot: string, now: Date): RunEfficiency;
|
|
69
|
+
/**
|
|
70
|
+
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
71
|
+
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runEfficiencyOf(store: Store, evidenceRoot: string, runId: number, now: Date): RunEfficiency | null;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { LIVE_RETAIN_MS, legacyLiveLogPath, liveLogPath, liveWindowRecords } from "./live.js";
|
|
4
|
+
export const RUN_EFFICIENCY_VERSION = 3;
|
|
5
|
+
/** Budgets for one derivation: never more than the live file's own cap, and a bounded record count. */
|
|
6
|
+
export const EFFICIENCY_READ_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
export const EFFICIENCY_RECORDS = 20_000;
|
|
8
|
+
const LONGEST = 5;
|
|
9
|
+
const num = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
10
|
+
const msBetween = (from, to) => {
|
|
11
|
+
if (from === null || to === null)
|
|
12
|
+
return null;
|
|
13
|
+
const ms = Date.parse(to) - Date.parse(from);
|
|
14
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
15
|
+
};
|
|
16
|
+
/** Tokens and cache from the provider's own usage report, read per provider (their counts mean different things). */
|
|
17
|
+
export function usageOf(run) {
|
|
18
|
+
let raw = {};
|
|
19
|
+
try {
|
|
20
|
+
const parsed = run.usageJson == null ? null : JSON.parse(run.usageJson);
|
|
21
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
22
|
+
raw = parsed;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
raw = {};
|
|
26
|
+
}
|
|
27
|
+
const base = { provider: run.provider, model: run.model ?? null, out: num(raw["output_tokens"]) ?? run.tokensOut ?? null };
|
|
28
|
+
if (run.provider === "claude") {
|
|
29
|
+
// Claude's input_tokens leave out what was read from or written to the cache: the three add up to what it read, so
|
|
30
|
+
// all three must be reported before a total or a share is claimed (a missing one is unknown, never zero).
|
|
31
|
+
const input = num(raw["input_tokens"]), read = num(raw["cache_read_input_tokens"]), write = num(raw["cache_creation_input_tokens"]);
|
|
32
|
+
const total = input === null || read === null || write === null ? null : input + read + write;
|
|
33
|
+
return { ...base, in: total, cacheRead: read, cacheWrite: write, cacheShare: total === null || total === 0 ? null : read / total };
|
|
34
|
+
}
|
|
35
|
+
if (run.provider === "codex") {
|
|
36
|
+
// Codex's input_tokens already include what came from its cache: both reported, or no share.
|
|
37
|
+
const input = num(raw["input_tokens"]), read = num(raw["cached_input_tokens"]);
|
|
38
|
+
return { ...base, in: input ?? run.tokensIn ?? null, cacheRead: read, cacheWrite: null,
|
|
39
|
+
cacheShare: read === null || input === null || input === 0 || read > input ? null : read / input };
|
|
40
|
+
}
|
|
41
|
+
return { ...base, in: run.tokensIn ?? null, cacheRead: null, cacheWrite: null, cacheShare: null };
|
|
42
|
+
}
|
|
43
|
+
/** The live records' steps, bounded: counts by kind, failures, observed times and repeats (never their words). */
|
|
44
|
+
/** `measured`: the recorder saw this provider's steps start and end as they happened (Claude's stream does); for the
|
|
45
|
+
* others their times are partly made up (Codex reports a step when it completes, ACP agents may report late), so no
|
|
46
|
+
* duration is claimed for them. */
|
|
47
|
+
export function stepsOf(records, truncated, measured = true) {
|
|
48
|
+
let detail = "base";
|
|
49
|
+
const open = new Map();
|
|
50
|
+
const byKind = {};
|
|
51
|
+
const longest = [];
|
|
52
|
+
let total = 0, failed = 0, unknown = 0;
|
|
53
|
+
for (const record of records) {
|
|
54
|
+
if (record.k === "open")
|
|
55
|
+
detail = record.detail;
|
|
56
|
+
else if (record.k === "step") {
|
|
57
|
+
if (record.kind === "session" || record.kind === "message")
|
|
58
|
+
continue;
|
|
59
|
+
total++;
|
|
60
|
+
byKind[record.kind] = (byKind[record.kind] ?? 0) + 1;
|
|
61
|
+
open.set(record.id, { kind: record.kind, at: record.at, once: record.once === true });
|
|
62
|
+
}
|
|
63
|
+
else if (record.k === "done") {
|
|
64
|
+
const started = open.get(record.id);
|
|
65
|
+
if (started === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
open.delete(record.id);
|
|
68
|
+
if (record.ok === false)
|
|
69
|
+
failed++;
|
|
70
|
+
// Not a measured time: a provider whose times the recorder can't see as they happen, a start and end made up
|
|
71
|
+
// together (`once`), a step the recorder closed itself when the run ended (outcome unknown), or no time between.
|
|
72
|
+
const ms = !measured || started.once || record.ok === null ? null : msBetween(started.at || null, record.at || null);
|
|
73
|
+
if (ms === null || ms === 0) {
|
|
74
|
+
unknown++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
longest.push({ kind: started.kind, ms });
|
|
78
|
+
longest.sort((a, b) => b.ms - a.ms);
|
|
79
|
+
if (longest.length > LONGEST)
|
|
80
|
+
longest.pop();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Steps that never closed (the run ended, or the file was cut) have no measured time.
|
|
84
|
+
unknown += open.size;
|
|
85
|
+
// Repeats are never claimed: the step records keep a display form of each step (a file name, a host, a command with
|
|
86
|
+
// its shell wrapper taken off), so two different steps can read alike. Said, not guessed.
|
|
87
|
+
const repeatsReason = detail === "commands"
|
|
88
|
+
? "Repeats can't be told apart: the step records keep a short form of each step, not its whole input."
|
|
89
|
+
: "Repeats can't be told apart: this project doesn't record step details.";
|
|
90
|
+
const repeats = null;
|
|
91
|
+
return { state: truncated === null ? "complete" : "truncated", reason: truncated, total, failed, unknownDuration: unknown, byKind, longest, repeats, repeatsReason };
|
|
92
|
+
}
|
|
93
|
+
const noSteps = (state, reason) => ({ state, reason, total: null, failed: null, unknownDuration: null, byKind: {}, longest: [], repeats: null, repeatsReason: null });
|
|
94
|
+
/** Read the run's live records once, strictly bounded: its own descriptor (no link followed, in a real live
|
|
95
|
+
* directory), measured once, and never more than the budget read whatever the file does meanwhile; then parsed line by
|
|
96
|
+
* line. A file over the budget, in the older text format, with lines that don't read, or cut or capped while the agent
|
|
97
|
+
* ran, says its steps are incomplete. Null when there is no step file at all. */
|
|
98
|
+
function readRecords(evidenceRoot, runId) {
|
|
99
|
+
try {
|
|
100
|
+
const dir = lstatSync(join(evidenceRoot, "live"));
|
|
101
|
+
if (!dir.isDirectory() || dir.isSymbolicLink())
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
let fd;
|
|
108
|
+
try {
|
|
109
|
+
fd = openSync(liveLogPath(evidenceRoot, runId), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code !== "ENOENT")
|
|
113
|
+
return { unreadable: "The step records couldn't be read." };
|
|
114
|
+
// No records file: an older run's plain text file has no step detail; with neither, there is nothing.
|
|
115
|
+
try {
|
|
116
|
+
lstatSync(legacyLiveLogPath(evidenceRoot, runId));
|
|
117
|
+
return { unreadable: "This run's steps were recorded in an older format that has no step detail." };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let text, short = false;
|
|
124
|
+
try {
|
|
125
|
+
const stat = fstatSync(fd);
|
|
126
|
+
if (!stat.isFile())
|
|
127
|
+
return { unreadable: "The step records aren't a plain file, so they weren't read." };
|
|
128
|
+
if (stat.size > EFFICIENCY_READ_BYTES)
|
|
129
|
+
return { unreadable: "The step records are larger than they can be read here." };
|
|
130
|
+
// At most what it measured, and never past the budget: growth after the measure is not read.
|
|
131
|
+
const buffer = Buffer.alloc(stat.size);
|
|
132
|
+
let at = 0;
|
|
133
|
+
while (at < buffer.length) {
|
|
134
|
+
const got = readSync(fd, buffer, at, buffer.length - at, at);
|
|
135
|
+
if (got <= 0)
|
|
136
|
+
break;
|
|
137
|
+
at += got;
|
|
138
|
+
}
|
|
139
|
+
// Less than it measured: the file changed under the read.
|
|
140
|
+
short = at < buffer.length;
|
|
141
|
+
text = buffer.subarray(0, at).toString("utf8");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return { unreadable: "The step records couldn't be read." };
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
closeSync(fd);
|
|
148
|
+
}
|
|
149
|
+
const lines = text.split("\n");
|
|
150
|
+
// The last line may be unfinished (the agent's final write): it is read when it parses, never counted as a loss.
|
|
151
|
+
const unfinished = lines.at(-1) ?? "";
|
|
152
|
+
const whole = lines.slice(0, -1).filter(line => line.trim() !== "");
|
|
153
|
+
if (whole.length > EFFICIENCY_RECORDS)
|
|
154
|
+
whole.length = EFFICIENCY_RECORDS;
|
|
155
|
+
const keys = { inKey: false };
|
|
156
|
+
// One record per line: a whole line that doesn't read is a record lost.
|
|
157
|
+
const read = liveWindowRecords({ text: whole.length === 0 ? "" : `${whole.join("\n")}\n`, format: "records" }, keys);
|
|
158
|
+
const lost = whole.length - read.length;
|
|
159
|
+
// A finished run's last line without its newline is read like any other; one that doesn't read is a record lost.
|
|
160
|
+
const tail = unfinished.trim() === "" ? [] : liveWindowRecords({ text: `${unfinished}\n`, format: "records" }, keys);
|
|
161
|
+
const records = [...read, ...tail];
|
|
162
|
+
const lostAll = lost + (unfinished.trim() !== "" && tail.length === 0 ? 1 : 0);
|
|
163
|
+
if (records.length === 0 && text.trim() !== "")
|
|
164
|
+
return { unreadable: "The step records couldn't be read." };
|
|
165
|
+
const truncated = short ? "the step records changed while they were read"
|
|
166
|
+
: lines.length - 1 > EFFICIENCY_RECORDS ? `only the first ${EFFICIENCY_RECORDS.toLocaleString("en-US")} records were read`
|
|
167
|
+
: records.some(one => one.k === "cap") ? "the step records reached their size limit while the agent ran"
|
|
168
|
+
: records.some(one => one.k === "cut") ? "a step record was too long to keep"
|
|
169
|
+
: lostAll > 0 ? `${lostAll} step record${lostAll === 1 ? "" : "s"} couldn't be read`
|
|
170
|
+
: null;
|
|
171
|
+
return { records, truncated };
|
|
172
|
+
}
|
|
173
|
+
/** Derive a finished run's How it ran from its records. */
|
|
174
|
+
export function deriveRunEfficiency(run, evidenceRoot, now) {
|
|
175
|
+
const finished = run.finishedAt;
|
|
176
|
+
const read = readRecords(evidenceRoot, run.id);
|
|
177
|
+
const steps = read !== null && "unreadable" in read ? noSteps("truncated", read.unreadable)
|
|
178
|
+
: read !== null ? stepsOf(read.records, read.truncated, run.provider === "claude")
|
|
179
|
+
: finished !== null && now.getTime() - Date.parse(finished) > LIVE_RETAIN_MS ? noSteps("expired", "Step details are kept for a day after a run; this one's have expired.")
|
|
180
|
+
: noSteps("absent", "No step records were kept for this run.");
|
|
181
|
+
return {
|
|
182
|
+
version: RUN_EFFICIENCY_VERSION,
|
|
183
|
+
time: { waitMs: msBetween(run.startedAt, run.providerStartedAt), agentMs: msBetween(run.providerStartedAt, finished) },
|
|
184
|
+
turns: run.agentTurns ?? null,
|
|
185
|
+
usage: usageOf(run),
|
|
186
|
+
steps,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
191
|
+
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved.
|
|
192
|
+
*/
|
|
193
|
+
export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
194
|
+
const run = store.getRun(runId);
|
|
195
|
+
if (run === null)
|
|
196
|
+
return null;
|
|
197
|
+
const usageJson = store.handle.prepare("SELECT usage_json FROM run WHERE id = ?").get(runId)?.["usage_json"];
|
|
198
|
+
const full = { ...run, usageJson: typeof usageJson === "string" ? usageJson : null };
|
|
199
|
+
if (run.finishedAt === null)
|
|
200
|
+
return deriveRunEfficiency(full, evidenceRoot, now);
|
|
201
|
+
let saved;
|
|
202
|
+
try {
|
|
203
|
+
saved = store.handle.prepare("SELECT parser_version, payload_json FROM run_efficiency WHERE run = ?").get(runId);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
saved = undefined;
|
|
207
|
+
}
|
|
208
|
+
if (saved !== undefined && Number(saved["parser_version"]) === RUN_EFFICIENCY_VERSION) {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(String(saved["payload_json"]));
|
|
211
|
+
}
|
|
212
|
+
catch { /* derived again below */ }
|
|
213
|
+
}
|
|
214
|
+
const derived = deriveRunEfficiency(full, evidenceRoot, now);
|
|
215
|
+
const payload = JSON.stringify(derived);
|
|
216
|
+
// A row too large for its column (it can't be: the counts are bounded) is shown, never saved.
|
|
217
|
+
// Saving is a convenience: a store that can't take the row (read-only, busy) still shows what was derived.
|
|
218
|
+
if (payload.length <= 16_384) {
|
|
219
|
+
try {
|
|
220
|
+
store.handle.prepare(`INSERT INTO run_efficiency (run, parser_version, steps, steps_reason, payload_json, derived_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
221
|
+
ON CONFLICT(run) DO UPDATE SET parser_version = excluded.parser_version, steps = excluded.steps, steps_reason = excluded.steps_reason,
|
|
222
|
+
payload_json = excluded.payload_json, derived_at = excluded.derived_at`)
|
|
223
|
+
.run(runId, RUN_EFFICIENCY_VERSION, derived.steps.state, derived.steps.reason, payload, now.toISOString());
|
|
224
|
+
}
|
|
225
|
+
catch { /* shown, not saved: derived again next time */ }
|
|
226
|
+
}
|
|
227
|
+
return derived;
|
|
228
|
+
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* again. Reaping on the second tells you a *machine* is gone, and everything
|
|
20
20
|
* it was holding — claims and worktrees alike — needs recovering together.
|
|
21
21
|
*/
|
|
22
|
+
import { type AcpProviderId } from "./acp-agents.js";
|
|
23
|
+
import type { AcpAttestation } from "./store.js";
|
|
22
24
|
import type { Store, Mutation } from "./store.js";
|
|
23
25
|
import { type ProviderId } from "./provider.js";
|
|
24
26
|
import type { ReadinessObservation } from "./phase-routing.js";
|
|
@@ -264,7 +266,9 @@ export type ReadinessProbe = (file: string, args: readonly string[], options?: {
|
|
|
264
266
|
notFound: boolean;
|
|
265
267
|
timedOut: boolean;
|
|
266
268
|
}>;
|
|
267
|
-
export declare function observeProviderReadiness(probe: ReadinessProbe, env?: Record<string, string | undefined>, providers?: readonly ProviderId[]
|
|
269
|
+
export declare function observeProviderReadiness(probe: ReadinessProbe, env?: Record<string, string | undefined>, providers?: readonly ProviderId[],
|
|
270
|
+
/** An ACP agent's live check on this runner (store.acpAttestationFor), when the caller can read one. */
|
|
271
|
+
acpChecked?: (provider: AcpProviderId) => AcpAttestation | null): Promise<Omit<ReadinessObservation, "runner" | "observedAt">[]>;
|
|
268
272
|
/**
|
|
269
273
|
* Record a runner's observations under its OWN authenticated name: the
|
|
270
274
|
* credential is verified and the rows written in one transaction, so a
|
package/dist/runner.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* again. Reaping on the second tells you a *machine* is gone, and everything
|
|
20
20
|
* it was holding — claims and worktrees alike — needs recovering together.
|
|
21
21
|
*/
|
|
22
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
23
|
+
import { verifyLaunch } from "./acp-attest.js";
|
|
24
|
+
import { inspectAcpAgent, inspectionWords } from "./acp-inspect.js";
|
|
22
25
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
23
26
|
import { resolve } from "node:path";
|
|
24
27
|
import { canonicalProject } from "./project.js";
|
|
@@ -363,8 +366,26 @@ function sameDigest(left, right) {
|
|
|
363
366
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
364
367
|
}
|
|
365
368
|
const READINESS_PROBE_TIMEOUT_MS = 5_000;
|
|
366
|
-
export async function observeProviderReadiness(probe, env = process.env, providers = PROVIDER_IDS
|
|
369
|
+
export async function observeProviderReadiness(probe, env = process.env, providers = PROVIDER_IDS,
|
|
370
|
+
/** An ACP agent's live check on this runner (store.acpAttestationFor), when the caller can read one. */
|
|
371
|
+
acpChecked) {
|
|
367
372
|
return Promise.all(providers.map(async (provider) => {
|
|
373
|
+
// An ACP agent is looked at, never run, outside the gateway (acp-inspect.ts). It is ready only once its
|
|
374
|
+
// conformance check passed on this computer; until then it says so.
|
|
375
|
+
if (isAcpProvider(provider)) {
|
|
376
|
+
const checked = acpChecked?.(provider) ?? null;
|
|
377
|
+
if (checked !== null) {
|
|
378
|
+
// Ready only while every file its check bound is still the one that passed.
|
|
379
|
+
const verified = verifyLaunch(checked.launch);
|
|
380
|
+
return verified.ok
|
|
381
|
+
? { provider, state: "ready", reason: `${checked.agent.name} ${checked.agent.version}, checked ${checked.recordedAt.slice(0, 10)}`, probe: "version" }
|
|
382
|
+
: { provider, state: "unavailable", reason: `${verified.problem}`, probe: "version" };
|
|
383
|
+
}
|
|
384
|
+
const seen = inspectAcpAgent(provider, env);
|
|
385
|
+
return seen.found
|
|
386
|
+
? { provider, state: "unavailable", reason: `${inspectionWords(seen)}; not checked on this computer — run \`toolroll agents check ${provider}\``, probe: "version" }
|
|
387
|
+
: { provider, state: "unavailable", reason: inspectionWords(seen), probe: "version" };
|
|
388
|
+
}
|
|
368
389
|
const facts = inspectionOf(provider);
|
|
369
390
|
const version = await probe(facts.binary, ["--version"], { timeoutMs: READINESS_PROBE_TIMEOUT_MS, omitEnv: ALL_CREDENTIAL_ENV });
|
|
370
391
|
if (version.notFound) {
|