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/lead.js
CHANGED
|
@@ -4,11 +4,11 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { leadTimeoutNotice } from "./store.js";
|
|
5
5
|
import { RESULT_IMAGES_PER_TURN_CAP } from "./chat-evidence.js";
|
|
6
6
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
7
|
-
import { LEAD_MAX_CALLS_PER_STEP, LEAD_MAX_STEPS, LEAD_TOOL_RESULT_CAP_BYTES, TURN_WALL_CLOCK_MS, composeLeadRequest, credentialKeyOf, isDirectChatProvider, leadWorstCaseForPrice, performLeadRequest, priceForConfig,
|
|
7
|
+
import { LEAD_MAX_CALLS_PER_STEP, LEAD_MAX_STEPS, LEAD_TOOL_RESULT_CAP_BYTES, TURN_WALL_CLOCK_MS, composeLeadRequest, credentialKeyOf, isDirectChatProvider, leadWorstCaseForPrice, performLeadRequest, priceForConfig, cacheWorstPrice, settleWithCache, subscriptionCredentialKey, } from "./converse.js";
|
|
8
8
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
9
9
|
import { redactSecretAssignments } from "./builder.js";
|
|
10
10
|
import { LEAD_CONTRACT } from "./lead-contract.js";
|
|
11
|
-
import { LEAD_MAX_PROPOSALS_PER_TURN, LEAD_TOOL_SCHEMAS, executeLeadTool, isLeadTool, leadViewContextFor, projectLabelForLead, redactForLead, toolResultBytes } from "./lead-tools.js";
|
|
11
|
+
import { LEAD_CORE_TOOLS, LEAD_MAX_PROPOSALS_PER_TURN, LEAD_TOOL_SCHEMAS, executeLeadTool, isLeadTool, leadViewContextFor, projectLabelForLead, redactForLead, toolResultBytes } from "./lead-tools.js";
|
|
12
12
|
import { composeSubscriptionLeadPrompt, performSubscriptionLeadRequest } from "./subscription-chat.js";
|
|
13
13
|
import { LEAD_REPLY_LIMITS, leadContext } from './lead-context.js';
|
|
14
14
|
import { shortenAsk } from './text-limits.js';
|
|
@@ -34,6 +34,7 @@ export const LEAD_REFUSAL_COPY = {
|
|
|
34
34
|
"thread-closed": "This conversation is closed. Start a new one to keep going.",
|
|
35
35
|
latched: "An earlier reply stopped before its cost was known, so chat is paused. Acknowledge that reply on the Chat page, then send again.",
|
|
36
36
|
concurrent: "The lead is still answering your last message. Wait for that reply, then send this one.",
|
|
37
|
+
backlog: "Your earlier messages are still waiting for the lead. This one goes after them.",
|
|
37
38
|
"daily-cap": "You've reached today's limit on chat replies, so this one wasn't sent. Try again tomorrow.",
|
|
38
39
|
"session-exhausted": "This chat has used its spending limit. Start a new chat to keep going.",
|
|
39
40
|
"session-ended": "This chat has ended. Start a new chat to keep going.",
|
|
@@ -69,12 +70,25 @@ export const LEAD_FAILURE_COPY = {
|
|
|
69
70
|
tooLongUnknownCost: leadTimeoutNotice(false, true),
|
|
70
71
|
lostUnknownCost: "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
71
72
|
unreadableUnknownCost: "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
73
|
+
overReserved: "This reply reported more use than was set aside for it, so its cost is counted as unknown and chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
72
74
|
empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.",
|
|
73
75
|
unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.",
|
|
74
76
|
};
|
|
75
77
|
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
|
|
76
|
-
export function leadFailureText(reason) {
|
|
78
|
+
export function leadFailureText(reason, unknownSpend = false) {
|
|
79
|
+
// A turn whose cost is unknown paused chat: say what happened (its reason's first sentence), then that the person must
|
|
80
|
+
// confirm the cost before chat runs again — never "send it again", which would only meet the pause.
|
|
81
|
+
if (unknownSpend) {
|
|
82
|
+
if (reason === "malformed-reply")
|
|
83
|
+
return LEAD_FAILURE_COPY.unreadableUnknownCost;
|
|
84
|
+
if (reason === "over-reservation")
|
|
85
|
+
return LEAD_FAILURE_COPY.overReserved;
|
|
86
|
+
// Whether proposals were kept depends on how it ended (a timeout keeps what it drafted): this says nothing about them.
|
|
87
|
+
const happened = /^[^.]*\./.exec(leadFailureText(reason))?.[0] ?? "The lead's reply didn't finish.";
|
|
88
|
+
return `${happened} Its cost isn't known, so chat is paused. Confirm the cost on the Chat page to turn chat back on.`;
|
|
89
|
+
}
|
|
77
90
|
switch (reason) {
|
|
91
|
+
case "over-reservation": return "This reply reported more use than was set aside for it, so it was stopped and its cost counted. Send your message again.";
|
|
78
92
|
case "timeout":
|
|
79
93
|
case "crashed": return LEAD_FAILURE_COPY.tooLong;
|
|
80
94
|
case "malformed-reply": return LEAD_FAILURE_COPY.unreadable;
|
|
@@ -98,20 +112,22 @@ function settleBy(work, ms) {
|
|
|
98
112
|
}
|
|
99
113
|
const READ_TOOLS = new Set(["get_brief", "get_project_context", "get_actions", "get_action_status", "get_skills", "get_acceptance_evidence", "recap", "list_repos", "list_tasks", "get_task", "get_result", "get_result_images", "get_controls", "get_agents", "get_project_knowledge", "get_task_conversation", "get_diff", "get_check_log", "get_project_tools", "get_flows", "get_flow_insights", "list_decisions", "get_decision", "queue"]);
|
|
100
114
|
/** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
|
|
101
|
-
export function historyFor(store, thread, queuedMessageId) {
|
|
102
|
-
|
|
115
|
+
export function historyFor(store, thread, queuedMessageId, named = queuedMessageId !== undefined) {
|
|
116
|
+
// Admitted messages only (v128): a queued message — this one, a teammate's, or one removed while waiting — never
|
|
117
|
+
// reaches the model before its own turn takes it.
|
|
118
|
+
const rows = store.listLeadHistory(thread, LEAD_HISTORY_MAX_MESSAGES);
|
|
103
119
|
const kept = [];
|
|
104
120
|
let bytes = 0;
|
|
105
121
|
for (let index = rows.length - 1; index >= 0; index--) {
|
|
106
122
|
const row = rows[index];
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
if (
|
|
123
|
+
// The current message appears once (it is added after the history). Messages still waiting — a teammate's or this
|
|
124
|
+
// person's — are already left out by listLeadHistory (pending); a mirrored channel question is history like any other.
|
|
125
|
+
if (row.id === queuedMessageId)
|
|
110
126
|
continue;
|
|
111
127
|
bytes += Buffer.byteLength(row.text, "utf8");
|
|
112
128
|
if (bytes > LEAD_HISTORY_CAP_BYTES)
|
|
113
129
|
break;
|
|
114
|
-
const author =
|
|
130
|
+
const author = named && row.role === 'operator' && row.turn !== null ? store.getLeadTurn(row.turn)?.approver : undefined;
|
|
115
131
|
kept.unshift(row.role === "operator" ? { role: "operator", text: author ? `From ${author}:\n${row.text}` : row.text } : { role: "assistant", text: row.text, calls: [] });
|
|
116
132
|
}
|
|
117
133
|
// A history must open with the operator: a leading assistant reply without its question is dropped.
|
|
@@ -120,8 +136,9 @@ export function historyFor(store, thread, queuedMessageId) {
|
|
|
120
136
|
return kept;
|
|
121
137
|
}
|
|
122
138
|
/** "read 3 · proposed 1 · 2 steps" — counts only, never results (ruling 11). */
|
|
123
|
-
export function activitySummary(reads, proposals, steps) {
|
|
124
|
-
|
|
139
|
+
export function activitySummary(reads, proposals, steps, cacheShare = null) {
|
|
140
|
+
// How much of what it read came from the provider's cache, when every request reported it (docs/plans/run-efficiency.md).
|
|
141
|
+
return [`read ${reads}`, `proposed ${proposals}`, `${steps} step${steps === 1 ? "" : "s"}`, ...(cacheShare === null ? [] : [`${Math.round(cacheShare * 100)}% cached`])].join(" · ");
|
|
125
142
|
}
|
|
126
143
|
/** A tool result as the model sees it, measured as embedded; over the cap it becomes a typed refusal. */
|
|
127
144
|
function capped(value) {
|
|
@@ -175,7 +192,12 @@ export async function runLeadTurn(input) {
|
|
|
175
192
|
const sharedThread = liveThread !== null && store.canUseTeamMateThread(who.name, who.generation, liveThread.id);
|
|
176
193
|
if (liveSession?.approver !== who.name || (liveThread?.approver !== who.name && !sharedThread) || liveSession.credentialKey !== credentialKey)
|
|
177
194
|
return refuse("not-yours");
|
|
178
|
-
|
|
195
|
+
// A queued message binds only from a shared team thread, or as the exact personal queue claim of this person's own
|
|
196
|
+
// thread (its row is this approver's, in this thread, claimed under that runner and generation).
|
|
197
|
+
const personalQueued = input.queueClaim !== undefined && input.queueClaim.message === input.queuedMessageId &&
|
|
198
|
+
store.handle.prepare("SELECT 1 FROM lead_queue WHERE message = ? AND approver = ? AND thread = ? AND status = 'running' AND runner = ? AND generation = ? AND turn_id IS NULL")
|
|
199
|
+
.get(input.queueClaim.message, who.name, thread.id, input.queueClaim.runner, input.queueClaim.generation) !== undefined;
|
|
200
|
+
if (input.queuedMessageId !== undefined && ((!sharedThread && !personalQueued) || !Number.isSafeInteger(input.queuedMessageId) || input.onAdmitted === undefined))
|
|
179
201
|
return refuse('invalid-request');
|
|
180
202
|
if (liveSession.endedAt !== null)
|
|
181
203
|
return refuse("session-ended");
|
|
@@ -197,18 +219,29 @@ export async function runLeadTurn(input) {
|
|
|
197
219
|
const document = leadContext(store, who.repos, now, { owner: who.name, thread: thread.id, redact: text => redactForLead(text, view),
|
|
198
220
|
projectName: (path, index) => projectLabelForLead(path, index, view.names),
|
|
199
221
|
...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.channel === undefined ? {} : { channel: input.channel }), ...(input.leadName === undefined ? {} : { leadName: input.leadName }) });
|
|
200
|
-
|
|
222
|
+
// A team room names who wrote each message; a person's own queued message is just theirs.
|
|
223
|
+
const authoredMessage = input.queuedMessageId === undefined || personalQueued ? message : `From ${who.name}:\n${message}`;
|
|
201
224
|
const historyMessage = input.context === undefined ? authoredMessage : `${redactForLead(input.context, view)}\n\n${authoredMessage}`;
|
|
202
|
-
const history = [...historyFor(store, thread.id, input.queuedMessageId), { role: "operator", text: historyMessage }];
|
|
203
|
-
|
|
225
|
+
const history = [...historyFor(store, thread.id, input.queuedMessageId, input.queuedMessageId !== undefined && !personalQueued), { role: "operator", text: historyMessage }];
|
|
226
|
+
// Tools on demand (docs/plans/lead-deferred-tools.md): a request advertises the core tools, then the ones find_tools
|
|
227
|
+
// found earlier in this reply, in that order (so the core prefix stays the same as tools are found).
|
|
228
|
+
const findTools = { searches: 0, found: new Set() };
|
|
229
|
+
const advertisedTools = () => [
|
|
230
|
+
...LEAD_TOOL_SCHEMAS.filter(one => LEAD_CORE_TOOLS.has(one.name)),
|
|
231
|
+
...[...findTools.found].map(name => LEAD_TOOL_SCHEMAS.find(one => one.name === name)).filter((one) => one !== undefined),
|
|
232
|
+
];
|
|
233
|
+
const composeDirect = (key, tools) => {
|
|
204
234
|
if (!direct)
|
|
205
235
|
throw new Error("not a direct chat provider");
|
|
206
|
-
|
|
236
|
+
// Anthropic prompt caching: the core tools (always first) end the first breakpoint (docs/plans/lead-deferred-tools.md).
|
|
237
|
+
const cache = directProvider === "anthropic-api" ? { coreTools: tools.filter(one => LEAD_CORE_TOOLS.has(one.name)).length } : undefined;
|
|
238
|
+
return composeLeadRequest({ provider: directProvider, model: config.model, key, system: LEAD_CONTRACT, dataDocument: document, history, tools, ...(cache === undefined ? {} : { cache }) });
|
|
207
239
|
};
|
|
208
|
-
const composeSubscription = () => composeSubscriptionLeadPrompt({ system: LEAD_CONTRACT, dataDocument: document, history, tools
|
|
209
|
-
// The
|
|
210
|
-
//
|
|
211
|
-
|
|
240
|
+
const composeSubscription = (tools) => composeSubscriptionLeadPrompt({ system: LEAD_CONTRACT, dataDocument: document, history, tools });
|
|
241
|
+
// The reservation-only base: the FULL registry, serialized as a request would be, so any tool found later in this
|
|
242
|
+
// reply is already covered by the worst case. What is sent each round is smaller (the advertised set) and is scanned
|
|
243
|
+
// whole before every dispatch. Only direct API traffic needs a dollar reservation; subscription traffic records zero.
|
|
244
|
+
const base = direct ? composeDirect("", LEAD_TOOL_SCHEMAS).body : composeSubscription(LEAD_TOOL_SCHEMAS);
|
|
212
245
|
if (scanForSecrets(base).length > 0)
|
|
213
246
|
return refuse("secret-in-context");
|
|
214
247
|
// Fit the bounded tool loop to the already-authorized remaining allowance.
|
|
@@ -216,7 +249,9 @@ export async function runLeadTurn(input) {
|
|
|
216
249
|
// Each chosen step retains the same worst-case call/output accounting.
|
|
217
250
|
let maxSteps = LEAD_MAX_STEPS;
|
|
218
251
|
const baseBytes = Buffer.byteLength(base, "utf8");
|
|
219
|
-
|
|
252
|
+
// With prompt caching on (Anthropic), the worst case reserves every input token at the cache-write rate (the dearest).
|
|
253
|
+
const reservePrice = direct && directProvider === "anthropic-api" ? cacheWorstPrice(price) : price;
|
|
254
|
+
const reserveFor = (steps) => direct ? leadWorstCaseForPrice(reservePrice, baseBytes, { steps }) : 0;
|
|
220
255
|
if (direct) {
|
|
221
256
|
const remaining = Math.min(liveSession.ceilingMicrousd - liveSession.spentMicrousd, config.weeklyCeilingMicrousd - store.chatWeeklySpendMicrousd(credentialKey, now));
|
|
222
257
|
while (maxSteps > 1 && reserveFor(maxSteps) > remaining)
|
|
@@ -239,6 +274,7 @@ export async function runLeadTurn(input) {
|
|
|
239
274
|
weeklyCeilingMicrousd: config.weeklyCeilingMicrousd,
|
|
240
275
|
deadlineMs: TURN_WALL_CLOCK_MS + 10_000,
|
|
241
276
|
provider: config.provider,
|
|
277
|
+
...(input.queueClaim === undefined ? {} : { queueClaim: input.queueClaim }),
|
|
242
278
|
}, now);
|
|
243
279
|
if (!opened.ok)
|
|
244
280
|
return opened;
|
|
@@ -249,7 +285,8 @@ export async function runLeadTurn(input) {
|
|
|
249
285
|
if (input.queuedMessageId === undefined)
|
|
250
286
|
store.appendLeadMessage({ thread: thread.id, turn: turnId, role: "operator", text: message }, now);
|
|
251
287
|
else {
|
|
252
|
-
|
|
288
|
+
// Binding admits it: the same statement clears pending, so it joins history exactly when its turn exists (v128).
|
|
289
|
+
const bound = store.handle.prepare("UPDATE lead_message SET turn=?, pending=0 WHERE id=? AND thread=? AND role='operator' AND turn IS NULL AND text=?")
|
|
253
290
|
.run(turnId, input.queuedMessageId, thread.id, message);
|
|
254
291
|
if (Number(bound.changes) !== 1)
|
|
255
292
|
throw new Error('The queued message changed before admission.');
|
|
@@ -281,6 +318,8 @@ export async function runLeadTurn(input) {
|
|
|
281
318
|
/** The turn's one question to its owner: shown with the reply only once the turn answers; a failed turn drops it. */
|
|
282
319
|
const ask = (question, options) => store.recordLeadAsk({ turn: turnId, thread: thread.id, question, options }, clock());
|
|
283
320
|
let tokensIn = 0;
|
|
321
|
+
// What its requests read from the provider's cache; known only while every request reported it.
|
|
322
|
+
let cacheRead = 0, cacheKnown = true;
|
|
284
323
|
let tokensOut = 0;
|
|
285
324
|
let settled = 0;
|
|
286
325
|
const draft = (kind, payload) => {
|
|
@@ -363,7 +402,10 @@ export async function runLeadTurn(input) {
|
|
|
363
402
|
const remainingMs = TURN_WALL_CLOCK_MS - (now.getTime() - turnStartedAt);
|
|
364
403
|
if (remainingMs <= 0)
|
|
365
404
|
return timedOut(false);
|
|
366
|
-
|
|
405
|
+
// This request's advertised tools: fixed for the request; a tool found in its reply is advertised from the next one.
|
|
406
|
+
const advertised = advertisedTools();
|
|
407
|
+
const advertisedNames = new Set(advertised.map(one => one.name));
|
|
408
|
+
const request = direct ? composeDirect(input.key, advertised) : composeSubscription(advertised);
|
|
367
409
|
// Tool results join the outbound body: scanned again before every dispatch.
|
|
368
410
|
const outbound = typeof request === "string" ? request : request.body;
|
|
369
411
|
if (scanForSecrets(outbound).length > 0) {
|
|
@@ -402,7 +444,7 @@ export async function runLeadTurn(input) {
|
|
|
402
444
|
system: LEAD_CONTRACT,
|
|
403
445
|
dataDocument: document,
|
|
404
446
|
history,
|
|
405
|
-
tools:
|
|
447
|
+
tools: advertised,
|
|
406
448
|
timeoutMs: remainingMs,
|
|
407
449
|
signal: controller.signal,
|
|
408
450
|
// The reply as it is written; text that looks like a secret is
|
|
@@ -452,11 +494,24 @@ export async function runLeadTurn(input) {
|
|
|
452
494
|
}
|
|
453
495
|
// The pinned math, or the provider's own reported charge when HIGHER.
|
|
454
496
|
const stepSettled = direct
|
|
455
|
-
? Math.max(
|
|
497
|
+
? Math.max(settleWithCache(price, answer), answer.reportedCostMicrousd ?? 0)
|
|
456
498
|
: 0;
|
|
457
|
-
|
|
499
|
+
// The reservation is the bound: a step whose reported usage would settle past what this reply reserved (the
|
|
500
|
+
// estimate counts a token per three bytes; a provider may report more) ends the turn with its cost unknown, which
|
|
501
|
+
// settles the whole reservation and pauses chat until the cost is confirmed — never a debit past the reservation
|
|
502
|
+
// or the session's ceiling.
|
|
503
|
+
if (direct && settled + stepSettled > reserved) {
|
|
504
|
+
finishStep({ state: "failed", failureReason: "over-budget", settledMicrousd: null, unknownSpend: true });
|
|
505
|
+
return fail("over-reservation", LEAD_FAILURE_COPY.overReserved, true);
|
|
506
|
+
}
|
|
507
|
+
finishStep({ state: "answered", tokensIn: answer.tokensIn, tokensOut: answer.tokensOut, settledMicrousd: stepSettled, replyBytes: Buffer.byteLength(answer.text, "utf8"),
|
|
508
|
+
cacheReadTokens: answer.cacheReadTokens ?? null, cacheWriteTokens: answer.cacheWriteTokens ?? null });
|
|
458
509
|
tokensIn += answer.tokensIn;
|
|
459
510
|
tokensOut += answer.tokensOut;
|
|
511
|
+
if (answer.cacheReadTokens === undefined)
|
|
512
|
+
cacheKnown = false;
|
|
513
|
+
else
|
|
514
|
+
cacheRead += answer.cacheReadTokens;
|
|
460
515
|
settled += stepSettled;
|
|
461
516
|
lastText = answer.text;
|
|
462
517
|
// After the wait (finding 5): the row may have been failed under us by
|
|
@@ -502,7 +557,11 @@ export async function runLeadTurn(input) {
|
|
|
502
557
|
// Position within this turn is unique even if a provider reuses call ids.
|
|
503
558
|
const progressId = `${turnId}:${steps}:${index}`;
|
|
504
559
|
progress({ kind: "tool", turn: turnId, step: steps, id: progressId, label: leadToolLabel(call.name) });
|
|
505
|
-
|
|
560
|
+
// A registered tool this request did not advertise is refused before it is read or run (the only production
|
|
561
|
+
// caller of executeLeadTool): the model learns the road (find_tools, then the next step) and the reply goes on.
|
|
562
|
+
const outcome = !advertisedNames.has(call.name)
|
|
563
|
+
? { ok: false, message: `${call.name} isn't available in this step. Call find_tools with what you want to do; tools it finds can be called from your next step.` }
|
|
564
|
+
: executeLeadTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, findTools, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
|
|
506
565
|
let displayOutcome = { state: "succeeded" };
|
|
507
566
|
if (!outcome.ok) {
|
|
508
567
|
const reason = leadToolFailureReason(call.name, outcome.message);
|
|
@@ -510,7 +569,8 @@ export async function runLeadTurn(input) {
|
|
|
510
569
|
? reason : `That step didn't work (${leadToolLabel(call.name)}).` };
|
|
511
570
|
}
|
|
512
571
|
progress({ kind: "tool-result", turn: turnId, step: steps, id: progressId, outcome: displayOutcome });
|
|
513
|
-
|
|
572
|
+
// A call refused because this request did not advertise it read nothing.
|
|
573
|
+
if (READ_TOOLS.has(call.name) && advertisedNames.has(call.name))
|
|
514
574
|
reads++;
|
|
515
575
|
if (call.name === "show_control" && outcome.ok)
|
|
516
576
|
shownControl = true;
|
|
@@ -534,7 +594,7 @@ export async function runLeadTurn(input) {
|
|
|
534
594
|
if (scanForSecrets(reply).length > 0)
|
|
535
595
|
return fail("secret-refused", LEAD_FAILURE_COPY.secretReply, false);
|
|
536
596
|
now = clock();
|
|
537
|
-
const activity = activitySummary(reads, proposals, steps);
|
|
597
|
+
const activity = activitySummary(reads, proposals, steps, cacheKnown && tokensIn > 0 ? Math.min(1, cacheRead / tokensIn) : null);
|
|
538
598
|
// One write (finding 12): settle, debit, promote the drafts, append the assistant text.
|
|
539
599
|
const finalized = store.finalizeLeadTurn(turnId, started.generation, { state: "answered", settledMicrousd: settled, tokensIn, tokensOut, message: { text: reply, activity } }, now);
|
|
540
600
|
if (!finalized)
|
package/dist/limits-ui.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* costs nothing per run, so its windows are what bind it; budgets bind work
|
|
5
5
|
* billed to an API key. An instance operator's view.
|
|
6
6
|
*/
|
|
7
|
-
import type { BrowserLimits } from "./browser-workspace.js";
|
|
7
|
+
import type { BrowserLimits, BrowserUsageChip } from "./browser-workspace.js";
|
|
8
8
|
import { type BudgetState } from "./spend.js";
|
|
9
9
|
import { type Html } from "./html.js";
|
|
10
10
|
/** "in 25 min", "in 2 h 10 min", or a day and hour ("Fri 11 PM") beyond a day. */
|
|
@@ -22,6 +22,13 @@ export declare function limitsView(windows: readonly {
|
|
|
22
22
|
project: (repo: string) => string;
|
|
23
23
|
subagent: (id: number) => string;
|
|
24
24
|
}, now: Date): BrowserLimits | null;
|
|
25
|
+
/**
|
|
26
|
+
* The usage chip (cockpit §3, R16): the subscription window with the highest use the providers last reported, as
|
|
27
|
+
* "Claude 5-hour · 82% · resets 3h". A reading older than 30 minutes stays visibly stale ("as of 2h ago"), judged by
|
|
28
|
+
* the reading's age as the Limits view judges it; a window with no known reset says "resets: unknown". API-key budgets
|
|
29
|
+
* are not compared here (they stay in the details). An operator's view, as the Limits view is. Null: nothing reported.
|
|
30
|
+
*/
|
|
31
|
+
export declare function usageChipOf(windows: Parameters<typeof limitsView>[0], now: Date): BrowserUsageChip | null;
|
|
25
32
|
/** The same tiles for the page without scripts. */
|
|
26
33
|
export declare function limitsHtml(limits: BrowserLimits | null): Html;
|
|
27
34
|
export declare const LIMITS_CSS: string;
|
package/dist/limits-ui.js
CHANGED
|
@@ -48,6 +48,32 @@ export function limitsView(windows, budgets, names, now) {
|
|
|
48
48
|
}
|
|
49
49
|
return tiles.length === 0 ? null : { tiles };
|
|
50
50
|
}
|
|
51
|
+
/** One short reset: "3h", "25 min", or "Fri 11 PM". */
|
|
52
|
+
const shortReset = (resetsAt, now) => {
|
|
53
|
+
const minutes = Math.max(1, Math.round((Date.parse(resetsAt) - now.getTime()) / 60_000));
|
|
54
|
+
return minutes < 60 ? `${minutes} min` : minutes < 24 * 60 ? `${Math.round(minutes / 60)}h` : resetWords(resetsAt, now);
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The usage chip (cockpit §3, R16): the subscription window with the highest use the providers last reported, as
|
|
58
|
+
* "Claude 5-hour · 82% · resets 3h". A reading older than 30 minutes stays visibly stale ("as of 2h ago"), judged by
|
|
59
|
+
* the reading's age as the Limits view judges it; a window with no known reset says "resets: unknown". API-key budgets
|
|
60
|
+
* are not compared here (they stay in the details). An operator's view, as the Limits view is. Null: nothing reported.
|
|
61
|
+
*/
|
|
62
|
+
export function usageChipOf(windows, now) {
|
|
63
|
+
// A window whose reset has passed since its reading is not measured again yet: it ranks below every current one, and
|
|
64
|
+
// shows what was last read, as stale — never an inferred 0%.
|
|
65
|
+
const expired = (one) => one.resetsAt !== null && Date.parse(one.resetsAt) <= now.getTime();
|
|
66
|
+
const rank = (one) => (expired(one) ? -1_000 : 0) + one.usedPercent + (one.reached && !expired(one) ? 1_000 : 0);
|
|
67
|
+
const top = [...windows].sort((a, b) => rank(b) - rank(a))[0];
|
|
68
|
+
if (top === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
const old = expired(top);
|
|
71
|
+
const stale = old || now.getTime() - Date.parse(top.observedAt) > 30 * 60_000;
|
|
72
|
+
// A window with no known length or reset has no reset to name.
|
|
73
|
+
const reset = old ? "window reset" : top.resetsAt === null || top.windowMinutes === null ? "resets: unknown" : `resets ${shortReset(top.resetsAt, now)}`;
|
|
74
|
+
const text = `${PROVIDER_NAMES[top.provider] ?? top.provider} ${windowLabel(top.window, top.windowMinutes)} · ${Math.round(top.usedPercent)}% · ${reset}${stale ? ` · as of ${ago(top.observedAt, now)}` : ""}`;
|
|
75
|
+
return { text, tone: old ? "neutral" : toneOf(top.usedPercent, top.reached), stale, href: "/work", title: `What the providers last reported: ${text}` };
|
|
76
|
+
}
|
|
51
77
|
/** The same tiles for the page without scripts. */
|
|
52
78
|
export function limitsHtml(limits) {
|
|
53
79
|
if (limits === null)
|
package/dist/live-bus.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export declare class LiveConnection {
|
|
|
85
85
|
private readonly proofs;
|
|
86
86
|
private readonly timer;
|
|
87
87
|
private closed;
|
|
88
|
+
private refusing;
|
|
88
89
|
constructor(response: ServerResponse, options?: {
|
|
89
90
|
limitBytes?: number;
|
|
90
91
|
keepAliveMs?: number;
|
|
@@ -95,6 +96,10 @@ export declare class LiveConnection {
|
|
|
95
96
|
get open(): boolean;
|
|
96
97
|
/** An event for one room: its data says which. */
|
|
97
98
|
send(room: string, event: string, data?: Record<string, unknown>): void;
|
|
99
|
+
/** A room leaves because its proof failed (a sign-out, a revoked or narrowed account): if that departure ends the
|
|
100
|
+
* connection, the page is told the stream `ended` and stops, rather than reconnecting at once into a request that
|
|
101
|
+
* can't sign in. Any other close — the page going, a later shutdown, an overflow — says nothing of it. */
|
|
102
|
+
refuse(leave: () => void): void;
|
|
98
103
|
/** Run when the connection ends; returns the undo for a room that leaves first. */
|
|
99
104
|
onClose(leave: () => void): () => void;
|
|
100
105
|
close(): void;
|
|
@@ -120,6 +125,9 @@ export type LiveRoomOptions = {
|
|
|
120
125
|
bus?: LiveBus | null;
|
|
121
126
|
safetyNetMs?: number;
|
|
122
127
|
minIntervalMs?: number;
|
|
128
|
+
/** A room whose read is costly reads at most once per this window after a signal; every viewer is still re-proved
|
|
129
|
+
* the moment a signal arrives (and again at the read). */
|
|
130
|
+
readSettleMs?: number;
|
|
123
131
|
};
|
|
124
132
|
/**
|
|
125
133
|
* Rooms of open pages that hear the bus. `presence` says who else is here, as
|
package/dist/live-bus.js
CHANGED
|
@@ -201,6 +201,7 @@ export class LiveConnection {
|
|
|
201
201
|
proofs = new Set();
|
|
202
202
|
timer;
|
|
203
203
|
closed = false;
|
|
204
|
+
refusing = false;
|
|
204
205
|
constructor(response, options = {}) {
|
|
205
206
|
this.response = response;
|
|
206
207
|
this.stream = new LiveStream(response, options.limitBytes, () => this.close());
|
|
@@ -222,6 +223,19 @@ export class LiveConnection {
|
|
|
222
223
|
get open() { return !this.closed && this.stream.open; }
|
|
223
224
|
/** An event for one room: its data says which. */
|
|
224
225
|
send(room, event, data = {}) { this.stream.send(event, { room, ...data }); }
|
|
226
|
+
/** A room leaves because its proof failed (a sign-out, a revoked or narrowed account): if that departure ends the
|
|
227
|
+
* connection, the page is told the stream `ended` and stops, rather than reconnecting at once into a request that
|
|
228
|
+
* can't sign in. Any other close — the page going, a later shutdown, an overflow — says nothing of it. */
|
|
229
|
+
refuse(leave) {
|
|
230
|
+
const outer = this.refusing;
|
|
231
|
+
this.refusing = true;
|
|
232
|
+
try {
|
|
233
|
+
leave();
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
this.refusing = outer;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
225
239
|
/** Run when the connection ends; returns the undo for a room that leaves first. */
|
|
226
240
|
onClose(leave) { this.leaves.add(leave); return () => { this.leaves.delete(leave); }; }
|
|
227
241
|
close() {
|
|
@@ -233,6 +247,10 @@ export class LiveConnection {
|
|
|
233
247
|
leave();
|
|
234
248
|
this.leaves.clear();
|
|
235
249
|
this.proofs.clear();
|
|
250
|
+
if (this.refusing && this.stream.open) {
|
|
251
|
+
this.response.end("event: ended\ndata: {}\n\n");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
236
254
|
if (!this.response.writableEnded)
|
|
237
255
|
this.response.end();
|
|
238
256
|
}
|
|
@@ -251,6 +269,7 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
|
|
|
251
269
|
const bus = options.bus ?? null;
|
|
252
270
|
const safetyNetMs = options.safetyNetMs ?? SAFETY_NET_MS;
|
|
253
271
|
const minIntervalMs = options.minIntervalMs ?? 0;
|
|
272
|
+
const readSettleMs = options.readSettleMs ?? 0;
|
|
254
273
|
const rooms = new Map();
|
|
255
274
|
let unsubscribe = null;
|
|
256
275
|
const announce = (key, room) => {
|
|
@@ -287,6 +306,8 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
|
|
|
287
306
|
clearTimeout(room.retry);
|
|
288
307
|
if (room.holding !== null)
|
|
289
308
|
clearTimeout(room.holding);
|
|
309
|
+
if (room.reading !== null)
|
|
310
|
+
clearTimeout(room.reading);
|
|
290
311
|
rooms.delete(key);
|
|
291
312
|
if (rooms.size === 0 && unsubscribe !== null) {
|
|
292
313
|
unsubscribe();
|
|
@@ -312,7 +333,10 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
|
|
|
312
333
|
viewer.connection.send(viewer.room, "gone");
|
|
313
334
|
}
|
|
314
335
|
finally {
|
|
315
|
-
|
|
336
|
+
if (valid)
|
|
337
|
+
leave(key, viewer);
|
|
338
|
+
else
|
|
339
|
+
viewer.connection.refuse(() => leave(key, viewer));
|
|
316
340
|
}
|
|
317
341
|
return false;
|
|
318
342
|
}
|
|
@@ -366,8 +390,20 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
|
|
|
366
390
|
deliver(key, room, viewer, "change", { at: room.last, revision: room.revision });
|
|
367
391
|
};
|
|
368
392
|
const heard = (change) => {
|
|
369
|
-
for (const [key, room] of [...rooms])
|
|
370
|
-
|
|
393
|
+
for (const [key, room] of [...rooms]) {
|
|
394
|
+
if (readSettleMs <= 0) {
|
|
395
|
+
refresh(key, room, change.revision);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// Re-prove now; read once the burst settles.
|
|
399
|
+
for (const viewer of [...room.viewers.keys()])
|
|
400
|
+
deliver(key, room, viewer);
|
|
401
|
+
if (rooms.get(key) !== room || room.reading !== null)
|
|
402
|
+
continue;
|
|
403
|
+
room.reading = setTimeout(() => { room.reading = null; if (rooms.get(key) === room)
|
|
404
|
+
refresh(key, room, null); }, readSettleMs);
|
|
405
|
+
room.reading.unref();
|
|
406
|
+
}
|
|
371
407
|
};
|
|
372
408
|
return {
|
|
373
409
|
join(key, viewer) {
|
|
@@ -378,7 +414,7 @@ export function createLiveRooms(fingerprint, presence, options = {}) {
|
|
|
378
414
|
last = fingerprint(key);
|
|
379
415
|
}
|
|
380
416
|
catch { /* the next signal or the safety net reads it */ }
|
|
381
|
-
const created = { viewers: new Map(), last, timer: setInterval(() => refresh(key, created, null), safetyNetMs), settling: null, retry: null, saidAt: 0, holding: null, revision: null };
|
|
417
|
+
const created = { viewers: new Map(), last, timer: setInterval(() => refresh(key, created, null), safetyNetMs), settling: null, retry: null, saidAt: 0, holding: null, revision: null, reading: null };
|
|
382
418
|
created.timer.unref();
|
|
383
419
|
room = created;
|
|
384
420
|
rooms.set(key, room);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
/** The consent a "Show commands" form carries: a single-use nonce minted for this person and these exact terms. */
|
|
3
|
+
export declare const LIVE_VIEW_CONSENT = "live-view";
|
|
4
|
+
export declare function liveViewConsentDigest(repo: string): string;
|
|
5
|
+
export type LiveDetailSetting = {
|
|
6
|
+
repo: string;
|
|
7
|
+
commands: boolean;
|
|
8
|
+
updatedBy: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
};
|
|
11
|
+
/** Whether this project's live conversations show commands. Unknown or absent is off. */
|
|
12
|
+
export declare function liveCommandsShown(store: Store, repo: string | null): boolean;
|
|
13
|
+
export declare function liveDetailSetting(store: Store, repo: string): LiveDetailSetting | null;
|
|
14
|
+
/** Save the setting and its ledger line in one transaction. The caller has proved who is asking (and, to turn it on, their password). */
|
|
15
|
+
export declare function setLiveCommands(store: Store, change: {
|
|
16
|
+
repo: string;
|
|
17
|
+
commands: boolean;
|
|
18
|
+
by: string;
|
|
19
|
+
via: string;
|
|
20
|
+
}, now: Date): {
|
|
21
|
+
said: string;
|
|
22
|
+
};
|
|
23
|
+
/** A run's trusted context for its live file: its own checkout, and its project's setting, read now. */
|
|
24
|
+
export declare function liveLogContext(store: Store, taskRef: number, worktree: string | null): {
|
|
25
|
+
base: string | null;
|
|
26
|
+
commands: boolean;
|
|
27
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project's "Show commands in the live view" (docs/plans/live-conversation.md):
|
|
3
|
+
* whether a running agent's command text and command output are written to
|
|
4
|
+
* its live conversation. Off unless an approver turned it on for the
|
|
5
|
+
* project with their password; turning it off is one step. Every change is
|
|
6
|
+
* an action-ledger line.
|
|
7
|
+
*
|
|
8
|
+
* The writer reads it once, when it opens a run's live file: what is off
|
|
9
|
+
* never reaches disk, and a change applies from the next run.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/** The consent a "Show commands" form carries: a single-use nonce minted for this person and these exact terms. */
|
|
13
|
+
export const LIVE_VIEW_CONSENT = "live-view";
|
|
14
|
+
export function liveViewConsentDigest(repo) {
|
|
15
|
+
return createHash("sha256").update(JSON.stringify(["live-view", repo, "commands-on"])).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
/** Whether this project's live conversations show commands. Unknown or absent is off. */
|
|
18
|
+
export function liveCommandsShown(store, repo) {
|
|
19
|
+
if (repo === null)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const row = store.handle.prepare("SELECT commands FROM live_detail_setting WHERE repo = ?").get(repo);
|
|
23
|
+
return row !== undefined && Number(row["commands"]) === 1;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function liveDetailSetting(store, repo) {
|
|
30
|
+
const row = store.handle.prepare("SELECT repo, commands, updated_by, updated_at FROM live_detail_setting WHERE repo = ?").get(repo);
|
|
31
|
+
if (row === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
return { repo: String(row["repo"]), commands: Number(row["commands"]) === 1, updatedBy: String(row["updated_by"]), updatedAt: String(row["updated_at"]) };
|
|
34
|
+
}
|
|
35
|
+
/** Save the setting and its ledger line in one transaction. The caller has proved who is asking (and, to turn it on, their password). */
|
|
36
|
+
export function setLiveCommands(store, change, now) {
|
|
37
|
+
store.transact(() => {
|
|
38
|
+
store.handle
|
|
39
|
+
.prepare(`INSERT INTO live_detail_setting (repo, commands, updated_by, updated_at) VALUES (?,?,?,?)
|
|
40
|
+
ON CONFLICT (repo) DO UPDATE SET commands = excluded.commands, updated_by = excluded.updated_by, updated_at = excluded.updated_at`)
|
|
41
|
+
.run(change.repo, change.commands ? 1 : 0, change.by, now.toISOString());
|
|
42
|
+
store.recordAction({ at: now.toISOString(), actor: change.by, repo: change.repo, taskId: null, runId: null,
|
|
43
|
+
action: "live view commands", outcome: change.commands ? "on" : "off", source: "request", detail: `via ${change.via}` });
|
|
44
|
+
});
|
|
45
|
+
return { said: change.commands ? "Commands now show in the live view for this project, from the next run." : "Commands are hidden in the live view for this project, from the next run." };
|
|
46
|
+
}
|
|
47
|
+
/** A run's trusted context for its live file: its own checkout, and its project's setting, read now. */
|
|
48
|
+
export function liveLogContext(store, taskRef, worktree) {
|
|
49
|
+
let repo = null;
|
|
50
|
+
try {
|
|
51
|
+
repo = store.refForId(taskRef)?.repo ?? null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
repo = null;
|
|
55
|
+
}
|
|
56
|
+
return { base: worktree, commands: liveCommandsShown(store, repo) };
|
|
57
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export declare const RUN_NOTES_SHOWN = 20;
|
|
3
|
+
export type RunNote = {
|
|
4
|
+
id: number;
|
|
5
|
+
author: string;
|
|
6
|
+
/** Whether the person looking wrote it: only then does the view say "You". */
|
|
7
|
+
mine: boolean;
|
|
8
|
+
at: string;
|
|
9
|
+
text: string;
|
|
10
|
+
standing: string;
|
|
11
|
+
readAt: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** This run's notes, oldest first: the newest RUN_NOTES_SHOWN, and how many older ones the task page keeps. */
|
|
14
|
+
export declare function runNotes(store: Store, run: {
|
|
15
|
+
id: number;
|
|
16
|
+
taskRef: number;
|
|
17
|
+
}, viewer: string, now: Date): {
|
|
18
|
+
notes: RunNote[];
|
|
19
|
+
older: number;
|
|
20
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The steering notes that belong in one run's live conversation
|
|
3
|
+
* (docs/plans/live-conversation.md §5): every note this run reached —
|
|
4
|
+
* attached to its brief, mailed into its folder, or read by it, as kept in
|
|
5
|
+
* steer_note_run when it happened — and, only while this run is the task's
|
|
6
|
+
* live build, notes still waiting for it. A note never shows on a run it did
|
|
7
|
+
* not reach, and is credited as read only to the run that read it.
|
|
8
|
+
*/
|
|
9
|
+
import { steerStandingParts } from "./steer-status.js";
|
|
10
|
+
export const RUN_NOTES_SHOWN = 20;
|
|
11
|
+
/** Where a note stands as seen from this run. */
|
|
12
|
+
function standingFor(note, run, live) {
|
|
13
|
+
if (note.authorshipState === "verified" && note.supersededAt === null && note.deliveredAt === null && !live) {
|
|
14
|
+
// This run ended (or is not the one reading notes) without reading it: it waits for the next attempt.
|
|
15
|
+
return { standing: "Not read in this attempt — the next one gets it", readAt: null };
|
|
16
|
+
}
|
|
17
|
+
// Once read, a note's attached run is the run that read it (ackSteerNote, settleSteerDelivered).
|
|
18
|
+
if (note.deliveredAt !== null && note.attachedRun !== run)
|
|
19
|
+
return { standing: "Read in another attempt", readAt: null };
|
|
20
|
+
const { words, readAt } = steerStandingParts(note, live);
|
|
21
|
+
return { standing: words, readAt };
|
|
22
|
+
}
|
|
23
|
+
/** This run's notes, oldest first: the newest RUN_NOTES_SHOWN, and how many older ones the task page keeps. */
|
|
24
|
+
export function runNotes(store, run, viewer, now) {
|
|
25
|
+
const live = store.steeringLiveBuild(run.taskRef, now) === run.id;
|
|
26
|
+
const reached = new Map(store.steerNotesReaching(run.id).filter(note => note.taskRef === run.taskRef).map(note => [note.id, note]));
|
|
27
|
+
if (live) {
|
|
28
|
+
for (const note of store.listSteerNotes(run.taskRef)) {
|
|
29
|
+
const waiting = note.authorshipState === "verified" && note.deliveredAt === null && note.supersededAt === null;
|
|
30
|
+
if (waiting && !reached.has(note.id))
|
|
31
|
+
reached.set(note.id, note);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const all = [...reached.values()].sort((a, b) => a.id - b.id);
|
|
35
|
+
const shown = all.slice(-RUN_NOTES_SHOWN);
|
|
36
|
+
return {
|
|
37
|
+
older: all.length - shown.length,
|
|
38
|
+
notes: shown.map(note => ({
|
|
39
|
+
id: note.id,
|
|
40
|
+
author: note.author,
|
|
41
|
+
mine: note.author === viewer,
|
|
42
|
+
at: note.createdAt,
|
|
43
|
+
text: note.note,
|
|
44
|
+
...standingFor(note, run.id, live),
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|