toolroll 0.9.73 → 0.9.74
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 +32 -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/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 +6 -0
- package/dist/builder.js +1 -1
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-quiet.js +3 -3
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +35 -2
- package/dist/exec.js +13 -16
- package/dist/first-run.d.ts +17 -1
- package/dist/first-run.js +38 -0
- 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-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/operate.js +3 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- 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/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 +34 -23
- 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 +88 -0
- package/dist/server/chat.js +84 -30
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +81 -1
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +8 -4
- package/dist/server/render-pages.js +55 -50
- package/dist/server/render-tasks.js +10 -3
- package/dist/server/route-table.js +5 -1
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +3 -3
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +59 -41
- package/dist/store.d.ts +31 -1
- package/dist/store.js +75 -2
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subscription-chat.js +5 -3
- package/package.json +1 -1
|
@@ -211,6 +211,10 @@ export type BrowserTasksView = {
|
|
|
211
211
|
newTask: BrowserLink;
|
|
212
212
|
/** v105: subscription windows and monthly budgets, one tile each (an instance operator's; null when there are none). */
|
|
213
213
|
limits: BrowserLimits | null;
|
|
214
|
+
/** The stored-state filter (Any state, Queued, Running, Done, Failed, Cancelled), each a real link starting on page one. */
|
|
215
|
+
states?: (BrowserLink & {
|
|
216
|
+
active: boolean;
|
|
217
|
+
})[];
|
|
214
218
|
};
|
|
215
219
|
/** One limit: a plan's usage window ("Claude · 5-hour, 48%") or a monthly budget ("shop · Budget, $4.20 of $10"). */
|
|
216
220
|
export type BrowserTaskGroup = WorkIndexGroup;
|
|
@@ -1445,6 +1449,8 @@ export type BrowserAgentsView = {
|
|
|
1445
1449
|
selected: BrowserAgentDetail | null;
|
|
1446
1450
|
/** Shown when nothing is on the board. */
|
|
1447
1451
|
empty: string | null;
|
|
1452
|
+
/** The Live grid (?view=live): every running agent's live output, watching only. */
|
|
1453
|
+
live?: boolean;
|
|
1448
1454
|
};
|
|
1449
1455
|
export type BrowserView = BrowserTasksView | BrowserSettingsView | BrowserTaskView | BrowserProjectsView | BrowserResultView | BrowserFlowView | BrowserAgentsView;
|
|
1450
1456
|
export type BrowserWorkspace = {
|
package/dist/builder.js
CHANGED
|
@@ -1240,7 +1240,7 @@ export async function build(store, request) {
|
|
|
1240
1240
|
// Every streaming transport emits events now (peek); a file that cannot
|
|
1241
1241
|
// open is a null, and a null never costs a build.
|
|
1242
1242
|
// What the agent did last rides the same stream into one row (live tasks).
|
|
1243
|
-
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId, liveLogContext(store, taskRef, worktree)), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1243
|
+
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId, liveLogContext(store, request.runId, taskRef, worktree, clock())), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1244
1244
|
recordAgentTurn(store, { run: request.runId, attemptKey, state, at }); }, clock, (open, at) => { if (attemptKey !== null)
|
|
1245
1245
|
noteOpenTool(store, request.runId, attemptKey, open, at); });
|
|
1246
1246
|
// The retry base (steering fix for run 1465's proof): the SAME pinned
|
package/dist/chat-continuity.js
CHANGED
|
@@ -125,6 +125,8 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
125
125
|
if(got===null)return;
|
|
126
126
|
var data=got.data||{};
|
|
127
127
|
if(data.ok===true){
|
|
128
|
+
// A conversation shown before it existed (session 0) is started by its first send: that is this one.
|
|
129
|
+
if(session==='0'&&typeof data.session==='number'&&data.session>0)adopt(String(data.session));
|
|
128
130
|
if(String(data.session)!==session){changed();return;}
|
|
129
131
|
say(data.queued===true?'Queued. It goes to the lead after the reply in progress.':'Sent. Waiting for the reply…');later(300);return;
|
|
130
132
|
}
|
|
@@ -273,6 +275,9 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
273
275
|
}
|
|
274
276
|
return true;
|
|
275
277
|
}
|
|
278
|
+
// The conversation this page's first send started: its binding and its draft move to it (a reload restores from there).
|
|
279
|
+
function adopt(started){session=started;document.querySelectorAll('form[action="/chat"] [name="request-session"]').forEach(function(bound){bound.value=started;});
|
|
280
|
+
try{sessionStorage.removeItem(key);}catch(e){}key='standing-orders:chat-draft:'+session+':'+task;save();}
|
|
276
281
|
function later(ms){if(stale)return;clearTimeout(timer);timer=setTimeout(check,ms);}
|
|
277
282
|
async function check(){
|
|
278
283
|
if(polling||stale)return;
|
|
@@ -289,6 +294,11 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
289
294
|
if(!r.ok)throw new Error('connection');
|
|
290
295
|
var data=await r.json();
|
|
291
296
|
if(!valid(data))throw new Error('response');
|
|
297
|
+
// Not started yet (session 0): only this send's own receipt in the started conversation proves it is the one.
|
|
298
|
+
if(session==='0'&&typeof data.session==='number'&&data.session>0&&asked&&data.received===true)adopt(String(data.session));
|
|
299
|
+
// A poll that lands while the first send is on its way decides nothing: that send's answer (or the poll it
|
|
300
|
+
// asks for afterwards, with its request) says whether the started conversation is this page's.
|
|
301
|
+
if(session==='0'&&sending&&data.session!==null&&String(data.session)!=='0'){later(1000);return;}
|
|
292
302
|
if(data.session===null||String(data.session)!==session){changed();return;}
|
|
293
303
|
// A late answer for another lens never lands here; the poll goes on.
|
|
294
304
|
if(data.task!==task){later(5000);return;}
|
package/dist/chat-quiet.js
CHANGED
|
@@ -101,7 +101,7 @@ export function quietCardView(store, taskRefs, now, root, viewer) {
|
|
|
101
101
|
const heading = `${lines.length} tasks`;
|
|
102
102
|
const projects = [...new Set(lines.map(one => projectLabel(one.project)))];
|
|
103
103
|
const text = [heading, ...lines.map(one => `${one.icon} ${phoneText(one.title, 60)} · ${one.status}`), "", projects.join(", ")].join("\n");
|
|
104
|
-
return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/
|
|
104
|
+
return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/work" } };
|
|
105
105
|
}
|
|
106
106
|
/** Facts that carry their own controls or must stand alone (a security alert, a decision, a flow card, a
|
|
107
107
|
* screenshot, an acceptance packet) are never folded into a batch. */
|
|
@@ -176,7 +176,7 @@ export function finishedView(store, batch, now, root, viewer) {
|
|
|
176
176
|
return null;
|
|
177
177
|
const text = batchLine(lines.map(one => one.fact));
|
|
178
178
|
if (lines.length > 1)
|
|
179
|
-
return { text, entities: [], link: { label: "Open", path: "/
|
|
179
|
+
return { text, entities: [], link: { label: "Open", path: "/work" } };
|
|
180
180
|
const target = lines[0].target ?? null;
|
|
181
181
|
return { text, entities: [], link: lines[0].link, ...(lines[0].also.length === 0 ? {} : { also: lines[0].also }), ...(target === null ? {} : { target }) };
|
|
182
182
|
}
|
|
@@ -242,7 +242,7 @@ export function enqueueEveningDigests(store, now, root) {
|
|
|
242
242
|
if (body === null)
|
|
243
243
|
return;
|
|
244
244
|
if (store.enqueueNotification({ dedupeKey: `digest:evening:${person.account}:${day}`, kind: "evening-digest", subject: "Evening digest", body,
|
|
245
|
-
link: "/
|
|
245
|
+
link: "/work", source: { installation: true }, recipient: person.account }, now))
|
|
246
246
|
queued++;
|
|
247
247
|
});
|
|
248
248
|
}
|
package/dist/deploy-gate.js
CHANGED
|
@@ -18,8 +18,11 @@ export function deploymentGate(store, input) {
|
|
|
18
18
|
if (run === null || ref === null || head === null)
|
|
19
19
|
return refuse(`Run ${input.runId} is not a built candidate.`);
|
|
20
20
|
// `task regate` reruns the approved check on the last built commit and records no-change: a candidate only for that commit.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A prepared candidate's build records a commit of its own holding exactly the candidate's tree; its rerun under the same
|
|
22
|
+
// signed scope is the same check of that commit (the digest is held to the approved one below).
|
|
23
|
+
const built = store.runsFor(run.taskRef).find(one => one.id < input.runId && one.role === "builder" && one.outcome === "built");
|
|
24
|
+
const regated = run.outcome === "no-change" && built?.headRevision === head &&
|
|
25
|
+
(scope?.candidate === head || ((scope?.candidate ?? null) !== null && built.scopeDigest === run.scopeDigest));
|
|
23
26
|
if (run.role !== "builder" || !(run.outcome === "built" || regated))
|
|
24
27
|
return refuse(`Run ${input.runId} is not a built candidate at ${head.slice(0, 7)}.`);
|
|
25
28
|
if (scope === null || scope.approvedDigest !== scope.digest || scope.digest !== run.scopeDigest)
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
95
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
96
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
+
version?: never;
|
|
97
98
|
active: boolean;
|
|
98
99
|
phase: string;
|
|
99
100
|
detail: string;
|
|
@@ -107,7 +108,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
107
108
|
wasRunning?: never;
|
|
108
109
|
backupPath?: never;
|
|
109
110
|
updateId?: never;
|
|
110
|
-
version?: never;
|
|
111
111
|
buildId?: never;
|
|
112
112
|
workDir?: never;
|
|
113
113
|
} | {
|
package/dist/dispose.js
CHANGED
|
@@ -23,7 +23,7 @@ import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
|
23
23
|
import {} from "./proof.js";
|
|
24
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
25
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
26
|
-
import { propose, approve } from "./scope.js";
|
|
26
|
+
import { propose, approve, liveReleaseOf } from "./scope.js";
|
|
27
27
|
import { headWithin } from "./names.js";
|
|
28
28
|
/** The reasons tick classifies as the attempt itself breaking. */
|
|
29
29
|
const TICK_FAILURE_REASONS = new Set([
|
|
@@ -456,6 +456,35 @@ export function regateTask(store, taskId, now, approval) {
|
|
|
456
456
|
return { ok: false, reason: "no-attempt", message: `${taskId} has no scope` };
|
|
457
457
|
const head = last.headRevision;
|
|
458
458
|
const actor = approval.kind === "operator" ? approval.name : `mode ${approval.name}`;
|
|
459
|
+
// A prepared candidate (a release gate, the nightly check) already names the approved commit, and the builder loads
|
|
460
|
+
// exactly its tree on every attempt. The rerun keeps that scope and its approval as they are: nothing is proposed or
|
|
461
|
+
// sealed again, so a regate can never swap the approved commit, its release terms or the person's yes for others
|
|
462
|
+
// (2026-10-27: a regate rewrote release-0.9.73's candidate to its own build commit and the release refused it).
|
|
463
|
+
if ((scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null) {
|
|
464
|
+
const candidate = scope.candidate ?? null;
|
|
465
|
+
if (candidate === null || scope.approvedDigest !== scope.digest || last.scopeDigest !== scope.digest) {
|
|
466
|
+
return { ok: false, reason: "not-approved", message: `${taskId}'s approved commit or scope changed since attempt #${last.id}, or its approval no longer stands; approve its scope again instead` };
|
|
467
|
+
}
|
|
468
|
+
// A release's task runs again only while its one approval still stands for it (the dispatch would refuse it anyway).
|
|
469
|
+
const release = liveReleaseOf(store, taskId, "dispatch", now);
|
|
470
|
+
if (release.applies && !release.ok)
|
|
471
|
+
return { ok: false, reason: "not-approved", message: `${taskId} can't run again: ${release.why}` };
|
|
472
|
+
// Any other seal stands only while its authority does (an expired mode, a revoked approver): the queue would never
|
|
473
|
+
// dispatch it, so the regate says so instead of queueing a rerun that can't start.
|
|
474
|
+
if (!store.modeApprovalLive(ref.id, now))
|
|
475
|
+
return { ok: false, reason: "not-approved", message: `${taskId}'s approval no longer stands; approve its scope again instead` };
|
|
476
|
+
return store.transact(() => {
|
|
477
|
+
if (store.getTask(taskId)?.state !== "queued") {
|
|
478
|
+
const queued = store.requeueTask(taskId, actor, now);
|
|
479
|
+
if (!queued.ok) {
|
|
480
|
+
const why = { claimed: "claimed", "accepted-result": "accepted-result", published: "published" };
|
|
481
|
+
return { ok: false, reason: why[queued.reason] ?? "not-rejected", message: `${taskId} could not be queued again: ${queued.reason}` };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
store.addRunNote(last.id, "Toolroll", `The check will run again on the approved commit ${candidate.slice(0, 7)}, under the same approval, as a new attempt, ${approval.kind === "operator" ? `on ${approval.name}'s say-so` : "automatically once, because the failure was outside this change"}.`, now);
|
|
485
|
+
return { ok: true, run: last.id, head: candidate };
|
|
486
|
+
});
|
|
487
|
+
}
|
|
459
488
|
return store.transact(() => {
|
|
460
489
|
const proposed = propose(store, {
|
|
461
490
|
taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd,
|
|
@@ -520,7 +549,11 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
|
|
|
520
549
|
// to a person. No repair draft, no third pass, on either road.
|
|
521
550
|
{
|
|
522
551
|
const rerunTask = store.refById(run.taskRef)?.externalId ?? "";
|
|
523
|
-
|
|
552
|
+
// A prepared candidate's attempts each record their own commit of the candidate's tree, so a rerun under the same
|
|
553
|
+
// approved scope is the same commit's check even when its head isn't the candidate itself.
|
|
554
|
+
const rerunScope = store.getScope(rerunTask);
|
|
555
|
+
const sameCommit = rerunScope !== null && (rerunScope.candidate === run.headRevision || ((rerunScope.candidate ?? null) !== null && rerunScope.digest === run.scopeDigest));
|
|
556
|
+
if (run.headRevision && sameCommit && store.builderAttemptsAt(run.taskRef, run.headRevision) >= 2) {
|
|
524
557
|
const contradicted = cause === "review";
|
|
525
558
|
store.enqueueNotification({ source: { run: sourceRunId }, dedupeKey: `regate-failed:${sourceRunId}`, kind: "repair-evidence", pushClass: "attention",
|
|
526
559
|
subject: contradicted ? "The rerun on the same commit was contradicted by its review" : "The project check failed again on the same commit",
|
package/dist/exec.js
CHANGED
|
@@ -16,6 +16,7 @@ import { jsonlDiscriminants } from "./jsonl-discriminants.js";
|
|
|
16
16
|
import { createContainer, currentContainment } from "./containment.js";
|
|
17
17
|
import { claudeLimitsOf, noteLimits } from "./provider-limits.js";
|
|
18
18
|
import { sleep as defaultSleep } from "./sleep.js";
|
|
19
|
+
import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
|
|
19
20
|
/** Shell convention for "could not be executed": the refusal before any target ran. */
|
|
20
21
|
export const CONTAINMENT_REFUSED_CODE = 126;
|
|
21
22
|
/** Live provider children, for the deterministic stop. Registered only when `processGroup` was set. */
|
|
@@ -1760,23 +1761,17 @@ function syntheticMessageLine(content, truncated) {
|
|
|
1760
1761
|
*
|
|
1761
1762
|
* - a bounded identity witness for `system`/`init`: the first init signal,
|
|
1762
1763
|
* the first usable session id, and one conflicting id
|
|
1763
|
-
* - the FIRST primary `result` event, selected STRUCTURALLY (finding 10
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1764
|
+
* - the FIRST primary `result` event, selected STRUCTURALLY (finding 10;
|
|
1765
|
+
* stream-scope.ts `primaryResultEvent`): top-level, and origin absent or
|
|
1766
|
+
* `origin.kind === "human"` — an allowlist, so a subagent's or background
|
|
1767
|
+
* task's result (or any future origin kind) can never be mistaken for the
|
|
1768
|
+
* main query's accounting. Later results are dropped. A subagent's init
|
|
1769
|
+
* never enters the identity witness.
|
|
1767
1770
|
*
|
|
1768
1771
|
* The result's stdout is the retained lines joined — a synthetic, bounded
|
|
1769
1772
|
* envelope `claudeParse` reads exactly like test fixtures. Timeout,
|
|
1770
1773
|
* process-group, kill, and spawn semantics match `runStreamJsonl`.
|
|
1771
1774
|
*/
|
|
1772
|
-
function primaryResultOrigin(event) {
|
|
1773
|
-
const origin = event["origin"];
|
|
1774
|
-
if (origin === undefined || origin === null)
|
|
1775
|
-
return true;
|
|
1776
|
-
if (typeof origin === "object" && String(origin["kind"] ?? "") === "human")
|
|
1777
|
-
return true;
|
|
1778
|
-
return false; // task-notification, channel, peer, coordinator, unknown: never the envelope
|
|
1779
|
-
}
|
|
1780
1775
|
/** The usage fields Claude reports per message (not per session): a live session's account adds them up. */
|
|
1781
1776
|
const LIVE_SUMMED_USAGE = ["input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens"];
|
|
1782
1777
|
/**
|
|
@@ -1934,7 +1929,10 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1934
1929
|
if (event === null || typeof event !== "object")
|
|
1935
1930
|
return;
|
|
1936
1931
|
const type = String(event["type"] ?? "");
|
|
1937
|
-
|
|
1932
|
+
// A subagent's init, words and result are not the session's (stream-scope.ts): they never witness its identity,
|
|
1933
|
+
// name its session, fire its receipt or stand as its result. They still reach the observers below, which filter
|
|
1934
|
+
// for themselves.
|
|
1935
|
+
if (type === "system" && String(event["subtype"] ?? "") === "init" && topLevelEvent(event)) {
|
|
1938
1936
|
if (initLine === null)
|
|
1939
1937
|
initLine = line;
|
|
1940
1938
|
const id = transportSessionId(event["session_id"]);
|
|
@@ -1963,15 +1961,14 @@ export function runClaudeStreamJsonl(file, args, options = {}) {
|
|
|
1963
1961
|
// Top-level only (finding 11): assistant events carry no origin;
|
|
1964
1962
|
// parent_tool_use_id is the correlation field, and a subagent's
|
|
1965
1963
|
// words prove nothing about the main prompt.
|
|
1966
|
-
|
|
1967
|
-
if (parent === undefined || parent === null)
|
|
1964
|
+
if (topLevelEvent(event))
|
|
1968
1965
|
fireReceipt();
|
|
1969
1966
|
}
|
|
1970
1967
|
else if (type === "rate_limit_event") {
|
|
1971
1968
|
// v105: the plan's usage windows, as Claude says them each turn (the Tasks page's limits).
|
|
1972
1969
|
noteLimits(claudeLimitsOf(event));
|
|
1973
1970
|
}
|
|
1974
|
-
else if (type === "result" && (resultLine === null || live !== null) &&
|
|
1971
|
+
else if (type === "result" && (resultLine === null || live !== null) && primaryResultEvent(event)) {
|
|
1975
1972
|
// A live session ends a turn with a result each time: the run's account is the last one, with every main
|
|
1976
1973
|
// result's usage and turns added up (account). After an oversized main result the retained result stays the
|
|
1977
1974
|
// fail-closed overflow, but later results still count toward its cost.
|
package/dist/first-run.d.ts
CHANGED
|
@@ -77,10 +77,13 @@ export declare function firstResultWords(since: string, at: string): string | nu
|
|
|
77
77
|
export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
|
|
78
78
|
export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
|
|
79
79
|
/** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
|
|
80
|
+
/** `skipped`: a stage the task went past without it happening (its checks didn't run or couldn't be read). `needsYou`:
|
|
81
|
+
* the current or stuck step waits on a person, so it is the one step drawn amber. */
|
|
80
82
|
export type JourneyStep = {
|
|
81
83
|
key: JourneyKey;
|
|
82
84
|
label: string;
|
|
83
|
-
state: "done" | "current" | "stuck" | "todo";
|
|
85
|
+
state: "done" | "current" | "stuck" | "skipped" | "todo";
|
|
86
|
+
needsYou?: true;
|
|
84
87
|
};
|
|
85
88
|
/**
|
|
86
89
|
* Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
|
|
@@ -91,4 +94,17 @@ export declare function firstTaskJourney(read: {
|
|
|
91
94
|
stage: "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
92
95
|
need?: string | undefined;
|
|
93
96
|
}, approved: boolean, built?: boolean): JourneyStep[];
|
|
97
|
+
/** What a task's checks recorded, for its road: `null` when nothing says (the step is then shown skipped, never done). */
|
|
98
|
+
export type RoadChecks = "passed" | "running" | "failed" | "not-run" | "unread" | null;
|
|
99
|
+
/**
|
|
100
|
+
* Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
|
|
101
|
+
* from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
|
|
102
|
+
* stands there again); a task that never needed one shows it skipped. Checks is done only when a check passed; a
|
|
103
|
+
* running, failed, missing or unread check says so, and with no checks fact it is skipped. A research report has
|
|
104
|
+
* no Checks step: a report stopped there stops at Ready. The step that waits on a person carries `needsYou`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function taskRoad(read: Parameters<typeof firstTaskJourney>[0], approved: boolean, built: boolean, facts: {
|
|
107
|
+
report?: boolean;
|
|
108
|
+
checks?: RoadChecks;
|
|
109
|
+
}): JourneyStep[];
|
|
94
110
|
export {};
|
package/dist/first-run.js
CHANGED
|
@@ -141,3 +141,41 @@ export function firstTaskJourney(read, approved, built = false) {
|
|
|
141
141
|
return JOURNEY_STEPS.map(([key, label], index) => ({ key, label,
|
|
142
142
|
state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
|
|
146
|
+
* from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
|
|
147
|
+
* stands there again); a task that never needed one shows it skipped. Checks is done only when a check passed; a
|
|
148
|
+
* running, failed, missing or unread check says so, and with no checks fact it is skipped. A research report has
|
|
149
|
+
* no Checks step: a report stopped there stops at Ready. The step that waits on a person carries `needsYou`.
|
|
150
|
+
*/
|
|
151
|
+
export function taskRoad(read, approved, built, facts) {
|
|
152
|
+
const replan = read.stage === "needs-you" && read.need === "replan";
|
|
153
|
+
const plainApproved = approved && !replan;
|
|
154
|
+
let steps = firstTaskJourney(replan ? { stage: "needs-you", need: "approval" } : read, plainApproved, built);
|
|
155
|
+
steps = steps.map(step => {
|
|
156
|
+
if (step.key === "approve" && step.state === "done" && !plainApproved)
|
|
157
|
+
return { ...step, state: "skipped" };
|
|
158
|
+
if (step.key !== "checks" || (step.state !== "done" && step.state !== "current"))
|
|
159
|
+
return step;
|
|
160
|
+
// At Checks now: only a recorded failure or a running check changes how it reads.
|
|
161
|
+
if (step.state === "current")
|
|
162
|
+
return facts.checks === "failed" ? { ...step, state: "stuck", label: "Checks failed" } : facts.checks === "running" ? { ...step, label: "Checks running" } : step;
|
|
163
|
+
switch (facts.checks ?? null) {
|
|
164
|
+
case "passed": return step;
|
|
165
|
+
case "running": return { ...step, state: "current", label: "Checks running" };
|
|
166
|
+
case "failed": return { ...step, state: "stuck", label: "Checks failed" };
|
|
167
|
+
case "not-run": return { ...step, state: "skipped", label: "Checks not run" };
|
|
168
|
+
case "unread": return { ...step, state: "skipped", label: "Checks unread" };
|
|
169
|
+
default: return { ...step, state: "skipped" };
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (facts.report === true) {
|
|
173
|
+
const checks = steps.find(step => step.key === "checks");
|
|
174
|
+
if (checks !== undefined && (checks.state === "current" || checks.state === "stuck")) {
|
|
175
|
+
steps = steps.map(step => step.key === "ready" ? { ...step, state: checks.state } : step);
|
|
176
|
+
}
|
|
177
|
+
steps = steps.filter(step => step.key !== "checks");
|
|
178
|
+
}
|
|
179
|
+
const waiting = read.stage === "needs-you";
|
|
180
|
+
return steps.map(step => waiting && (step.state === "current" || step.state === "stuck") ? { ...step, needsYou: true } : step);
|
|
181
|
+
}
|
package/dist/invoke.d.ts
CHANGED
|
@@ -190,3 +190,16 @@ export declare function checkoutRunner(store: Store, repo: string, checkout: str
|
|
|
190
190
|
export declare function checkoutSandboxOf(store: Store, repo: string, runIds: readonly number[]): LaunchSandbox;
|
|
191
191
|
/** The digest of a launch's whole fence (its denied paths and grants, canonical), as stamped on its run (C11). */
|
|
192
192
|
export declare function fenceDigest(fence: readonly string[], grants: FenceGrants): string;
|
|
193
|
+
/** What a Claude turn's own stream says about how it billed (v105). */
|
|
194
|
+
export type ClaudeBillingSeen = {
|
|
195
|
+
keySource: string | null;
|
|
196
|
+
model: string | null;
|
|
197
|
+
planWindows: boolean;
|
|
198
|
+
answered: boolean;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* One stream event's billing observation: the key source and model from the session's own init, plan windows from a
|
|
202
|
+
* rate-limit event, and whether the main query answered. The session's own events only (stream-scope.ts): a
|
|
203
|
+
* subagent's init, rate-limit event or result says nothing about this turn's billing.
|
|
204
|
+
*/
|
|
205
|
+
export declare function observeClaudeBilling(seen: ClaudeBillingSeen, event: Record<string, unknown>): void;
|
package/dist/invoke.js
CHANGED
|
@@ -44,6 +44,7 @@ import { buildCacheEnv, buildCacheFolder, grantFolder, writeAllowlist, WRITES_DE
|
|
|
44
44
|
import { proxyEnv, providerOnlyNetwork, repositoryEcosystems, resolveNetwork, startEgressProxy } from "./egress.js";
|
|
45
45
|
import { claudeConfigDir, claudeDefaults, codexDefaults, codexHomeDir, isolatedCodexHome } from "./provider-home.js";
|
|
46
46
|
import { billingOf, claudeBillingFrom, seenBilling } from "./spend.js";
|
|
47
|
+
import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
|
|
47
48
|
/** "Within seconds": how soon a silent failed run must end for its stderr to
|
|
48
49
|
* be read for the sign-in signal. */
|
|
49
50
|
const AUTH_EARLY_EXIT_MS = 30_000;
|
|
@@ -507,18 +508,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
507
508
|
...(workspace === null ? {} : { cwd: workspace }),
|
|
508
509
|
// v105: how the CLI really billed this turn (its key source, its plan's windows), read off its own stream.
|
|
509
510
|
onStreamEvent: event => {
|
|
510
|
-
|
|
511
|
-
if (typeof event["apiKeySource"] === "string")
|
|
512
|
-
billingSeen.keySource = event["apiKeySource"].slice(0, 80);
|
|
513
|
-
if (typeof event["model"] === "string")
|
|
514
|
-
billingSeen.model = event["model"].slice(0, 200);
|
|
515
|
-
}
|
|
516
|
-
else if (event["type"] === "rate_limit_event") {
|
|
517
|
-
billingSeen.planWindows = true;
|
|
518
|
-
}
|
|
519
|
-
else if (event["type"] === "result" && event["is_error"] !== true && event["subtype"] === "success") {
|
|
520
|
-
billingSeen.answered = true;
|
|
521
|
-
}
|
|
511
|
+
observeClaudeBilling(billingSeen, event);
|
|
522
512
|
runOptions.onStreamEvent?.(event);
|
|
523
513
|
},
|
|
524
514
|
owner: runOwnerTag(store, runId),
|
|
@@ -1303,3 +1293,22 @@ export function checkoutSandboxOf(store, repo, runIds) {
|
|
|
1303
1293
|
export function fenceDigest(fence, grants) {
|
|
1304
1294
|
return createHash("sha256").update(`toolroll:fence:${canonicalJson({ fence: [...fence], grants: grants })}`).digest("hex").slice(0, 32);
|
|
1305
1295
|
}
|
|
1296
|
+
/**
|
|
1297
|
+
* One stream event's billing observation: the key source and model from the session's own init, plan windows from a
|
|
1298
|
+
* rate-limit event, and whether the main query answered. The session's own events only (stream-scope.ts): a
|
|
1299
|
+
* subagent's init, rate-limit event or result says nothing about this turn's billing.
|
|
1300
|
+
*/
|
|
1301
|
+
export function observeClaudeBilling(seen, event) {
|
|
1302
|
+
if (event["type"] === "system" && event["subtype"] === "init" && topLevelEvent(event)) {
|
|
1303
|
+
if (typeof event["apiKeySource"] === "string")
|
|
1304
|
+
seen.keySource = event["apiKeySource"].slice(0, 80);
|
|
1305
|
+
if (typeof event["model"] === "string")
|
|
1306
|
+
seen.model = event["model"].slice(0, 200);
|
|
1307
|
+
}
|
|
1308
|
+
else if (event["type"] === "rate_limit_event" && topLevelEvent(event)) {
|
|
1309
|
+
seen.planWindows = true;
|
|
1310
|
+
}
|
|
1311
|
+
else if (primaryResultEvent(event) && event["is_error"] !== true && event["subtype"] === "success") {
|
|
1312
|
+
seen.answered = true;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
package/dist/knowledge-ui.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type KnowledgeDraft, type KnowledgeSelection, type KnowledgeView } from './project-knowledge.js';
|
|
2
2
|
import { type Html } from './html.js';
|
|
3
|
-
|
|
3
|
+
/** A section as a frame (after ReUI's Frame, MIT): its heading on the raised well, its body one paper panel. The
|
|
4
|
+
* cards inside become hairline-divided rows of that panel, never cards in a card. */
|
|
5
|
+
export declare function framedSection(heading: string, body: Html | string, extra?: string): Html;
|
|
6
|
+
export declare const KNOWLEDGE_CSS = ".knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span+span::before{content:\"\u00B7\";margin-right:.4rem}.knowledge.frame{margin:0 0 16px}.knowledge.frame>.frame-head h2,.knowledge.frame>summary.frame-head{margin:0;font-size:13px;font-weight:600;line-height:1.4}.knowledge.frame>summary.frame-head{min-height:44px;padding:4px 10px}.knowledge.frame>.frame-panel{min-width:0}.knowledge.frame>.frame-panel>:first-child{margin-top:0}.knowledge.frame>.frame-panel>:last-child{margin-bottom:0}.knowledge .frame-panel>details.card,.knowledge .frame-panel .learning .card{margin:0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.knowledge .frame-panel>details.card+details.card,.knowledge .frame-panel .learning .card+.card{border-top:1px solid var(--so-line);padding-top:4px}.knowledge.frame>summary.frame-head{justify-content:flex-start}.knowledge form.field-row{grid-template-columns:minmax(0,1fr) auto;align-items:end}.knowledge form.field-row>label,.knowledge form.field-row :is(input,select,button){margin:0}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}";
|
|
4
7
|
export declare function knowledgeHtml(view: KnowledgeView, csrf: string, canManage: boolean, draft?: KnowledgeDraft, error?: string): Html;
|
|
5
8
|
export declare function knowledgeContextHtml(snapshot: KnowledgeSelection | null): Html | '';
|
|
6
9
|
import type { Decision, MemoryHit } from './project-memory.js';
|
package/dist/knowledge-ui.js
CHANGED
|
@@ -2,7 +2,12 @@ import { STALE_KNOWLEDGE } from './project-knowledge.js';
|
|
|
2
2
|
import { html, postForm } from './html.js';
|
|
3
3
|
// Hidden fields as they always were: a missing value is an empty field.
|
|
4
4
|
const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? '')]));
|
|
5
|
-
|
|
5
|
+
/** A section as a frame (after ReUI's Frame, MIT): its heading on the raised well, its body one paper panel. The
|
|
6
|
+
* cards inside become hairline-divided rows of that panel, never cards in a card. */
|
|
7
|
+
export function framedSection(heading, body, extra = '') {
|
|
8
|
+
return html `<section class="${`knowledge frame${extra ? ` ${extra}` : ''}`}"><div class="frame-head"><h2>${heading}</h2></div><div class="frame-panel">${body}</div></section>`;
|
|
9
|
+
}
|
|
10
|
+
export const KNOWLEDGE_CSS = `.knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span+span::before{content:"·";margin-right:.4rem}.knowledge.frame{margin:0 0 16px}.knowledge.frame>.frame-head h2,.knowledge.frame>summary.frame-head{margin:0;font-size:13px;font-weight:600;line-height:1.4}.knowledge.frame>summary.frame-head{min-height:44px;padding:4px 10px}.knowledge.frame>.frame-panel{min-width:0}.knowledge.frame>.frame-panel>:first-child{margin-top:0}.knowledge.frame>.frame-panel>:last-child{margin-bottom:0}.knowledge .frame-panel>details.card,.knowledge .frame-panel .learning .card{margin:0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.knowledge .frame-panel>details.card+details.card,.knowledge .frame-panel .learning .card+.card{border-top:1px solid var(--so-line);padding-top:4px}.knowledge.frame>summary.frame-head{justify-content:flex-start}.knowledge form.field-row{grid-template-columns:minmax(0,1fr) auto;align-items:end}.knowledge form.field-row>label,.knowledge form.field-row :is(input,select,button){margin:0}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}`;
|
|
6
11
|
export function knowledgeHtml(view, csrf, canManage, draft, error) {
|
|
7
12
|
const manage = canManage && !!csrf;
|
|
8
13
|
const base = { repo: view.repo, identity: view.identity, revision: view.revision };
|
|
@@ -18,7 +23,7 @@ export function knowledgeHtml(view, csrf, canManage, draft, error) {
|
|
|
18
23
|
}
|
|
19
24
|
const instructions = html `<pre class="knowledge-instructions">${view.knowledge.instructions || 'No project instructions yet.'}</pre>`;
|
|
20
25
|
const editor = manage ? html `<details class="knowledge-editor"${draft?.instructions !== undefined ? html ` open` : ''}><summary>${view.knowledge.instructions ? 'Edit instructions' : 'Add instructions'}</summary>${form('instructions', html `<textarea aria-label="Project instructions" name="instructions" rows="5" maxlength="4000" placeholder="Goals, preferences, and what good work looks like…">${draft?.instructions ?? view.knowledge.instructions}</textarea><button>Save instructions</button>`)}</details>` : '';
|
|
21
|
-
return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}
|
|
26
|
+
return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}${framedSection('Instructions', html `${instructions}${editor}`, 'knowledge-intro')}${framedSection('References', html `${refs.length ? refs : html `<p class="meta">Add a spec, design guide, or useful project document.</p>`}${manage ? html `<details class="card"${draft?.title && !draft.id ? html ` open` : ''}><summary>Add reference</summary>${form('save', referenceFields(draft?.id ? {} : draft ?? {}))}</details>` : ''}`)}${history.length ? html `<details class="knowledge frame"><summary class="frame-head">Change history</summary><div class="frame-panel"><ol>${history}</ol></div></details>` : ''}</section>`;
|
|
22
27
|
}
|
|
23
28
|
export function knowledgeContextHtml(snapshot) {
|
|
24
29
|
if (!snapshot)
|
|
@@ -32,17 +37,17 @@ export function decisionsHtml(repo, decisions, csrf, canManage, draft, error) {
|
|
|
32
37
|
const form = (action, body, extra = {}) => postForm('/settings/knowledge/decision', body, { hidden: fields({ ...base, action, ...extra }) });
|
|
33
38
|
const rows = decisions.map(d => html `<details class="card decision" id="decision-${d.id}"><summary><strong>${d.claim}</strong> <span class="meta">#${d.id} · ${d.decidedBy} · ${d.decidedAt.slice(0, 10)}</span></summary><p>${d.why}</p><p class="meta">${d.sourceKind}${d.sourceRef ? ` · ${d.sourceRef}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}</p>${manage ? form('retire', html `<label>Why it no longer holds<input name="reason" maxlength="500" required></label><button type="submit">Retire</button>`, { decision: d.id }) : ''}</details>`);
|
|
34
39
|
const add = manage ? html `<details class="knowledge-editor"${draft?.claim !== undefined ? html ` open` : ''}><summary>Record a decision</summary>${form('record', html `<label>The choice, in one sentence<input name="claim" maxlength="240" value="${draft?.claim}" required></label><label>Why<textarea name="why" rows="3" maxlength="2000" required>${draft?.why}</textarea></label><button type="submit">Record</button>`)}</details>` : '';
|
|
35
|
-
return html
|
|
40
|
+
return framedSection('Decisions', html `${error ? html `<p class="problem" role="alert">${error}</p>` : ''}${rows.length ? rows : html `<p class="meta">No decisions yet. The lead suggests one when a conversation settles a choice.</p>`}${add}`);
|
|
36
41
|
}
|
|
37
42
|
export function memorySearchHtml(repo, query, hits) {
|
|
38
43
|
const kind = (h) => h.kind === 'decision' ? html `<a href="#decision-${h.ref}">decision #${h.ref}</a>` : h.kind === 'conversation' ? 'conversation' : h.kind;
|
|
39
44
|
const results = hits === null ? '' : hits.length === 0 ? html `<p class="meta">Nothing in project memory matches.</p>` : html `<ul class="memory-hits">${hits.map(h => html `<li><strong>${kind(h)}</strong> · ${h.title}<br><span class="meta">${h.snippet}</span></li>`)}</ul>`;
|
|
40
|
-
return html `<
|
|
45
|
+
return framedSection('Search project memory', html `<form class="field-row" method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${repo}"><label><span class="visually-hidden">Search project memory</span><input name="memory" value="${query}" maxlength="300" placeholder="Search decisions, references, lessons…" type="search" autocomplete="off"></label><button type="submit">Search</button></form>${results}`, 'memory-search');
|
|
41
46
|
}
|
|
42
47
|
export function proposalsHtml(repo, proposals, status, csrf, canManage) {
|
|
43
48
|
const manage = canManage && !!csrf;
|
|
44
49
|
const line = status === null ? '' : html `<p class="meta memory-stats"><span>${status.sessions.analyzed}/${status.sessions.total} sessions read${status.sessions.failed ? ` · ${status.sessions.failed} failed` : ''}</span><span>${status.openGaps} open gap${status.openGaps === 1 ? '' : 's'}</span><span>Instructions ${status.instructionBytes}/${status.budgetBytes} bytes</span></p><details class="settings-more"><summary>Read new sessions</summary><p class="meta">Run <code>toolroll memory propose --repo ${repo}</code> on this computer.</p></details>`;
|
|
45
50
|
const decide = (p, decision, label) => postForm('/settings/knowledge/proposal', html `<button type="submit">${label}</button>`, { attrs: { class: 'inline' }, hidden: fields({ repo, proposal: p.id, decision }) });
|
|
46
51
|
const cards = proposals.map(p => html `<details class="card proposal" open><summary><strong>${p.title}</strong> <span class="meta">#${p.id} · ${p.kind} · ${p.sessions} sessions</span></summary><p>${p.rationale}</p>${p.beforeText ? html `<pre class="knowledge-instructions">- ${p.beforeText}</pre>` : ''}${p.afterText ? html `<pre class="knowledge-instructions">+ ${p.afterText}</pre>` : ''}<ul class="memory-evidence">${p.evidence.map(ev => html `<li>“${ev.quote.slice(0, 200)}” <span class="meta">${ev.session.split(':')[0] ?? ''}</span></li>`)}</ul>${manage ? html `${decide(p, 'accept', 'Accept')} ${decide(p, 'reject', 'Reject')}` : ''}</details>`);
|
|
47
|
-
return
|
|
52
|
+
return framedSection('Proposed from sessions', html `${line}${cards.length ? cards : html `<p class="meta">Nothing to review. Suggestions appear when the same mistake shows up in two sessions.</p>`}`);
|
|
48
53
|
}
|
package/dist/lead-progress.d.ts
CHANGED
|
@@ -20,11 +20,14 @@ export type LeadProgress = {
|
|
|
20
20
|
step: number;
|
|
21
21
|
id: string;
|
|
22
22
|
outcome: LeadToolOutcome;
|
|
23
|
-
}
|
|
23
|
+
}
|
|
24
|
+
/** `final`: the provider's finished answer for this step, sent once and secret-checked whole. */
|
|
25
|
+
| {
|
|
24
26
|
kind: "text";
|
|
25
27
|
turn: number;
|
|
26
28
|
step: number;
|
|
27
29
|
text: string;
|
|
30
|
+
final?: true;
|
|
28
31
|
};
|
|
29
32
|
export type LeadToolOutcome = {
|
|
30
33
|
state: "succeeded";
|
package/dist/lead-progress.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Live progress of one lead turn (chat streaming): what the lead is doing
|
|
2
2
|
* and the reply as it is written. Nothing here is durable or authoritative —
|
|
3
3
|
* the finished turn's saved message and cards remain the record. */
|
|
4
|
+
import { topLevelEvent } from "./stream-scope.js";
|
|
4
5
|
/** Plain words for each tool the lead uses, as the person watching reads them. */
|
|
5
6
|
const TOOL_LABELS = {
|
|
6
7
|
get_brief: "Catching up on your work",
|
|
@@ -127,7 +128,8 @@ export function claudeStreamReader(onText, capBytes = 1_048_576) {
|
|
|
127
128
|
catch {
|
|
128
129
|
continue;
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
// A subagent's frames (parent_tool_use_id set) are not the lead's reply: they never start, feed or end it.
|
|
132
|
+
if (!record(event) || !topLevelEvent(event) || event["type"] !== "stream_event" || !record(event["event"]))
|
|
131
133
|
continue;
|
|
132
134
|
const inner = event["event"];
|
|
133
135
|
if (inner["type"] === "content_block_start") {
|
package/dist/lead.js
CHANGED
|
@@ -486,6 +486,10 @@ export async function runLeadTurn(input) {
|
|
|
486
486
|
return fail("malformed-reply", LEAD_FAILURE_COPY.unreadableUnknownCost, true);
|
|
487
487
|
}
|
|
488
488
|
const answer = result.answer;
|
|
489
|
+
// The streamed reply's finished text, once, secret-checked whole: only now may its last word show live.
|
|
490
|
+
if (!direct && input.onProgress !== undefined && !controller.signal.aborted && answer.text !== "" && scanForSecrets(answer.text).length === 0) {
|
|
491
|
+
progress({ kind: "text", turn: turnId, step: steps, text: answer.text, final: true });
|
|
492
|
+
}
|
|
489
493
|
// Usage that cannot be true — more input tokens than bytes sent — is a
|
|
490
494
|
// malformed reply with unknown cost, never a number to settle by.
|
|
491
495
|
if (direct && answer.tokensIn > requestBytes) {
|
package/dist/live-bus.d.ts
CHANGED
|
@@ -128,6 +128,8 @@ export type LiveRoomOptions = {
|
|
|
128
128
|
/** A room whose read is costly reads at most once per this window after a signal; every viewer is still re-proved
|
|
129
129
|
* the moment a signal arrives (and again at the read). */
|
|
130
130
|
readSettleMs?: number;
|
|
131
|
+
/** Told when a room's last viewer has left and the room is gone (state kept per room can go with it). */
|
|
132
|
+
onEmpty?: (key: unknown) => void;
|
|
131
133
|
};
|
|
132
134
|
/**
|
|
133
135
|
* Rooms of open pages that hear the bus. `presence` says who else is here, as
|
package/dist/live-bus.js
CHANGED
|
@@ -309,6 +309,7 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
|
|
|
309
309
|
if (room.reading !== null)
|
|
310
310
|
clearTimeout(room.reading);
|
|
311
311
|
rooms.delete(key);
|
|
312
|
+
options.onEmpty?.(key);
|
|
312
313
|
if (rooms.size === 0 && unsubscribe !== null) {
|
|
313
314
|
unsubscribe();
|
|
314
315
|
unsubscribe = null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type LiveRecord } from "./live-records.js";
|
|
2
|
+
export declare const CURSOR_CAP: number;
|
|
3
|
+
/** Longer than any real key block: past this much of a stream's text with no END marker, its key state is known again. */
|
|
4
|
+
export declare const UNKNOWN_KEY_BYTES: number;
|
|
5
|
+
export type ChildView = {
|
|
6
|
+
k: "child";
|
|
7
|
+
s: number;
|
|
8
|
+
at?: string;
|
|
9
|
+
parent: string | null;
|
|
10
|
+
via?: number;
|
|
11
|
+
desc?: string;
|
|
12
|
+
} | {
|
|
13
|
+
k: "end";
|
|
14
|
+
s: number;
|
|
15
|
+
at?: string;
|
|
16
|
+
ok: boolean | null;
|
|
17
|
+
} | {
|
|
18
|
+
k: "over";
|
|
19
|
+
}
|
|
20
|
+
/** The file stopped at its size cap: nothing more will be written. */
|
|
21
|
+
| {
|
|
22
|
+
k: "cap";
|
|
23
|
+
} | (LiveRecord & {
|
|
24
|
+
s: number;
|
|
25
|
+
});
|
|
26
|
+
export type ChildWindow = {
|
|
27
|
+
ok: true;
|
|
28
|
+
records: ChildView[];
|
|
29
|
+
cursor: string;
|
|
30
|
+
/** A cursor was given but didn't hold (forged, stale, another run's or file's): this read started over. */
|
|
31
|
+
restarted: boolean;
|
|
32
|
+
/** Output before this read's start isn't shown (the read began at a checkpoint, or waits for one). */
|
|
33
|
+
earlier: boolean;
|
|
34
|
+
/** Records that failed a check: counted, never shown. */
|
|
35
|
+
quarantined: number;
|
|
36
|
+
/** Lines too long to show that were skipped. */
|
|
37
|
+
skipped: number;
|
|
38
|
+
/** More is already written: read again. */
|
|
39
|
+
more: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
reason: "missing" | "unavailable";
|
|
43
|
+
};
|
|
44
|
+
/** Seal a reader's state under the caller's key (the same form for the run's own file and its subagents'). */
|
|
45
|
+
export declare function sealCursor(state: object, key: Buffer): string;
|
|
46
|
+
/** A sealed state's object when the seal holds and it is within the cap; null otherwise (the caller re-checks it). */
|
|
47
|
+
export declare function openCursor(cursor: string, key: Buffer): Record<string, unknown> | null;
|
|
48
|
+
/** One window of a run's subagents' file. `commands` is the run's own captured answer (store.ts runLiveCommands). */
|
|
49
|
+
export declare function readChildWindow(root: string, runId: number, cursor: string | null, options: {
|
|
50
|
+
key: Buffer;
|
|
51
|
+
commands: boolean;
|
|
52
|
+
}): ChildWindow;
|