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/store.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
2
|
+
import { PROJECT_NETWORK_DEFAULT, projectNetworkFromValue, runNetworkFromValue } from "./egress.js";
|
|
3
|
+
import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
|
|
4
|
+
import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
|
|
1
5
|
import { reclaimDatabase } from "./database-reclaim.js";
|
|
2
6
|
import { BASELINE_SCHEMA, BASELINE_SCHEMA_VERSION } from "./store-baseline.js";
|
|
3
7
|
import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
|
|
@@ -77,6 +81,7 @@ import { parseCheckProgressSnapshot } from "./check-progress.js";
|
|
|
77
81
|
import { accountReachesSql, normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
|
|
78
82
|
import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installLedgerTriggers } from "./action-ledger.js";
|
|
79
83
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
84
|
+
import { CONSOLE_INSTANCE_SCHEMA } from "./console-instance.js";
|
|
80
85
|
import { endUnattendedGrants, FLOW_UNATTENDED_SCHEMA, grantStandsSql } from "./flow-unattended.js";
|
|
81
86
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
82
87
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
@@ -273,6 +278,29 @@ export function leadTurnKeepsProposals(turn) {
|
|
|
273
278
|
/** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
|
|
274
279
|
* predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
|
|
275
280
|
* next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
|
|
281
|
+
/** Lead fan-out (docs/plans/lead-fan-out.md): a sweep is N ordinary tasks under one shared brief, started with one yes,
|
|
282
|
+
* at most `max_parallel` of them claimed at once. Membership is by task family root, so a revision of a child is that
|
|
283
|
+
* child. Additive (no schema version bump), like lead_commitment. */
|
|
284
|
+
export const LEAD_SWEEP_SCHEMA = `
|
|
285
|
+
CREATE TABLE IF NOT EXISTS lead_sweep (
|
|
286
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
287
|
+
owner TEXT NOT NULL,
|
|
288
|
+
thread INTEGER,
|
|
289
|
+
repo TEXT NOT NULL,
|
|
290
|
+
title TEXT NOT NULL,
|
|
291
|
+
brief TEXT NOT NULL,
|
|
292
|
+
max_parallel INTEGER NOT NULL CHECK (max_parallel BETWEEN 1 AND 16),
|
|
293
|
+
proposal INTEGER UNIQUE,
|
|
294
|
+
created_at TEXT NOT NULL
|
|
295
|
+
);
|
|
296
|
+
CREATE TABLE IF NOT EXISTS lead_sweep_task (
|
|
297
|
+
sweep INTEGER NOT NULL REFERENCES lead_sweep(id),
|
|
298
|
+
root_task TEXT NOT NULL UNIQUE,
|
|
299
|
+
ordinal INTEGER NOT NULL,
|
|
300
|
+
label TEXT NOT NULL,
|
|
301
|
+
PRIMARY KEY (sweep, ordinal)
|
|
302
|
+
);
|
|
303
|
+
`;
|
|
276
304
|
export const LEAD_COMMITMENT_SCHEMA = `
|
|
277
305
|
CREATE TABLE IF NOT EXISTS lead_commitment (
|
|
278
306
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -409,6 +437,25 @@ CREATE TABLE IF NOT EXISTS run_resume (
|
|
|
409
437
|
);
|
|
410
438
|
CREATE INDEX IF NOT EXISTS run_resume_by_run ON run_resume (run, id);
|
|
411
439
|
`;
|
|
440
|
+
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
441
|
+
export const LIVE_DETAIL_SCHEMA = `
|
|
442
|
+
CREATE TABLE IF NOT EXISTS live_detail_setting (
|
|
443
|
+
repo TEXT PRIMARY KEY,
|
|
444
|
+
commands INTEGER NOT NULL CHECK (commands IN (0,1)),
|
|
445
|
+
updated_by TEXT NOT NULL,
|
|
446
|
+
updated_at TEXT NOT NULL
|
|
447
|
+
);
|
|
448
|
+
-- Every run a steering note reached, kept as it happens (live-notes.ts): a
|
|
449
|
+
-- note's own attached/mailed fields move to the next attempt, this does not.
|
|
450
|
+
CREATE TABLE IF NOT EXISTS steer_note_run (
|
|
451
|
+
note INTEGER NOT NULL REFERENCES task_steer(id) ON DELETE CASCADE,
|
|
452
|
+
run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,
|
|
453
|
+
how TEXT NOT NULL CHECK (how IN ('attached','mailed','read')),
|
|
454
|
+
at TEXT NOT NULL,
|
|
455
|
+
PRIMARY KEY (note, run, how)
|
|
456
|
+
);
|
|
457
|
+
CREATE INDEX IF NOT EXISTS steer_note_run_by_run ON steer_note_run (run, note);
|
|
458
|
+
`;
|
|
412
459
|
export const CHAT_APPROVAL_SCHEMA = `
|
|
413
460
|
CREATE TABLE IF NOT EXISTS chat_approval_setting (
|
|
414
461
|
approver TEXT NOT NULL,
|
|
@@ -645,7 +692,33 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
645
692
|
// v123 adds, and changes no saved row: flows that run on their own (flow_script_provenance, flow_unattended_grant,
|
|
646
693
|
// flow_unattended_run, plan_authorization.basis, flow_trigger_event.on_time and ran_script) and the tool-secret
|
|
647
694
|
// sealing key's record (tool_secret_key, tool_secret_legacy).
|
|
648
|
-
|
|
695
|
+
// v124 adds, and changes no saved row: the live conversation's project setting (live_detail_setting: "Show commands in
|
|
696
|
+
// the live view", absent means off) and the runs each steering note reached (steer_note_run; older notes have none).
|
|
697
|
+
// v125 changes no saved row: ACP agents become providers (the provider CHECKs of phase_config, phase_tier_config,
|
|
698
|
+
// phase_tier_alternate and provider_readiness widen, and provider_auth_pause's auth mode admits 'agent-login'; each
|
|
699
|
+
// table is rebuilt with every row and id carried over) and a run keeps what its agent itself reported it used
|
|
700
|
+
// (run.agent_reported_usage_json, never priced; saved runs read none).
|
|
701
|
+
// v126 adds, and changes no saved row: signed webhooks into tasks (docs/plans/webhook-tasks.md): a receipt's typed state,
|
|
702
|
+
// when its card was made and the item it holds while it waits for room (flow_trigger_event.state, accepted_at,
|
|
703
|
+
// item_json); which saved secret a trigger's deliveries are proved with and its turned-away deliveries (flow_trigger.hook_credential,
|
|
704
|
+
// dropped_count, dropped_since, limit_noted_at); who made a card (flow_card.created_kind); and the outside source a
|
|
705
|
+
// task was filed from (task_source). Saved receipts read their state from their card, saved cards read a person.
|
|
706
|
+
// v127 adds, and changes no saved row: the running console's instance record (console_instance, one row: its secret,
|
|
707
|
+
// pid, port and build) that bare `toolroll` proves a console with before attaching (docs/plans/agents-cockpit.md B1).
|
|
708
|
+
// v128 adds, and changes no saved row's existing columns: the personal lead queue (lead_queue; docs/plans/
|
|
709
|
+
// lead-message-queue.md) and whether a lead message is still waiting for its turn (lead_message.pending, indexed with
|
|
710
|
+
// its thread so the model's history reads only admitted messages). A saved message reads waiting exactly when it is an
|
|
711
|
+
// operator message of a team queue row never bound to a turn (V128_ADDED_COLUMNS).
|
|
712
|
+
// v129 adds, and changes no saved row: how a run went (docs/plans/run-efficiency.md) — the agent's turn count where its
|
|
713
|
+
// provider reports it (run.agent_turns), a lead request's cache read and write tokens (chat_turn.cache_read_tokens,
|
|
714
|
+
// cache_write_tokens), each NULL (unknown) on saved rows, and the derived, display-only run_efficiency row a result's
|
|
715
|
+
// How it ran reads (a cache of the run's own records, written on first view, never a verdict).
|
|
716
|
+
// v130 adds, and changes no saved row: what a project's builds may reach on the network (project_network; egress.ts,
|
|
717
|
+
// docs/plans/agent-baseline-hardening.md arc 1) and whether this installation lets an agent run with no OS confinement
|
|
718
|
+
// (confinement_setting, one row; off unless an instance operator turns it on).
|
|
719
|
+
// v131 adds, and changes no saved row: where a project's builds may write (project_writes; agent-fence.ts, docs/plans/
|
|
720
|
+
// agent-baseline-hardening.md arc 2) — only their workspace unless an approver chose anywhere.
|
|
721
|
+
export const SCHEMA_VERSION = 131;
|
|
649
722
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
650
723
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
651
724
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -773,9 +846,12 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
773
846
|
* new tables and the columns in V121_ADDED_COLUMNS (null, or no sends), checked the same way. v122 only adds a new
|
|
774
847
|
* table (branch_base_move) and the nullable column in V122_ADDED_COLUMNS (a project's recorded remote), checked the
|
|
775
848
|
* same way. v123 only adds new tables (unattended flows, tool secrets) and the columns in V123_ADDED_COLUMNS ('mode',
|
|
776
|
-
* 0 or null), checked the same way.
|
|
849
|
+
* 0 or null), checked the same way. v124 only adds new tables (the live conversation's setting and note history).
|
|
850
|
+
* v125 rebuilds five tables with wider CHECKs, every row and id carried over (the rehearsal's history rules find them
|
|
851
|
+
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
852
|
+
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
777
853
|
*/
|
|
778
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123]);
|
|
854
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131]);
|
|
779
855
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
780
856
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
781
857
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -836,6 +912,140 @@ export const V123_ADDED_COLUMNS = Object.freeze({
|
|
|
836
912
|
ran_script: { definition: "TEXT", saved: "NULL" },
|
|
837
913
|
},
|
|
838
914
|
});
|
|
915
|
+
/** v125: the column it adds, with the value every row saved before it reads: what a run's ACP agent itself reported it
|
|
916
|
+
* used (acp-client.ts; never priced). Runs saved before it read none. */
|
|
917
|
+
export const V125_ADDED_COLUMNS = Object.freeze({
|
|
918
|
+
run: {
|
|
919
|
+
agent_reported_usage_json: { definition: "TEXT", saved: "NULL" },
|
|
920
|
+
// An ACP run's billing, stamped when it is admitted (AcpBillingStamp); every other run, and every saved one, none.
|
|
921
|
+
acp_billing_json: { definition: "TEXT", saved: "NULL" },
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
/** v126: the columns it adds, with the value every row saved before it reads (docs/plans/webhook-tasks.md). A saved
|
|
925
|
+
* receipt reads no state (accepted when it has a card, skipped otherwise), no acceptance time (its arrival counts) and
|
|
926
|
+
* no held item; a saved trigger is on its first credential generation with nothing turned away; a saved card was made
|
|
927
|
+
* by a person. */
|
|
928
|
+
export const V126_ADDED_COLUMNS = Object.freeze({
|
|
929
|
+
flow_trigger_event: {
|
|
930
|
+
state: { definition: "TEXT CHECK (state IN ('accepted','skipped','held','ignored'))", saved: "NULL" },
|
|
931
|
+
accepted_at: { definition: "TEXT", saved: "NULL" },
|
|
932
|
+
item_json: { definition: "TEXT", saved: "NULL" },
|
|
933
|
+
},
|
|
934
|
+
flow_trigger: {
|
|
935
|
+
// Which saved secret proves its deliveries: 'legacy' (the address every secret saved before v126 has), '' (none:
|
|
936
|
+
// deliveries are refused until one is saved), or the id of one written whole before the trigger pointed at it.
|
|
937
|
+
hook_credential: { definition: "TEXT NOT NULL DEFAULT 'legacy'", saved: "'legacy'" },
|
|
938
|
+
dropped_count: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" },
|
|
939
|
+
dropped_since: { definition: "TEXT", saved: "NULL" },
|
|
940
|
+
limit_noted_at: { definition: "TEXT", saved: "NULL" },
|
|
941
|
+
},
|
|
942
|
+
flow_card: {
|
|
943
|
+
created_kind: { definition: "TEXT CHECK (created_kind IN ('person','automation'))", saved: "NULL" },
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
/** v128: whether a lead message waits for its turn. A message saved before v128 waits exactly when it is an operator
|
|
947
|
+
* message that a team queue row holds and no turn has taken (the team runtime's own unadmitted messages); every other
|
|
948
|
+
* saved message is history (`saved` is evaluated per row). */
|
|
949
|
+
export const V128_ADDED_COLUMNS = Object.freeze({
|
|
950
|
+
lead_message: {
|
|
951
|
+
pending: {
|
|
952
|
+
definition: "INTEGER NOT NULL DEFAULT 0 CHECK (pending IN (0, 1))",
|
|
953
|
+
saved: "(CASE WHEN role = 'operator' AND turn IS NULL AND EXISTS (SELECT 1 FROM team_message q WHERE q.message = lead_message.id) THEN 1 ELSE 0 END)",
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
/** v129: how a run went (docs/plans/run-efficiency.md). Recorded from now on; a saved row reads unknown (NULL), never 0. */
|
|
958
|
+
export const V129_ADDED_COLUMNS = Object.freeze({
|
|
959
|
+
run: { agent_turns: { definition: "INTEGER CHECK (agent_turns IS NULL OR agent_turns >= 0)", saved: "NULL" } },
|
|
960
|
+
chat_turn: {
|
|
961
|
+
cache_read_tokens: { definition: "INTEGER CHECK (cache_read_tokens IS NULL OR cache_read_tokens >= 0)", saved: "NULL" },
|
|
962
|
+
cache_write_tokens: { definition: "INTEGER CHECK (cache_write_tokens IS NULL OR cache_write_tokens >= 0)", saved: "NULL" },
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
/** v130: a project's network (an approver's setting, egress.ts ProjectNetwork as JSON) and the installation's choice
|
|
966
|
+
* whether an agent may run where no OS confinement exists (one row; 0 until an instance operator turns it on). */
|
|
967
|
+
export const CONFINEMENT_SCHEMA = `
|
|
968
|
+
CREATE TABLE IF NOT EXISTS project_network (
|
|
969
|
+
repo TEXT PRIMARY KEY,
|
|
970
|
+
network_json TEXT NOT NULL CHECK (length(network_json) <= 16384),
|
|
971
|
+
updated_at TEXT NOT NULL,
|
|
972
|
+
updated_by TEXT NOT NULL
|
|
973
|
+
);
|
|
974
|
+
CREATE TABLE IF NOT EXISTS confinement_setting (
|
|
975
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
976
|
+
unfenced_allowed INTEGER NOT NULL DEFAULT 0 CHECK (unfenced_allowed IN (0, 1)),
|
|
977
|
+
updated_at TEXT,
|
|
978
|
+
updated_by TEXT
|
|
979
|
+
);
|
|
980
|
+
INSERT OR IGNORE INTO confinement_setting (id, unfenced_allowed) VALUES (1, 0);
|
|
981
|
+
`;
|
|
982
|
+
/** v131: where a project's builds may write (an approver's setting, agent-fence.ts WritesMode). */
|
|
983
|
+
export const PROJECT_WRITES_SCHEMA = `
|
|
984
|
+
CREATE TABLE IF NOT EXISTS project_writes (
|
|
985
|
+
repo TEXT PRIMARY KEY,
|
|
986
|
+
mode TEXT NOT NULL CHECK (mode IN ('workspace', 'anywhere')),
|
|
987
|
+
updated_at TEXT NOT NULL,
|
|
988
|
+
updated_by TEXT NOT NULL
|
|
989
|
+
);
|
|
990
|
+
`;
|
|
991
|
+
/** v129: a finished run's How it ran, derived from its own records on first view (display only: never a verdict). Its
|
|
992
|
+
* parser version says when to derive again; `steps` says how complete the step detail was, and why not. */
|
|
993
|
+
export const RUN_EFFICIENCY_SCHEMA = `
|
|
994
|
+
CREATE TABLE IF NOT EXISTS run_efficiency (
|
|
995
|
+
run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
|
|
996
|
+
parser_version INTEGER NOT NULL,
|
|
997
|
+
steps TEXT NOT NULL CHECK (steps IN ('complete','truncated','expired','absent')),
|
|
998
|
+
steps_reason TEXT,
|
|
999
|
+
payload_json TEXT NOT NULL CHECK (length(payload_json) <= 16384),
|
|
1000
|
+
derived_at TEXT NOT NULL
|
|
1001
|
+
);
|
|
1002
|
+
`;
|
|
1003
|
+
/** v128: the personal lead queue (docs/plans/lead-message-queue.md). A message sent while one of its author's personal
|
|
1004
|
+
* lead turns runs (or while earlier ones wait) waits here, one turn per message, in order. Its words are the
|
|
1005
|
+
* lead_message row (pending until admitted); terminal rows keep only their request identity (a tombstone). */
|
|
1006
|
+
export const LEAD_QUEUE_SCHEMA = `
|
|
1007
|
+
CREATE TABLE IF NOT EXISTS lead_queue (
|
|
1008
|
+
message INTEGER PRIMARY KEY REFERENCES lead_message(id) ON DELETE CASCADE,
|
|
1009
|
+
thread INTEGER NOT NULL REFERENCES lead_thread(id) ON DELETE CASCADE,
|
|
1010
|
+
approver TEXT NOT NULL,
|
|
1011
|
+
approver_generation INTEGER NOT NULL,
|
|
1012
|
+
session INTEGER NOT NULL,
|
|
1013
|
+
ceiling_digest TEXT NOT NULL,
|
|
1014
|
+
request_id TEXT NOT NULL,
|
|
1015
|
+
payload_hash TEXT NOT NULL,
|
|
1016
|
+
context_json TEXT,
|
|
1017
|
+
status TEXT NOT NULL CHECK (status IN ('queued','running','answered','failed','cancelled','uncertain')),
|
|
1018
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
1019
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
1020
|
+
runner TEXT,
|
|
1021
|
+
claimed_at TEXT,
|
|
1022
|
+
turn_id INTEGER REFERENCES mate_turn(id),
|
|
1023
|
+
error TEXT,
|
|
1024
|
+
created_at TEXT NOT NULL,
|
|
1025
|
+
settled_at TEXT,
|
|
1026
|
+
UNIQUE (session, request_id)
|
|
1027
|
+
);
|
|
1028
|
+
CREATE INDEX IF NOT EXISTS lead_queue_backlog ON lead_queue (approver, status, message);
|
|
1029
|
+
CREATE UNIQUE INDEX IF NOT EXISTS lead_queue_one_running ON lead_queue (approver) WHERE status = 'running';
|
|
1030
|
+
CREATE INDEX IF NOT EXISTS lead_queue_admitted ON lead_queue (turn_id) WHERE turn_id IS NOT NULL;
|
|
1031
|
+
`;
|
|
1032
|
+
/** v126: the outside source a task was filed from, kept with the task (a card's own task link moves on; this never
|
|
1033
|
+
* does). Written in the filing's transaction, copied whole to a revision, never changed. */
|
|
1034
|
+
export const TASK_SOURCE_SCHEMA = `
|
|
1035
|
+
CREATE TABLE IF NOT EXISTS task_source (
|
|
1036
|
+
task_id TEXT PRIMARY KEY,
|
|
1037
|
+
flow INTEGER NOT NULL,
|
|
1038
|
+
card INTEGER NOT NULL,
|
|
1039
|
+
card_entry INTEGER NOT NULL,
|
|
1040
|
+
source_json TEXT NOT NULL,
|
|
1041
|
+
at TEXT NOT NULL
|
|
1042
|
+
);
|
|
1043
|
+
CREATE TRIGGER IF NOT EXISTS task_source_immutable BEFORE UPDATE ON task_source
|
|
1044
|
+
BEGIN
|
|
1045
|
+
SELECT RAISE(ABORT, 'a task source is immutable');
|
|
1046
|
+
END;
|
|
1047
|
+
CREATE INDEX IF NOT EXISTS flow_trigger_event_accepted ON flow_trigger_event (trigger, accepted_at);
|
|
1048
|
+
`;
|
|
839
1049
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
840
1050
|
export function updateSafeSchema(version) {
|
|
841
1051
|
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
@@ -970,6 +1180,19 @@ export const BUILT_IN = "built-in";
|
|
|
970
1180
|
/** States from which no further work is dispatched. */
|
|
971
1181
|
export const TERMINAL_STATES = ["done", "failed", "cancelled"];
|
|
972
1182
|
export const LEAD_THREAD = { kind: "lead" };
|
|
1183
|
+
/** A lead_message row as a LeadMessage. */
|
|
1184
|
+
function readLeadMessageRow(row) {
|
|
1185
|
+
return {
|
|
1186
|
+
id: Number(row["id"]),
|
|
1187
|
+
thread: Number(row["thread"]),
|
|
1188
|
+
turn: row["turn"] === null ? null : Number(row["turn"]),
|
|
1189
|
+
role: String(row["role"]),
|
|
1190
|
+
text: String(row["text"]),
|
|
1191
|
+
activity: row["activity"] === null ? null : String(row["activity"]),
|
|
1192
|
+
createdAt: String(row["created_at"]),
|
|
1193
|
+
...(Number(row["pending"] ?? 0) === 1 ? { pending: true } : {}),
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
973
1196
|
function readSubagentMemory(row) {
|
|
974
1197
|
return { id: Number(row["id"]), subagent: Number(row["subagent"]), text: String(row["text"]), source: String(row["source"]), card: row["card"] === null ? null : Number(row["card"]),
|
|
975
1198
|
createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedBy: String(row["updated_by"]), updatedAt: String(row["updated_at"]), usedAt: row["used_at"] === null ? null : String(row["used_at"]) };
|
|
@@ -1032,7 +1255,9 @@ function readFlowCardRow(row) {
|
|
|
1032
1255
|
const optional = (key) => row[key] === null || row[key] === undefined ? null : String(row[key]);
|
|
1033
1256
|
return { id: Number(row["id"]), flow: Number(row["flow"]), title: String(row["title"]), description: optional("description"), stage: String(row["stage"]), entry: Number(row["entry"]),
|
|
1034
1257
|
state: String(row["state"]), task: optional("task"), primaryTask: optional("primary_task"), note: optional("note"), waiting: optional("waiting"), outputs, attached,
|
|
1035
|
-
createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedAt: String(row["updated_at"]), source: readFlowCardSource(row["source_json"]), owner: optional("owner")
|
|
1258
|
+
createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedAt: String(row["updated_at"]), source: readFlowCardSource(row["source_json"]), owner: optional("owner"),
|
|
1259
|
+
// v126: a card saved before it was a person's (what filing always assumed).
|
|
1260
|
+
createdKind: row["created_kind"] === "automation" ? "automation" : "person" };
|
|
1036
1261
|
}
|
|
1037
1262
|
function readFlowCardSource(value) {
|
|
1038
1263
|
if (typeof value !== "string")
|
|
@@ -1044,12 +1269,17 @@ function readFlowCardSource(value) {
|
|
|
1044
1269
|
const parsed = read.value;
|
|
1045
1270
|
const mail = parsed["mail"] !== null && typeof parsed["mail"] === "object" ? parsed["mail"] : null;
|
|
1046
1271
|
const chat = parsed["chat"] !== null && typeof parsed["chat"] === "object" ? parsed["chat"] : null;
|
|
1272
|
+
const external = parsed["external"] !== null && typeof parsed["external"] === "object" ? parsed["external"] : null;
|
|
1047
1273
|
return { kind: String(parsed["kind"] ?? ""), label: parsed["label"], url: typeof parsed["url"] === "string" ? parsed["url"] : null,
|
|
1048
1274
|
...(mail === null || typeof mail["from"] !== "string" ? {} : { mail: { id: typeof mail["id"] === "string" ? mail["id"] : null, references: Array.isArray(mail["references"]) ? mail["references"].filter((one) => typeof one === "string") : [],
|
|
1049
1275
|
subject: typeof mail["subject"] === "string" ? mail["subject"] : "", from: mail["from"] } }),
|
|
1050
1276
|
...(chat === null || typeof chat["app"] !== "string" || typeof chat["chat"] !== "string" || typeof chat["thread"] !== "string" || typeof chat["binding"] !== "number" ? {} : { chat: {
|
|
1051
1277
|
app: chat["app"], installation: String(chat["installation"] ?? ""), chat: chat["chat"], conversation: typeof chat["conversation"] === "string" ? chat["conversation"] : chat["chat"], thread: chat["thread"], binding: chat["binding"]
|
|
1052
|
-
} })
|
|
1278
|
+
} }),
|
|
1279
|
+
// v126: kept as they were written at receipt; a card saved before them has none.
|
|
1280
|
+
...(typeof parsed["verified"] === "string" ? { verified: parsed["verified"] } : {}),
|
|
1281
|
+
...(external === null ? {} : { external: { id: typeof external["id"] === "string" ? external["id"] : null, url: typeof external["url"] === "string" ? external["url"] : null } }),
|
|
1282
|
+
...(typeof parsed["receipt"] === "string" ? { receipt: parsed["receipt"] } : {}) };
|
|
1053
1283
|
}
|
|
1054
1284
|
catch {
|
|
1055
1285
|
return null;
|
|
@@ -1059,7 +1289,14 @@ function readFlowTriggerRow(row) {
|
|
|
1059
1289
|
const optional = (key) => row[key] === null || row[key] === undefined ? null : String(row[key]);
|
|
1060
1290
|
return { id: Number(row["id"]), flow: Number(row["flow"]), kind: String(row["kind"]), configJson: String(row["config_json"]), state: String(row["state"]),
|
|
1061
1291
|
hookHash: optional("hook_hash"), cursor: optional("cursor"), nextAt: optional("next_at"), lastAt: optional("last_at"), lastOutcome: optional("last_outcome"),
|
|
1062
|
-
failures: Number(row["failures"] ?? 0), createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedAt: String(row["updated_at"])
|
|
1292
|
+
failures: Number(row["failures"] ?? 0), createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedAt: String(row["updated_at"]),
|
|
1293
|
+
hookCredential: row["hook_credential"] === null || row["hook_credential"] === undefined ? "legacy" : String(row["hook_credential"]), droppedCount: Number(row["dropped_count"] ?? 0), droppedSince: optional("dropped_since") };
|
|
1294
|
+
}
|
|
1295
|
+
function readFlowReceipt(row) {
|
|
1296
|
+
const card = row["card"] === null || row["card"] === undefined ? null : Number(row["card"]);
|
|
1297
|
+
const state = row["state"] === null || row["state"] === undefined ? (card === null ? "skipped" : "accepted") : String(row["state"]);
|
|
1298
|
+
const optional = (key) => row[key] === null || row[key] === undefined ? null : String(row[key]);
|
|
1299
|
+
return { state, card, note: optional("note"), at: String(row["at"]), acceptedAt: optional("accepted_at"), itemJson: optional("item_json") };
|
|
1063
1300
|
}
|
|
1064
1301
|
/** `kind:name`, the unambiguous way a requirement names a capability. */
|
|
1065
1302
|
export function capabilityKey(capability) {
|
|
@@ -1313,7 +1550,7 @@ CREATE TABLE IF NOT EXISTS mcp_idempotency (
|
|
|
1313
1550
|
CREATE TABLE IF NOT EXISTS phase_config (
|
|
1314
1551
|
scope TEXT NOT NULL,
|
|
1315
1552
|
phase TEXT NOT NULL CHECK (phase IN ('plan','build','repair','review')),
|
|
1316
|
-
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
1553
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini','opencode','cursor','cline','qwen','goose')),
|
|
1317
1554
|
model TEXT,
|
|
1318
1555
|
updated_at TEXT NOT NULL,
|
|
1319
1556
|
updated_by TEXT NOT NULL,
|
|
@@ -1329,7 +1566,7 @@ CREATE TABLE IF NOT EXISTS phase_tier_config (
|
|
|
1329
1566
|
scope TEXT NOT NULL,
|
|
1330
1567
|
phase TEXT NOT NULL CHECK (phase IN ('plan','build','repair','review')),
|
|
1331
1568
|
tier TEXT NOT NULL CHECK (tier IN ('light','strong')),
|
|
1332
|
-
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
1569
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini','opencode','cursor','cline','qwen','goose')),
|
|
1333
1570
|
model TEXT,
|
|
1334
1571
|
updated_at TEXT NOT NULL,
|
|
1335
1572
|
updated_by TEXT NOT NULL,
|
|
@@ -1345,7 +1582,7 @@ CREATE TABLE IF NOT EXISTS phase_tier_config (
|
|
|
1345
1582
|
-- substitutes for it.
|
|
1346
1583
|
CREATE TABLE IF NOT EXISTS provider_readiness (
|
|
1347
1584
|
runner TEXT NOT NULL REFERENCES runner(name) ON DELETE CASCADE,
|
|
1348
|
-
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
1585
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini','opencode','cursor','cline','qwen','goose')),
|
|
1349
1586
|
state TEXT NOT NULL CHECK (state IN ('ready','unavailable','unknown')),
|
|
1350
1587
|
reason TEXT NOT NULL,
|
|
1351
1588
|
probe TEXT NOT NULL,
|
|
@@ -2635,7 +2872,7 @@ CREATE TABLE IF NOT EXISTS quota (
|
|
|
2635
2872
|
-- key. auth_mode + a stable NON-SECRET
|
|
2636
2873
|
-- credential fingerprint join the key. Defaults keep every pre-v30 row
|
|
2637
2874
|
-- identical (mode 'subscription', empty fp) since that is what they were.
|
|
2638
|
-
auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription','api-key')),
|
|
2875
|
+
auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription','api-key','agent-login')),
|
|
2639
2876
|
credential_fp TEXT NOT NULL DEFAULT '',
|
|
2640
2877
|
state TEXT NOT NULL CHECK (state IN ('exhausted','half-open')),
|
|
2641
2878
|
reason TEXT NOT NULL,
|
|
@@ -3566,6 +3803,9 @@ function initializeStore(db, file) {
|
|
|
3566
3803
|
db.exec(CHAT_DECIDE_SCHEMA);
|
|
3567
3804
|
// An owner's lasting chat-approval setting (no version bump: additive only; absent means off).
|
|
3568
3805
|
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
3806
|
+
// v124: the live conversation: a project's "Show commands in the live view" (absent means off) and every run a
|
|
3807
|
+
// steering note reached (older notes simply have no rows). New tables only.
|
|
3808
|
+
db.exec(LIVE_DETAIL_SCHEMA);
|
|
3569
3809
|
// What a running agent did last (no version bump: additive only; absent means no word yet).
|
|
3570
3810
|
db.exec(RUN_ACTIVITY_SCHEMA);
|
|
3571
3811
|
// v122: a task branch moved to current main before an attempt (fresh base): a new table, nothing carried over.
|
|
@@ -3593,6 +3833,18 @@ function initializeStore(db, file) {
|
|
|
3593
3833
|
addColumn(db, "task_ref", "route_tier", "TEXT");
|
|
3594
3834
|
widenPhaseTierSchema(db);
|
|
3595
3835
|
db.exec(PHASE_TIER_ALTERNATE_SCHEMA);
|
|
3836
|
+
// v125: ACP agents as providers, their own sign-in as an auth mode (rows carry over, CHECKs widen; the auth schema
|
|
3837
|
+
// runs again to put back the open-pause index the rebuild dropped), and what an agent itself reported it used
|
|
3838
|
+
// (V125_ADDED_COLUMNS: never priced, saved runs read none).
|
|
3839
|
+
widenForAcp(db);
|
|
3840
|
+
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
3841
|
+
db.exec(ACP_ATTESTATION_SCHEMA);
|
|
3842
|
+
db.exec(ACP_BILLING_SCHEMA);
|
|
3843
|
+
db.exec(ACP_CHECK_SCHEMA);
|
|
3844
|
+
for (const [table, columns] of Object.entries(V125_ADDED_COLUMNS)) {
|
|
3845
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3846
|
+
addColumn(db, table, column, definition);
|
|
3847
|
+
}
|
|
3596
3848
|
db.exec(REVIEW_SCHEMA);
|
|
3597
3849
|
db.exec(RETENTION_SCHEMA);
|
|
3598
3850
|
// 1-day evidence: an older file's retention_setting only allowed 7 days or more (no version bump: its rows carry over).
|
|
@@ -3623,6 +3875,7 @@ function initializeStore(db, file) {
|
|
|
3623
3875
|
// What a person's lead knows about them, beside its name and persona (an older lead_config gains it).
|
|
3624
3876
|
addColumn(db, "lead_config", "about_json", "TEXT NOT NULL DEFAULT '[]'");
|
|
3625
3877
|
db.exec(LEAD_COMMITMENT_SCHEMA);
|
|
3878
|
+
db.exec(LEAD_SWEEP_SCHEMA);
|
|
3626
3879
|
mergeCheckTables(db);
|
|
3627
3880
|
// v110: the action ledger admits remote commands ('api', 'mcp').
|
|
3628
3881
|
rebuildLedger(db);
|
|
@@ -3646,7 +3899,36 @@ function initializeStore(db, file) {
|
|
|
3646
3899
|
addColumn(db, "workflow_preview", "flow_id", "INTEGER");
|
|
3647
3900
|
// v115: the task each standing-order firing filed, kept on the firing itself (a card's task changes as it moves).
|
|
3648
3901
|
addColumn(db, "flow_trigger_event", "task", "TEXT");
|
|
3902
|
+
// v126: signed webhooks into tasks (V126_ADDED_COLUMNS: saved rows read null or 0) and each task's outside source.
|
|
3903
|
+
for (const [table, columns] of Object.entries(V126_ADDED_COLUMNS)) {
|
|
3904
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3905
|
+
addColumn(db, table, column, definition);
|
|
3906
|
+
}
|
|
3907
|
+
db.exec(TASK_SOURCE_SCHEMA);
|
|
3649
3908
|
db.exec(PLAN_AUTO_SCHEMA);
|
|
3909
|
+
// v127 (cockpit B1): the running console's instance record (one row; a new table, additive only).
|
|
3910
|
+
db.exec(CONSOLE_INSTANCE_SCHEMA);
|
|
3911
|
+
// v128: the personal lead queue (a new table) and lead_message.pending, set once from each saved row's provenance
|
|
3912
|
+
// (exactly V128_ADDED_COLUMNS' `saved` expression), then the history index the model's reads use.
|
|
3913
|
+
if (!hasColumn(db, "lead_message", "pending")) {
|
|
3914
|
+
addColumn(db, "lead_message", "pending", V128_ADDED_COLUMNS.lead_message.pending.definition);
|
|
3915
|
+
db.exec(`UPDATE lead_message SET pending = ${V128_ADDED_COLUMNS.lead_message.pending.saved}`);
|
|
3916
|
+
}
|
|
3917
|
+
db.exec("CREATE INDEX IF NOT EXISTS lead_message_history ON lead_message (thread, pending, id)");
|
|
3918
|
+
db.exec(LEAD_QUEUE_SCHEMA);
|
|
3919
|
+
// v128: the memory search indexes a waiting message when a turn takes it, found by the turn that took it.
|
|
3920
|
+
db.exec("CREATE INDEX IF NOT EXISTS team_message_admitted ON team_message (turn_id) WHERE turn_id IS NOT NULL");
|
|
3921
|
+
// v129: how a run went — its turn count and a lead request's cache tokens (V129_ADDED_COLUMNS: saved rows read NULL),
|
|
3922
|
+
// and the derived How it ran row (a new table).
|
|
3923
|
+
for (const [table, columns] of Object.entries(V129_ADDED_COLUMNS)) {
|
|
3924
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3925
|
+
addColumn(db, table, column, definition);
|
|
3926
|
+
}
|
|
3927
|
+
db.exec(RUN_EFFICIENCY_SCHEMA);
|
|
3928
|
+
// v130: a project's network and whether an agent may run unconfined (new tables only).
|
|
3929
|
+
db.exec(CONFINEMENT_SCHEMA);
|
|
3930
|
+
// v131: where a project's builds may write (a new table only).
|
|
3931
|
+
db.exec(PROJECT_WRITES_SCHEMA);
|
|
3650
3932
|
// v123: a flow that runs on its own (flow-unattended.ts): its grants and daily runs (new tables), and which authority
|
|
3651
3933
|
// armed a plan's unchanged-plan approval (plan_authorization.basis, V123_ADDED_COLUMNS: saved rows read the mode).
|
|
3652
3934
|
db.exec(FLOW_UNATTENDED_SCHEMA);
|
|
@@ -4143,7 +4425,11 @@ function rebuildExact(db, table, oldDdl, targetDdl, columns) {
|
|
|
4143
4425
|
? db.prepare("SELECT name, seq FROM sqlite_sequence ORDER BY rowid").all()
|
|
4144
4426
|
: [];
|
|
4145
4427
|
db.exec(targetDdl(`${table}_next`));
|
|
4146
|
-
|
|
4428
|
+
// A table keyed by anything but an INTEGER PRIMARY KEY still has a hidden rowid, and a plain copy renumbers it;
|
|
4429
|
+
// the update's history check reads it (count and highest rowid), so it is carried whole like any id.
|
|
4430
|
+
const key = db.prepare(`PRAGMA table_info(${table})`).all().filter(one => Number(one["pk"]) > 0);
|
|
4431
|
+
const aliased = key.length === 1 && String(key[0]["type"]).toUpperCase() === "INTEGER";
|
|
4432
|
+
const names = (aliased || /WITHOUT\s+ROWID/i.test(stored) ? columns : ["rowid", ...columns]).join(", ");
|
|
4147
4433
|
db.exec(`INSERT INTO ${table}_next (${names}) SELECT ${names} FROM ${table}`);
|
|
4148
4434
|
db.exec(`DROP TABLE ${table}`);
|
|
4149
4435
|
db.exec(`ALTER TABLE ${table}_next RENAME TO ${table}`);
|
|
@@ -4743,13 +5029,119 @@ CREATE TABLE IF NOT EXISTS phase_tier_alternate (
|
|
|
4743
5029
|
scope TEXT NOT NULL,
|
|
4744
5030
|
phase TEXT NOT NULL CHECK (phase IN ('plan','build')),
|
|
4745
5031
|
tier TEXT NOT NULL CHECK (tier IN ('light','routine','strong')),
|
|
4746
|
-
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
5032
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini','opencode','cursor','cline','qwen','goose')),
|
|
4747
5033
|
model TEXT NOT NULL,
|
|
4748
5034
|
updated_at TEXT NOT NULL,
|
|
4749
5035
|
updated_by TEXT NOT NULL,
|
|
4750
5036
|
PRIMARY KEY (scope, phase, tier, provider)
|
|
4751
5037
|
);
|
|
4752
5038
|
`;
|
|
5039
|
+
/**
|
|
5040
|
+
* v125: an ACP agent's passed conformance checks (acp-attest.ts): per provider and runner, how to launch the exact
|
|
5041
|
+
* binary that passed and every file that launch depends on, what the agent said it is, the models it offered and the
|
|
5042
|
+
* checks it passed. At most one live row per agent and runner; revoking keeps the row as history.
|
|
5043
|
+
*/
|
|
5044
|
+
export const ACP_ATTESTATION_SCHEMA = `
|
|
5045
|
+
CREATE TABLE IF NOT EXISTS acp_attestation (
|
|
5046
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5047
|
+
provider TEXT NOT NULL CHECK (provider IN ('opencode','cursor','cline','qwen','goose')),
|
|
5048
|
+
runner TEXT NOT NULL,
|
|
5049
|
+
conformance_version INTEGER NOT NULL,
|
|
5050
|
+
launch_json TEXT NOT NULL,
|
|
5051
|
+
agent_name TEXT NOT NULL,
|
|
5052
|
+
agent_version TEXT NOT NULL,
|
|
5053
|
+
cli_version TEXT NOT NULL,
|
|
5054
|
+
models_json TEXT NOT NULL,
|
|
5055
|
+
checks_json TEXT NOT NULL,
|
|
5056
|
+
recorded_at TEXT NOT NULL,
|
|
5057
|
+
recorded_by TEXT NOT NULL,
|
|
5058
|
+
revoked_at TEXT,
|
|
5059
|
+
revoked_by TEXT
|
|
5060
|
+
);
|
|
5061
|
+
CREATE UNIQUE INDEX IF NOT EXISTS acp_attestation_live ON acp_attestation (provider, runner) WHERE revoked_at IS NULL;
|
|
5062
|
+
`;
|
|
5063
|
+
/**
|
|
5064
|
+
* v125: how an ACP agent's work on one model is billed, per runner (docs/plans/acp-adapter.md § Billing): metered
|
|
5065
|
+
* (the default, and what no row means — cost unknown, charged as unknown cost is) or the agent's own plan ($0,
|
|
5066
|
+
* counted as unmeasured). Each change is a new row; the newest is the declaration, its id the revision a run stamps.
|
|
5067
|
+
*/
|
|
5068
|
+
/**
|
|
5069
|
+
* v125: a requested ACP agent check (docs/plans/acp-adapter.md § Layer E): asked for with a password, claimed and run
|
|
5070
|
+
* by one watch of the named runner — one running check per runner, which the partial unique index holds — through two
|
|
5071
|
+
* bounded steps, each bound to its run and graded on plane evidence; the attestation is recorded only in the same
|
|
5072
|
+
* transaction that marks the request passed.
|
|
5073
|
+
*/
|
|
5074
|
+
export const ACP_CHECK_SCHEMA = `
|
|
5075
|
+
CREATE TABLE IF NOT EXISTS acp_check_request (
|
|
5076
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5077
|
+
provider TEXT NOT NULL CHECK (provider IN ('opencode','cursor','cline','qwen','goose')),
|
|
5078
|
+
runner TEXT NOT NULL,
|
|
5079
|
+
model TEXT NOT NULL,
|
|
5080
|
+
conformance_version INTEGER NOT NULL,
|
|
5081
|
+
requested_by TEXT NOT NULL,
|
|
5082
|
+
requested_at TEXT NOT NULL,
|
|
5083
|
+
expires_at TEXT NOT NULL,
|
|
5084
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','running','passed','failed','expired')),
|
|
5085
|
+
watch_repo TEXT,
|
|
5086
|
+
watch_owner TEXT,
|
|
5087
|
+
task_ref INTEGER REFERENCES task_ref(id),
|
|
5088
|
+
launch_json TEXT,
|
|
5089
|
+
lease_id TEXT,
|
|
5090
|
+
steps_used INTEGER NOT NULL DEFAULT 0 CHECK (steps_used BETWEEN 0 AND 2),
|
|
5091
|
+
step1_run INTEGER REFERENCES run(id),
|
|
5092
|
+
step2_run INTEGER REFERENCES run(id),
|
|
5093
|
+
step1_passed INTEGER NOT NULL DEFAULT 0 CHECK (step1_passed IN (0,1)),
|
|
5094
|
+
step2_passed INTEGER NOT NULL DEFAULT 0 CHECK (step2_passed IN (0,1)),
|
|
5095
|
+
result TEXT,
|
|
5096
|
+
finished_at TEXT,
|
|
5097
|
+
settled_at TEXT,
|
|
5098
|
+
settle_by TEXT,
|
|
5099
|
+
settle_until TEXT
|
|
5100
|
+
);
|
|
5101
|
+
CREATE UNIQUE INDEX IF NOT EXISTS acp_check_running ON acp_check_request (runner) WHERE state = 'running';
|
|
5102
|
+
`;
|
|
5103
|
+
export const ACP_BILLING_SCHEMA = `
|
|
5104
|
+
CREATE TABLE IF NOT EXISTS acp_billing (
|
|
5105
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5106
|
+
provider TEXT NOT NULL CHECK (provider IN ('opencode','cursor','cline','qwen','goose')),
|
|
5107
|
+
runner TEXT NOT NULL,
|
|
5108
|
+
model TEXT NOT NULL,
|
|
5109
|
+
kind TEXT NOT NULL CHECK (kind IN ('metered','plan')),
|
|
5110
|
+
set_at TEXT NOT NULL,
|
|
5111
|
+
set_by TEXT NOT NULL
|
|
5112
|
+
);
|
|
5113
|
+
CREATE INDEX IF NOT EXISTS acp_billing_by ON acp_billing (provider, runner, model, id);
|
|
5114
|
+
`;
|
|
5115
|
+
/** The provider CHECK before ACP agents joined (acp-agents.ts), and after: the same list, five ids longer. */
|
|
5116
|
+
const PROVIDERS_BEFORE_ACP = "CHECK (provider IN ('claude','codex','openrouter','gemini'))";
|
|
5117
|
+
const PROVIDERS_WITH_ACP = "CHECK (provider IN ('claude','codex','openrouter','gemini','opencode','cursor','cline','qwen','goose'))";
|
|
5118
|
+
/** The sign-in pause's auth mode before an ACP agent's own sign-in ('agent-login'), and after. */
|
|
5119
|
+
const AUTH_MODES_BEFORE_ACP = "CHECK (auth_mode IN ('subscription', 'api-key'))";
|
|
5120
|
+
const AUTH_MODES_WITH_ACP = "CHECK (auth_mode IN ('subscription', 'api-key', 'agent-login'))";
|
|
5121
|
+
/**
|
|
5122
|
+
* v125: ACP agents as providers (docs/plans/acp-adapter.md): every row carries over whole, the CHECKs only widen. Each table whose stored DDL still has the old list is rebuilt with the wider one, through rebuildExact — the
|
|
5123
|
+
* stored DDL must be exactly a known shape with the old clause appearing once, or the open refuses. Indexes the rebuild
|
|
5124
|
+
* drops are put back by their own schema's IF NOT EXISTS, which the caller runs again after.
|
|
5125
|
+
*/
|
|
5126
|
+
function widenForAcp(db) {
|
|
5127
|
+
const widen = (table, before, after) => {
|
|
5128
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
5129
|
+
if (row === undefined)
|
|
5130
|
+
return;
|
|
5131
|
+
const stored = String(row["sql"]);
|
|
5132
|
+
if (stored.includes(after))
|
|
5133
|
+
return;
|
|
5134
|
+
if (stored.split(before).length !== 2)
|
|
5135
|
+
throw new Error(`the ${table} table's DDL is not a shape this migration knows — refusing to widen it`);
|
|
5136
|
+
const named = (sql) => (name) => sql.replace(new RegExp(`^CREATE TABLE (?:IF NOT EXISTS )?"?${table}"?`), `CREATE TABLE ${name}`);
|
|
5137
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all().map(one => String(one["name"]));
|
|
5138
|
+
rebuildExact(db, table, named(stored), named(stored.replace(before, after)), columns);
|
|
5139
|
+
};
|
|
5140
|
+
for (const table of ["phase_config", "phase_tier_config", "phase_tier_alternate", "provider_readiness"])
|
|
5141
|
+
widen(table, PROVIDERS_BEFORE_ACP, PROVIDERS_WITH_ACP);
|
|
5142
|
+
widen("provider_auth_pause", AUTH_MODES_BEFORE_ACP, AUTH_MODES_WITH_ACP);
|
|
5143
|
+
widen("quota", "CHECK (auth_mode IN ('subscription','api-key'))", "CHECK (auth_mode IN ('subscription','api-key','agent-login'))");
|
|
5144
|
+
}
|
|
4753
5145
|
/** A file from before the light tier keeps `tier IN ('strong')` in phase_tier_config's CHECK: rebuild the table with
|
|
4754
5146
|
* the wider one, keeping every row, in one transaction. A file already widened is untouched. */
|
|
4755
5147
|
function widenPhaseTierSchema(db) {
|
|
@@ -4762,13 +5154,13 @@ function widenPhaseTierSchema(db) {
|
|
|
4762
5154
|
scope TEXT NOT NULL,
|
|
4763
5155
|
phase TEXT NOT NULL CHECK (phase IN ('plan','build','repair','review')),
|
|
4764
5156
|
tier TEXT NOT NULL CHECK (tier IN ('light','strong')),
|
|
4765
|
-
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini')),
|
|
5157
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude','codex','openrouter','gemini','opencode','cursor','cline','qwen','goose')),
|
|
4766
5158
|
model TEXT,
|
|
4767
5159
|
updated_at TEXT NOT NULL,
|
|
4768
5160
|
updated_by TEXT NOT NULL,
|
|
4769
5161
|
PRIMARY KEY (scope, phase, tier)
|
|
4770
5162
|
)`);
|
|
4771
|
-
db.exec("INSERT INTO phase_tier_config_next SELECT scope, phase, tier, provider, model, updated_at, updated_by FROM phase_tier_config");
|
|
5163
|
+
db.exec("INSERT INTO phase_tier_config_next (rowid, scope, phase, tier, provider, model, updated_at, updated_by) SELECT rowid, scope, phase, tier, provider, model, updated_at, updated_by FROM phase_tier_config");
|
|
4772
5164
|
db.exec("DROP TABLE phase_tier_config");
|
|
4773
5165
|
db.exec("ALTER TABLE phase_tier_config_next RENAME TO phase_tier_config");
|
|
4774
5166
|
db.exec("COMMIT");
|
|
@@ -6228,7 +6620,8 @@ export class Store {
|
|
|
6228
6620
|
? { ...profile, permissionArgv: "bypassPermissions" }
|
|
6229
6621
|
: profile.provider === "gemini"
|
|
6230
6622
|
? { ...profile, approvalArgv: "yolo" }
|
|
6231
|
-
:
|
|
6623
|
+
: isAcpProfile(profile) ? profile
|
|
6624
|
+
: { ...profile, sandboxMode: "danger-full-access" };
|
|
6232
6625
|
}
|
|
6233
6626
|
// Anything still above the ceiling files at the highest level beneath it its provider has; a provider with none
|
|
6234
6627
|
// keeps its terms, and approval refuses them in words.
|
|
@@ -9167,9 +9560,23 @@ export class Store {
|
|
|
9167
9560
|
}
|
|
9168
9561
|
/** Stamp a task as the revision it is: source task + immutable brief. */
|
|
9169
9562
|
markRevision(taskRef, revisionOf, briefArtifact) {
|
|
9170
|
-
this.
|
|
9171
|
-
.
|
|
9172
|
-
|
|
9563
|
+
this.transact(() => {
|
|
9564
|
+
this.db
|
|
9565
|
+
.prepare("UPDATE task_ref SET revision_of = ?, revision_brief_artifact = ? WHERE id = ?")
|
|
9566
|
+
.run(revisionOf, briefArtifact, taskRef);
|
|
9567
|
+
// v126: a revision keeps where the work came from, copied whole from the task it revises.
|
|
9568
|
+
this.db.prepare(`INSERT OR IGNORE INTO task_source (task_id, flow, card, card_entry, source_json, at)
|
|
9569
|
+
SELECT r.external_id, s.flow, s.card, s.card_entry, s.source_json, s.at FROM task_ref r JOIN task_source s ON s.task_id = ? WHERE r.id = ?`).run(revisionOf, taskRef);
|
|
9570
|
+
});
|
|
9571
|
+
}
|
|
9572
|
+
/** v126: where a task came from, written once in its filing's transaction (task-source.ts), never changed. */
|
|
9573
|
+
recordTaskSource(taskId, from, sourceJson, now) {
|
|
9574
|
+
this.db.prepare("INSERT OR IGNORE INTO task_source (task_id, flow, card, card_entry, source_json, at) VALUES (?, ?, ?, ?, ?, ?)").run(taskId, from.flow, from.card, from.entry, sourceJson, now.toISOString());
|
|
9575
|
+
}
|
|
9576
|
+
/** v126: where a task came from, as kept when it (or the task it revises) was filed; null for every other task. */
|
|
9577
|
+
taskSourceOf(taskId) {
|
|
9578
|
+
const row = this.db.prepare("SELECT flow, card, card_entry, source_json FROM task_source WHERE task_id = ?").get(taskId);
|
|
9579
|
+
return row === undefined ? null : { flow: Number(row["flow"]), card: Number(row["card"]), entry: Number(row["card_entry"]), sourceJson: String(row["source_json"]) };
|
|
9173
9580
|
}
|
|
9174
9581
|
// ---- the revision boundary: ONE field-by-field policy -------------------
|
|
9175
9582
|
/**
|
|
@@ -9939,7 +10346,10 @@ export class Store {
|
|
|
9939
10346
|
return "published";
|
|
9940
10347
|
return null;
|
|
9941
10348
|
}
|
|
9942
|
-
requeueTask(taskId, by, now
|
|
10349
|
+
requeueTask(taskId, by, now,
|
|
10350
|
+
/** Approve & build again: requeue only while this exact build is the finished task's latest result and the scope
|
|
10351
|
+
* is still the one just approved, read inside this transaction ("moved" otherwise). */
|
|
10352
|
+
expect) {
|
|
9943
10353
|
return this.transact(() => {
|
|
9944
10354
|
const ref = this.db
|
|
9945
10355
|
.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?")
|
|
@@ -9985,6 +10395,12 @@ export class Store {
|
|
|
9985
10395
|
else if (incidents.length === 0 && state !== "failed") {
|
|
9986
10396
|
return { ok: false, reason: "not-stalled" };
|
|
9987
10397
|
}
|
|
10398
|
+
if (expect !== undefined) {
|
|
10399
|
+
const scope = this.db.prepare("SELECT digest, approved_digest FROM task_scope WHERE task_id = ?").get(taskId);
|
|
10400
|
+
if (state !== "done" || rejectedRun !== expect.run || scope === undefined || String(scope["digest"]) !== expect.scopeDigest ||
|
|
10401
|
+
scope["approved_digest"] !== scope["digest"])
|
|
10402
|
+
return { ok: false, reason: "moved" };
|
|
10403
|
+
}
|
|
9988
10404
|
for (const incident of incidents)
|
|
9989
10405
|
this.resolveIncidentLocked(incident, by, now);
|
|
9990
10406
|
this.resetStrikes(taskRef);
|
|
@@ -11265,9 +11681,9 @@ export class Store {
|
|
|
11265
11681
|
this.spendCache = null; // its spend counts toward the next budget check
|
|
11266
11682
|
const changed = this.db
|
|
11267
11683
|
.prepare(`UPDATE chat_turn SET state = ?, failure_reason = ?, tokens_in = ?, tokens_out = ?,
|
|
11268
|
-
settled_microusd = ?, unknown_spend = ?, reply_bytes = ?, candidate_count = ?, finished_at = ?
|
|
11684
|
+
settled_microusd = ?, unknown_spend = ?, reply_bytes = ?, candidate_count = ?, cache_read_tokens = ?, cache_write_tokens = ?, finished_at = ?
|
|
11269
11685
|
WHERE id = ? AND generation = ? AND state IN ('queued','running')`)
|
|
11270
|
-
.run(outcome.state, outcome.failureReason ?? null, outcome.tokensIn ?? null, outcome.tokensOut ?? null, outcome.settledMicrousd, outcome.unknownSpend === true ? 1 : 0, outcome.replyBytes ?? null, outcome.candidateCount ?? null, now.toISOString(), id, generation);
|
|
11686
|
+
.run(outcome.state, outcome.failureReason ?? null, outcome.tokensIn ?? null, outcome.tokensOut ?? null, outcome.settledMicrousd, outcome.unknownSpend === true ? 1 : 0, outcome.replyBytes ?? null, outcome.candidateCount ?? null, outcome.cacheReadTokens ?? null, outcome.cacheWriteTokens ?? null, now.toISOString(), id, generation);
|
|
11271
11687
|
return Number(changed.changes) === 1;
|
|
11272
11688
|
}
|
|
11273
11689
|
/**
|
|
@@ -11990,6 +12406,11 @@ export class Store {
|
|
|
11990
12406
|
// duplicate before any request is consumed.
|
|
11991
12407
|
reviewAttempt);
|
|
11992
12408
|
const id = Number(inserted.lastInsertRowid);
|
|
12409
|
+
// An ACP run's billing is the declaration standing for its runner and model NOW, stamped with the row it bills:
|
|
12410
|
+
// settlement reads this stamp, never the live declaration (docs/plans/acp-adapter.md § Billing).
|
|
12411
|
+
if (provider !== undefined && provider !== null && isAcpProvider(provider) && model !== null) {
|
|
12412
|
+
this.db.prepare("UPDATE run SET acp_billing_json = ? WHERE id = ?").run(JSON.stringify(this.acpBillingFor(provider, run.runner, model)), id);
|
|
12413
|
+
}
|
|
11993
12414
|
// The attempt key (Agents board): opaque, minted with the row, and the only key its stream state is recorded under.
|
|
11994
12415
|
this.db.prepare("UPDATE run SET attempt_key = ? WHERE id = ?").run(mintAttemptKey(), id);
|
|
11995
12416
|
// The provenance row lands in the same transaction as the run row —
|
|
@@ -12423,6 +12844,216 @@ export class Store {
|
|
|
12423
12844
|
* structural signal is gone. `auth-expired` pauses its provider; every
|
|
12424
12845
|
* other class is observational.
|
|
12425
12846
|
*/
|
|
12847
|
+
/**
|
|
12848
|
+
* Record an ACP agent's passed conformance check on a runner (acp-attest.ts), replacing the live one — one
|
|
12849
|
+
* transaction, so there is never a moment with two or with a gap that reads as the old one.
|
|
12850
|
+
*/
|
|
12851
|
+
recordAcpAttestation(input, now) {
|
|
12852
|
+
return this.transact(() => {
|
|
12853
|
+
this.db.prepare("UPDATE acp_attestation SET revoked_at = ?, revoked_by = ? WHERE provider = ? AND runner = ? AND revoked_at IS NULL")
|
|
12854
|
+
.run(now.toISOString(), `${input.recordedBy} (checked again)`, input.provider, input.runner);
|
|
12855
|
+
const inserted = this.db.prepare(`INSERT INTO acp_attestation (provider, runner, conformance_version, launch_json, agent_name, agent_version, cli_version, models_json, checks_json, recorded_at, recorded_by)
|
|
12856
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.provider, input.runner, input.conformanceVersion ?? ACP_CONFORMANCE_VERSION, JSON.stringify(input.launch), input.agent.name, input.agent.version, input.cliVersion, JSON.stringify(input.models), JSON.stringify(input.checks), now.toISOString(), input.recordedBy);
|
|
12857
|
+
return Number(inserted.lastInsertRowid);
|
|
12858
|
+
});
|
|
12859
|
+
}
|
|
12860
|
+
/** An ACP agent's live attestation on a runner, under this build's conformance version; null when there is none. */
|
|
12861
|
+
acpAttestationFor(provider, runner) {
|
|
12862
|
+
const row = this.db.prepare("SELECT * FROM acp_attestation WHERE provider = ? AND runner = ? AND revoked_at IS NULL").get(provider, runner);
|
|
12863
|
+
if (row === undefined || Number(row["conformance_version"]) !== ACP_CONFORMANCE_VERSION)
|
|
12864
|
+
return null;
|
|
12865
|
+
const launch = launchFromJson(String(row["launch_json"]));
|
|
12866
|
+
const list = (json) => {
|
|
12867
|
+
try {
|
|
12868
|
+
const value = JSON.parse(String(json));
|
|
12869
|
+
return Array.isArray(value) && value.every(one => typeof one === "string") ? value : null;
|
|
12870
|
+
}
|
|
12871
|
+
catch {
|
|
12872
|
+
return null;
|
|
12873
|
+
}
|
|
12874
|
+
};
|
|
12875
|
+
const models = list(row["models_json"]);
|
|
12876
|
+
const checks = list(row["checks_json"]);
|
|
12877
|
+
if (launch === null || models === null || checks === null)
|
|
12878
|
+
return null;
|
|
12879
|
+
return {
|
|
12880
|
+
id: Number(row["id"]), provider: String(row["provider"]), runner: String(row["runner"]), conformanceVersion: Number(row["conformance_version"]),
|
|
12881
|
+
launch, agent: { name: String(row["agent_name"]), version: String(row["agent_version"]) }, cliVersion: String(row["cli_version"]),
|
|
12882
|
+
models, checks, recordedAt: String(row["recorded_at"]), recordedBy: String(row["recorded_by"]),
|
|
12883
|
+
};
|
|
12884
|
+
}
|
|
12885
|
+
/** Declare how an ACP agent's work on one model is billed on a runner; the new revision. */
|
|
12886
|
+
declareAcpBilling(input, now) {
|
|
12887
|
+
return Number(this.db.prepare("INSERT INTO acp_billing (provider, runner, model, kind, set_at, set_by) VALUES (?, ?, ?, ?, ?, ?)")
|
|
12888
|
+
.run(input.provider, input.runner, input.model, input.kind, now.toISOString(), input.by).lastInsertRowid);
|
|
12889
|
+
}
|
|
12890
|
+
/** The billing declaration standing for an ACP agent's model on a runner: metered, revision 0, when none was made. */
|
|
12891
|
+
acpBillingFor(provider, runner, model) {
|
|
12892
|
+
const row = this.db.prepare("SELECT id, kind FROM acp_billing WHERE provider = ? AND runner = ? AND model = ? ORDER BY id DESC LIMIT 1").get(provider, runner, model);
|
|
12893
|
+
return row === undefined ? { kind: "metered", revision: 0, model } : { kind: row["kind"] === "plan" ? "plan" : "metered", revision: Number(row["id"]), model };
|
|
12894
|
+
}
|
|
12895
|
+
/** The billing an ACP run was admitted under, or null (not an ACP run, or unreadable — then it is metered). */
|
|
12896
|
+
acpBillingOfRun(runId) {
|
|
12897
|
+
const raw = this.db.prepare("SELECT acp_billing_json FROM run WHERE id = ?").get(runId)?.["acp_billing_json"];
|
|
12898
|
+
if (typeof raw !== "string")
|
|
12899
|
+
return null;
|
|
12900
|
+
try {
|
|
12901
|
+
const value = JSON.parse(raw);
|
|
12902
|
+
if ((value["kind"] === "metered" || value["kind"] === "plan") && typeof value["revision"] === "number" && typeof value["model"] === "string") {
|
|
12903
|
+
return { kind: value["kind"], revision: value["revision"], model: value["model"] };
|
|
12904
|
+
}
|
|
12905
|
+
}
|
|
12906
|
+
catch { /* unreadable: metered */ }
|
|
12907
|
+
return null;
|
|
12908
|
+
}
|
|
12909
|
+
/** Ask for an ACP agent check on a runner (the caller proved the password); the request's id. Valid 30 minutes. */
|
|
12910
|
+
requestAcpCheck(input, now) {
|
|
12911
|
+
return Number(this.db.prepare(`INSERT INTO acp_check_request (provider, runner, model, conformance_version, requested_by, requested_at, expires_at)
|
|
12912
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(input.provider, input.runner, input.model, ACP_CONFORMANCE_VERSION, input.by, now.toISOString(), new Date(now.getTime() + 30 * 60_000).toISOString()).lastInsertRowid);
|
|
12913
|
+
}
|
|
12914
|
+
acpCheck(id) {
|
|
12915
|
+
const row = this.db.prepare("SELECT * FROM acp_check_request WHERE id = ?").get(id);
|
|
12916
|
+
return row === undefined ? null : readAcpCheck(row);
|
|
12917
|
+
}
|
|
12918
|
+
/** The checks asked for an agent, newest first (for the providers screen). */
|
|
12919
|
+
acpChecksFor(provider, limit = 10) {
|
|
12920
|
+
return this.db.prepare("SELECT * FROM acp_check_request WHERE provider = ? ORDER BY id DESC LIMIT ?").all(provider, limit).map(readAcpCheck);
|
|
12921
|
+
}
|
|
12922
|
+
/** The SQL that proves a watch still holds its lease (runner, repo, owner) at `?` — appended with its bindings. */
|
|
12923
|
+
static WATCH_LIVE = "EXISTS (SELECT 1 FROM watch_lease w WHERE w.runner = acp_check_request.runner AND w.repo = acp_check_request.watch_repo AND w.owner = acp_check_request.watch_owner AND w.expires_at > ?)";
|
|
12924
|
+
/**
|
|
12925
|
+
* Claim the oldest pending, unexpired check for this runner, for the watch that will run it — one statement; the
|
|
12926
|
+
* partial unique index refuses a second running check on the runner. Null when there is none (or another runs).
|
|
12927
|
+
*/
|
|
12928
|
+
claimAcpCheck(runner, watch, now) {
|
|
12929
|
+
const at = now.toISOString();
|
|
12930
|
+
try {
|
|
12931
|
+
const claimed = this.db.prepare(`UPDATE acp_check_request SET state = 'running', watch_repo = ?, watch_owner = ?
|
|
12932
|
+
WHERE id = (SELECT id FROM acp_check_request WHERE runner = ? AND state = 'pending' AND expires_at > ? ORDER BY id LIMIT 1)
|
|
12933
|
+
AND EXISTS (SELECT 1 FROM watch_lease w WHERE w.runner = ? AND w.repo = ? AND w.owner = ? AND w.expires_at > ?)
|
|
12934
|
+
RETURNING id`).get(watch.repo, watch.owner, runner, at, runner, watch.repo, watch.owner, at);
|
|
12935
|
+
return claimed === undefined ? null : this.acpCheck(Number(claimed["id"]));
|
|
12936
|
+
}
|
|
12937
|
+
catch (error) {
|
|
12938
|
+
if (/UNIQUE constraint failed/.test(String(error)))
|
|
12939
|
+
return null;
|
|
12940
|
+
throw error;
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
/** Bind a running check, still owned and unexpired, to its task and the launch it inspected. */
|
|
12944
|
+
bindAcpCheck(id, binding, now) {
|
|
12945
|
+
const at = now.toISOString();
|
|
12946
|
+
return Number(this.db.prepare(`UPDATE acp_check_request SET task_ref = ?, launch_json = ?, lease_id = ?
|
|
12947
|
+
WHERE id = ? AND state = 'running' AND expires_at > ? AND task_ref IS NULL AND ${Store.WATCH_LIVE}`).run(binding.taskRef, binding.launchJson, binding.leaseId, id, at, at).changes) === 1;
|
|
12948
|
+
}
|
|
12949
|
+
/** Consume step 1 or 2 for its run: in order, once, owned, unexpired. */
|
|
12950
|
+
consumeAcpCheckStep(id, step, runId, now) {
|
|
12951
|
+
const at = now.toISOString();
|
|
12952
|
+
const column = step === 1 ? "step1_run" : "step2_run";
|
|
12953
|
+
return Number(this.db.prepare(`UPDATE acp_check_request SET steps_used = ?, ${column} = ?
|
|
12954
|
+
WHERE id = ? AND state = 'running' AND steps_used = ? AND ${column} IS NULL AND expires_at > ? AND ${Store.WATCH_LIVE}
|
|
12955
|
+
${step === 2 ? "AND step1_passed = 1" : ""}`).run(step, runId, id, step - 1, at, at).changes) === 1;
|
|
12956
|
+
}
|
|
12957
|
+
/** Record a step as passed, once its grading did: owned, unexpired, and only for the run the step was bound to. */
|
|
12958
|
+
passAcpCheckStep(id, step, runId, now) {
|
|
12959
|
+
const at = now.toISOString();
|
|
12960
|
+
const run = step === 1 ? "step1_run" : "step2_run";
|
|
12961
|
+
const passed = step === 1 ? "step1_passed" : "step2_passed";
|
|
12962
|
+
return Number(this.db.prepare(`UPDATE acp_check_request SET ${passed} = 1
|
|
12963
|
+
WHERE id = ? AND state = 'running' AND ${run} = ? AND ${passed} = 0 AND expires_at > ? AND ${Store.WATCH_LIVE}`).run(id, runId, at, at).changes) === 1;
|
|
12964
|
+
}
|
|
12965
|
+
/**
|
|
12966
|
+
* Publish a passed check: in one transaction, the request moves to passed — only if it is still running, unexpired,
|
|
12967
|
+
* owned by its live watch, with both steps used, bound and passed, and both their runs finished as agent-check
|
|
12968
|
+
* refusals — and the attestation is recorded. Anything else publishes nothing.
|
|
12969
|
+
*/
|
|
12970
|
+
publishAcpCheck(id, attestation, now) {
|
|
12971
|
+
const at = now.toISOString();
|
|
12972
|
+
return this.transact(() => {
|
|
12973
|
+
// The attestation must be for this request's agent and runner, and offer the model it was checked on.
|
|
12974
|
+
const request = this.acpCheck(id);
|
|
12975
|
+
if (request === null || request.provider !== attestation.provider || request.runner !== attestation.runner || !attestation.models.includes(request.model))
|
|
12976
|
+
return false;
|
|
12977
|
+
const moved = Number(this.db.prepare(`UPDATE acp_check_request SET state = 'passed', finished_at = ?, result = 'passed'
|
|
12978
|
+
WHERE id = ? AND state = 'running' AND expires_at > ? AND ${Store.WATCH_LIVE}
|
|
12979
|
+
AND steps_used = 2 AND step1_passed = 1 AND step2_passed = 1 AND conformance_version = ?
|
|
12980
|
+
AND (SELECT COUNT(*) FROM run r WHERE r.id IN (step1_run, step2_run) AND r.task_ref = acp_check_request.task_ref
|
|
12981
|
+
AND r.outcome = 'refused' AND r.reason = 'agent-check' AND r.finished_at IS NOT NULL) = 2`)
|
|
12982
|
+
.run(at, id, at, at, ACP_CONFORMANCE_VERSION).changes);
|
|
12983
|
+
if (moved !== 1)
|
|
12984
|
+
return false;
|
|
12985
|
+
this.recordAcpAttestation(attestation, now);
|
|
12986
|
+
return true;
|
|
12987
|
+
});
|
|
12988
|
+
}
|
|
12989
|
+
/** The check request, when it still admits this run as its current step: running, unexpired, its watch's lease
|
|
12990
|
+
* live, and the step bound to exactly this run. Null otherwise. */
|
|
12991
|
+
acpCheckAdmitting(id, step, runId, now) {
|
|
12992
|
+
const at = now.toISOString();
|
|
12993
|
+
const row = this.db.prepare(`SELECT * FROM acp_check_request WHERE id = ? AND state = 'running' AND expires_at > ? AND ${Store.WATCH_LIVE}
|
|
12994
|
+
AND steps_used = ? AND ${step === 1 ? "step1_run" : "step2_run"} = ?`).get(id, at, at, step, runId);
|
|
12995
|
+
return row === undefined ? null : readAcpCheck(row);
|
|
12996
|
+
}
|
|
12997
|
+
/** Fail a check that hasn't finished, with its words. */
|
|
12998
|
+
failAcpCheck(id, words, now) {
|
|
12999
|
+
return Number(this.db.prepare("UPDATE acp_check_request SET state = 'failed', result = ?, finished_at = ? WHERE id = ? AND state IN ('pending','running')")
|
|
13000
|
+
.run(words.slice(0, 2000), now.toISOString(), id).changes) === 1;
|
|
13001
|
+
}
|
|
13002
|
+
/** Running checks on this runner no live watch holds any more, or past their time: the ones recovery may close. */
|
|
13003
|
+
abandonedAcpChecks(runner, now) {
|
|
13004
|
+
const at = now.toISOString();
|
|
13005
|
+
return this.db.prepare(`SELECT * FROM acp_check_request WHERE runner = ? AND state = 'running' AND (expires_at <= ? OR NOT ${Store.WATCH_LIVE})`)
|
|
13006
|
+
.all(runner, at, at).map(readAcpCheck);
|
|
13007
|
+
}
|
|
13008
|
+
/** Finished checks on this runner whose claim, checkout and task are not yet let go: cleanup's to-do, retried each pass. */
|
|
13009
|
+
unsettledAcpChecks(runner) {
|
|
13010
|
+
return this.db.prepare("SELECT * FROM acp_check_request WHERE runner = ? AND state IN ('passed','failed') AND settled_at IS NULL ORDER BY id").all(runner).map(readAcpCheck);
|
|
13011
|
+
}
|
|
13012
|
+
/** The claim a finished check's cleanup now holds on its task (its first one expired): the only one it may release. */
|
|
13013
|
+
setAcpCheckLease(id, leaseId) {
|
|
13014
|
+
return Number(this.db.prepare("UPDATE acp_check_request SET lease_id = ? WHERE id = ? AND state IN ('passed','failed') AND settled_at IS NULL").run(leaseId, id).changes) === 1;
|
|
13015
|
+
}
|
|
13016
|
+
/** A finished check holds something again (a checkout that landed after recovery settled it): cleanup reopens. */
|
|
13017
|
+
unsettleAcpCheck(id) {
|
|
13018
|
+
return Number(this.db.prepare("UPDATE acp_check_request SET settled_at = NULL WHERE id = ? AND state IN ('passed','failed') AND settled_at IS NOT NULL").run(id).changes) === 1;
|
|
13019
|
+
}
|
|
13020
|
+
/** Take — or renew — the one cleanup attempt a finished check may have at a time: free, lapsed, or already this one. */
|
|
13021
|
+
holdAcpSettle(id, attempt, now, ttlMs) {
|
|
13022
|
+
const at = now.toISOString();
|
|
13023
|
+
return Number(this.db.prepare(`UPDATE acp_check_request SET settle_by = ?, settle_until = ?
|
|
13024
|
+
WHERE id = ? AND state IN ('passed','failed') AND settled_at IS NULL AND (settle_by IS NULL OR settle_by = ? OR settle_until <= ?)`)
|
|
13025
|
+
.run(attempt, new Date(now.getTime() + ttlMs).toISOString(), id, attempt, at).changes) === 1;
|
|
13026
|
+
}
|
|
13027
|
+
/** Whether this cleanup attempt still holds the check. */
|
|
13028
|
+
holdsAcpSettle(id, attempt, now) {
|
|
13029
|
+
return this.db.prepare("SELECT 1 FROM acp_check_request WHERE id = ? AND settle_by = ? AND settle_until > ?").get(id, attempt, now.toISOString()) !== undefined;
|
|
13030
|
+
}
|
|
13031
|
+
/** Let go of a cleanup attempt (only this one). */
|
|
13032
|
+
endAcpSettle(id, attempt) {
|
|
13033
|
+
this.db.prepare("UPDATE acp_check_request SET settle_by = NULL, settle_until = NULL WHERE id = ? AND settle_by = ?").run(id, attempt);
|
|
13034
|
+
}
|
|
13035
|
+
/** Mark a finished check's holdings let go — by the attempt holding it, and only if no checkout of its branch is
|
|
13036
|
+
* left (one that landed late keeps it open, whatever the attempt last saw). */
|
|
13037
|
+
settleAcpCheck(id, attempt, branch, now) {
|
|
13038
|
+
return Number(this.db.prepare(`UPDATE acp_check_request SET settled_at = ?, settle_by = NULL, settle_until = NULL
|
|
13039
|
+
WHERE id = ? AND state IN ('passed','failed') AND settled_at IS NULL AND settle_by = ? AND settle_until > ?
|
|
13040
|
+
AND NOT EXISTS (SELECT 1 FROM worktree w WHERE w.task_ref = acp_check_request.task_ref AND w.branch = ?)`)
|
|
13041
|
+
.run(now.toISOString(), id, attempt, now.toISOString(), branch).changes) === 1;
|
|
13042
|
+
}
|
|
13043
|
+
/** Pending checks past their time are expired, untouched. */
|
|
13044
|
+
expireAcpChecks(now) {
|
|
13045
|
+
return Number(this.db.prepare("UPDATE acp_check_request SET state = 'expired', finished_at = ? WHERE state = 'pending' AND expires_at <= ?").run(now.toISOString(), now.toISOString()).changes);
|
|
13046
|
+
}
|
|
13047
|
+
/** Revoke an ACP agent's live attestation on a runner; whether there was one. */
|
|
13048
|
+
revokeAcpAttestation(provider, runner, by, now) {
|
|
13049
|
+
return Number(this.db.prepare("UPDATE acp_attestation SET revoked_at = ?, revoked_by = ? WHERE provider = ? AND runner = ? AND revoked_at IS NULL").run(now.toISOString(), by, provider, runner).changes) > 0;
|
|
13050
|
+
}
|
|
13051
|
+
/** What a run's ACP agent itself reported it used (never priced; acp-client.ts). */
|
|
13052
|
+
stampAgentReportedUsage(id, usage) {
|
|
13053
|
+
if (usage === null)
|
|
13054
|
+
return;
|
|
13055
|
+
this.db.prepare("UPDATE run SET agent_reported_usage_json = ? WHERE id = ?").run(usage.slice(0, 8192), id);
|
|
13056
|
+
}
|
|
12426
13057
|
stampTerminalClass(id, authMode, terminalClass) {
|
|
12427
13058
|
// FIRST-WRITE only (Codex E2/E3 review, finding 8): the gateway stamps
|
|
12428
13059
|
// the disposal class exactly once, before disposition; nothing may
|
|
@@ -12449,9 +13080,10 @@ export class Store {
|
|
|
12449
13080
|
.prepare(`UPDATE run SET tokens_in = COALESCE(?, tokens_in),
|
|
12450
13081
|
tokens_out = COALESCE(?, tokens_out),
|
|
12451
13082
|
cost_usd = COALESCE(?, cost_usd),
|
|
12452
|
-
usage_json = COALESCE(?, usage_json)
|
|
13083
|
+
usage_json = COALESCE(?, usage_json),
|
|
13084
|
+
agent_turns = COALESCE(?, agent_turns)
|
|
12453
13085
|
WHERE id = ?`)
|
|
12454
|
-
.run(usage.tokensIn ?? null, usage.tokensOut ?? null, usage.costUsd ?? null, usage.usageJson ?? null, id);
|
|
13086
|
+
.run(usage.tokensIn ?? null, usage.tokensOut ?? null, usage.costUsd ?? null, usage.usageJson ?? null, usage.agentTurns ?? null, id);
|
|
12455
13087
|
this.settleRunSpend(id, new Date());
|
|
12456
13088
|
}
|
|
12457
13089
|
/** v105: price a run as it stands (reported cost, or tokens at the catalogue price, or unpriced), frozen at the
|
|
@@ -12461,8 +13093,11 @@ export class Store {
|
|
|
12461
13093
|
if (row === undefined)
|
|
12462
13094
|
return;
|
|
12463
13095
|
// How the run is billed is fixed the first time it's settled (its chain entry's, or the provider's login or key).
|
|
13096
|
+
// An ACP run is billed as its stamp says: its agent's own plan reads as a plan ($0), anything else as metered.
|
|
13097
|
+
const acpStamp = isAcpProvider(String(row["provider"])) ? this.acpBillingOfRun(id) : null;
|
|
12464
13098
|
const billing = row["billing"] === "subscription" || row["billing"] === "api-key" ? row["billing"]
|
|
12465
|
-
: row["
|
|
13099
|
+
: isAcpProvider(String(row["provider"])) ? (acpStamp?.kind === "plan" ? "subscription" : "api-key")
|
|
13100
|
+
: row["auth_mode"] === "subscription" || row["auth_mode"] === "api-key" ? row["auth_mode"] : billingOf(String(row["provider"]), this.db);
|
|
12466
13101
|
const priced = priceWork(this.db, { provider: String(row["provider"]), model: row["model"] == null ? null : String(row["model"]), billing,
|
|
12467
13102
|
costUsd: row["cost_usd"] == null ? null : Number(row["cost_usd"]), tokensIn: row["tokens_in"] == null ? null : Number(row["tokens_in"]), tokensOut: row["tokens_out"] == null ? null : Number(row["tokens_out"]) });
|
|
12468
13103
|
this.db.prepare(`INSERT INTO run_spend (run, microusd, source, billing, price_model, input_usd, output_usd, priced_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -12732,6 +13367,15 @@ export class Store {
|
|
|
12732
13367
|
return { ...none, remainingMicrousd: remaining };
|
|
12733
13368
|
};
|
|
12734
13369
|
}
|
|
13370
|
+
/** How one agent would bill new work now: an ACP agent by its declaration for that exact model on that runner
|
|
13371
|
+
* (plan reads as a plan, anything else as metered), every other provider as agentsFor says. */
|
|
13372
|
+
agentFor(provider, model, runner) {
|
|
13373
|
+
if (isAcpProvider(provider)) {
|
|
13374
|
+
const declared = model === null || runner === null ? null : this.acpBillingFor(provider, runner, model);
|
|
13375
|
+
return { provider, billing: declared?.kind === "plan" ? "subscription" : "api-key" };
|
|
13376
|
+
}
|
|
13377
|
+
return this.agentsFor([provider])[0];
|
|
13378
|
+
}
|
|
12735
13379
|
/** How each provider would bill new work now (see spend.billingOf). */
|
|
12736
13380
|
agentsFor(providers) {
|
|
12737
13381
|
return [...new Set(providers)].map(provider => ({ provider, billing: billingOf(provider, this.db) }));
|
|
@@ -13291,6 +13935,60 @@ export class Store {
|
|
|
13291
13935
|
return { ok: true, id };
|
|
13292
13936
|
}), result => result.ok);
|
|
13293
13937
|
}
|
|
13938
|
+
/**
|
|
13939
|
+
* File a note for the exact run a person is watching (live-conversation.md
|
|
13940
|
+
* §6): in ONE transaction, the run must still be the task's live attempt
|
|
13941
|
+
* (or, `onward`, any attempt of this task: the note goes to the next one),
|
|
13942
|
+
* and a repeated `key` from the same author hands back the same note — only
|
|
13943
|
+
* for the same task, run and text, whichever of the two ways it was sent;
|
|
13944
|
+
* any other reuse is refused. Nothing is filed on a refusal.
|
|
13945
|
+
*/
|
|
13946
|
+
fileSteerNoteForRun(taskId, author, note, now, expect) {
|
|
13947
|
+
return this.transact(() => {
|
|
13948
|
+
const valid = validateNote(note);
|
|
13949
|
+
if (!valid.ok)
|
|
13950
|
+
return { ok: false, reason: "invalid-note", problem: valid.problem };
|
|
13951
|
+
const task = this.db.prepare("SELECT state FROM task WHERE id = ?").get(taskId);
|
|
13952
|
+
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
|
|
13953
|
+
if (task === undefined || ref === undefined)
|
|
13954
|
+
return { ok: false, reason: "unknown-task" };
|
|
13955
|
+
const taskRef = Number(ref["id"]);
|
|
13956
|
+
const key = expect.key === null ? null : `steer-note:${author}:${expect.key}`;
|
|
13957
|
+
const digest = createHash("sha256").update(JSON.stringify([taskId, expect.run, valid.note])).digest("hex");
|
|
13958
|
+
if (key !== null) {
|
|
13959
|
+
const seen = this.db.prepare("SELECT operation, result FROM mutation WHERE idempotency_key = ?").get(key);
|
|
13960
|
+
if (seen !== undefined) {
|
|
13961
|
+
const saved = parseStoreColumn("mutation.result", seen["result"]);
|
|
13962
|
+
if (seen["operation"] === "fileSteerNoteForRun" && saved !== null && saved.digest === digest && typeof saved.id === "number") {
|
|
13963
|
+
return { ok: true, id: saved.id, replayed: true };
|
|
13964
|
+
}
|
|
13965
|
+
return { ok: false, reason: "key-reused" };
|
|
13966
|
+
}
|
|
13967
|
+
}
|
|
13968
|
+
const state = String(task["state"]);
|
|
13969
|
+
if (state === "done" || state === "cancelled")
|
|
13970
|
+
return { ok: false, reason: "task-finished" };
|
|
13971
|
+
const shown = this.db.prepare("SELECT task_ref, outcome, lease_id FROM run WHERE id = ?").get(expect.run);
|
|
13972
|
+
if (shown === undefined || Number(shown["task_ref"]) !== taskRef)
|
|
13973
|
+
return { ok: false, reason: "attempt-ended" };
|
|
13974
|
+
// Sent on from an attempt that ended (`onward`), the note is for the task's next attempt. Otherwise the run on
|
|
13975
|
+
// screen must still be the task's live attempt: unfinished, under the current lease. How the note reaches it —
|
|
13976
|
+
// its brief, its steering folder, or stdin — is steering's own business, as for any note.
|
|
13977
|
+
if (expect.onward !== true) {
|
|
13978
|
+
const lease = this.currentLiveLease(taskRef, now);
|
|
13979
|
+
if (shown["outcome"] !== null || lease === null || shown["lease_id"] !== lease)
|
|
13980
|
+
return { ok: false, reason: "attempt-ended" };
|
|
13981
|
+
}
|
|
13982
|
+
const id = this.insertSteerNote(taskRef, author, valid.note, now);
|
|
13983
|
+
if (key !== null) {
|
|
13984
|
+
this.db
|
|
13985
|
+
.prepare("INSERT INTO mutation (idempotency_key, operation, result, actor, created_at) VALUES (?, ?, ?, ?, ?)")
|
|
13986
|
+
.run(key, "fileSteerNoteForRun", JSON.stringify({ id, digest }), author, now.toISOString());
|
|
13987
|
+
}
|
|
13988
|
+
this.bumpWake();
|
|
13989
|
+
return { ok: true, id, replayed: false };
|
|
13990
|
+
});
|
|
13991
|
+
}
|
|
13294
13992
|
/** One verified note, with the identity a running build's stdin copy is sent under: filing it is also what queues
|
|
13295
13993
|
* its live delivery, in the same transaction (the builder's dispatcher sends whatever is filed and unsent). */
|
|
13296
13994
|
insertSteerNote(taskRef, author, note, now) {
|
|
@@ -13353,6 +14051,7 @@ export class Store {
|
|
|
13353
14051
|
this.db
|
|
13354
14052
|
.prepare("UPDATE task_steer SET attached_run = ?, attached_at = ?, live_sends = 0, live_sent_at = NULL, live_missed_at = NULL WHERE id = ?")
|
|
13355
14053
|
.run(runId, stamp, Number(row["id"]));
|
|
14054
|
+
this.noteSteerReached(Number(row["id"]), runId, "attached", stamp);
|
|
13356
14055
|
}
|
|
13357
14056
|
return rows.map(row => readSteerNote({ ...row, attached_run: runId, attached_at: stamp, live_sends: 0, live_sent_at: null, live_missed_at: null }));
|
|
13358
14057
|
});
|
|
@@ -13364,9 +14063,13 @@ export class Store {
|
|
|
13364
14063
|
* first receipt wins, and a replay changes nothing.
|
|
13365
14064
|
*/
|
|
13366
14065
|
settleSteerDelivered(runId, now) {
|
|
14066
|
+
const stamp = now.toISOString();
|
|
14067
|
+
const settled = this.db.prepare("SELECT id FROM task_steer WHERE attached_run = ? AND delivered_at IS NULL").all(runId);
|
|
13367
14068
|
const { changes } = this.db
|
|
13368
14069
|
.prepare("UPDATE task_steer SET delivered_at = ? WHERE attached_run = ? AND delivered_at IS NULL")
|
|
13369
|
-
.run(
|
|
14070
|
+
.run(stamp, runId);
|
|
14071
|
+
for (const row of settled)
|
|
14072
|
+
this.noteSteerReached(Number(row["id"]), runId, "read", stamp);
|
|
13370
14073
|
return Number(changes);
|
|
13371
14074
|
}
|
|
13372
14075
|
/**
|
|
@@ -13420,8 +14123,24 @@ export class Store {
|
|
|
13420
14123
|
live_missed_at = CASE WHEN mailed_run IS ? THEN live_missed_at ELSE NULL END
|
|
13421
14124
|
WHERE id = ? AND delivered_at IS NULL AND superseded_at IS NULL`)
|
|
13422
14125
|
.run(runId, now.toISOString(), runId, runId, runId, noteId);
|
|
14126
|
+
if (Number(changes) > 0)
|
|
14127
|
+
this.noteSteerReached(noteId, runId, "mailed", now.toISOString());
|
|
13423
14128
|
return Number(changes) > 0;
|
|
13424
14129
|
}
|
|
14130
|
+
/** One run a note reached, kept for its live conversation (live-notes.ts); display state, so a failure never blocks steering. */
|
|
14131
|
+
noteSteerReached(noteId, runId, how, at) {
|
|
14132
|
+
try {
|
|
14133
|
+
this.db.prepare("INSERT OR IGNORE INTO steer_note_run (note, run, how, at) VALUES (?, ?, ?, ?)").run(noteId, runId, how, at);
|
|
14134
|
+
}
|
|
14135
|
+
catch { /* display state */ }
|
|
14136
|
+
}
|
|
14137
|
+
/** The notes this run reached, filing order — attached to its brief, mailed into its folder, or read by it. */
|
|
14138
|
+
steerNotesReaching(runId) {
|
|
14139
|
+
return this.db
|
|
14140
|
+
.prepare("SELECT * FROM task_steer WHERE id IN (SELECT note FROM steer_note_run WHERE run = ?) ORDER BY id")
|
|
14141
|
+
.all(runId)
|
|
14142
|
+
.map(readSteerNote);
|
|
14143
|
+
}
|
|
13425
14144
|
/**
|
|
13426
14145
|
* The agent moved this note to handled/ (v120): it was read, by THIS run.
|
|
13427
14146
|
* Acknowledgement is the only road from mailed to delivered — a hook firing
|
|
@@ -13434,6 +14153,8 @@ export class Store {
|
|
|
13434
14153
|
.prepare(`UPDATE task_steer SET acked_at = ?, delivered_at = ?, attached_run = ?, attached_at = COALESCE(mailed_at, ?)
|
|
13435
14154
|
WHERE id = ? AND mailed_run = ? AND acked_at IS NULL AND delivered_at IS NULL AND superseded_at IS NULL`)
|
|
13436
14155
|
.run(stamp, stamp, runId, stamp, noteId, runId);
|
|
14156
|
+
if (Number(changes) > 0)
|
|
14157
|
+
this.noteSteerReached(noteId, runId, "read", stamp);
|
|
13437
14158
|
if (Number(changes) > 0)
|
|
13438
14159
|
this.bumpWake();
|
|
13439
14160
|
return Number(changes) > 0;
|
|
@@ -14531,17 +15252,31 @@ export class Store {
|
|
|
14531
15252
|
return row === undefined ? null : readLeadSession(row);
|
|
14532
15253
|
}
|
|
14533
15254
|
endLeadSession(id, by, now) {
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
15255
|
+
return this.transact(() => {
|
|
15256
|
+
const changed = this.db
|
|
15257
|
+
.prepare("UPDATE lead_session SET ended_at = ?, ended_by = ? WHERE id = ? AND ended_at IS NULL")
|
|
15258
|
+
.run(now.toISOString(), by, id);
|
|
15259
|
+
// Its waiting messages can never run under it (each is bound to the session it was sent in): they end now, Not
|
|
15260
|
+
// sent, and keep no words (v128 lead queue).
|
|
15261
|
+
if (Number(changed.changes) === 1) {
|
|
15262
|
+
const waiting = this.db.prepare("SELECT message FROM lead_queue WHERE session = ? AND status = 'queued'").all(id).map(row => Number(row["message"]));
|
|
15263
|
+
for (const message of waiting) {
|
|
15264
|
+
this.db.prepare("UPDATE lead_queue SET status = 'cancelled', error = 'Not sent: this conversation ended before it ran.', revision = revision + 1, settled_at = ? WHERE message = ?").run(now.toISOString(), message);
|
|
15265
|
+
this.forgetQueuedLeadMessage(message);
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
return Number(changed.changes) === 1;
|
|
15269
|
+
});
|
|
14538
15270
|
}
|
|
14539
15271
|
/** Revocation's companion (ruling 10): an approver's standing ends every session it minted. */
|
|
14540
15272
|
endLeadSessionsFor(approver, by, now) {
|
|
14541
|
-
|
|
14542
|
-
.prepare("
|
|
14543
|
-
|
|
14544
|
-
|
|
15273
|
+
return this.transact(() => {
|
|
15274
|
+
const ending = this.db.prepare("SELECT id FROM lead_session WHERE approver = ? AND ended_at IS NULL").all(approver).map(row => Number(row["id"]));
|
|
15275
|
+
// Each ends as endLeadSession ends one: its waiting messages too (v128 lead queue).
|
|
15276
|
+
for (const id of ending)
|
|
15277
|
+
this.endLeadSession(id, by, now);
|
|
15278
|
+
return ending.length;
|
|
15279
|
+
});
|
|
14545
15280
|
}
|
|
14546
15281
|
/** The live thread for this approver and scope under THIS ceiling; a
|
|
14547
15282
|
* thread under another ceiling is closed first (ruling 9) — the surface
|
|
@@ -14627,11 +15362,14 @@ export class Store {
|
|
|
14627
15362
|
const { changes } = this.db.prepare("UPDATE flow SET state = 'archived', updated_by = ?, updated_at = ? WHERE id = ? AND state = 'active'").run(by, now.toISOString(), id);
|
|
14628
15363
|
return Number(changes) === 1;
|
|
14629
15364
|
}
|
|
15365
|
+
/** A new card. `byKind` (v126) is decided where the card is made, never read from `by`: a delivery, a poll, a schedule
|
|
15366
|
+
* a public form or a chat channel's message is automation whatever its label says; a signed-in person's own card is a
|
|
15367
|
+
* person's. Every caller says which: filing passes it on exactly, and never reads it from `by`. */
|
|
14630
15368
|
addFlowCard(card, now) {
|
|
14631
15369
|
return this.transact(() => {
|
|
14632
15370
|
const stamp = now.toISOString();
|
|
14633
|
-
const id = Number(this.db.prepare("INSERT INTO flow_card (flow, title, description, stage, state, created_by, created_at, updated_at, source_json) VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?)")
|
|
14634
|
-
.run(card.flow, card.title, card.description, card.stage, card.by, stamp, stamp, card.source === undefined ? null : JSON.stringify(card.source)).lastInsertRowid);
|
|
15371
|
+
const id = Number(this.db.prepare("INSERT INTO flow_card (flow, title, description, stage, state, created_by, created_at, updated_at, source_json, created_kind) VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)")
|
|
15372
|
+
.run(card.flow, card.title, card.description, card.stage, card.by, stamp, stamp, card.source === undefined ? null : JSON.stringify(card.source), card.byKind).lastInsertRowid);
|
|
14635
15373
|
this.db.prepare("INSERT INTO flow_event (card, from_stage, to_stage, outcome, actor, note, at) VALUES (?, NULL, ?, 'created', ?, NULL, ?)").run(id, card.stage, card.by, stamp);
|
|
14636
15374
|
return id;
|
|
14637
15375
|
});
|
|
@@ -15208,7 +15946,80 @@ export class Store {
|
|
|
15208
15946
|
* firing) the task it filed. False when it was already recorded.
|
|
15209
15947
|
*/
|
|
15210
15948
|
recordFlowTriggerEvent(trigger, key, card, note, now, task = null, onTime = false, ranScript = null) {
|
|
15211
|
-
const
|
|
15949
|
+
const at = now.toISOString();
|
|
15950
|
+
const { changes } = this.db.prepare("INSERT OR IGNORE INTO flow_trigger_event (trigger, key, card, note, at, task, on_time, ran_script, state, accepted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
15951
|
+
.run(trigger, key, card, note, at, task, onTime ? 1 : 0, ranScript, card === null ? "skipped" : "accepted", card === null ? null : at);
|
|
15952
|
+
return Number(changes) === 1;
|
|
15953
|
+
}
|
|
15954
|
+
/**
|
|
15955
|
+
* v126: point a trigger at another saved secret ("" for none), and (when given) its new settings, in one write — only
|
|
15956
|
+
* from the secret and settings the caller read, so of two changes at once exactly one lands. False when it changed
|
|
15957
|
+
* meanwhile (or the trigger is gone): the caller then removes only the secret it wrote itself.
|
|
15958
|
+
*/
|
|
15959
|
+
swapFlowHookCredential(id, from, to, configJson, now) {
|
|
15960
|
+
// The address moves with it when given: one write, from the very secret, settings and address the caller read.
|
|
15961
|
+
const { changes } = this.db.prepare(`UPDATE flow_trigger SET hook_credential = ?, hook_hash = COALESCE(?, hook_hash), config_json = COALESCE(?, config_json), updated_at = ?
|
|
15962
|
+
WHERE id = ? AND hook_credential = ? AND config_json = ? AND hook_hash IS ? AND state != 'removed'`)
|
|
15963
|
+
.run(to.credential, to.hookHash ?? null, configJson, now.toISOString(), id, from.credential, from.configJson, from.hookHash);
|
|
15964
|
+
return Number(changes) === 1;
|
|
15965
|
+
}
|
|
15966
|
+
/** v126: what a trigger did with one delivery, or null when it never received it. */
|
|
15967
|
+
flowReceipt(trigger, key) {
|
|
15968
|
+
const row = this.db.prepare("SELECT card, note, at, state, accepted_at, item_json FROM flow_trigger_event WHERE trigger = ? AND key = ?").get(trigger, key);
|
|
15969
|
+
return row === undefined ? null : readFlowReceipt(row);
|
|
15970
|
+
}
|
|
15971
|
+
/** v126: a delivery received but made no card for: ignored (not for this flow), or held until there's room (its
|
|
15972
|
+
* prepared item kept whole). False when the key was already received. */
|
|
15973
|
+
recordFlowReceipt(trigger, key, state, note, itemJson, now) {
|
|
15974
|
+
if ((state === "held") !== (itemJson !== null))
|
|
15975
|
+
throw new Error("a held receipt keeps its item, and only a held one");
|
|
15976
|
+
const { changes } = this.db.prepare("INSERT OR IGNORE INTO flow_trigger_event (trigger, key, card, note, at, state, item_json) VALUES (?, ?, NULL, ?, ?, ?, ?)")
|
|
15977
|
+
.run(trigger, key, note, now.toISOString(), state, itemJson);
|
|
15978
|
+
return Number(changes) === 1;
|
|
15979
|
+
}
|
|
15980
|
+
/** v126: a held delivery became its card. Only ever from held, in the card's own transaction: false (the caller rolls
|
|
15981
|
+
* the card back) when it is no longer held. */
|
|
15982
|
+
acceptHeldReceipt(trigger, key, card, now) {
|
|
15983
|
+
const { changes } = this.db.prepare("UPDATE flow_trigger_event SET state = 'accepted', card = ?, accepted_at = ?, item_json = NULL WHERE trigger = ? AND key = ? AND state = 'held'")
|
|
15984
|
+
.run(card, now.toISOString(), trigger, key);
|
|
15985
|
+
return Number(changes) === 1;
|
|
15986
|
+
}
|
|
15987
|
+
/** v126: the cards a trigger made since `since`, counted when each was made (a held delivery counts when it became
|
|
15988
|
+
* its card, not when it arrived). A receipt saved before v126 counts at its arrival. */
|
|
15989
|
+
flowCardsAcceptedSince(trigger, since) {
|
|
15990
|
+
const at = since.toISOString();
|
|
15991
|
+
const row = this.db.prepare(`SELECT
|
|
15992
|
+
(SELECT COUNT(*) FROM flow_trigger_event WHERE trigger = :trigger AND accepted_at > :at)
|
|
15993
|
+
+ (SELECT COUNT(*) FROM flow_trigger_event WHERE trigger = :trigger AND accepted_at IS NULL AND state IS NULL AND card IS NOT NULL AND at > :at) AS n`).get({ trigger, at });
|
|
15994
|
+
return Number(row?.["n"] ?? 0);
|
|
15995
|
+
}
|
|
15996
|
+
/** v126: a trigger's held deliveries, oldest first. */
|
|
15997
|
+
heldFlowReceipts(trigger, limit) {
|
|
15998
|
+
return this.db.prepare("SELECT key, item_json, at FROM flow_trigger_event WHERE trigger = ? AND state = 'held' ORDER BY at, rowid LIMIT ?").all(trigger, limit)
|
|
15999
|
+
.map(row => ({ key: String(row["key"]), itemJson: String(row["item_json"]), at: String(row["at"]) }));
|
|
16000
|
+
}
|
|
16001
|
+
/** v126: a held delivery that can't become its card (its words no longer pass): left out, with why. */
|
|
16002
|
+
skipHeldReceipt(trigger, key, note) {
|
|
16003
|
+
const { changes } = this.db.prepare("UPDATE flow_trigger_event SET state = 'skipped', note = ?, item_json = NULL WHERE trigger = ? AND key = ? AND state = 'held'").run(note, trigger, key);
|
|
16004
|
+
return Number(changes) === 1;
|
|
16005
|
+
}
|
|
16006
|
+
/** v126: how many deliveries a trigger holds. */
|
|
16007
|
+
heldFlowReceiptCount(trigger) {
|
|
16008
|
+
return Number(this.db.prepare("SELECT COUNT(*) AS n FROM flow_trigger_event WHERE trigger = ? AND state = 'held'").get(trigger)?.["n"] ?? 0);
|
|
16009
|
+
}
|
|
16010
|
+
/** v126: one delivery turned away because the hold queue was full: counted, and since when, until a person clears it. */
|
|
16011
|
+
noteFlowDeliveryDropped(trigger, now) {
|
|
16012
|
+
this.db.prepare("UPDATE flow_trigger SET dropped_count = dropped_count + 1, dropped_since = COALESCE(dropped_since, ?) WHERE id = ?").run(now.toISOString(), trigger);
|
|
16013
|
+
}
|
|
16014
|
+
/** v126: a person has seen the turned-away deliveries. */
|
|
16015
|
+
clearFlowDeliveriesDropped(trigger) {
|
|
16016
|
+
this.db.prepare("UPDATE flow_trigger SET dropped_count = 0, dropped_since = NULL WHERE id = ?").run(trigger);
|
|
16017
|
+
}
|
|
16018
|
+
/** v126: whether this hour's "too many" note on a trigger is still to be written: true at most once an hour, claimed
|
|
16019
|
+
* atomically so two deliveries at once write it once. */
|
|
16020
|
+
claimFlowLimitNote(trigger, now) {
|
|
16021
|
+
const { changes } = this.db.prepare("UPDATE flow_trigger SET limit_noted_at = ? WHERE id = ? AND (limit_noted_at IS NULL OR limit_noted_at <= ?)")
|
|
16022
|
+
.run(now.toISOString(), trigger, new Date(now.getTime() - 3_600_000).toISOString());
|
|
15212
16023
|
return Number(changes) === 1;
|
|
15213
16024
|
}
|
|
15214
16025
|
/** The last card a trigger made, if any. */
|
|
@@ -15307,6 +16118,25 @@ export class Store {
|
|
|
15307
16118
|
this.db.prepare(`INSERT INTO run_tool (run, tools_json, created_at) VALUES (?, json_object('fence', json(?)), ?)
|
|
15308
16119
|
ON CONFLICT (run) DO UPDATE SET tools_json = json_set(tools_json, '$.fence', json(?))`).run(run, JSON.stringify(fence), now.toISOString(), JSON.stringify(fence));
|
|
15309
16120
|
}
|
|
16121
|
+
/** What one attempt's network was and what its proxy saw (egress.ts), kept beside its fence. */
|
|
16122
|
+
recordRunNetwork(run, network, now) {
|
|
16123
|
+
const json = JSON.stringify(network);
|
|
16124
|
+
this.db.prepare(`INSERT INTO run_tool (run, tools_json, created_at) VALUES (?, json_object('network', json(?)), ?)
|
|
16125
|
+
ON CONFLICT (run) DO UPDATE SET tools_json = json_set(tools_json, '$.network', json(?))`).run(run, json, now.toISOString(), json);
|
|
16126
|
+
}
|
|
16127
|
+
runNetwork(run) {
|
|
16128
|
+
const row = this.db.prepare("SELECT json_extract(tools_json, '$.network') AS network FROM run_tool WHERE run = ?").get(run);
|
|
16129
|
+
if (row === undefined || row["network"] === null)
|
|
16130
|
+
return null;
|
|
16131
|
+
let parsed;
|
|
16132
|
+
try {
|
|
16133
|
+
parsed = JSON.parse(String(row["network"]));
|
|
16134
|
+
}
|
|
16135
|
+
catch {
|
|
16136
|
+
return null;
|
|
16137
|
+
}
|
|
16138
|
+
return runNetworkFromValue(parsed);
|
|
16139
|
+
}
|
|
15310
16140
|
runFence(run) {
|
|
15311
16141
|
const row = this.db.prepare("SELECT json_extract(tools_json, '$.fence') AS fence FROM run_tool WHERE run = ?").get(run);
|
|
15312
16142
|
if (row === undefined || row["fence"] === null)
|
|
@@ -15357,27 +16187,30 @@ export class Store {
|
|
|
15357
16187
|
return rows.length;
|
|
15358
16188
|
});
|
|
15359
16189
|
}
|
|
16190
|
+
/** `pending`: a queued message (personal or team) that waits for its turn — out of the model's history until the
|
|
16191
|
+
* admission that binds it clears the flag in the same statement (v128). */
|
|
15360
16192
|
appendLeadMessage(message, now) {
|
|
15361
16193
|
const inserted = this.db
|
|
15362
|
-
.prepare("INSERT INTO lead_message (thread, turn, role, text, activity, created_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
15363
|
-
.run(message.thread, message.turn, message.role, message.text, message.activity ?? null, now.toISOString());
|
|
16194
|
+
.prepare("INSERT INTO lead_message (thread, turn, role, text, activity, created_at, pending) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
16195
|
+
.run(message.thread, message.turn, message.role, message.text, message.activity ?? null, now.toISOString(), message.pending === true ? 1 : 0);
|
|
15364
16196
|
this.db.prepare("UPDATE lead_thread SET last_turn_at = ? WHERE id = ?").run(now.toISOString(), message.thread);
|
|
15365
16197
|
return Number(inserted.lastInsertRowid);
|
|
15366
16198
|
}
|
|
16199
|
+
/** The model's history: the last `limit` ADMITTED messages, oldest first — a message still waiting for its turn (or
|
|
16200
|
+
* removed, expired or refused while waiting) is never read back to the lead. An index range scan whatever the backlog
|
|
16201
|
+
* (lead_message_history). People see the whole thread through listLeadMessages. */
|
|
16202
|
+
listLeadHistory(thread, limit) {
|
|
16203
|
+
return this.db
|
|
16204
|
+
.prepare("SELECT * FROM (SELECT * FROM lead_message WHERE thread = ? AND pending = 0 ORDER BY id DESC LIMIT ?) ORDER BY id ASC")
|
|
16205
|
+
.all(thread, limit)
|
|
16206
|
+
.map(readLeadMessageRow);
|
|
16207
|
+
}
|
|
15367
16208
|
/** The last `limit` messages, oldest first. */
|
|
15368
16209
|
listLeadMessages(thread, limit) {
|
|
15369
16210
|
return this.db
|
|
15370
16211
|
.prepare("SELECT * FROM (SELECT * FROM lead_message WHERE thread = ? ORDER BY id DESC LIMIT ?) ORDER BY id ASC")
|
|
15371
16212
|
.all(thread, limit)
|
|
15372
|
-
.map(
|
|
15373
|
-
id: Number(row["id"]),
|
|
15374
|
-
thread: Number(row["thread"]),
|
|
15375
|
-
turn: row["turn"] === null ? null : Number(row["turn"]),
|
|
15376
|
-
role: String(row["role"]),
|
|
15377
|
-
text: String(row["text"]),
|
|
15378
|
-
activity: row["activity"] === null ? null : String(row["activity"]),
|
|
15379
|
-
createdAt: String(row["created_at"]),
|
|
15380
|
-
}));
|
|
16213
|
+
.map(readLeadMessageRow);
|
|
15381
16214
|
}
|
|
15382
16215
|
/** A proposal is inert (`drafting`) until its turn finalizes. */
|
|
15383
16216
|
draftLeadProposal(proposal, now) {
|
|
@@ -15558,6 +16391,18 @@ export class Store {
|
|
|
15558
16391
|
const liveChat = this.db.prepare("SELECT 1 FROM chat_turn WHERE approver=? AND state IN ('queued','running') AND mate_turn IS NULL LIMIT 1").get(args.approver);
|
|
15559
16392
|
if (live !== undefined || liveChat !== undefined)
|
|
15560
16393
|
return { ok: false, reason: "concurrent" };
|
|
16394
|
+
// v128: while something of this person's waits in the personal queue (or is claimed but not yet bound), the only
|
|
16395
|
+
// personal turn admitted is the one carrying the exact claim of the backlog's oldest message — a follow pass, a
|
|
16396
|
+
// channel turn or another send waits behind it. Team turns are counted per thread and are not held.
|
|
16397
|
+
if (!teamBinding) {
|
|
16398
|
+
const oldest = this.db.prepare("SELECT message, runner, generation, status FROM lead_queue WHERE approver = ? AND (status = 'queued' OR (status = 'running' AND turn_id IS NULL)) ORDER BY message LIMIT 1").get(args.approver);
|
|
16399
|
+
if (oldest !== undefined) {
|
|
16400
|
+
const claim = args.queueClaim;
|
|
16401
|
+
const own = claim !== undefined && Number(oldest["message"]) === claim.message && oldest["status"] === "running" && oldest["runner"] === claim.runner && Number(oldest["generation"]) === claim.generation;
|
|
16402
|
+
if (!own)
|
|
16403
|
+
return { ok: false, reason: "backlog" };
|
|
16404
|
+
}
|
|
16405
|
+
}
|
|
15561
16406
|
if (this.chatTurnsToday(args.approver, now) >= args.dailyTurns)
|
|
15562
16407
|
return { ok: false, reason: "daily-cap" };
|
|
15563
16408
|
if (session.endedAt !== null) {
|
|
@@ -16053,7 +16898,7 @@ export class Store {
|
|
|
16053
16898
|
handoff: row["handoff"] === null ? null : String(row["handoff"]),
|
|
16054
16899
|
costUsd: row["cost_usd"] === null ? null : Number(row["cost_usd"]),
|
|
16055
16900
|
provider: row["provider"] === null || row["provider"] === undefined ? null : String(row["provider"]),
|
|
16056
|
-
authMode: row["auth_mode"] === "subscription" || row["auth_mode"] === "api-key"
|
|
16901
|
+
authMode: row["auth_mode"] === "subscription" || row["auth_mode"] === "api-key" || row["auth_mode"] === "agent-login"
|
|
16057
16902
|
? row["auth_mode"]
|
|
16058
16903
|
: null,
|
|
16059
16904
|
ranMinutes: row["started_at"] === null || row["finished_at"] === null || row["started_at"] === undefined
|
|
@@ -16110,7 +16955,7 @@ export class Store {
|
|
|
16110
16955
|
? null
|
|
16111
16956
|
: Math.max(1, Math.round((new Date(String(row["finished_at"])).getTime() - new Date(String(row["started_at"])).getTime()) / 60_000)),
|
|
16112
16957
|
costUsd: row["cost_usd"] === null ? null : Number(row["cost_usd"]),
|
|
16113
|
-
authMode: row["auth_mode"] === "subscription" || row["auth_mode"] === "api-key"
|
|
16958
|
+
authMode: row["auth_mode"] === "subscription" || row["auth_mode"] === "api-key" || row["auth_mode"] === "agent-login"
|
|
16114
16959
|
? row["auth_mode"]
|
|
16115
16960
|
: null,
|
|
16116
16961
|
prNumber: row["pr_number"] === null || row["pr_number"] === undefined ? null : Number(row["pr_number"]),
|
|
@@ -16932,6 +17777,11 @@ export class Store {
|
|
|
16932
17777
|
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
|
|
16933
17778
|
.run(runId, hostname(), group ? 1 : 0, now.toISOString(), currentBootId()).lastInsertRowid);
|
|
16934
17779
|
}
|
|
17780
|
+
/** The run's process witnesses not yet proved exited — after first saving whatever exit evidence is available. */
|
|
17781
|
+
openRunProcessCount(runId, now) {
|
|
17782
|
+
this.recordRunProcessExits(runId, now);
|
|
17783
|
+
return Number(this.db.prepare("SELECT COUNT(*) AS n FROM run_process WHERE run = ? AND exited_at IS NULL").get(runId).n);
|
|
17784
|
+
}
|
|
16935
17785
|
/** Save positive OS exit evidence while it is available, before a numeric
|
|
16936
17786
|
* PID can be reused. Completion, missing ancestry and age prove nothing.
|
|
16937
17787
|
* Existing rows stay intact; only a previously unknown exit is recorded.
|
|
@@ -17382,6 +18232,62 @@ export class Store {
|
|
|
17382
18232
|
return counts;
|
|
17383
18233
|
}
|
|
17384
18234
|
/** A project's build concurrency changed (project-concurrency.ts): the ledger keeps before → after. */
|
|
18235
|
+
/** A project's network (egress.ts): its saved setting, or the default when none is saved or it doesn't read. */
|
|
18236
|
+
projectNetwork(repo) {
|
|
18237
|
+
const row = this.db.prepare("SELECT network_json FROM project_network WHERE repo = ?").get(repo);
|
|
18238
|
+
if (row === undefined)
|
|
18239
|
+
return PROJECT_NETWORK_DEFAULT;
|
|
18240
|
+
let parsed = null;
|
|
18241
|
+
try {
|
|
18242
|
+
parsed = JSON.parse(String(row["network_json"]));
|
|
18243
|
+
}
|
|
18244
|
+
catch {
|
|
18245
|
+
parsed = null;
|
|
18246
|
+
}
|
|
18247
|
+
// A damaged row reads as the narrowest setting, never a wider one.
|
|
18248
|
+
return projectNetworkFromValue(parsed) ?? { mode: "allowlist", hosts: [], loopback: "proxy-only" };
|
|
18249
|
+
}
|
|
18250
|
+
/** An approver changed a project's network: saved, and in the ledger as before → after. */
|
|
18251
|
+
setProjectNetwork(repo, network, by, now) {
|
|
18252
|
+
return this.transact(() => {
|
|
18253
|
+
const before = this.projectNetwork(repo);
|
|
18254
|
+
const json = JSON.stringify({ mode: network.mode, hosts: [...new Set(network.hosts)].sort(), loopback: network.loopback });
|
|
18255
|
+
this.db.prepare(`INSERT INTO project_network (repo, network_json, updated_at, updated_by) VALUES (?, ?, ?, ?)
|
|
18256
|
+
ON CONFLICT (repo) DO UPDATE SET network_json = excluded.network_json, updated_at = excluded.updated_at, updated_by = excluded.updated_by`).run(repo, json, now.toISOString(), by);
|
|
18257
|
+
const after = this.projectNetwork(repo);
|
|
18258
|
+
const words = (one) => `${one.mode}${one.hosts.length === 0 ? "" : ` +${one.hosts.join(",")}`}${one.loopback === "proxy-only" ? " no-local" : ""}`;
|
|
18259
|
+
this.recordPolicy(by, repo, "network", words(before), words(after), now);
|
|
18260
|
+
return { before, after };
|
|
18261
|
+
});
|
|
18262
|
+
}
|
|
18263
|
+
/** Where a project's builds may write (agent-fence.ts): its saved setting, or only their workspace. */
|
|
18264
|
+
projectWrites(repo) {
|
|
18265
|
+
const mode = this.db.prepare("SELECT mode FROM project_writes WHERE repo = ?").get(repo)?.["mode"];
|
|
18266
|
+
// A damaged row reads as the narrower setting, never the wider one.
|
|
18267
|
+
return mode === "anywhere" ? "anywhere" : WRITES_DEFAULT;
|
|
18268
|
+
}
|
|
18269
|
+
/** An approver changed where a project's builds may write: saved, and in the ledger as before → after. */
|
|
18270
|
+
setProjectWrites(repo, mode, by, now) {
|
|
18271
|
+
return this.transact(() => {
|
|
18272
|
+
const before = this.projectWrites(repo);
|
|
18273
|
+
this.db.prepare(`INSERT INTO project_writes (repo, mode, updated_at, updated_by) VALUES (?, ?, ?, ?)
|
|
18274
|
+
ON CONFLICT (repo) DO UPDATE SET mode = excluded.mode, updated_at = excluded.updated_at, updated_by = excluded.updated_by`).run(repo, mode, now.toISOString(), by);
|
|
18275
|
+
const after = this.projectWrites(repo);
|
|
18276
|
+
this.recordPolicy(by, repo, "build writes", before, after, now);
|
|
18277
|
+
return { before, after };
|
|
18278
|
+
});
|
|
18279
|
+
}
|
|
18280
|
+
/** Whether this installation lets an agent run where no OS confinement exists (off unless an operator said so). */
|
|
18281
|
+
unfencedAgentsAllowed() {
|
|
18282
|
+
return Number(this.db.prepare("SELECT unfenced_allowed FROM confinement_setting WHERE id = 1").get()?.["unfenced_allowed"] ?? 0) === 1;
|
|
18283
|
+
}
|
|
18284
|
+
setUnfencedAgentsAllowed(allowed, by, now) {
|
|
18285
|
+
this.transact(() => {
|
|
18286
|
+
const before = this.unfencedAgentsAllowed();
|
|
18287
|
+
this.db.prepare("UPDATE confinement_setting SET unfenced_allowed = ?, updated_at = ?, updated_by = ? WHERE id = 1").run(allowed ? 1 : 0, now.toISOString(), by);
|
|
18288
|
+
this.recordPolicy(by, null, "unconfined agents", before ? "allowed" : "refused", allowed ? "allowed" : "refused", now);
|
|
18289
|
+
});
|
|
18290
|
+
}
|
|
17385
18291
|
recordProjectConcurrency(by, repo, before, after, now) {
|
|
17386
18292
|
this.recordPolicy(by, repo, "builds at once", String(before), String(after), now);
|
|
17387
18293
|
}
|
|
@@ -17406,6 +18312,7 @@ export class Store {
|
|
|
17406
18312
|
markTelegramUpdateApplied(...args) { return this.telegramChat().markUpdateApplied(...args); }
|
|
17407
18313
|
// ---- the conversation queue (v62) ------------------------------------------
|
|
17408
18314
|
enqueueTelegramConversation(...args) { return this.telegramChat().enqueueMessage(...args); }
|
|
18315
|
+
telegramMessagesWaiting(binding) { return this.telegramChat().messagesWaiting(binding); }
|
|
17409
18316
|
telegramConversationWaitingOn(...args) { return this.telegramChat().messageWaitingOn(...args); }
|
|
17410
18317
|
getTelegramConversation(...args) { return this.telegramChat().getMessage(...args); }
|
|
17411
18318
|
listTelegramConversations(...args) { return this.telegramChat().listMessages(...args); }
|
|
@@ -17440,6 +18347,7 @@ export class Store {
|
|
|
17440
18347
|
this.telegramChat().deferUntil(botId, until);
|
|
17441
18348
|
}
|
|
17442
18349
|
telegramDeliveryProblem(...args) { return this.telegramChat().deliveryProblem(...args); }
|
|
18350
|
+
telegramWatchFence(...args) { return this.telegramChat().watchFence(...args); }
|
|
17443
18351
|
recordTelegramMessage(...args) { return this.telegramChat().recordMessage(...args); }
|
|
17444
18352
|
telegramMessageOf(...args) { return this.telegramChat().messageOf(...args); }
|
|
17445
18353
|
finalizeTelegramDelivery(...args) { return this.telegramChat().finalizeDelivery(...args); }
|
|
@@ -18032,7 +18940,7 @@ export class Store {
|
|
|
18032
18940
|
* the subscription live under its enrolled generation, the notification
|
|
18033
18941
|
* unresolved and still stamped. Null = do not transmit.
|
|
18034
18942
|
*/
|
|
18035
|
-
pushSendFence(id, owner, generation) {
|
|
18943
|
+
pushSendFence(id, owner, generation, now = new Date()) {
|
|
18036
18944
|
const pair = this.db
|
|
18037
18945
|
.prepare(`SELECT * FROM push_delivery
|
|
18038
18946
|
WHERE id = ? AND state = 'claimed' AND claim_owner = ? AND claim_generation = ?`)
|
|
@@ -18052,7 +18960,20 @@ export class Store {
|
|
|
18052
18960
|
.get(Number(pair["notification"]));
|
|
18053
18961
|
if (notification === undefined)
|
|
18054
18962
|
return null;
|
|
18055
|
-
|
|
18963
|
+
// The watch, again at the last moment (cockpit §3): a person who opened the agent since the claim holds the ping
|
|
18964
|
+
// (back to pending, no attempt spent) or, for news they have now seen, retires it.
|
|
18965
|
+
const row = readNotification(notification), approverName = String(subscription["approver"]);
|
|
18966
|
+
const watched = watchedPing(this, row, approverName, now);
|
|
18967
|
+
if (watched === "skip") {
|
|
18968
|
+
this.db.prepare("UPDATE push_delivery SET state = 'retired', last_error = 'seen', claim_owner = NULL WHERE id = ? AND state = 'claimed' AND claim_owner = ? AND claim_generation = ?").run(id, owner, generation);
|
|
18969
|
+
return null;
|
|
18970
|
+
}
|
|
18971
|
+
if (watched !== "send") {
|
|
18972
|
+
this.db.prepare(`UPDATE push_delivery SET state = 'pending', claim_owner = NULL, claim_expires_at = NULL, next_attempt_at = ?
|
|
18973
|
+
WHERE id = ? AND state = 'claimed' AND claim_owner = ? AND claim_generation = ?`).run(watched.hold, id, owner, generation);
|
|
18974
|
+
return null;
|
|
18975
|
+
}
|
|
18976
|
+
return { ...readPushPair(pair), subscriptionRow: readPushSubscription(subscription), notificationRow: row };
|
|
18056
18977
|
}
|
|
18057
18978
|
/** Settle a pair, conditionally on owner + generation (finding 25). */
|
|
18058
18979
|
settlePushPair(id, owner, generation, outcome, now) {
|
|
@@ -18145,6 +19066,87 @@ export class Store {
|
|
|
18145
19066
|
.run(idempotencyKey, operation, JSON.stringify(result ?? null), actor, at.toISOString());
|
|
18146
19067
|
return result;
|
|
18147
19068
|
}
|
|
19069
|
+
/** A kept web session's changes: never inserts, so a session ended elsewhere is not brought back by a cached copy.
|
|
19070
|
+
* False when it is gone. (Kept last in the class: the freshness tripwire pins method positions.) */
|
|
19071
|
+
updateWebSession(session) {
|
|
19072
|
+
return Number(this.db.prepare("UPDATE web_session SET last_seen = ?, project = ?, project_revision = ?, sso_at = ? WHERE id_hash = ?")
|
|
19073
|
+
.run(session.lastSeen, session.project, session.projectRevision, session.ssoAt, session.idHash).changes) > 0;
|
|
19074
|
+
}
|
|
19075
|
+
// ---- lead sweeps (docs/plans/lead-fan-out.md). Kept last in the class: the freshness tripwire pins method positions.
|
|
19076
|
+
/** A new sweep record (inside the caller's filing transaction). */
|
|
19077
|
+
createSweep(input, now) {
|
|
19078
|
+
const row = this.db.prepare("INSERT INTO lead_sweep (owner, thread, repo, title, brief, max_parallel, proposal, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
19079
|
+
.run(input.owner, input.thread, input.repo, input.title, input.brief, input.maxParallel, input.proposal, now.toISOString());
|
|
19080
|
+
return Number(row.lastInsertRowid);
|
|
19081
|
+
}
|
|
19082
|
+
/** Link one filed child (its family root) to its sweep, in order. */
|
|
19083
|
+
addSweepTask(sweep, rootTask, ordinal, label) {
|
|
19084
|
+
this.db.prepare("INSERT INTO lead_sweep_task (sweep, root_task, ordinal, label) VALUES (?, ?, ?, ?)").run(sweep, rootTask, ordinal, label);
|
|
19085
|
+
}
|
|
19086
|
+
getSweep(id) {
|
|
19087
|
+
const row = this.db.prepare("SELECT * FROM lead_sweep WHERE id = ?").get(id);
|
|
19088
|
+
if (row === undefined)
|
|
19089
|
+
return null;
|
|
19090
|
+
return { id: Number(row["id"]), owner: String(row["owner"]), thread: row["thread"] === null ? null : Number(row["thread"]), repo: String(row["repo"]), title: String(row["title"]),
|
|
19091
|
+
brief: String(row["brief"]), maxParallel: Number(row["max_parallel"]), proposal: row["proposal"] === null ? null : Number(row["proposal"]), createdAt: String(row["created_at"]) };
|
|
19092
|
+
}
|
|
19093
|
+
/** The sweep a proposal filed, if it did (a replayed confirm finds it). */
|
|
19094
|
+
sweepOfProposal(proposal) {
|
|
19095
|
+
const row = this.db.prepare("SELECT id FROM lead_sweep WHERE proposal = ?").get(proposal);
|
|
19096
|
+
return row === undefined ? null : Number(row["id"]);
|
|
19097
|
+
}
|
|
19098
|
+
/** A sweep's children, in order: each one's family root and its label. */
|
|
19099
|
+
sweepTasks(sweep) {
|
|
19100
|
+
return this.db.prepare("SELECT root_task, ordinal, label FROM lead_sweep_task WHERE sweep = ? ORDER BY ordinal").all(sweep)
|
|
19101
|
+
.map(row => ({ rootTask: String(row["root_task"]), ordinal: Number(row["ordinal"]), label: String(row["label"]) }));
|
|
19102
|
+
}
|
|
19103
|
+
/** The sweep a task belongs to through its family root (a revision of a child belongs too), or null. */
|
|
19104
|
+
sweepOfTask(taskId) {
|
|
19105
|
+
const { chain } = this.revisionAncestryStatus(taskId);
|
|
19106
|
+
const root = chain.at(-1) ?? taskId;
|
|
19107
|
+
const row = this.db.prepare("SELECT s.id, s.max_parallel FROM lead_sweep_task t JOIN lead_sweep s ON s.id = t.sweep WHERE t.root_task = ?").get(root);
|
|
19108
|
+
return row === undefined ? null : { id: Number(row["id"]), maxParallel: Number(row["max_parallel"]) };
|
|
19109
|
+
}
|
|
19110
|
+
/**
|
|
19111
|
+
* How many of a sweep's families hold a live claim now (the latest lease of any task in the family, unreleased and
|
|
19112
|
+
* unexpired, across every runner) — claimed-but-not-started counts. `except` leaves one task_ref out (the one being
|
|
19113
|
+
* claimed). Read inside acquire's own write transaction, so two runners can't both take the last slot.
|
|
19114
|
+
*/
|
|
19115
|
+
sweepLiveClaims(sweep, stamp, except = null) {
|
|
19116
|
+
const row = this.db.prepare(`WITH RECURSIVE fam(root, id, depth) AS (
|
|
19117
|
+
SELECT t.root_task, t.root_task, 0 FROM lead_sweep_task t WHERE t.sweep = ?
|
|
19118
|
+
UNION SELECT fam.root, r.external_id, fam.depth + 1 FROM task_ref r JOIN fam ON r.revision_of = fam.id WHERE r.backend = ? AND fam.depth < 64),
|
|
19119
|
+
refs AS (SELECT DISTINCT fam.root, r.id AS ref FROM fam JOIN task_ref r ON r.backend = ? AND r.external_id = fam.id),
|
|
19120
|
+
latest AS (SELECT refs.root, c.released_at, c.expires_at FROM refs JOIN claim c ON c.task_ref = refs.ref
|
|
19121
|
+
WHERE refs.ref IS NOT ? AND c.lease_generation = (SELECT MAX(lease_generation) FROM claim WHERE task_ref = refs.ref))
|
|
19122
|
+
SELECT COUNT(DISTINCT root) AS n FROM latest WHERE released_at IS NULL AND expires_at > ?`).get(sweep, BUILT_IN, BUILT_IN, except, stamp);
|
|
19123
|
+
return Number(row?.["n"] ?? 0);
|
|
19124
|
+
}
|
|
19125
|
+
/** Whether a request of this lead turn ended with its cost unknown and that cost is still unconfirmed — the turn is
|
|
19126
|
+
* what keeps chat paused on its credential. Once confirmed, it no longer is. */
|
|
19127
|
+
leadTurnCostUnconfirmed(leadTurn) {
|
|
19128
|
+
return this.db.prepare("SELECT 1 AS hit FROM chat_turn WHERE mate_turn = ? AND unknown_spend = 1 AND acknowledged_at IS NULL LIMIT 1").get(leadTurn) !== undefined;
|
|
19129
|
+
}
|
|
19130
|
+
/** The lead turn a chat step belongs to (a lead reply's request), or null for a standalone chat turn. */
|
|
19131
|
+
leadTurnOfStep(step) {
|
|
19132
|
+
const row = this.db.prepare("SELECT mate_turn FROM chat_turn WHERE id = ?").get(step);
|
|
19133
|
+
return row === undefined || row["mate_turn"] === null ? null : this.getLeadTurn(Number(row["mate_turn"]));
|
|
19134
|
+
}
|
|
19135
|
+
/** A waiting message that will never run keeps nothing of what it said (v128 lead queue): its words, the context it was
|
|
19136
|
+
* sent with, and any copy the memory search made of it. Only while it is still pending — a message a turn took stays. */
|
|
19137
|
+
/** Whether a send of this session reached the plane: the engine took it, or the lead queue holds it (waiting, run,
|
|
19138
|
+
* removed or expired — any of those means the composer's draft was accepted and must not be sent again). */
|
|
19139
|
+
leadRequestReceived(session, request) {
|
|
19140
|
+
return this.leadRequestReceipt(session, request) !== null
|
|
19141
|
+
|| this.db.prepare("SELECT 1 FROM lead_queue WHERE session = ? AND request_id = ?").get(session, request) !== undefined;
|
|
19142
|
+
}
|
|
19143
|
+
forgetQueuedLeadMessage(message) {
|
|
19144
|
+
const blanked = this.db.prepare("UPDATE lead_message SET text = '' WHERE id = ? AND pending = 1").run(message);
|
|
19145
|
+
if (Number(blanked.changes) !== 1)
|
|
19146
|
+
return;
|
|
19147
|
+
this.db.prepare("UPDATE lead_queue SET context_json = NULL WHERE message = ?").run(message);
|
|
19148
|
+
this.db.prepare("DELETE FROM memory_search WHERE kind = 'conversation' AND ref = ?").run(`message:${message}`);
|
|
19149
|
+
}
|
|
18148
19150
|
}
|
|
18149
19151
|
/** A stored count or id that must be a whole, non-negative number — a
|
|
18150
19152
|
* NULL, an empty string, a fraction, or text reads as null (v48 authority repair). */
|
|
@@ -18192,6 +19194,7 @@ function readRun(row) {
|
|
|
18192
19194
|
: String(row["provider_started_at"]),
|
|
18193
19195
|
tokensIn: row["tokens_in"] === null || row["tokens_in"] === undefined ? null : Number(row["tokens_in"]),
|
|
18194
19196
|
tokensOut: row["tokens_out"] === null || row["tokens_out"] === undefined ? null : Number(row["tokens_out"]),
|
|
19197
|
+
agentTurns: row["agent_turns"] === null || row["agent_turns"] === undefined ? null : Number(row["agent_turns"]),
|
|
18195
19198
|
costUsd: row["cost_usd"] === null || row["cost_usd"] === undefined ? null : Number(row["cost_usd"]),
|
|
18196
19199
|
headRevision: row["head_revision"] === null || row["head_revision"] === undefined
|
|
18197
19200
|
? null
|
|
@@ -18203,7 +19206,7 @@ function readRun(row) {
|
|
|
18203
19206
|
? String(row["phase"])
|
|
18204
19207
|
: null,
|
|
18205
19208
|
// An auth mode is exactly one of the two words — anything else reads as unknown.
|
|
18206
|
-
authMode: row["auth_mode"] === "api-key" ? "api-key" : row["auth_mode"] === "subscription" ? "subscription" : null,
|
|
19209
|
+
authMode: row["auth_mode"] === "api-key" ? "api-key" : row["auth_mode"] === "subscription" ? "subscription" : row["auth_mode"] === "agent-login" ? "agent-login" : null,
|
|
18207
19210
|
terminalClass: row["terminal_class"] === null || row["terminal_class"] === undefined
|
|
18208
19211
|
? null
|
|
18209
19212
|
: String(row["terminal_class"]),
|
|
@@ -18777,6 +19780,8 @@ export function permissionModeOfProfile(profile) {
|
|
|
18777
19780
|
return profile.permissionArgv === "bypassPermissions" ? "bypassPermissions" : "auto";
|
|
18778
19781
|
if (profile.provider === "gemini")
|
|
18779
19782
|
return profile.approvalArgv === "yolo" ? "bypassPermissions" : "auto";
|
|
19783
|
+
if (isAcpProfile(profile))
|
|
19784
|
+
return "bypassPermissions";
|
|
18780
19785
|
return profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
|
|
18781
19786
|
}
|
|
18782
19787
|
export function revisionTermsOf(sourceRef, sourceScope, coverage, permissionDefault, qualityDefault) {
|
|
@@ -19042,3 +20047,18 @@ function readGrant(row) {
|
|
|
19042
20047
|
export function verifiedAuthor(name) {
|
|
19043
20048
|
return name;
|
|
19044
20049
|
}
|
|
20050
|
+
function readAcpCheck(row) {
|
|
20051
|
+
const text = (key) => row[key] == null ? null : String(row[key]);
|
|
20052
|
+
const num = (key) => row[key] == null ? null : Number(row[key]);
|
|
20053
|
+
const state = String(row["state"]);
|
|
20054
|
+
return {
|
|
20055
|
+
id: Number(row["id"]), provider: String(row["provider"]), runner: String(row["runner"]), model: String(row["model"]),
|
|
20056
|
+
conformanceVersion: Number(row["conformance_version"]), requestedBy: String(row["requested_by"]), requestedAt: String(row["requested_at"]),
|
|
20057
|
+
expiresAt: String(row["expires_at"]),
|
|
20058
|
+
state: state === "running" || state === "passed" || state === "failed" || state === "expired" ? state : "pending",
|
|
20059
|
+
watch: row["watch_repo"] == null || row["watch_owner"] == null ? null : { repo: String(row["watch_repo"]), owner: String(row["watch_owner"]) },
|
|
20060
|
+
taskRef: num("task_ref"), launchJson: text("launch_json"), leaseId: text("lease_id"), stepsUsed: Number(row["steps_used"]), step1Run: num("step1_run"), step2Run: num("step2_run"),
|
|
20061
|
+
step1Passed: Number(row["step1_passed"]) === 1, step2Passed: Number(row["step2_passed"]) === 1, result: text("result"), finishedAt: text("finished_at"),
|
|
20062
|
+
settledAt: text("settled_at"),
|
|
20063
|
+
};
|
|
20064
|
+
}
|