toolroll 0.9.63 → 0.9.67
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 +108 -0
- package/README.md +33 -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 +117 -2
- package/dist/agent-fence.js +439 -12
- 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 +39 -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/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -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 +10 -1
- package/dist/builder.js +87 -24
- 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 +13 -4
- package/dist/contracts/console-api.js +13 -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 +35 -0
- package/dist/invoke.js +347 -28
- 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 +269 -0
- package/dist/live-terminal.js +686 -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 +4 -0
- package/dist/operate.js +335 -19
- 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 +18 -0
- package/dist/project-delete-ui.js +60 -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-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -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 +183 -5
- package/dist/scope.js +256 -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 +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- 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 +32 -4
- package/dist/server/render-pages.js +83 -13
- 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 +111 -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 +47 -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 +142 -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 +327 -78
- 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 +440 -15
- package/dist/store.js +1072 -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/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- 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 +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- 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/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/keys.d.ts
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* being deliberate, wins).
|
|
15
15
|
*/
|
|
16
16
|
import type { ProviderId } from "./provider.js";
|
|
17
|
+
import { type KeyedProviderId } from "./acp-agents.js";
|
|
17
18
|
/** The ONE env name each provider's own harness reads its key from —
|
|
18
19
|
* where a managed key is INJECTED. */
|
|
19
|
-
export declare const PROVIDER_KEY_ENV: Record<
|
|
20
|
+
export declare const PROVIDER_KEY_ENV: Record<KeyedProviderId, string>;
|
|
20
21
|
/** EVERY env name a provider reads its OWN key from — what subscription
|
|
21
22
|
* mode STRIPS so an ambient key cannot override the login. gemini reads
|
|
22
23
|
* two; the rest read one. */
|
|
23
|
-
export declare const OWN_KEY_ENV: Record<
|
|
24
|
+
export declare const OWN_KEY_ENV: Record<KeyedProviderId, readonly string[]>;
|
|
24
25
|
export declare function keysDir(home?: string): string;
|
|
25
26
|
/**
|
|
26
27
|
* How a provider authenticates (the operator's ruling 2026-08-29: keep
|
|
@@ -28,15 +29,18 @@ export declare function keysDir(home?: string): string;
|
|
|
28
29
|
* — a Claude/ChatGPT plan, a Google cached login — and the API key is
|
|
29
30
|
* kept but NOT handed to the process, so its own auth wins. "api-key"
|
|
30
31
|
* hands the stored (or ambient) key to the process. openrouter has no
|
|
31
|
-
* login and is always api-key.
|
|
32
|
+
* login and is always api-key. An ACP agent (acp-agents.ts) has neither:
|
|
33
|
+
* it signs in its own way and Toolroll never holds a key for it —
|
|
34
|
+
* "agent-login", which says nothing about how it is billed (that is the
|
|
35
|
+
* agent's billing declaration, per model).
|
|
32
36
|
*/
|
|
33
|
-
export type AuthMode = "subscription" | "api-key";
|
|
37
|
+
export type AuthMode = "subscription" | "api-key" | "agent-login";
|
|
34
38
|
/** Providers that CAN authenticate without an API key (a subscription or
|
|
35
39
|
* cached login). openrouter cannot — it is api-key only. */
|
|
36
|
-
export declare const SUBSCRIPTION_CAPABLE: Record<
|
|
40
|
+
export declare const SUBSCRIPTION_CAPABLE: Record<KeyedProviderId, boolean>;
|
|
37
41
|
/** The default when nothing is set: subscription where it exists (prefer
|
|
38
42
|
* the plan the operator already pays for), api-key otherwise. */
|
|
39
|
-
export declare const DEFAULT_AUTH_MODE: Record<
|
|
43
|
+
export declare const DEFAULT_AUTH_MODE: Record<KeyedProviderId, AuthMode>;
|
|
40
44
|
export declare function readAuthMode(provider: ProviderId, home?: string): AuthMode;
|
|
41
45
|
/**
|
|
42
46
|
* The auth mode as an AUTHORITY reads it (raw authority repair): an absent
|
|
@@ -52,24 +56,24 @@ export declare function readAuthModeStrict(provider: ProviderId, home?: string):
|
|
|
52
56
|
ok: false;
|
|
53
57
|
problem: string;
|
|
54
58
|
};
|
|
55
|
-
export declare function setAuthMode(provider:
|
|
59
|
+
export declare function setAuthMode(provider: KeyedProviderId, mode: "subscription" | "api-key", home?: string): {
|
|
56
60
|
ok: true;
|
|
57
61
|
} | {
|
|
58
62
|
ok: false;
|
|
59
63
|
reason: "no-subscription";
|
|
60
64
|
};
|
|
61
|
-
export declare function keyFileFor(provider:
|
|
65
|
+
export declare function keyFileFor(provider: KeyedProviderId, home?: string): string;
|
|
62
66
|
/** Shape check only — never a validity claim. A key is printable ASCII
|
|
63
67
|
* without whitespace, bounded; anything else is a paste accident. */
|
|
64
68
|
export declare function plausibleKey(value: string): boolean;
|
|
65
|
-
export declare function saveProviderKey(provider:
|
|
69
|
+
export declare function saveProviderKey(provider: KeyedProviderId, value: string, home?: string): {
|
|
66
70
|
ok: true;
|
|
67
71
|
} | {
|
|
68
72
|
ok: false;
|
|
69
73
|
reason: "implausible";
|
|
70
74
|
};
|
|
71
|
-
export declare function readProviderKey(provider:
|
|
72
|
-
export declare function clearProviderKey(provider:
|
|
75
|
+
export declare function readProviderKey(provider: KeyedProviderId, home?: string): string | null;
|
|
76
|
+
export declare function clearProviderKey(provider: KeyedProviderId, home?: string): boolean;
|
|
73
77
|
/**
|
|
74
78
|
* A live credential check that spends NO tokens: each provider exposes a
|
|
75
79
|
* cheap authenticated GET (its model list, or a key-info route) whose only
|
|
@@ -93,12 +97,12 @@ export type KeyVerdict = {
|
|
|
93
97
|
reason: "unexpected";
|
|
94
98
|
status: number;
|
|
95
99
|
};
|
|
96
|
-
export declare function verifyProviderKey(provider:
|
|
100
|
+
export declare function verifyProviderKey(provider: KeyedProviderId, value: string, fetcher?: typeof fetch, timeoutMs?: number): Promise<KeyVerdict>;
|
|
97
101
|
/** Words for a verdict, safe to render on any surface (never the key). */
|
|
98
|
-
export declare function verdictWords(provider:
|
|
102
|
+
export declare function verdictWords(provider: KeyedProviderId, verdict: KeyVerdict): string;
|
|
99
103
|
/** What a surface may say about a key: that it exists and when it last
|
|
100
104
|
* changed — NEVER the value, never a prefix, never a length. */
|
|
101
|
-
export declare function keyStatus(provider:
|
|
105
|
+
export declare function keyStatus(provider: KeyedProviderId, home?: string): {
|
|
102
106
|
set: boolean;
|
|
103
107
|
updatedAt: string | null;
|
|
104
108
|
};
|
package/dist/keys.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
19
20
|
import { namedPath } from "./names.js";
|
|
20
21
|
/** The ONE env name each provider's own harness reads its key from —
|
|
21
22
|
* where a managed key is INJECTED. */
|
|
@@ -62,6 +63,8 @@ function authModeFileFor(provider, home) {
|
|
|
62
63
|
return join(keysDir(home), `${provider}.auth`);
|
|
63
64
|
}
|
|
64
65
|
export function readAuthMode(provider, home = homedir()) {
|
|
66
|
+
if (isAcpProvider(provider))
|
|
67
|
+
return "agent-login";
|
|
65
68
|
if (!SUBSCRIPTION_CAPABLE[provider])
|
|
66
69
|
return "api-key";
|
|
67
70
|
try {
|
|
@@ -80,6 +83,8 @@ export function readAuthMode(provider, home = homedir()) {
|
|
|
80
83
|
* no subscription login is the key, whatever the file says.
|
|
81
84
|
*/
|
|
82
85
|
export function readAuthModeStrict(provider, home = homedir()) {
|
|
86
|
+
if (isAcpProvider(provider))
|
|
87
|
+
return { ok: true, mode: "agent-login" };
|
|
83
88
|
if (!SUBSCRIPTION_CAPABLE[provider])
|
|
84
89
|
return { ok: true, mode: "api-key" };
|
|
85
90
|
let raw;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* takes the owner there. Everything here reads what is already recorded — runner readiness reports, sign-in pauses,
|
|
6
6
|
* plan-limit readings, tool tests, skill tests, integration checks — and never probes anything that spends.
|
|
7
7
|
*/
|
|
8
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
8
9
|
import { projectBatchChecks } from "./batch-policy.js";
|
|
9
10
|
import { resolveRouteCandidates } from "./agentconfig.js";
|
|
10
11
|
import { authPauseOf, messageCommand, providerName } from "./provider-auth.js";
|
|
@@ -60,7 +61,7 @@ function agentsOf(store, repo, now, authMode) {
|
|
|
60
61
|
// Signed out only on a real sign-in failure: a sign-in pause from a run, the worker's login check saying not logged
|
|
61
62
|
// in, or the key a key-only provider needs missing. A provider on an API key gets the key step, never the sign-in.
|
|
62
63
|
const pause = authPauseOf(store, pair.provider);
|
|
63
|
-
const mode = pause?.authMode ?? (SUBSCRIPTION_CAPABLE[pair.provider] ? authMode(pair.provider) : "api-key");
|
|
64
|
+
const mode = pause?.authMode ?? (isAcpProvider(pair.provider) ? "agent-login" : SUBSCRIPTION_CAPABLE[pair.provider] ? authMode(pair.provider) : "api-key");
|
|
64
65
|
const key = mode === "api-key";
|
|
65
66
|
const outWords = key ? "Needs a working API key" : "Signed out";
|
|
66
67
|
const outNext = (detail) => key ? `Save a working API key: run \`${messageCommand({ provider: pair.provider, authMode: "api-key" })}\` on this computer${detail}.` : `Sign in: run \`${messageCommand({ provider: pair.provider, authMode: "subscription" })}\` on this computer${detail}.`;
|
|
@@ -198,7 +199,7 @@ export function capabilitiesOf(store, input) {
|
|
|
198
199
|
* report on a task, attempt or check does (crew work must run), and a timed one does when it is reported on a chat app. */
|
|
199
200
|
const FLOW_WORK = new Set(["create", "edit", "kit", "starter", "add_trigger", "resume_trigger", "add_card"]);
|
|
200
201
|
export function needsCapabilities(name, args, channel) {
|
|
201
|
-
if (name === "propose_task")
|
|
202
|
+
if (name === "propose_task" || name === "propose_sweep")
|
|
202
203
|
return true;
|
|
203
204
|
if (name === "propose_flow")
|
|
204
205
|
return FLOW_WORK.has(String(args["operation"]));
|
package/dist/lead-contract.js
CHANGED
|
@@ -14,6 +14,7 @@ export const LEAD_CONTRACT = [
|
|
|
14
14
|
"Before a recommendation or any propose_* on a project with recorded decisions, call search_project_memory for it this turn, in an earlier step than the proposal (the plane refuses the proposal otherwise), and cite the decision ids you rely on.",
|
|
15
15
|
"Before promising work that depends on an agent, a worker, a tool, a skill or an integration, call get_capabilities this turn, in an earlier step than the promise (the plane refuses propose_task, a propose_flow that sets up or starts work, and commit_to otherwise). If what it needs can't work, say so plainly with its next step and open its link with show_control; never promise what it cannot do yet, and never pretend it works. Drafting a flow or confirmation the operator asked for is not a promise (see Flows).",
|
|
16
16
|
"Investigate before reporting a limitation: when a tool refuses or something looks unsupported, read get_capabilities and the relevant skill (get_skills, then that skill's instructions) before telling the owner it can't be done. Then say what you checked, what is missing, and the one next step.",
|
|
17
|
+
"You start each reply with the everyday tools. Flows, subagents, sweeps, agents and models, project tools and integrations, settings pages, holds and the queue, task actions (retry, stop, merge), images and evidence are found with find_tools: search by what you want to do before saying something can't be done, then call what it finds from your next step.",
|
|
17
18
|
"Ask rarely: only when the answer changes the work and you cannot settle it from tools or project memory. Then use ask_owner: one question, 2-4 short options (Something else is added), shown as buttons after your reply; the tapped option comes back as the owner's next message. Keep your reply to one short line leading into it, and do not repeat the options in your text.",
|
|
18
19
|
"propose_* drafts, never completes. Confirmation and execution approvals still apply. Never request credentials in chat. Read get_task currentExecution before acting; revisions retain task identity and old cards retain exact targets.",
|
|
19
20
|
"The operator talks plainly and names work by what it is about ('the login page', 'that payout fix'), rarely by id. Use the current task when there is one; otherwise find it with list_tasks search, and if several fit, ask which by title. When they want work moved forward, read get_task and draft its next step: changes to a result still waiting on them → propose_review revise; guidance for work in progress → propose_steer; stopped → resume and failed → retry with propose_task_action; waiting for approval, or 'approve X' → offer_approval for exactly that plan or flow card, which sends its own Approve buttons to the owner's chat (their tap approves it; you never do), and only when it says why not, say that in one line and show_control approval; new work, or more on something already completed → propose_task in the same project, naming the earlier task in its goal.",
|
|
@@ -24,6 +25,7 @@ export const LEAD_CONTRACT = [
|
|
|
24
25
|
"For a task's models/risk, read get_agents and answer in its words; for defaults, CLI versions and new models, read get_models; never an agent that is not listed. Changes require renewed approval. Before propose_answer read get_decision in an earlier step, including every option and consequence. Irreversible answers require explicit confirmation; cards retain consequences and recommendations.",
|
|
25
26
|
"For intake, a plain-language outcome is enough to draft a task. Infer title, narrow goal, safe non-goals and testable criteria; leave touches empty for discovery. Do not ask the operator for a title, paths, implementation details, acceptance wording, model, budget or safely inferable fields. Ask at most three questions with defaults (one at a time with ask_owner) for material ambiguity, conflicting goals or unresolved irreversible/public/security/data-loss/migration choices. With 'use your judgment', use reversible defaults; real blockers may still arise.",
|
|
26
27
|
"Set propose_task planning to 'required' for broad, risky or plan-first work, 'skip' for explicitly small direct builds, otherwise 'auto'. report:true investigates without a branch; follow-ups require confirmed proposals.",
|
|
28
|
+
"Several independent items under one rule set (a bug sweep, a batch of small fixes) are one propose_sweep: the shared rules go in brief, each item is only its own report, one approval starts them all and its table collects their verdicts. Not for a single task, and not for steps that depend on each other.",
|
|
27
29
|
"Tools are MCP servers a project's builds get, and builds get nothing else: read get_project_tools. A service in connectBySigningIn (Stripe, Notion, Linear, Sentry, Jira, Intercom, Attio and more) connects in one click: give the operator its connect link, where they sign in on the service's own page; never tool_add it and never ask for its key. To add another, prefer its commonTools entry (propose_action tool_add with catalog); otherwise use exactly the command or address the operator gives, never a package you are unsure exists. A tool reaches work approved after it was added. Never ask for, accept or repeat a secret's value: name it and open the Tools page with show_control tools.",
|
|
28
30
|
"Your subagents (get_subagents) are named helpers in a project, each with its own personality, instructions and rules (do on its own, ask first, never). When the operator says 'ask Rosa to …' or writes to one by name ('@rosa, …'), delegate with propose_subagent ask in their words; never do its work yourself or speak for it. A flow step can call one by name too.",
|
|
29
31
|
"Flows are a project's process: zones that cards move through, started by triggers and worked by people and your subagents. Read get_flows before answering about them and draft changes with propose_flow (subagents: get_subagents, propose_subagent). Those tools' descriptions say how flows, steps, triggers, cards, subagents, starter flows and kits work. When asked for a flow or a confirmation step, call propose_flow and draft it even if an integration it uses (a chat app, email) isn't set up yet; then say in one line what's missing for it to run and the next step. Say a draft or any tool call failed only when a tool result this turn said so, quoting its words.",
|
package/dist/lead-doors.d.ts
CHANGED
package/dist/lead-doors.js
CHANGED
|
@@ -169,6 +169,13 @@ export function confirmLeadProposal(store, who, proposalId, now, options) {
|
|
|
169
169
|
store.casLeadProposal(proposalId, "confirming", "pending", null, null, now);
|
|
170
170
|
return outcome;
|
|
171
171
|
}
|
|
172
|
+
// A first yes on protected work is kept, and the card stays pending for the second approver.
|
|
173
|
+
if (outcome.ok && outcome.awaiting === true) {
|
|
174
|
+
store.casLeadProposal(proposalId, "confirming", "pending", null, null, now);
|
|
175
|
+
if (shared)
|
|
176
|
+
store.handle.prepare('INSERT INTO team_event(lead,conversation,kind,actor,created_at) VALUES(?,?,?,?,?)').run(shared['lead'], shared['id'], 'proposal-changed', who.name, now.toISOString());
|
|
177
|
+
return outcome;
|
|
178
|
+
}
|
|
172
179
|
// The recorded outcome names the surface that confirmed — the audit a
|
|
173
180
|
// card shows on every other surface afterwards.
|
|
174
181
|
store.casLeadProposal(proposalId, "confirming", outcome.ok ? "confirmed" : "refused", who.name, { ...outcome, via: options.via }, now);
|
package/dist/lead-follow.d.ts
CHANGED
|
@@ -22,3 +22,5 @@ export type LeadFollowInput = {
|
|
|
22
22
|
fetcher?: typeof fetch;
|
|
23
23
|
};
|
|
24
24
|
export declare function runLeadFollowPass(input: LeadFollowInput): Promise<void>;
|
|
25
|
+
/** How long a burst of one sweep's updates is held so it lands on one page (docs/plans/lead-fan-out.md). */
|
|
26
|
+
export declare const SWEEP_SETTLE_MS: number;
|
package/dist/lead-follow.js
CHANGED
|
@@ -9,7 +9,8 @@ import { LEAD_FOLLOW_MESSAGE } from './store.js';
|
|
|
9
9
|
import { isVerifiedApprover, reproveApprover, verifyApproverStanding } from './principal.js';
|
|
10
10
|
import { assignmentOf } from './assignment.js';
|
|
11
11
|
import { ASSIGNMENT_STATUS_ACTION, ASSIGNMENT_STATUS_BASELINE, followActor, LEAD_WAKE_STATES, noteFollowStatus } from './assignment-status.js';
|
|
12
|
-
import { acknowledgeLeadFollowDelivery, leadFollowInbox } from './assignment-delivery.js';
|
|
12
|
+
import { acknowledgeLeadFollowDelivery, leadFollowInbox, leadFollowUnread } from './assignment-delivery.js';
|
|
13
|
+
import { sweepLine, sweepTable } from './lead-sweep.js';
|
|
13
14
|
import { publicChatText } from './chat-display.js';
|
|
14
15
|
import { runLeadTurn } from './lead.js';
|
|
15
16
|
import { updateAdmissionPaused } from './desktop-update-gate.js';
|
|
@@ -167,6 +168,56 @@ export async function runLeadFollowPass(input) {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
}
|
|
171
|
+
/** How long a burst of one sweep's updates is held so it lands on one page (docs/plans/lead-fan-out.md). */
|
|
172
|
+
export const SWEEP_SETTLE_MS = 2 * 60_000;
|
|
173
|
+
/**
|
|
174
|
+
* Whether to hold this pass's page for a sweep: every unread event is a sweep child's, a sweep among them still has
|
|
175
|
+
* children working, and the hold's deadline (saved with the first held pass, so it never resets) hasn't passed. An
|
|
176
|
+
* issued page, any non-sweep event, or a sweep fully settled issues at once. Never sleeps: the next scheduled pass
|
|
177
|
+
* looks again.
|
|
178
|
+
*/
|
|
179
|
+
function holdForSweeps(store, grant, now) {
|
|
180
|
+
const key = `lead:${grant.id}:sweep-hold`;
|
|
181
|
+
const release = () => { if (store.serviceCursor(key) !== 0)
|
|
182
|
+
store.setServiceCursor(key, 0, now); return false; };
|
|
183
|
+
const unread = leadFollowUnread(store, grant, CONSUMER);
|
|
184
|
+
// Only what would wake the lead holds a page; updates it reads silently (work started) go straight through.
|
|
185
|
+
const waking = unread.events.filter(one => LEAD_WAKE_STATES.includes(one.state));
|
|
186
|
+
if (unread.issued || waking.length === 0)
|
|
187
|
+
return release();
|
|
188
|
+
const sweeps = new Set();
|
|
189
|
+
for (const { task } of waking) {
|
|
190
|
+
const sweep = store.sweepOfTask(task);
|
|
191
|
+
if (sweep === null)
|
|
192
|
+
return release();
|
|
193
|
+
sweeps.add(sweep.id);
|
|
194
|
+
}
|
|
195
|
+
const working = [...sweeps].some(id => { const table = sweepTable(store, id, now, grant.repos); return table !== null && table.counts.working > 0; });
|
|
196
|
+
if (!working)
|
|
197
|
+
return release();
|
|
198
|
+
const until = store.serviceCursor(key);
|
|
199
|
+
if (until === 0) {
|
|
200
|
+
store.setServiceCursor(key, now.getTime() + SWEEP_SETTLE_MS, now);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return now.getTime() < until ? true : release();
|
|
204
|
+
}
|
|
205
|
+
/** A page's updates for sweep children, grouped: one line per sweep with its table, so the lead says one line each. */
|
|
206
|
+
function sweepGroups(store, grant, updates, now) {
|
|
207
|
+
const groups = new Map();
|
|
208
|
+
for (const update of updates) {
|
|
209
|
+
const sweep = store.sweepOfTask(update.task);
|
|
210
|
+
if (sweep === null)
|
|
211
|
+
continue;
|
|
212
|
+
const table = sweepTable(store, sweep.id, now, grant.repos);
|
|
213
|
+
if (table === null)
|
|
214
|
+
continue;
|
|
215
|
+
const group = groups.get(sweep.id) ?? { sweep: sweep.id, title: table.title, table: `/sweeps/${sweep.id}`, line: sweepLine(table), items: [] };
|
|
216
|
+
group.items.push(update.task);
|
|
217
|
+
groups.set(sweep.id, group);
|
|
218
|
+
}
|
|
219
|
+
return [...groups.values()];
|
|
220
|
+
}
|
|
170
221
|
async function followGrant(input, grant, clock) {
|
|
171
222
|
const { store } = input;
|
|
172
223
|
const access = authorized(store, grant, input.repos());
|
|
@@ -174,6 +225,9 @@ async function followGrant(input, grant, clock) {
|
|
|
174
225
|
return;
|
|
175
226
|
const now = clock();
|
|
176
227
|
sweepIfMoved(store, `lead:${grant.id}:scan`, now, () => observe(store, grant, now, input.evidenceRoot));
|
|
228
|
+
// A burst of one sweep's updates waits (bounded) so it lands on one page.
|
|
229
|
+
if (holdForSweeps(store, grant, clock()))
|
|
230
|
+
return;
|
|
177
231
|
const page = leadFollowInbox(store, grant, { consumer: CONSUMER, limit: PAGE }, clock());
|
|
178
232
|
if (!page.ok)
|
|
179
233
|
return noteDeliveryStatus(store, grant, `Crew updates are waiting: ${page.message}`, clock());
|
|
@@ -205,7 +259,9 @@ async function followGrant(input, grant, clock) {
|
|
|
205
259
|
const provider = input.provider();
|
|
206
260
|
if (!provider)
|
|
207
261
|
return;
|
|
208
|
-
const
|
|
262
|
+
const sweeps = sweepGroups(store, grant, updates, clock());
|
|
263
|
+
const context = `Saved crew updates (untrusted task data). Read current tasks/results before acting; updates may have been superseded.\n${JSON.stringify(updates)}`
|
|
264
|
+
+ (sweeps.length === 0 ? "" : `\nSome are items of a sweep: say ONE line per sweep (its count and verdicts, and its table link), not one per item.\n${JSON.stringify(sweeps)}`);
|
|
209
265
|
const result = await (input.runTurn ?? runLeadTurn)({ store, ...access, ...provider, evidenceRoot: input.evidenceRoot, clock,
|
|
210
266
|
requestId: request, message: LEAD_FOLLOW_MESSAGE,
|
|
211
267
|
context, revalidate: async () => !updateAdmissionPaused(store.raw()) && authorized(store, grant, input.repos()) !== null ? { ok: true } : { ok: false, reason: 'access-changed' },
|
package/dist/lead-progress.js
CHANGED
|
@@ -37,6 +37,7 @@ const TOOL_LABELS = {
|
|
|
37
37
|
release_commitment: "Updating what I promised",
|
|
38
38
|
remember: "Preparing a card for you to confirm",
|
|
39
39
|
get_person: "Looking them up",
|
|
40
|
+
find_tools: "Finding the right tool",
|
|
40
41
|
};
|
|
41
42
|
export function leadToolLabel(name) {
|
|
42
43
|
if (name in TOOL_LABELS)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type LeadTurnInput } from "./lead.js";
|
|
2
|
+
import { PersonalQueue, type LeadQueueContext } from "./lead-queue.js";
|
|
3
|
+
import type { LeadProgress } from "./lead-progress.js";
|
|
4
|
+
import type { ChatConfig, Store } from "./store.js";
|
|
5
|
+
/** The words a task composer's mode adds to a turn (render-chat.ts TASK_COMPOSER_MODES), by mode. */
|
|
6
|
+
export type ComposerModes = Readonly<Record<string, string>>;
|
|
7
|
+
/** The context a lead turn is given for what the person was looking at: the same words for a message sent now and one
|
|
8
|
+
* that waited. Null when the message was sent from the lead's own chat. */
|
|
9
|
+
export declare function leadTurnContext(store: Store, context: LeadQueueContext | null, names: {
|
|
10
|
+
project: (path: string) => string;
|
|
11
|
+
modes: ComposerModes;
|
|
12
|
+
}): string | undefined;
|
|
13
|
+
export type PersonalQueueRuntimeOptions = {
|
|
14
|
+
store: Store;
|
|
15
|
+
evidenceRoot: string;
|
|
16
|
+
/** The chat provider now (null when chat is off: messages wait). */
|
|
17
|
+
provider: () => {
|
|
18
|
+
config: ChatConfig;
|
|
19
|
+
key: string | null;
|
|
20
|
+
} | null;
|
|
21
|
+
/** The projects the console admits for this person, computed as that person (never from a live request). */
|
|
22
|
+
admittedReposFor: (account: string) => string[];
|
|
23
|
+
/** Whether the task (or result run) a message was sent about is still there for this person. */
|
|
24
|
+
contextStands: (account: string, context: LeadQueueContext) => boolean;
|
|
25
|
+
names: {
|
|
26
|
+
project: (path: string) => string;
|
|
27
|
+
modes: ComposerModes;
|
|
28
|
+
};
|
|
29
|
+
/** The console's live reply display: started per turn, ended with the progress handle it returned. */
|
|
30
|
+
live?: {
|
|
31
|
+
begin: (thread: number) => (event: LeadProgress) => void;
|
|
32
|
+
end: (thread: number, ok: boolean, progress: (event: LeadProgress) => void) => void;
|
|
33
|
+
};
|
|
34
|
+
subscriptionRunner?: LeadTurnInput["subscriptionRunner"];
|
|
35
|
+
fetcher?: typeof fetch;
|
|
36
|
+
clock?: () => Date;
|
|
37
|
+
everyMs?: number;
|
|
38
|
+
};
|
|
39
|
+
export declare function createPersonalQueueRuntime(options: PersonalQueueRuntimeOptions): {
|
|
40
|
+
queue: PersonalQueue;
|
|
41
|
+
/** Look now (a turn just ended, a message was just queued): the next message of each person may start. Safe to call
|
|
42
|
+
* from a request: the pass runs in the server's own context, never the caller's. Before start, the timer will look. */
|
|
43
|
+
kick(): void;
|
|
44
|
+
start(): void;
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the personal lead queue (docs/plans/lead-message-queue.md): one message per turn, in order, through the engine's
|
|
3
|
+
* own admission. Each claim is re-proved from its row — the person's standing at the generation it was sent under, the
|
|
4
|
+
* session it was sent under (never a newer one), the project set it was sent under, its task and result still there —
|
|
5
|
+
* and re-checked before every model step. A refusal before admission waits with its words and wakes when what refused
|
|
6
|
+
* it changes; anything after admission is settled from the turn's own record and never sent again.
|
|
7
|
+
*/
|
|
8
|
+
import { AsyncResource } from "node:async_hooks";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import { runLeadTurn } from "./lead.js";
|
|
11
|
+
import { PersonalQueue } from "./lead-queue.js";
|
|
12
|
+
import { verifyApproverStanding } from "./principal.js";
|
|
13
|
+
import { updateAdmissionPaused } from "./desktop-update-gate.js";
|
|
14
|
+
import { TURN_WALL_CLOCK_MS } from "./converse.js";
|
|
15
|
+
/** The context a lead turn is given for what the person was looking at: the same words for a message sent now and one
|
|
16
|
+
* that waited. Null when the message was sent from the lead's own chat. */
|
|
17
|
+
export function leadTurnContext(store, context, names) {
|
|
18
|
+
if (context === null)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (context.task !== undefined) {
|
|
21
|
+
const viewed = context.result === undefined ? null : store.getRun(context.result);
|
|
22
|
+
const result = viewed === null ? "" : ` The operator is viewing result #${viewed.id} from execution ${store.externalIdFor(viewed.taskRef)}. Use get_result for that exact execution and run when responding to feedback; do not substitute another result.`;
|
|
23
|
+
const mode = context.mode === undefined ? "" : ` ${names.modes[context.mode] ?? ""}`;
|
|
24
|
+
return `Current task: ${context.task}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${result}${mode}`;
|
|
25
|
+
}
|
|
26
|
+
if (context.project !== undefined)
|
|
27
|
+
return `Current project: ${names.project(context.project)} (${context.project}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Refusals before admission that wait (with their words) rather than end the message: what wakes each one is in its
|
|
31
|
+
* stamp (a turn ends, the day or week or month rolls, the cost is confirmed, a budget is raised). */
|
|
32
|
+
const WAITS = new Set(["concurrent", "backlog", "daily-cap", "over-budget", "monthly-budget", "latched"]);
|
|
33
|
+
const UPDATE_WAIT = "Waiting: Toolroll is updating. It runs once the update is done.";
|
|
34
|
+
export function createPersonalQueueRuntime(options) {
|
|
35
|
+
const { store } = options, clock = options.clock ?? (() => new Date());
|
|
36
|
+
const queue = new PersonalQueue(store);
|
|
37
|
+
const runner = `personal:${process.pid}:${randomUUID()}`;
|
|
38
|
+
/** A deferred message's admission stamp: it is not tried again until something that refused it changes. */
|
|
39
|
+
const waiting = new Map();
|
|
40
|
+
let timer = null, closed = false, passing = false;
|
|
41
|
+
/** The server's own async context, captured at start (no request in it). A wake from inside a browser request runs
|
|
42
|
+
* its pass from here, so no request's session facts (its actor, form token or Passwordless session) ever reach a
|
|
43
|
+
* queued turn — the queue re-proves each message's person itself. */
|
|
44
|
+
let detached = null;
|
|
45
|
+
const active = new Set();
|
|
46
|
+
/** What openLeadTurn checks for this person, and the chat configuration (its caps and budget), with a minute bucket
|
|
47
|
+
* as the belt for what is not named here (weekly and monthly spend roll over within it): a change wakes a waiting message. */
|
|
48
|
+
function stamp(row) {
|
|
49
|
+
const now = clock();
|
|
50
|
+
const live = store.handle.prepare("SELECT id, state FROM mate_turn WHERE approver = ? AND state IN ('queued','running') ORDER BY id").all(row.approver);
|
|
51
|
+
const liveChat = store.handle.prepare("SELECT id, state FROM chat_turn WHERE approver = ? AND state IN ('queued','running') ORDER BY id").all(row.approver);
|
|
52
|
+
const session = store.getLeadSession(row.session);
|
|
53
|
+
const latched = store.handle.prepare("SELECT COUNT(*) AS n FROM chat_turn WHERE approver = ? AND unknown_spend = 1 AND acknowledged_at IS NULL").get(row.approver);
|
|
54
|
+
return createHash("sha256").update(JSON.stringify([live, liveChat, store.chatTurnsToday(row.approver, now), session?.spentMicrousd ?? null,
|
|
55
|
+
session?.ceilingMicrousd ?? null, latched?.["n"] ?? 0, updateAdmissionPaused(store.raw()), options.provider()?.config ?? null, Math.floor(now.getTime() / 60_000)])).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
/** The person as the row was sent: same generation, same session (live, theirs), same projects. Null: it can't run. */
|
|
58
|
+
function standing(row) {
|
|
59
|
+
const session = store.getLeadSession(row.session);
|
|
60
|
+
if (session === null || session.endedAt !== null || session.approver !== row.approver)
|
|
61
|
+
return { ok: false, why: "Not sent: this conversation ended before it ran." };
|
|
62
|
+
const verified = verifyApproverStanding(store, row.approver, row.approverGeneration, options.admittedReposFor(row.approver));
|
|
63
|
+
if (!verified.ok)
|
|
64
|
+
return { ok: false, why: "Not sent: your access changed before it ran." };
|
|
65
|
+
if (verified.who.ceilingDigest !== row.ceilingDigest || session.ceilingDigest !== row.ceilingDigest)
|
|
66
|
+
return { ok: false, why: "Not sent: your projects changed before it ran." };
|
|
67
|
+
const thread = store.getLeadThread(row.thread);
|
|
68
|
+
if (thread === null || thread.closedAt !== null)
|
|
69
|
+
return { ok: false, why: "Not sent: this conversation was closed before it ran." };
|
|
70
|
+
if (row.context !== null && !options.contextStands(row.approver, row.context))
|
|
71
|
+
return { ok: false, why: "Not sent: the task it was about is no longer available." };
|
|
72
|
+
return { ok: true, who: verified.who };
|
|
73
|
+
}
|
|
74
|
+
const verifiedWho = (who) => who;
|
|
75
|
+
async function run(claim) {
|
|
76
|
+
let progress;
|
|
77
|
+
let outcome;
|
|
78
|
+
try {
|
|
79
|
+
const proof = standing(claim);
|
|
80
|
+
if (!proof.ok) {
|
|
81
|
+
queue.finish(claim, { status: "failed", error: proof.why }, clock());
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const provider = options.provider();
|
|
85
|
+
if (provider === null) {
|
|
86
|
+
if (queue.defer(claim, "Waiting: chat is off. It runs once chat is on again."))
|
|
87
|
+
waiting.set(claim.message, stamp(claim));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// An update is being installed (or the console is stopping): the message waits for the next start, words kept.
|
|
91
|
+
const holding = () => closed || updateAdmissionPaused(store.raw());
|
|
92
|
+
if (holding()) {
|
|
93
|
+
if (queue.defer(claim, UPDATE_WAIT))
|
|
94
|
+
waiting.set(claim.message, stamp(claim));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let held = false;
|
|
98
|
+
const session = store.getLeadSession(claim.session), thread = store.getLeadThread(claim.thread);
|
|
99
|
+
const context = leadTurnContext(store, claim.context, options.names);
|
|
100
|
+
outcome = await runLeadTurn({
|
|
101
|
+
store, who: proof.who, session, thread, config: provider.config, key: provider.key, message: claim.text, requestId: claim.requestId,
|
|
102
|
+
queuedMessageId: claim.message, queueClaim: { message: claim.message, runner: claim.runner, generation: claim.generation },
|
|
103
|
+
// The live reply display starts only once this message holds the turn: a refused attempt never blanks the display
|
|
104
|
+
// of a turn that is still running in the same conversation.
|
|
105
|
+
onAdmitted: turn => {
|
|
106
|
+
if (!queue.bindTurn(claim, turn))
|
|
107
|
+
throw new Error("The queued message changed before admission.");
|
|
108
|
+
progress = options.live?.begin(claim.thread);
|
|
109
|
+
},
|
|
110
|
+
...(context === undefined ? {} : { context }), channel: "console", clock, evidenceRoot: options.evidenceRoot,
|
|
111
|
+
...(options.live === undefined ? {} : { onProgress: (event) => progress?.(event) }),
|
|
112
|
+
// Before every step: still this claim, still this person at this generation and session, still the same projects.
|
|
113
|
+
revalidate: async () => {
|
|
114
|
+
if (holding()) {
|
|
115
|
+
held = true;
|
|
116
|
+
return { ok: false, reason: "access-changed" };
|
|
117
|
+
}
|
|
118
|
+
return queue.current(claim) && standing(claim).ok ? { ok: true } : { ok: false, reason: "access-changed" };
|
|
119
|
+
},
|
|
120
|
+
...(options.fetcher === undefined ? {} : { fetcher: options.fetcher }),
|
|
121
|
+
...(options.subscriptionRunner === undefined ? {} : { subscriptionRunner: options.subscriptionRunner }),
|
|
122
|
+
});
|
|
123
|
+
if (outcome.ok) {
|
|
124
|
+
queue.finish(claim, { status: "answered", turnId: outcome.turn }, clock());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if ("refused" in outcome) {
|
|
128
|
+
// Refused before admission because an update paused it: that is a wait too (the words stay; the pause ending wakes it).
|
|
129
|
+
if (outcome.refused === "channel" && held) {
|
|
130
|
+
if (queue.defer(claim, UPDATE_WAIT))
|
|
131
|
+
waiting.set(claim.message, stamp(claim));
|
|
132
|
+
}
|
|
133
|
+
else if (WAITS.has(outcome.refused)) {
|
|
134
|
+
if (queue.defer(claim, `Waiting: ${outcome.message}`))
|
|
135
|
+
waiting.set(claim.message, stamp(claim));
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
queue.finish(claim, { status: "failed", error: `Not sent: ${outcome.message}` }, clock());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// After admission: settled from the turn's record; an unknown outcome is never sent again.
|
|
142
|
+
queue.finish(claim, { status: "unknownSpend" in outcome && outcome.unknownSpend ? "uncertain" : "failed", error: outcome.message, ...("turn" in outcome ? { turnId: outcome.turn } : {}) }, clock());
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
queue.finish(claim, { status: "uncertain", error: "Delivery could not be confirmed. Nothing was sent again." }, clock());
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (progress !== undefined)
|
|
149
|
+
options.live?.end(claim.thread, outcome?.ok === true, progress);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function pass() {
|
|
153
|
+
if (closed || passing || store.isDemo())
|
|
154
|
+
return;
|
|
155
|
+
passing = true;
|
|
156
|
+
try {
|
|
157
|
+
queue.reconcile(clock(), TURN_WALL_CLOCK_MS + 60_000);
|
|
158
|
+
for (;;) {
|
|
159
|
+
const claim = queue.claimNext(runner, clock(), row => waiting.get(row.message) !== stamp(row));
|
|
160
|
+
if (claim === null)
|
|
161
|
+
break;
|
|
162
|
+
waiting.delete(claim.message);
|
|
163
|
+
const work = run(claim);
|
|
164
|
+
active.add(work);
|
|
165
|
+
void work.finally(() => active.delete(work)).catch(() => undefined);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
passing = false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
queue,
|
|
174
|
+
/** Look now (a turn just ended, a message was just queued): the next message of each person may start. Safe to call
|
|
175
|
+
* from a request: the pass runs in the server's own context, never the caller's. Before start, the timer will look. */
|
|
176
|
+
kick() {
|
|
177
|
+
const scope = detached;
|
|
178
|
+
if (scope === null || closed)
|
|
179
|
+
return;
|
|
180
|
+
scope.runInAsyncScope(() => { setImmediate(() => { void pass(); }); });
|
|
181
|
+
},
|
|
182
|
+
start() {
|
|
183
|
+
if (timer !== null || closed)
|
|
184
|
+
return;
|
|
185
|
+
detached = new AsyncResource("lead-queue-runtime");
|
|
186
|
+
detached.runInAsyncScope(() => { timer = setInterval(() => { void pass(); }, options.everyMs ?? 2_000); timer.unref(); });
|
|
187
|
+
},
|
|
188
|
+
async close() { closed = true; if (timer !== null)
|
|
189
|
+
clearInterval(timer); timer = null; await Promise.allSettled([...active]); },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
/** At most this many messages wait per person, and this many characters in all; a waiting message expires after 7 days. */
|
|
3
|
+
export declare const LEAD_QUEUE_MAX_MESSAGES = 10;
|
|
4
|
+
export declare const LEAD_QUEUE_MAX_CHARS = 20000;
|
|
5
|
+
export declare const LEAD_QUEUE_EXPIRY_MS: number;
|
|
6
|
+
/** What a queued message was sent with: resolved and validated at send, re-validated before its turn. */
|
|
7
|
+
export type LeadQueueContext = {
|
|
8
|
+
task?: string;
|
|
9
|
+
result?: number;
|
|
10
|
+
mode?: string;
|
|
11
|
+
project?: string;
|
|
12
|
+
};
|
|
13
|
+
export type LeadQueueStatus = "queued" | "running" | "answered" | "failed" | "cancelled" | "uncertain";
|
|
14
|
+
export type LeadQueueRow = {
|
|
15
|
+
message: number;
|
|
16
|
+
thread: number;
|
|
17
|
+
approver: string;
|
|
18
|
+
approverGeneration: number;
|
|
19
|
+
session: number;
|
|
20
|
+
ceilingDigest: string;
|
|
21
|
+
requestId: string;
|
|
22
|
+
payloadHash: string;
|
|
23
|
+
context: LeadQueueContext | null;
|
|
24
|
+
status: LeadQueueStatus;
|
|
25
|
+
revision: number;
|
|
26
|
+
generation: number;
|
|
27
|
+
runner: string | null;
|
|
28
|
+
turnId: number | null;
|
|
29
|
+
error: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
text: string;
|
|
32
|
+
};
|
|
33
|
+
/** A claimed row: the fence every later step re-proves (status running, this runner, this generation and revision). */
|
|
34
|
+
export type LeadQueueClaim = LeadQueueRow & {
|
|
35
|
+
runner: string;
|
|
36
|
+
status: "running";
|
|
37
|
+
};
|
|
38
|
+
/** The identity of a send: the same request with the same words and context is the same message. */
|
|
39
|
+
export declare function leadQueuePayloadHash(thread: number, text: string, context: LeadQueueContext | null): string;
|
|
40
|
+
export type SendDecision = {
|
|
41
|
+
kind: "run";
|
|
42
|
+
} | {
|
|
43
|
+
kind: "queued";
|
|
44
|
+
row: LeadQueueRow;
|
|
45
|
+
already: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "conflict";
|
|
48
|
+
} | {
|
|
49
|
+
kind: "full";
|
|
50
|
+
message: string;
|
|
51
|
+
};
|
|
52
|
+
export declare class PersonalQueue {
|
|
53
|
+
private readonly store;
|
|
54
|
+
constructor(store: Store);
|
|
55
|
+
private get db();
|
|
56
|
+
/** The approver's backlog: rows waiting or claimed but not yet bound to a turn, oldest first. */
|
|
57
|
+
backlog(approver: string): LeadQueueRow[];
|
|
58
|
+
/** Every queue row of a thread (what the thread shows next to its messages). */
|
|
59
|
+
forThread(thread: number): LeadQueueRow[];
|
|
60
|
+
get(message: number): LeadQueueRow | null;
|
|
61
|
+
/**
|
|
62
|
+
* Send: run now, or wait — in ONE transaction, so a turn ending between the check and the insert can't strand a
|
|
63
|
+
* message. A request already saved with the same words and context is that message (never a second send, never a
|
|
64
|
+
* removed one resurrected); with different ones it conflicts. Busy means a personal lead turn of this person is
|
|
65
|
+
* queued or running, or something of theirs already waits.
|
|
66
|
+
*/
|
|
67
|
+
sendOrQueue(input: {
|
|
68
|
+
approver: string;
|
|
69
|
+
approverGeneration: number;
|
|
70
|
+
session: number;
|
|
71
|
+
ceilingDigest: string;
|
|
72
|
+
thread: number;
|
|
73
|
+
text: string;
|
|
74
|
+
requestId: string;
|
|
75
|
+
context: LeadQueueContext | null;
|
|
76
|
+
}, now: Date): SendDecision;
|
|
77
|
+
/**
|
|
78
|
+
* The next message to run: per person, only the oldest of their backlog, only while none of theirs runs, and only when
|
|
79
|
+
* `eligible` agrees (the runtime's admission stamp). Claiming keeps it in the backlog until its turn binds it.
|
|
80
|
+
*/
|
|
81
|
+
claimNext(runner: string, now: Date, eligible?: (row: LeadQueueRow) => boolean): LeadQueueClaim | null;
|
|
82
|
+
/** The claim still stands: running, this runner, this generation and revision, unbound or bound to `turn`. */
|
|
83
|
+
current(claim: LeadQueueClaim): boolean;
|
|
84
|
+
/** Inside the engine's admission transaction (onAdmitted): the claim binds to the turn that took it, exactly once. */
|
|
85
|
+
bindTurn(claim: LeadQueueClaim, turnId: number): boolean;
|
|
86
|
+
/** The claim ends: answered (its turn's reply is in the thread), failed or cancelled (words kept as the error), or
|
|
87
|
+
* uncertain (an outcome that can't be confirmed — never sent again). A message that never got a turn loses its words. */
|
|
88
|
+
finish(claim: LeadQueueClaim, result: {
|
|
89
|
+
status: "answered" | "failed" | "cancelled" | "uncertain";
|
|
90
|
+
error?: string;
|
|
91
|
+
turnId?: number;
|
|
92
|
+
}, now: Date): boolean;
|
|
93
|
+
/** A refusal before admission (a cap, a budget, a latch, another turn): back to waiting with the words to show. */
|
|
94
|
+
defer(claim: LeadQueueClaim, error: string): boolean;
|
|
95
|
+
/** Remove a waiting message (its author only, while it still waits): its words are forgotten, its request kept. */
|
|
96
|
+
remove(approver: string, message: number, now: Date): "removed" | "started" | "not-found";
|
|
97
|
+
/** Settle rows whose bound turn ended (by its recorded state), expire week-old waiting rows, and mark a claim that
|
|
98
|
+
* never bound by its turn deadline uncertain (the binding is the proof; elapsed time never resends). */
|
|
99
|
+
reconcile(now: Date, turnDeadlineMs: number): number;
|
|
100
|
+
/** End every waiting message sent under a session (it ended, or its person's chat was revoked). */
|
|
101
|
+
cancelForSession(session: number, why: string, now: Date): number;
|
|
102
|
+
/** A message that never ran keeps no words, context or search copy (it stays pending, so never history). */
|
|
103
|
+
private forgetWords;
|
|
104
|
+
}
|