toolroll 0.9.91 → 0.9.93
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 +41 -0
- package/dist/approval-authority.d.ts +9 -1
- package/dist/approval-authority.js +15 -2
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/build-review.js +2 -0
- package/dist/builder.js +21 -2
- package/dist/chat-actions.d.ts +11 -3
- package/dist/chat-actions.js +47 -2
- package/dist/chat-decide.js +2 -0
- package/dist/chat-review.d.ts +2 -1
- package/dist/chat-review.js +2 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/chat-actions.d.ts +10 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/console-api.d.ts +3 -2
- package/dist/contracts/console-api.js +5 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/route-results.d.ts +121 -0
- package/dist/contracts/route-results.js +71 -0
- package/dist/contracts/route.d.ts +343 -5
- package/dist/contracts/route.js +36 -7
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispose.js +6 -0
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/flow-engine.js +1 -1
- package/dist/flow-pull-request.js +1 -1
- package/dist/lead-doors.js +4 -2
- package/dist/lead-tools.js +10 -2
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/memory-cli.d.ts +3 -0
- package/dist/memory-cli.js +3 -3
- package/dist/memory-pass.js +5 -1
- package/dist/model-choices.d.ts +7 -3
- package/dist/model-choices.js +26 -11
- package/dist/models-cli.d.ts +205 -0
- package/dist/models-cli.js +143 -4
- package/dist/models-ui.d.ts +57 -0
- package/dist/models-ui.js +44 -1
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate-remote-arguments.js +4 -4
- package/dist/operate-remote.js +1 -1
- package/dist/operate.js +191 -23
- package/dist/phase-routing.d.ts +82 -2
- package/dist/phase-routing.js +132 -18
- package/dist/project-delete.d.ts +4 -0
- package/dist/project-delete.js +16 -4
- package/dist/project-memory.d.ts +8 -0
- package/dist/project-memory.js +20 -1
- package/dist/pull-request-flow.js +1 -1
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +22 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +3 -0
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/route-hooks.d.ts +54 -0
- package/dist/route-hooks.js +179 -0
- package/dist/route-results.d.ts +70 -0
- package/dist/route-results.js +340 -0
- package/dist/route-rules.d.ts +324 -0
- package/dist/route-rules.js +781 -0
- package/dist/route-savings.d.ts +136 -0
- package/dist/route-savings.js +193 -0
- package/dist/route-store.d.ts +300 -0
- package/dist/route-store.js +598 -0
- package/dist/scope.d.ts +3 -1
- package/dist/scope.js +4 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.d.ts +7 -0
- package/dist/server/render-tasks.js +16 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +19 -1
- package/dist/server/settings.d.ts +2 -1
- package/dist/server/settings.js +87 -1
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +105 -20
- package/dist/settings-proofs.js +2 -0
- package/dist/spend-ui.d.ts +11 -0
- package/dist/spend-ui.js +49 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +106 -31
- package/dist/store.js +653 -128
- package/dist/surface.js +17 -5
- package/dist/task-outcome-cli.js +24 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -35,6 +35,7 @@ import { actorLabel, currentActor } from "./actor.js";
|
|
|
35
35
|
import { scanForSecrets } from "./evidence.js";
|
|
36
36
|
import { LIVE_ACTIVITY_KINDS } from "./live.js";
|
|
37
37
|
import { hashSecret, mintLeadToken, parseCredential, secretMatches, tokenPurpose, tokenRotationProblem } from "./api-tokens.js";
|
|
38
|
+
import { admitTrialRun, bumpModelsGeneration, forgetProjectRoutes, liveEarnedRules, liveTrialFor, noteExposureCancelled, noteExposureFiled, noteExposureSealed, releaseTrialPlaces, reserveTrialPlace, ruleHoldOf, settleTrialPlaces, trialSealProblem, writeRunCohort } from "./route-store.js";
|
|
38
39
|
/**
|
|
39
40
|
* The database: a small task store, and the operational overlay beside it.
|
|
40
41
|
*
|
|
@@ -1110,7 +1111,13 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1110
1111
|
// read 1), a browser session's authority (web_session.authority; saved rows read 'full'), its local generation, key
|
|
1111
1112
|
// epoch and key hash (saved rows read NULL: no local binding, and a stale key), and the running console's key epoch
|
|
1112
1113
|
// (console_instance.key_epoch; NULL). V147_ADDED_COLUMNS.
|
|
1113
|
-
|
|
1114
|
+
// v148 adds, and changes no saved row (docs/plans/results-routing.md): the results-routing tables (V148_SCHEMA, new
|
|
1115
|
+
// tables only) and a run's execution class, a revision's origin and a project's "Suggest cheaper models" switch
|
|
1116
|
+
// (V148_ADDED_COLUMNS: saved rows read NULL, NULL and 1).
|
|
1117
|
+
// v149 adds, and changes no saved row (docs/plans/learning-loops.md): every learning-loops table (V149_SCHEMA, new
|
|
1118
|
+
// tables only) — each check's recorded conditions and results, mixed-outcome observations, tracked identities, lesson
|
|
1119
|
+
// notes and their tallies, memory use, tidy-up proposals and decision merges.
|
|
1120
|
+
export const SCHEMA_VERSION = 149;
|
|
1114
1121
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1115
1122
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1116
1123
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1243,7 +1250,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1243
1250
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1244
1251
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1245
1252
|
*/
|
|
1246
|
-
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, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147]);
|
|
1253
|
+
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, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149]);
|
|
1247
1254
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1248
1255
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1249
1256
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1498,6 +1505,377 @@ export const V147_ADDED_COLUMNS = Object.freeze({
|
|
|
1498
1505
|
});
|
|
1499
1506
|
/** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
|
|
1500
1507
|
export const V147_STEPS = Object.freeze(["epoch", "local-mode", "password-set", "web-session", "console-instance"]);
|
|
1508
|
+
/**
|
|
1509
|
+
* v148 (docs/plans/results-routing.md): every table the results-routing arc uses, landed at once because a current
|
|
1510
|
+
* store runs no DDL. Slice 1 writes run_cohort, route_record and models_generation; route_rule, route_exposure* and
|
|
1511
|
+
* route_trial_reservation are written from later slices. src/route-store.ts is the only writer of all of them (the
|
|
1512
|
+
* migration aside), which release-auto.ts's routing scan (route-hooks.ts) holds a release to.
|
|
1513
|
+
*
|
|
1514
|
+
* - run_cohort: one row per code-writing run (builder, repair), written in the run's admission transaction beside its
|
|
1515
|
+
* route stamp and never updated: the sealed facts its eligibility was decided from. cohort_size NULL = not eligible,
|
|
1516
|
+
* with the reason in `ineligible`.
|
|
1517
|
+
* - route_record: a task family's result record (keyed by its root task_ref), recomputed by the reconcile pass.
|
|
1518
|
+
* `settled` = its verdict (state, reason, result, acceptance, decision time) is final; its spending and counts keep
|
|
1519
|
+
* accumulating through later revision and repair runs until `closed` (the family is quiet, or 90 days after its
|
|
1520
|
+
* first run: route-results.ts ACCUMULATE_FOR_MS).
|
|
1521
|
+
* - route_rule: an earned rule or a trial, with its sealed grant, price snapshot and the configuration generations it
|
|
1522
|
+
* depends on; one live winner per project and size (route_rule_one_winner).
|
|
1523
|
+
* - route_exposure / route_exposure_event: one exposure per task and rule, its events append-only.
|
|
1524
|
+
* - route_trial_reservation: a trial's place, reserved, then consumed (once, at admission) or released.
|
|
1525
|
+
* - models_generation: a counter per (scope, phase), moved by every Models row writer in the same transaction.
|
|
1526
|
+
*/
|
|
1527
|
+
export const V148_SCHEMA = `
|
|
1528
|
+
CREATE TABLE IF NOT EXISTS run_cohort (
|
|
1529
|
+
run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
|
|
1530
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
1531
|
+
root_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
1532
|
+
repo TEXT,
|
|
1533
|
+
role TEXT NOT NULL CHECK (role IN ('builder','repair')),
|
|
1534
|
+
size TEXT CHECK (size IS NULL OR size IN ('small','medium','large')),
|
|
1535
|
+
risky INTEGER CHECK (risky IS NULL OR risky IN (0,1)),
|
|
1536
|
+
quality_mode TEXT NOT NULL CHECK (quality_mode IN ('default','strict')),
|
|
1537
|
+
evidence_demands INTEGER NOT NULL CHECK (evidence_demands IN (0,1)),
|
|
1538
|
+
publication TEXT NOT NULL CHECK (publication IN ('none','notify','automerge')),
|
|
1539
|
+
release INTEGER NOT NULL CHECK (release IN (0,1)),
|
|
1540
|
+
protected INTEGER NOT NULL CHECK (protected IN (0,1)),
|
|
1541
|
+
selection_json TEXT NOT NULL,
|
|
1542
|
+
route_digest TEXT NOT NULL,
|
|
1543
|
+
provider TEXT NOT NULL,
|
|
1544
|
+
model TEXT,
|
|
1545
|
+
cohort_size TEXT CHECK (cohort_size IS NULL OR cohort_size IN ('small','medium')),
|
|
1546
|
+
ineligible TEXT,
|
|
1547
|
+
started_at TEXT NOT NULL,
|
|
1548
|
+
CHECK ((cohort_size IS NULL) = (ineligible IS NOT NULL))
|
|
1549
|
+
);
|
|
1550
|
+
CREATE INDEX IF NOT EXISTS run_cohort_by_task ON run_cohort (task_ref);
|
|
1551
|
+
CREATE INDEX IF NOT EXISTS run_cohort_by_root ON run_cohort (repo, root_ref);
|
|
1552
|
+
CREATE TABLE IF NOT EXISTS route_record (
|
|
1553
|
+
task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
1554
|
+
repo TEXT,
|
|
1555
|
+
cohort_size TEXT CHECK (cohort_size IS NULL OR cohort_size IN ('small','medium')),
|
|
1556
|
+
provider TEXT,
|
|
1557
|
+
model TEXT,
|
|
1558
|
+
state TEXT NOT NULL CHECK (state IN ('pending','provisional','mature','rejected','not-counted')),
|
|
1559
|
+
reason TEXT,
|
|
1560
|
+
result_run INTEGER REFERENCES run(id) ON DELETE SET NULL,
|
|
1561
|
+
first_started_at TEXT,
|
|
1562
|
+
accepted_at TEXT,
|
|
1563
|
+
decided_at TEXT,
|
|
1564
|
+
model_failures INTEGER NOT NULL DEFAULT 0 CHECK (model_failures >= 0),
|
|
1565
|
+
infra_failures INTEGER NOT NULL DEFAULT 0 CHECK (infra_failures >= 0),
|
|
1566
|
+
api_microusd INTEGER NOT NULL DEFAULT 0 CHECK (api_microusd >= 0),
|
|
1567
|
+
subscription_runs INTEGER NOT NULL DEFAULT 0 CHECK (subscription_runs >= 0),
|
|
1568
|
+
subscription_tokens INTEGER NOT NULL DEFAULT 0 CHECK (subscription_tokens >= 0),
|
|
1569
|
+
unknown_runs INTEGER NOT NULL DEFAULT 0 CHECK (unknown_runs >= 0),
|
|
1570
|
+
remediation_runs INTEGER NOT NULL DEFAULT 0 CHECK (remediation_runs >= 0),
|
|
1571
|
+
person_revisions INTEGER NOT NULL DEFAULT 0 CHECK (person_revisions >= 0),
|
|
1572
|
+
first_check_passed INTEGER CHECK (first_check_passed IS NULL OR first_check_passed IN (0,1)),
|
|
1573
|
+
agent_turns INTEGER NOT NULL DEFAULT 0 CHECK (agent_turns >= 0),
|
|
1574
|
+
turns_unknown_runs INTEGER NOT NULL DEFAULT 0 CHECK (turns_unknown_runs >= 0),
|
|
1575
|
+
provenance_json TEXT NOT NULL DEFAULT '[]',
|
|
1576
|
+
settled INTEGER NOT NULL DEFAULT 0 CHECK (settled IN (0,1)),
|
|
1577
|
+
closed INTEGER NOT NULL DEFAULT 0 CHECK (closed IN (0,1)),
|
|
1578
|
+
evaluated_at TEXT NOT NULL,
|
|
1579
|
+
CHECK ((state IN ('rejected','not-counted')) = (reason IS NOT NULL)),
|
|
1580
|
+
CHECK (closed = 0 OR settled = 1)
|
|
1581
|
+
);
|
|
1582
|
+
CREATE INDEX IF NOT EXISTS route_record_by_cohort ON route_record (repo, cohort_size, state, first_started_at);
|
|
1583
|
+
CREATE INDEX IF NOT EXISTS route_record_open ON route_record (evaluated_at) WHERE closed = 0;
|
|
1584
|
+
CREATE TABLE IF NOT EXISTS route_rule (
|
|
1585
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1586
|
+
repo TEXT NOT NULL,
|
|
1587
|
+
cohort_size TEXT NOT NULL CHECK (cohort_size IN ('small','medium')),
|
|
1588
|
+
kind TEXT NOT NULL CHECK (kind IN ('rule','trial')),
|
|
1589
|
+
provider TEXT NOT NULL,
|
|
1590
|
+
model TEXT NOT NULL,
|
|
1591
|
+
configured_provider TEXT NOT NULL,
|
|
1592
|
+
configured_model TEXT,
|
|
1593
|
+
state TEXT NOT NULL CHECK (state IN ('active','trialling','paused','switched-off','turned-off','expired','dropped','finished')),
|
|
1594
|
+
generation INTEGER NOT NULL DEFAULT 1 CHECK (generation >= 1),
|
|
1595
|
+
grant_json TEXT NOT NULL,
|
|
1596
|
+
prices_json TEXT NOT NULL,
|
|
1597
|
+
depends_json TEXT NOT NULL,
|
|
1598
|
+
trial_limit INTEGER CHECK (trial_limit IS NULL OR trial_limit >= 1),
|
|
1599
|
+
accepted_by TEXT NOT NULL,
|
|
1600
|
+
accepted_at TEXT NOT NULL,
|
|
1601
|
+
ended_at TEXT,
|
|
1602
|
+
ended_by TEXT,
|
|
1603
|
+
end_reason TEXT,
|
|
1604
|
+
CHECK ((kind = 'trial') = (trial_limit IS NOT NULL))
|
|
1605
|
+
);
|
|
1606
|
+
CREATE UNIQUE INDEX IF NOT EXISTS route_rule_one_winner ON route_rule (repo, cohort_size) WHERE state IN ('active','trialling','paused');
|
|
1607
|
+
CREATE TABLE IF NOT EXISTS route_exposure (
|
|
1608
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1609
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
1610
|
+
rule INTEGER NOT NULL REFERENCES route_rule(id) ON DELETE CASCADE,
|
|
1611
|
+
created_at TEXT NOT NULL,
|
|
1612
|
+
closed_at TEXT,
|
|
1613
|
+
UNIQUE (task_ref, rule)
|
|
1614
|
+
);
|
|
1615
|
+
CREATE TABLE IF NOT EXISTS route_exposure_event (
|
|
1616
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1617
|
+
exposure INTEGER NOT NULL REFERENCES route_exposure(id) ON DELETE CASCADE,
|
|
1618
|
+
kind TEXT NOT NULL CHECK (kind IN ('proposed','refiled','approved-as-proposed','approved-with-other','cancelled')),
|
|
1619
|
+
model TEXT,
|
|
1620
|
+
at TEXT NOT NULL,
|
|
1621
|
+
CHECK ((kind = 'approved-with-other') = (model IS NOT NULL))
|
|
1622
|
+
);
|
|
1623
|
+
CREATE INDEX IF NOT EXISTS route_exposure_event_by_exposure ON route_exposure_event (exposure, id);
|
|
1624
|
+
CREATE TABLE IF NOT EXISTS route_trial_reservation (
|
|
1625
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1626
|
+
rule INTEGER NOT NULL REFERENCES route_rule(id) ON DELETE CASCADE,
|
|
1627
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
1628
|
+
scope_digest TEXT NOT NULL,
|
|
1629
|
+
state TEXT NOT NULL CHECK (state IN ('reserved','consumed','released')),
|
|
1630
|
+
consumed_run INTEGER REFERENCES run(id) ON DELETE SET NULL,
|
|
1631
|
+
created_at TEXT NOT NULL,
|
|
1632
|
+
updated_at TEXT NOT NULL,
|
|
1633
|
+
UNIQUE (rule, task_ref),
|
|
1634
|
+
CHECK (state = 'consumed' OR consumed_run IS NULL)
|
|
1635
|
+
);
|
|
1636
|
+
CREATE TABLE IF NOT EXISTS models_generation (
|
|
1637
|
+
scope TEXT NOT NULL,
|
|
1638
|
+
phase TEXT NOT NULL,
|
|
1639
|
+
generation INTEGER NOT NULL CHECK (generation >= 1),
|
|
1640
|
+
changed_at TEXT NOT NULL,
|
|
1641
|
+
PRIMARY KEY (scope, phase)
|
|
1642
|
+
);
|
|
1643
|
+
`;
|
|
1644
|
+
/** The tables V148_SCHEMA adds, in the order a test drops them to make the v147 shape. */
|
|
1645
|
+
export const V148_TABLES = Object.freeze(["route_trial_reservation", "route_exposure_event", "route_exposure", "route_rule", "route_record", "run_cohort", "models_generation"]);
|
|
1646
|
+
/** v148: a run's execution class (saved runs read NULL: none kept, which the records count as the model's), a
|
|
1647
|
+
* revision's origin (saved revisions read NULL: not recorded, never a person), and a project's "Suggest cheaper models"
|
|
1648
|
+
* switch (saved projects read 1: on). */
|
|
1649
|
+
export const V148_ADDED_COLUMNS = Object.freeze({
|
|
1650
|
+
run: { exec_class: { definition: "TEXT CHECK (exec_class IS NULL OR exec_class IN ('model','infrastructure'))", saved: "NULL" } },
|
|
1651
|
+
task_ref: { revision_origin: { definition: "TEXT CHECK (revision_origin IS NULL OR revision_origin IN ('person','automatic-review','flow','lead','machine'))", saved: "NULL" } },
|
|
1652
|
+
project: { suggest_models: { definition: "INTEGER NOT NULL DEFAULT 1 CHECK (suggest_models IN (0,1))", saved: "1" } },
|
|
1653
|
+
});
|
|
1654
|
+
/** The v148 migration's steps after v147's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1655
|
+
export const V148_STEPS = Object.freeze(["results-tables", "exec-class", "revision-origin", "suggest-models"]);
|
|
1656
|
+
/**
|
|
1657
|
+
* v149 (docs/plans/learning-loops.md): every table the learning-loops arc uses, landed at once because a current store
|
|
1658
|
+
* runs no DDL. Slice 1 writes check_fact, check_result and learning_watch; the rest are written from later slices.
|
|
1659
|
+
* Each table has one writer module (the migration aside), which release-auto's learning scan (eval-hooks.ts
|
|
1660
|
+
* learningWriteProblem) holds a release to: check_fact and check_result → check-facts.ts; local_observation → flaky.ts;
|
|
1661
|
+
* ci_flaky_observation and ci_pairing_rejected → flaky-ci.ts; learning_watch → flaky.ts and lesson-notes.ts;
|
|
1662
|
+
* lesson_note and lesson_note_tally → lesson-notes.ts; memory_use → memory-use.ts; memory_tidy_proposal and
|
|
1663
|
+
* decision_merge → memory-tidy.ts.
|
|
1664
|
+
*
|
|
1665
|
+
* Nothing here is ever read for authority: a check's outcome, a verdict and an approval never change because of it.
|
|
1666
|
+
*
|
|
1667
|
+
* Deletion follows today's semantics (runs are never deleted by ordinary work; removing a project keeps its results).
|
|
1668
|
+
* The tables are keyed by repo path with no foreign key to `project`; their run keys are plain REFERENCES with no ON
|
|
1669
|
+
* DELETE action. check_fact, learning_watch, lesson_note, lesson_note_tally, memory_use, memory_tidy_proposal and
|
|
1670
|
+
* decision_merge are kept and never deleted (their triggers allow only the columns their one writer sets: a watch's
|
|
1671
|
+
* `until`, a tally's counters, a proposal's decision, a merge's undo). check_result, local_observation,
|
|
1672
|
+
* ci_flaky_observation and ci_pairing_rejected are pruned after 180 days by the storage sweep's own step (a later
|
|
1673
|
+
* slice), the only DELETE on them; their rows never change. The whole-project deletion (project-delete.ts), which
|
|
1674
|
+
* lifts every guard for its one transaction, takes them with the project as it takes everything else.
|
|
1675
|
+
*/
|
|
1676
|
+
export const V149_SCHEMA = `
|
|
1677
|
+
CREATE TABLE IF NOT EXISTS check_fact (
|
|
1678
|
+
run INTEGER PRIMARY KEY REFERENCES run(id),
|
|
1679
|
+
repo TEXT,
|
|
1680
|
+
task_ref INTEGER NOT NULL,
|
|
1681
|
+
head TEXT,
|
|
1682
|
+
tree TEXT,
|
|
1683
|
+
command_digest TEXT,
|
|
1684
|
+
access TEXT,
|
|
1685
|
+
harness_digest TEXT,
|
|
1686
|
+
lockfile TEXT,
|
|
1687
|
+
conditions_digest TEXT,
|
|
1688
|
+
conditions_class TEXT,
|
|
1689
|
+
purpose TEXT NOT NULL CHECK (purpose IN ('ordinary','release','nightly')),
|
|
1690
|
+
learn_state TEXT NOT NULL CHECK (learn_state IN ('recorded','not-recorded','not-asked')),
|
|
1691
|
+
learn_why TEXT,
|
|
1692
|
+
recorded_at TEXT NOT NULL,
|
|
1693
|
+
CHECK ((learn_state = 'not-recorded') = (learn_why IS NOT NULL)),
|
|
1694
|
+
CHECK ((conditions_digest IS NULL) = (conditions_class IS NULL)),
|
|
1695
|
+
CHECK (conditions_digest IS NULL OR tree IS NOT NULL)
|
|
1696
|
+
);
|
|
1697
|
+
CREATE INDEX IF NOT EXISTS check_fact_by_repo ON check_fact (repo, recorded_at);
|
|
1698
|
+
CREATE INDEX IF NOT EXISTS check_fact_by_tree ON check_fact (tree, conditions_digest);
|
|
1699
|
+
CREATE TABLE IF NOT EXISTS check_result (
|
|
1700
|
+
run INTEGER NOT NULL REFERENCES check_fact(run),
|
|
1701
|
+
identity TEXT NOT NULL CHECK (length(identity) BETWEEN 1 AND 2600),
|
|
1702
|
+
state TEXT NOT NULL CHECK (state IN ('failed','passed','skipped','passed-on-retry')),
|
|
1703
|
+
source TEXT NOT NULL CHECK (source IN ('receipt','report','log','learn')),
|
|
1704
|
+
recorded_at TEXT NOT NULL,
|
|
1705
|
+
PRIMARY KEY (run, identity, source)
|
|
1706
|
+
);
|
|
1707
|
+
CREATE INDEX IF NOT EXISTS check_result_by_identity ON check_result (identity, run);
|
|
1708
|
+
CREATE TABLE IF NOT EXISTS local_observation (
|
|
1709
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1710
|
+
repo TEXT NOT NULL,
|
|
1711
|
+
identity TEXT NOT NULL,
|
|
1712
|
+
failed_run INTEGER NOT NULL REFERENCES run(id),
|
|
1713
|
+
passed_run INTEGER NOT NULL REFERENCES run(id),
|
|
1714
|
+
tree TEXT NOT NULL,
|
|
1715
|
+
conditions_digest TEXT NOT NULL,
|
|
1716
|
+
kind TEXT NOT NULL CHECK (kind IN ('regate','same-tree','in-check-retry')),
|
|
1717
|
+
observed_at TEXT NOT NULL,
|
|
1718
|
+
UNIQUE (identity, failed_run, passed_run),
|
|
1719
|
+
CHECK ((kind = 'in-check-retry') = (failed_run = passed_run))
|
|
1720
|
+
);
|
|
1721
|
+
CREATE INDEX IF NOT EXISTS local_observation_by_repo ON local_observation (repo, identity, observed_at);
|
|
1722
|
+
CREATE TABLE IF NOT EXISTS ci_flaky_observation (
|
|
1723
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1724
|
+
repo TEXT NOT NULL,
|
|
1725
|
+
provider TEXT NOT NULL CHECK (provider = 'github'),
|
|
1726
|
+
repository TEXT NOT NULL,
|
|
1727
|
+
workflow TEXT NOT NULL,
|
|
1728
|
+
lane_id TEXT NOT NULL,
|
|
1729
|
+
lane_name TEXT NOT NULL,
|
|
1730
|
+
workflow_run_id INTEGER NOT NULL CHECK (workflow_run_id >= 1),
|
|
1731
|
+
head_sha TEXT NOT NULL,
|
|
1732
|
+
failed_attempt INTEGER NOT NULL CHECK (failed_attempt >= 1),
|
|
1733
|
+
failed_job_id INTEGER NOT NULL,
|
|
1734
|
+
failed_conclusion TEXT NOT NULL,
|
|
1735
|
+
passed_attempt INTEGER NOT NULL,
|
|
1736
|
+
passed_job_id INTEGER NOT NULL,
|
|
1737
|
+
passed_conclusion TEXT NOT NULL,
|
|
1738
|
+
observed_at TEXT NOT NULL,
|
|
1739
|
+
UNIQUE (provider, repository, workflow_run_id, lane_id, failed_attempt, passed_attempt),
|
|
1740
|
+
CHECK (passed_attempt > failed_attempt)
|
|
1741
|
+
);
|
|
1742
|
+
CREATE INDEX IF NOT EXISTS ci_flaky_observation_by_repo ON ci_flaky_observation (repo, lane_id, observed_at);
|
|
1743
|
+
CREATE TABLE IF NOT EXISTS ci_pairing_rejected (
|
|
1744
|
+
provider TEXT NOT NULL CHECK (provider = 'github'),
|
|
1745
|
+
repository TEXT NOT NULL,
|
|
1746
|
+
workflow_run_id INTEGER NOT NULL CHECK (workflow_run_id >= 1),
|
|
1747
|
+
attempt INTEGER NOT NULL CHECK (attempt >= 1),
|
|
1748
|
+
lane_id TEXT,
|
|
1749
|
+
why TEXT NOT NULL,
|
|
1750
|
+
at TEXT NOT NULL,
|
|
1751
|
+
PRIMARY KEY (provider, repository, workflow_run_id, attempt, why)
|
|
1752
|
+
);
|
|
1753
|
+
CREATE TABLE IF NOT EXISTS learning_watch (
|
|
1754
|
+
repo TEXT NOT NULL,
|
|
1755
|
+
identity TEXT NOT NULL CHECK (length(identity) BETWEEN 1 AND 2600),
|
|
1756
|
+
reason TEXT NOT NULL CHECK (reason IN ('note','observation')),
|
|
1757
|
+
source_ref TEXT NOT NULL,
|
|
1758
|
+
since TEXT NOT NULL,
|
|
1759
|
+
until TEXT,
|
|
1760
|
+
PRIMARY KEY (repo, identity, reason, source_ref),
|
|
1761
|
+
CHECK (until IS NULL OR until >= since)
|
|
1762
|
+
);
|
|
1763
|
+
CREATE INDEX IF NOT EXISTS learning_watch_open ON learning_watch (repo, since) WHERE until IS NULL;
|
|
1764
|
+
CREATE TABLE IF NOT EXISTS lesson_note (
|
|
1765
|
+
proposal INTEGER PRIMARY KEY REFERENCES memory_proposal(id),
|
|
1766
|
+
repo TEXT NOT NULL,
|
|
1767
|
+
failed_run INTEGER NOT NULL REFERENCES run(id),
|
|
1768
|
+
passing_run INTEGER NOT NULL REFERENCES run(id),
|
|
1769
|
+
repair_chain INTEGER REFERENCES repair_chain(id),
|
|
1770
|
+
revision_origin TEXT CHECK (revision_origin IS NULL OR revision_origin IN ('person','automatic-review','flow','lead','machine')),
|
|
1771
|
+
identities_json TEXT NOT NULL,
|
|
1772
|
+
changed_paths_json TEXT NOT NULL,
|
|
1773
|
+
test_files_changed_json TEXT NOT NULL,
|
|
1774
|
+
facts_digest TEXT NOT NULL,
|
|
1775
|
+
created_at TEXT NOT NULL,
|
|
1776
|
+
CHECK (failed_run <> passing_run)
|
|
1777
|
+
);
|
|
1778
|
+
CREATE INDEX IF NOT EXISTS lesson_note_by_repo ON lesson_note (repo, created_at);
|
|
1779
|
+
CREATE TABLE IF NOT EXISTS lesson_note_tally (
|
|
1780
|
+
note INTEGER NOT NULL REFERENCES lesson_note(proposal),
|
|
1781
|
+
month TEXT NOT NULL CHECK (month GLOB '[0-9][0-9][0-9][0-9]-[0-1][0-9]'),
|
|
1782
|
+
executions INTEGER NOT NULL DEFAULT 0 CHECK (executions >= 0),
|
|
1783
|
+
failures INTEGER NOT NULL DEFAULT 0 CHECK (failures >= 0 AND failures <= executions),
|
|
1784
|
+
mixed_failures INTEGER NOT NULL DEFAULT 0 CHECK (mixed_failures >= 0),
|
|
1785
|
+
received_executions INTEGER NOT NULL DEFAULT 0 CHECK (received_executions >= 0 AND received_executions <= executions),
|
|
1786
|
+
received_failures INTEGER NOT NULL DEFAULT 0 CHECK (received_failures >= 0 AND received_failures <= received_executions AND received_failures <= failures),
|
|
1787
|
+
unreported_checks INTEGER NOT NULL DEFAULT 0 CHECK (unreported_checks >= 0),
|
|
1788
|
+
sources_json TEXT NOT NULL DEFAULT '[]',
|
|
1789
|
+
partial INTEGER NOT NULL DEFAULT 0 CHECK (partial IN (0,1)),
|
|
1790
|
+
PRIMARY KEY (note, month)
|
|
1791
|
+
);
|
|
1792
|
+
CREATE TABLE IF NOT EXISTS memory_use (
|
|
1793
|
+
run INTEGER NOT NULL REFERENCES run(id),
|
|
1794
|
+
repo TEXT,
|
|
1795
|
+
kind TEXT NOT NULL CHECK (kind IN ('lesson','reference','decision')),
|
|
1796
|
+
item TEXT NOT NULL,
|
|
1797
|
+
version INTEGER,
|
|
1798
|
+
phase TEXT NOT NULL,
|
|
1799
|
+
included_at TEXT,
|
|
1800
|
+
PRIMARY KEY (run, kind, item)
|
|
1801
|
+
);
|
|
1802
|
+
CREATE INDEX IF NOT EXISTS memory_use_by_item ON memory_use (repo, kind, item, run);
|
|
1803
|
+
CREATE TABLE IF NOT EXISTS memory_tidy_proposal (
|
|
1804
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1805
|
+
repo TEXT NOT NULL,
|
|
1806
|
+
kind TEXT NOT NULL CHECK (kind IN ('retire','merge','contradiction','note-failing','restore','unmerge')),
|
|
1807
|
+
items_json TEXT NOT NULL,
|
|
1808
|
+
after_text TEXT,
|
|
1809
|
+
rationale TEXT NOT NULL,
|
|
1810
|
+
evidence_json TEXT NOT NULL,
|
|
1811
|
+
source TEXT NOT NULL CHECK (source IN ('usage','duplicate','model','note','person')),
|
|
1812
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','accepted','rejected','stale')),
|
|
1813
|
+
created_at TEXT NOT NULL,
|
|
1814
|
+
decided_by TEXT,
|
|
1815
|
+
decided_proof TEXT,
|
|
1816
|
+
decided_at TEXT,
|
|
1817
|
+
CHECK ((status = 'pending') = (decided_at IS NULL)),
|
|
1818
|
+
CHECK (status IN ('accepted','rejected') OR (decided_by IS NULL AND decided_proof IS NULL)),
|
|
1819
|
+
CHECK (status NOT IN ('accepted','rejected') OR decided_by IS NOT NULL)
|
|
1820
|
+
);
|
|
1821
|
+
CREATE INDEX IF NOT EXISTS memory_tidy_proposal_pending ON memory_tidy_proposal (repo, status, kind);
|
|
1822
|
+
CREATE TABLE IF NOT EXISTS decision_merge (
|
|
1823
|
+
merge INTEGER NOT NULL,
|
|
1824
|
+
successor INTEGER NOT NULL REFERENCES project_decision(id),
|
|
1825
|
+
successor_revision INTEGER NOT NULL CHECK (successor_revision >= 1),
|
|
1826
|
+
predecessor INTEGER NOT NULL REFERENCES project_decision(id),
|
|
1827
|
+
predecessor_revision INTEGER NOT NULL CHECK (predecessor_revision >= 1),
|
|
1828
|
+
at TEXT NOT NULL,
|
|
1829
|
+
by TEXT NOT NULL,
|
|
1830
|
+
undone_at TEXT,
|
|
1831
|
+
undone_by TEXT,
|
|
1832
|
+
PRIMARY KEY (merge, predecessor),
|
|
1833
|
+
CHECK (successor <> predecessor),
|
|
1834
|
+
CHECK ((undone_at IS NULL) = (undone_by IS NULL))
|
|
1835
|
+
);
|
|
1836
|
+
CREATE INDEX IF NOT EXISTS decision_merge_by_successor ON decision_merge (successor);
|
|
1837
|
+
CREATE INDEX IF NOT EXISTS decision_merge_by_predecessor ON decision_merge (predecessor);
|
|
1838
|
+
CREATE TRIGGER IF NOT EXISTS check_fact_no_update BEFORE UPDATE ON check_fact BEGIN SELECT RAISE(ABORT,'check facts are recorded once'); END;
|
|
1839
|
+
CREATE TRIGGER IF NOT EXISTS check_fact_no_delete BEFORE DELETE ON check_fact BEGIN SELECT RAISE(ABORT,'check facts are kept'); END;
|
|
1840
|
+
CREATE TRIGGER IF NOT EXISTS check_result_no_update BEFORE UPDATE ON check_result BEGIN SELECT RAISE(ABORT,'check results are recorded once'); END;
|
|
1841
|
+
CREATE TRIGGER IF NOT EXISTS local_observation_no_update BEFORE UPDATE ON local_observation BEGIN SELECT RAISE(ABORT,'observations are recorded once'); END;
|
|
1842
|
+
CREATE TRIGGER IF NOT EXISTS ci_flaky_observation_no_update BEFORE UPDATE ON ci_flaky_observation BEGIN SELECT RAISE(ABORT,'observations are recorded once'); END;
|
|
1843
|
+
CREATE TRIGGER IF NOT EXISTS ci_pairing_rejected_no_update BEFORE UPDATE ON ci_pairing_rejected BEGIN SELECT RAISE(ABORT,'rejected pairings are recorded once'); END;
|
|
1844
|
+
CREATE TRIGGER IF NOT EXISTS learning_watch_only_ends BEFORE UPDATE ON learning_watch
|
|
1845
|
+
WHEN NEW.repo IS NOT OLD.repo OR NEW.identity IS NOT OLD.identity OR NEW.reason IS NOT OLD.reason OR NEW.source_ref IS NOT OLD.source_ref
|
|
1846
|
+
OR NEW.since IS NOT OLD.since OR OLD.until IS NOT NULL OR NEW.until IS NULL
|
|
1847
|
+
BEGIN SELECT RAISE(ABORT,'a tracked identity only ends, once'); END;
|
|
1848
|
+
CREATE TRIGGER IF NOT EXISTS learning_watch_no_delete BEFORE DELETE ON learning_watch BEGIN SELECT RAISE(ABORT,'tracked identities are kept'); END;
|
|
1849
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_no_update BEFORE UPDATE ON lesson_note BEGIN SELECT RAISE(ABORT,'lesson note facts are recorded once'); END;
|
|
1850
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_no_delete BEFORE DELETE ON lesson_note BEGIN SELECT RAISE(ABORT,'lesson notes are kept'); END;
|
|
1851
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_tally_only_counts BEFORE UPDATE ON lesson_note_tally
|
|
1852
|
+
WHEN NEW.note IS NOT OLD.note OR NEW.month IS NOT OLD.month OR NEW.executions < OLD.executions OR NEW.failures < OLD.failures
|
|
1853
|
+
OR NEW.mixed_failures < OLD.mixed_failures OR NEW.received_executions < OLD.received_executions OR NEW.received_failures < OLD.received_failures
|
|
1854
|
+
OR NEW.unreported_checks < OLD.unreported_checks OR NEW.partial < OLD.partial
|
|
1855
|
+
BEGIN SELECT RAISE(ABORT,'a tally only counts up'); END;
|
|
1856
|
+
CREATE TRIGGER IF NOT EXISTS lesson_note_tally_no_delete BEFORE DELETE ON lesson_note_tally BEGIN SELECT RAISE(ABORT,'tallies are never pruned'); END;
|
|
1857
|
+
CREATE TRIGGER IF NOT EXISTS memory_use_no_update BEFORE UPDATE ON memory_use BEGIN SELECT RAISE(ABORT,'memory use is recorded once'); END;
|
|
1858
|
+
CREATE TRIGGER IF NOT EXISTS memory_use_no_delete BEFORE DELETE ON memory_use BEGIN SELECT RAISE(ABORT,'memory use is kept'); END;
|
|
1859
|
+
CREATE TRIGGER IF NOT EXISTS memory_tidy_proposal_only_decides BEFORE UPDATE ON memory_tidy_proposal
|
|
1860
|
+
WHEN NEW.id IS NOT OLD.id OR NEW.repo IS NOT OLD.repo OR NEW.kind IS NOT OLD.kind OR NEW.items_json IS NOT OLD.items_json
|
|
1861
|
+
OR NEW.after_text IS NOT OLD.after_text OR NEW.rationale IS NOT OLD.rationale OR NEW.evidence_json IS NOT OLD.evidence_json
|
|
1862
|
+
OR NEW.source IS NOT OLD.source OR NEW.created_at IS NOT OLD.created_at OR OLD.status <> 'pending'
|
|
1863
|
+
BEGIN SELECT RAISE(ABORT,'a tidy-up proposal is decided once'); END;
|
|
1864
|
+
CREATE TRIGGER IF NOT EXISTS memory_tidy_proposal_no_delete BEFORE DELETE ON memory_tidy_proposal BEGIN SELECT RAISE(ABORT,'tidy-up proposals are kept'); END;
|
|
1865
|
+
CREATE TRIGGER IF NOT EXISTS decision_merge_only_undone BEFORE UPDATE ON decision_merge
|
|
1866
|
+
WHEN NEW.merge IS NOT OLD.merge OR NEW.successor IS NOT OLD.successor OR NEW.successor_revision IS NOT OLD.successor_revision
|
|
1867
|
+
OR NEW.predecessor IS NOT OLD.predecessor OR NEW.predecessor_revision IS NOT OLD.predecessor_revision OR NEW.at IS NOT OLD.at
|
|
1868
|
+
OR NEW.by IS NOT OLD.by OR OLD.undone_at IS NOT NULL
|
|
1869
|
+
BEGIN SELECT RAISE(ABORT,'a merge is undone once, never rewritten'); END;
|
|
1870
|
+
CREATE TRIGGER IF NOT EXISTS decision_merge_no_delete BEFORE DELETE ON decision_merge BEGIN SELECT RAISE(ABORT,'merges are kept'); END;
|
|
1871
|
+
`;
|
|
1872
|
+
/** The tables V149_SCHEMA adds, in the order a test drops them to make the v148 shape (dependents first). */
|
|
1873
|
+
export const V149_TABLES = Object.freeze(["check_result", "check_fact", "local_observation", "ci_flaky_observation", "ci_pairing_rejected", "learning_watch",
|
|
1874
|
+
"lesson_note_tally", "lesson_note", "memory_use", "memory_tidy_proposal", "decision_merge"]);
|
|
1875
|
+
/** The v149 tables a whole-project deletion clears by repo (the rest hang off these or a deleted run or decision). */
|
|
1876
|
+
export const V149_REPO_TABLES = Object.freeze(["check_fact", "local_observation", "ci_flaky_observation", "learning_watch", "lesson_note", "memory_use", "memory_tidy_proposal"]);
|
|
1877
|
+
/** The v149 migration's steps after v148's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1878
|
+
export const V149_STEPS = Object.freeze(["learning-tables"]);
|
|
1501
1879
|
/**
|
|
1502
1880
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1503
1881
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4776,6 +5154,22 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4776
5154
|
for (const [column, { definition }] of Object.entries(V147_ADDED_COLUMNS["console_instance"]))
|
|
4777
5155
|
addColumn(db, "console_instance", column, definition);
|
|
4778
5156
|
step("console-instance");
|
|
5157
|
+
// v148: the results-routing tables (new tables only) and the execution class, revision origin and suggestion switch
|
|
5158
|
+
// (V148_ADDED_COLUMNS: saved rows read their declared values). Each step is idempotent, as v147's are.
|
|
5159
|
+
db.exec(V148_SCHEMA);
|
|
5160
|
+
step("results-tables");
|
|
5161
|
+
for (const [column, { definition }] of Object.entries(V148_ADDED_COLUMNS["run"]))
|
|
5162
|
+
addColumn(db, "run", column, definition);
|
|
5163
|
+
step("exec-class");
|
|
5164
|
+
for (const [column, { definition }] of Object.entries(V148_ADDED_COLUMNS["task_ref"]))
|
|
5165
|
+
addColumn(db, "task_ref", column, definition);
|
|
5166
|
+
step("revision-origin");
|
|
5167
|
+
for (const [column, { definition }] of Object.entries(V148_ADDED_COLUMNS["project"]))
|
|
5168
|
+
addColumn(db, "project", column, definition);
|
|
5169
|
+
step("suggest-models");
|
|
5170
|
+
// v149: the learning-loops tables (new tables only; docs/plans/learning-loops.md). Idempotent, as v148's steps are.
|
|
5171
|
+
db.exec(V149_SCHEMA);
|
|
5172
|
+
step("learning-tables");
|
|
4779
5173
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4780
5174
|
// nothing to reclaim.
|
|
4781
5175
|
if (!fresh)
|
|
@@ -6166,6 +6560,12 @@ export class Store {
|
|
|
6166
6560
|
}
|
|
6167
6561
|
if (ref !== undefined && prior?.["state"] !== "cancelled")
|
|
6168
6562
|
this.noteTaskAct(Number(ref["id"]), "cancelled", now);
|
|
6563
|
+
// Results routing: a cancel closes the task's open exposure to an earned rule.
|
|
6564
|
+
if (ref !== undefined)
|
|
6565
|
+
noteExposureCancelled(this, Number(ref["id"]), now);
|
|
6566
|
+
// …and gives up any trial place it holds unconsumed (a consumed place is never released).
|
|
6567
|
+
if (ref !== undefined)
|
|
6568
|
+
releaseTrialPlaces(this, Number(ref["id"]), now);
|
|
6169
6569
|
// A cancelled task's pending steering settles superseded in the SAME
|
|
6170
6570
|
// transaction, whichever road cancelled it — a note silently waiting
|
|
6171
6571
|
// for a build that can no longer happen is wrong on every road, not
|
|
@@ -7293,6 +7693,9 @@ export class Store {
|
|
|
7293
7693
|
const problems = routeProblems(route);
|
|
7294
7694
|
if (problems.length > 0)
|
|
7295
7695
|
unresolvedReason = problems.join("; ");
|
|
7696
|
+
// A trial's place is one task's (results routing slice 3): a frozen route never carries one to another.
|
|
7697
|
+
else if (legOf(route, "build").selection?.kind === "trial")
|
|
7698
|
+
unresolvedReason = "a frozen agent route can't try a cheaper model — a trial place belongs to the task it was reserved for";
|
|
7296
7699
|
}
|
|
7297
7700
|
}
|
|
7298
7701
|
else if (!candidatesResolved.ok) {
|
|
@@ -7308,7 +7711,16 @@ export class Store {
|
|
|
7308
7711
|
unresolvedReason = planPin.problem;
|
|
7309
7712
|
}
|
|
7310
7713
|
else {
|
|
7311
|
-
route
|
|
7714
|
+
// Results routing (route v3): the project's live earned rules and trial, and the two eligibility facts the
|
|
7715
|
+
// route doesn't carry. A release or candidate is read conservatively: terms this filing may still drop count.
|
|
7716
|
+
const earnedBase = ref === null || ref.backend !== BUILT_IN || ref.repo === null ? null : {
|
|
7717
|
+
rules: liveEarnedRules(this, ref.repo),
|
|
7718
|
+
release: (scope.candidate ?? null) !== null || options.releaseTerms != null || options.releaseCheck != null
|
|
7719
|
+
|| prior?.["release_terms_json"] != null || prior?.["release_check_json"] != null || releaseTaskRef(this, ref.id) || nightlyTaskRef(this, ref.id),
|
|
7720
|
+
protected: isProtectedWork(this.approvalRules(ref.repo), scope.touches),
|
|
7721
|
+
};
|
|
7722
|
+
const trial = earnedBase === null || ref === null ? null : liveTrialFor(this, ref.repo, ref.sizing?.size);
|
|
7723
|
+
const routeWith = (reservation) => recommendRoute({
|
|
7312
7724
|
qualityMode,
|
|
7313
7725
|
evidence: [...new Set(scope.acceptance.flatMap(one => one.evidence))],
|
|
7314
7726
|
publication: this.publicationAuthorityOf(ref?.repo ?? null, new Date(scope.proposedAt)),
|
|
@@ -7319,7 +7731,19 @@ export class Store {
|
|
|
7319
7731
|
// with the route like every other input.
|
|
7320
7732
|
size: ref?.sizing ?? null,
|
|
7321
7733
|
headroom: this.planHeadroom(new Date(scope.proposedAt)),
|
|
7734
|
+
earned: earnedBase === null ? null : { ...earnedBase, ...(trial === null ? {} : { trial: { ...trial, reservation } }) },
|
|
7322
7735
|
});
|
|
7736
|
+
// A TRIAL'S PLACE (results routing slice 3): asked first whether this filing would take the trial at all (a
|
|
7737
|
+
// pure probe, nothing written), then its place reserved here, in this write transaction, and the route made
|
|
7738
|
+
// with that place (or with none left: the configured builder runs it). A pin, an override or an ineligible
|
|
7739
|
+
// task never takes a place.
|
|
7740
|
+
route = routeWith(trial === null ? null : Number.MAX_SAFE_INTEGER);
|
|
7741
|
+
if (trial !== null && ref !== null && legOf(route, "build").selection?.kind === "trial") {
|
|
7742
|
+
route = routeWith(reserveTrialPlace(this, trial.id, ref.id, new Date(scope.proposedAt)));
|
|
7743
|
+
}
|
|
7744
|
+
else if (trial !== null) {
|
|
7745
|
+
route = routeWith(null);
|
|
7746
|
+
}
|
|
7323
7747
|
const problems = routeProblems(route);
|
|
7324
7748
|
if (problems.length > 0)
|
|
7325
7749
|
unresolvedReason = problems.join("; ");
|
|
@@ -7463,6 +7887,16 @@ export class Store {
|
|
|
7463
7887
|
// or a remote API run keeps any mark already there and never erases a
|
|
7464
7888
|
// quarantine by refiling, and the lead's own text is marked `lead`.
|
|
7465
7889
|
this.db.prepare("UPDATE task_scope SET proposed_via = ? WHERE task_id = ?").run(provenanceAfter(prior?.["proposed_via"], options.proposedVia ?? null), scope.taskId);
|
|
7890
|
+
// Results routing (exposure, route-store.ts): a build leg under an earned rule opens the task's one exposure to
|
|
7891
|
+
// it; a re-filing adds an event to an open one, never a row.
|
|
7892
|
+
if (ref !== null && route !== null)
|
|
7893
|
+
noteExposureFiled(this, ref.id, legOf(route, "build").selection, new Date(scope.proposedAt));
|
|
7894
|
+
// Results routing (trials, route-store.ts): the place this route seals is bound to the digest it was filed under
|
|
7895
|
+
// (repeated saves reuse it); any other place this task held unconsumed is released.
|
|
7896
|
+
if (ref !== null) {
|
|
7897
|
+
const selection = route === null ? undefined : legOf(route, "build").selection;
|
|
7898
|
+
settleTrialPlaces(this, ref.id, options.route !== undefined ? undefined : selection, boundDigest, new Date(scope.proposedAt));
|
|
7899
|
+
}
|
|
7466
7900
|
return null;
|
|
7467
7901
|
}));
|
|
7468
7902
|
}
|
|
@@ -7584,6 +8018,17 @@ export class Store {
|
|
|
7584
8018
|
return false;
|
|
7585
8019
|
if (routeProblems(authority.route).length > 0)
|
|
7586
8020
|
return false;
|
|
8021
|
+
// RESULTS ROUTING (route v3): a build leg selected by an earned rule that switched itself off since it was
|
|
8022
|
+
// filed seals nothing — re-filing routes it on the configured builder (route-rules.ts holds what was approved).
|
|
8023
|
+
const sealedSelection = legOf(authority.route, "build").selection;
|
|
8024
|
+
if (sealedSelection?.kind === "rule" && this.db.prepare("SELECT state FROM route_rule WHERE id = ?").get(sealedSelection.rule)?.["state"] === "switched-off")
|
|
8025
|
+
return false;
|
|
8026
|
+
// A TRIAL (results routing slice 3): only a person's own yes seals it — the ceremony or their paired-chat tap,
|
|
8027
|
+
// never a mode, a grant, a schedule, an automatic seal, a carry or the lead — and only while its place stands.
|
|
8028
|
+
const trialRef = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
|
|
8029
|
+
if (trialSealProblem(this, { taskRef: trialRef === undefined ? null : Number(trialRef["id"]), selection: sealedSelection, digest: String(working["digest"] ?? ""),
|
|
8030
|
+
basis: basis === undefined ? null : basis.kind, by, machine: machineActing() }) !== null)
|
|
8031
|
+
return false;
|
|
7587
8032
|
// RELEASE TERMS (release-once.md): only a person's own yes (the ceremony or a paired-chat tap) seals them, and
|
|
7588
8033
|
// that yes opens the release's authority record in this same transaction — or the seal doesn't stand.
|
|
7589
8034
|
const terms = storedReleaseTerms(working["release_terms_json"]);
|
|
@@ -7650,9 +8095,18 @@ export class Store {
|
|
|
7650
8095
|
if (basis === undefined || basis.kind === "chat")
|
|
7651
8096
|
this.noteTaskAct(Number(ref["id"]), "approved", now);
|
|
7652
8097
|
this.db.prepare("DELETE FROM hold WHERE owner_kind = 'backoff' AND owner_id = ?").run(`stale:${Number(ref["id"])}`);
|
|
8098
|
+
// Results routing: this yes closes the task's open exposure (as proposed, or with the model it sealed),
|
|
8099
|
+
// and lifts the hold a switched-off rule put on the approval it replaces.
|
|
8100
|
+
noteExposureSealed(this, Number(ref["id"]), { selection: sealedSelection, model: legOf(authority.route, "build").model }, now);
|
|
8101
|
+
this.db.prepare("DELETE FROM hold WHERE owner_kind = 'backoff' AND owner_id = ?").run(`rule-off:${Number(ref["id"])}`);
|
|
7653
8102
|
this.db
|
|
7654
8103
|
.prepare("UPDATE notification SET resolved_at = ? WHERE dedupe_key LIKE ? AND resolved_at IS NULL")
|
|
7655
8104
|
.run(now.toISOString(), `stale-approval:${Number(ref["id"])}:%`);
|
|
8105
|
+
// One-tap release approval (trusted-release-reports.md A5): the yes, by any road, retires the task's
|
|
8106
|
+
// "ready for your approval" rows, so a push not delivered yet is never sent after it.
|
|
8107
|
+
this.db
|
|
8108
|
+
.prepare("UPDATE notification SET resolved_at = ? WHERE kind = 'release-ready' AND task_ref = ? AND resolved_at IS NULL")
|
|
8109
|
+
.run(now.toISOString(), Number(ref["id"]));
|
|
7656
8110
|
// The approval fact (Telegram task updates): a NEW yes on these
|
|
7657
8111
|
// exact bytes. Re-sealing an approval that already stands on
|
|
7658
8112
|
// the same digest is the same state and says nothing.
|
|
@@ -11778,22 +12232,32 @@ export class Store {
|
|
|
11778
12232
|
}
|
|
11779
12233
|
/** Write one complete pair. Audited: who changed spend routing, and when. */
|
|
11780
12234
|
setPhaseConfig(scope, phase, provider, model, by, now) {
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
this.
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
12235
|
+
// Results routing (v148): a changed row moves its (scope, phase) generation in the same transaction, which pauses
|
|
12236
|
+
// any rule that depended on it (route-store.ts bumpModelsGeneration).
|
|
12237
|
+
this.transact(() => {
|
|
12238
|
+
const was = this.phaseConfig(scope, phase);
|
|
12239
|
+
const words = (agent) => agent === null ? "not set" : `${agent.provider} · ${agent.model ?? "its default model"}`;
|
|
12240
|
+
this.recordPolicy(by, scope === "installation" ? null : scope, `agent for ${phase} changed`, words(was), words({ provider, model }), now);
|
|
12241
|
+
this.db
|
|
12242
|
+
.prepare(`INSERT INTO phase_config (scope, phase, provider, model, updated_at, updated_by)
|
|
12243
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
12244
|
+
ON CONFLICT (scope, phase) DO UPDATE SET
|
|
12245
|
+
provider = excluded.provider, model = excluded.model,
|
|
12246
|
+
updated_at = excluded.updated_at, updated_by = excluded.updated_by`)
|
|
12247
|
+
.run(scope, phase, provider, model, now.toISOString(), by);
|
|
12248
|
+
if (was === null || was.provider !== provider || was.model !== model)
|
|
12249
|
+
bumpModelsGeneration(this, scope, phase, now);
|
|
12250
|
+
});
|
|
11791
12251
|
}
|
|
11792
12252
|
clearPhaseConfig(scope, phase) {
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
12253
|
+
return this.transact(() => {
|
|
12254
|
+
const { changes } = this.db
|
|
12255
|
+
.prepare("DELETE FROM phase_config WHERE scope = ? AND phase = ?")
|
|
12256
|
+
.run(scope, phase);
|
|
12257
|
+
if (Number(changes) > 0)
|
|
12258
|
+
bumpModelsGeneration(this, scope, phase, new Date());
|
|
12259
|
+
return Number(changes) > 0;
|
|
12260
|
+
});
|
|
11797
12261
|
}
|
|
11798
12262
|
// ---- phase routing (v47) -----------------------------------------------
|
|
11799
12263
|
/** The operator's named STRONG candidate for a phase at a scope, or null.
|
|
@@ -11827,16 +12291,25 @@ export class Store {
|
|
|
11827
12291
|
// A small change makes no plan and repairs resume the builder: only the build phase has a light agent.
|
|
11828
12292
|
if (tier === "light" && phase !== "build")
|
|
11829
12293
|
throw new Error(`a light agent is for the build phase only, not ${phase}`);
|
|
11830
|
-
this.
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
12294
|
+
this.transact(() => {
|
|
12295
|
+
const was = this.phaseTierConfig(scope, phase, tier);
|
|
12296
|
+
this.db
|
|
12297
|
+
.prepare(`INSERT INTO phase_tier_config (scope, phase, tier, provider, model, updated_at, updated_by)
|
|
12298
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
12299
|
+
ON CONFLICT (scope, phase, tier) DO UPDATE SET provider = excluded.provider, model = excluded.model,
|
|
12300
|
+
updated_at = excluded.updated_at, updated_by = excluded.updated_by`)
|
|
12301
|
+
.run(scope, phase, tier, provider, model, now.toISOString(), by);
|
|
12302
|
+
if (was === null || was.provider !== provider || was.model !== model)
|
|
12303
|
+
bumpModelsGeneration(this, scope, phase, now);
|
|
12304
|
+
});
|
|
11836
12305
|
}
|
|
11837
12306
|
clearPhaseTierConfig(scope, phase, tier) {
|
|
11838
|
-
|
|
11839
|
-
|
|
12307
|
+
return this.transact(() => {
|
|
12308
|
+
const { changes } = this.db.prepare("DELETE FROM phase_tier_config WHERE scope = ? AND phase = ? AND tier = ?").run(scope, phase, tier);
|
|
12309
|
+
if (Number(changes) > 0)
|
|
12310
|
+
bumpModelsGeneration(this, scope, phase, new Date());
|
|
12311
|
+
return Number(changes) > 0;
|
|
12312
|
+
});
|
|
11840
12313
|
}
|
|
11841
12314
|
/** A tier's candidates on other providers at a scope, in provider order (sized routing). */
|
|
11842
12315
|
phaseTierAlternates(scope, phase, tier) {
|
|
@@ -11863,19 +12336,28 @@ export class Store {
|
|
|
11863
12336
|
setPhaseTierAlternate(scope, phase, tier, provider, model, by, now) {
|
|
11864
12337
|
if (tier === "light" && phase !== "build")
|
|
11865
12338
|
throw new Error(`a light agent is for the build phase only, not ${phase}`);
|
|
11866
|
-
this.
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
12339
|
+
this.transact(() => {
|
|
12340
|
+
const was = this.phaseTierAlternates(scope, phase, tier).find(one => one.provider === provider) ?? null;
|
|
12341
|
+
this.db
|
|
12342
|
+
.prepare(`INSERT INTO phase_tier_alternate (scope, phase, tier, provider, model, updated_at, updated_by)
|
|
12343
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
12344
|
+
ON CONFLICT (scope, phase, tier, provider) DO UPDATE SET model = excluded.model,
|
|
12345
|
+
updated_at = excluded.updated_at, updated_by = excluded.updated_by`)
|
|
12346
|
+
.run(scope, phase, tier, provider, model, now.toISOString(), by);
|
|
12347
|
+
if (was === null || was.model !== model)
|
|
12348
|
+
bumpModelsGeneration(this, scope, phase, now);
|
|
12349
|
+
});
|
|
11872
12350
|
}
|
|
11873
12351
|
/** Remove one provider's candidate from a tier, or every one when no provider is named; how many went. */
|
|
11874
12352
|
clearPhaseTierAlternates(scope, phase, tier, provider) {
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
12353
|
+
return this.transact(() => {
|
|
12354
|
+
const { changes } = provider === null
|
|
12355
|
+
? this.db.prepare("DELETE FROM phase_tier_alternate WHERE scope = ? AND phase = ? AND tier = ?").run(scope, phase, tier)
|
|
12356
|
+
: this.db.prepare("DELETE FROM phase_tier_alternate WHERE scope = ? AND phase = ? AND tier = ? AND provider = ?").run(scope, phase, tier, provider);
|
|
12357
|
+
if (Number(changes) > 0)
|
|
12358
|
+
bumpModelsGeneration(this, scope, phase, new Date());
|
|
12359
|
+
return Number(changes);
|
|
12360
|
+
});
|
|
11879
12361
|
}
|
|
11880
12362
|
/** Each provider's plan headroom right now, from the latest readings of
|
|
11881
12363
|
* its usage windows — the routing policy's headroom input at filing. */
|
|
@@ -13288,6 +13770,14 @@ export class Store {
|
|
|
13288
13770
|
const problem = this.routeAdmissionProblem({ taskRef: run.taskRef, role, provider: provider ?? "claude", model }, run.route, run.now);
|
|
13289
13771
|
if (problem !== null)
|
|
13290
13772
|
refuse(problem);
|
|
13773
|
+
// RESULTS ROUTING (route v3): a task approved under an earned rule that then switched itself off on its sealed
|
|
13774
|
+
// trigger is held — its first build never starts on that model (no strike: nothing ran). A rule a person turned
|
|
13775
|
+
// off, or one that expired, keeps the sealed model; a started task's later attempts finish under it.
|
|
13776
|
+
if (role === "builder") {
|
|
13777
|
+
const held = ruleHoldOf(this, taskId);
|
|
13778
|
+
if (held !== null)
|
|
13779
|
+
refuse(`rule-switched-off: results for ${held.model} dropped after this was approved — approve it again or re-file it`);
|
|
13780
|
+
}
|
|
13291
13781
|
const stamp = run.route;
|
|
13292
13782
|
const qualityMode = role === "reviewer" && parent !== null
|
|
13293
13783
|
? parent.qualityMode ?? "default"
|
|
@@ -13319,6 +13809,16 @@ export class Store {
|
|
|
13319
13809
|
// The provenance row lands in the same transaction as the run row —
|
|
13320
13810
|
// proved above, written here; a run and its route are one fact.
|
|
13321
13811
|
this.writeRunRoute(id, stamp, run.now);
|
|
13812
|
+
// Results routing (v148): a code-writing run's cohort snapshot, beside its route in this same transaction and
|
|
13813
|
+
// never updated (route-store.ts writeRunCohort; a snapshot it can't compute is recorded as not eligible).
|
|
13814
|
+
if (role === "builder" || role === "repair") {
|
|
13815
|
+
writeRunCohort(this, { run: id, taskRef: run.taskRef, role, routeDigest: stamp.routeDigest, provider: provider ?? "claude", model, qualityMode, startedAt: run.now.toISOString() });
|
|
13816
|
+
// A TRIAL (results routing slice 3): a person's own approval, and its place consumed exactly once against the
|
|
13817
|
+
// sealed digest (a retry or repair of the same task consumes nothing more). Refused → the row rolls back.
|
|
13818
|
+
const trialProblem = admitTrialRun(this, { taskId, taskRef: run.taskRef, role, run: id, now: run.now });
|
|
13819
|
+
if (trialProblem !== null)
|
|
13820
|
+
refuse(trialProblem);
|
|
13821
|
+
}
|
|
13322
13822
|
if (role === "reviewer" && parent?.role === "reviewer" && parent.watchIncarnation != null) {
|
|
13323
13823
|
this.db.prepare("UPDATE run SET watch_incarnation = ? WHERE id = ?").run(parent.watchIncarnation, id);
|
|
13324
13824
|
}
|
|
@@ -18554,12 +19054,20 @@ export class Store {
|
|
|
18554
19054
|
* what kind of answer it is (needs-you.ts reads it): `alive` while
|
|
18555
19055
|
* something of the run may still be running, `elsewhere` when only
|
|
18556
19056
|
* another computer can tell, `unprovable` when nothing is known to run
|
|
18557
|
-
* but its exit was never recorded
|
|
18558
|
-
*
|
|
18559
|
-
*
|
|
18560
|
-
*
|
|
19057
|
+
* but its exit was never recorded (Toolroll's own look, stop-proof.ts,
|
|
19058
|
+
* settles it), `unknown` when this computer can't check, or that look
|
|
19059
|
+
* was blocked. `settling` already meets the automatic reconciliation
|
|
19060
|
+
* proof. Nobody is ever asked to confirm a build stopped. `cause` names
|
|
19061
|
+
* what stop-proof.ts can prove. */
|
|
18561
19062
|
stopQuiescenceFact(runId) {
|
|
18562
|
-
const fact = (run, kind, problem) =>
|
|
19063
|
+
const fact = (run, kind, problem, cause) => cause === undefined ? { run, kind, problem } : { run, kind, problem, cause };
|
|
19064
|
+
// What Toolroll can prove on its own (stop-proof.ts): until its own check of this computer's processes has run, the
|
|
19065
|
+
// answer is `unprovable` (it settles by itself); once that check could not look, or found a process it didn't start
|
|
19066
|
+
// in the build's folder, it is `unknown`, and the plain reason says why. Nobody is asked to vouch either way.
|
|
19067
|
+
const provable = (run, problem, cause) => {
|
|
19068
|
+
const verdict = stopProofVerdictOf(this, run);
|
|
19069
|
+
return verdict === null ? fact(run, "unprovable", problem, cause) : fact(run, "unknown", `run #${run} ${verdict}`, cause);
|
|
19070
|
+
};
|
|
18563
19071
|
const ids = this.ownedRunsOf(runId);
|
|
18564
19072
|
if (ids.length === 0)
|
|
18565
19073
|
return fact(runId, "open", `run #${runId} does not exist`);
|
|
@@ -18572,7 +19080,7 @@ export class Store {
|
|
|
18572
19080
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
18573
19081
|
let incompleteSpawn = false;
|
|
18574
19082
|
if (run.providerStartedAt !== null && witnesses.length === 0 && !this.processesSummarized(id)) {
|
|
18575
|
-
return
|
|
19083
|
+
return provable(id, `run #${id} may have spawned before its process witness was recorded; exit is unproven`, "no-witness");
|
|
18576
19084
|
}
|
|
18577
19085
|
for (const witness of witnesses) {
|
|
18578
19086
|
// A recorded group exit proves the whole group ended, not just its
|
|
@@ -18621,20 +19129,27 @@ export class Store {
|
|
|
18621
19129
|
return fact(id, "alive", `run #${id}'s workspace is still held by process ${occupied.by}`);
|
|
18622
19130
|
}
|
|
18623
19131
|
catch {
|
|
18624
|
-
// An unreadable workspace note is no proof either way;
|
|
18625
|
-
|
|
18626
|
-
|
|
19132
|
+
// An unreadable workspace note is no proof either way; Toolroll's own look at this computer's processes settles
|
|
19133
|
+
// it (stop-proof.ts), as an approver's recorded word did before.
|
|
19134
|
+
if (!this.workspaceConfirmedByApprover(id)) {
|
|
19135
|
+
const verdict = stopProofVerdictOf(this, id);
|
|
19136
|
+
return fact(id, "unknown", verdict === null ? `run #${id}'s workspace occupancy could not be established` : `run #${id} ${verdict}`, "workspace-unreadable");
|
|
19137
|
+
}
|
|
18627
19138
|
}
|
|
18628
19139
|
}
|
|
18629
|
-
if (incompleteSpawn)
|
|
18630
|
-
|
|
19140
|
+
if (incompleteSpawn) {
|
|
19141
|
+
if (this.settleableUnspawnedWitnesses(run, witnesses).length > 0)
|
|
19142
|
+
return fact(id, "settling", `run #${id} has an incomplete spawn witness; exit is unproven`);
|
|
19143
|
+
return provable(id, `run #${id} has an incomplete spawn witness; exit is unproven`, "incomplete-spawn");
|
|
19144
|
+
}
|
|
18631
19145
|
}
|
|
18632
19146
|
return null;
|
|
18633
19147
|
}
|
|
18634
|
-
/**
|
|
18635
|
-
*
|
|
19148
|
+
/** A run whose workspace note couldn't be read was proven free: by Toolroll's own look (stop-proof.ts), or, in
|
|
19149
|
+
* ledgers from before it, by an approver's recorded word (still read, never written now). */
|
|
18636
19150
|
workspaceConfirmedByApprover(runId) {
|
|
18637
|
-
|
|
19151
|
+
// An older approver's word still counts (ledger entries before stop-proof.ts); Toolroll's own proof counts the same.
|
|
19152
|
+
return this.db.prepare("SELECT 1 FROM action_ledger WHERE run_id = ? AND action IN ('workspace settled by approver', ?) LIMIT 1").get(runId, STOP_PROOF_WORKSPACE_FREE) !== undefined;
|
|
18638
19153
|
}
|
|
18639
19154
|
reserveRunProcess(runId, now, group = true) {
|
|
18640
19155
|
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
|
|
@@ -18723,7 +19238,7 @@ export class Store {
|
|
|
18723
19238
|
* here, every other witness of the run has exited and none of the run's
|
|
18724
19239
|
* process groups is alive, it settles as never started, with a ledger
|
|
18725
19240
|
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
18726
|
-
*
|
|
19241
|
+
* Toolroll's own look (stop-proof.ts). Native witnesses keep their OS-only road. */
|
|
18727
19242
|
settleUnspawnedWitnesses(now, onlyRun) {
|
|
18728
19243
|
const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
18729
19244
|
WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
|
|
@@ -18772,84 +19287,6 @@ export class Store {
|
|
|
18772
19287
|
return [];
|
|
18773
19288
|
return pidless;
|
|
18774
19289
|
}
|
|
18775
|
-
/** `run settle` (an approver's last resort): every witness of a finished
|
|
18776
|
-
* run that cannot be proven either way is recorded settled under the
|
|
18777
|
-
* approver's reason, in the ledger. Refuses while anything of the run is
|
|
18778
|
-
* alive or could be checked only from another host. Never signals. */
|
|
18779
|
-
settleRunWitnessesByApprover(args, now) {
|
|
18780
|
-
return this.transact(() => {
|
|
18781
|
-
const run = this.getRun(args.runId);
|
|
18782
|
-
if (run === null)
|
|
18783
|
-
return { ok: false, reason: "unknown-run", detail: `no run #${args.runId}` };
|
|
18784
|
-
if (run.outcome === null)
|
|
18785
|
-
return { ok: false, reason: "still-running", detail: `run #${args.runId} is still open` };
|
|
18786
|
-
const alive = (detail) => ({ ok: false, reason: "alive", detail });
|
|
18787
|
-
if (ownedProcessCount(runOwnerTag(this, args.runId)) > 0)
|
|
18788
|
-
return alive(`run #${args.runId} still has an owned subprocess`);
|
|
18789
|
-
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(args.runId);
|
|
18790
|
-
const open = rows.filter(row => row["exited_at"] === null);
|
|
18791
|
-
for (const row of rows) {
|
|
18792
|
-
if (row["pid"] === null)
|
|
18793
|
-
continue;
|
|
18794
|
-
if (row["host"] !== hostname()) {
|
|
18795
|
-
if (row["exited_at"] === null)
|
|
18796
|
-
return alive(`run #${args.runId}'s process ${String(row["pid"])} belongs to ${String(row["host"])}; settle it there`);
|
|
18797
|
-
continue;
|
|
18798
|
-
}
|
|
18799
|
-
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] }))
|
|
18800
|
-
return alive(`run #${args.runId}'s process ${String(row["pid"])} is still running`);
|
|
18801
|
-
}
|
|
18802
|
-
for (const row of open) {
|
|
18803
|
-
if (row["pid"] === null && row["host"] !== hostname())
|
|
18804
|
-
return alive(`run #${args.runId} has a witness from ${String(row["host"])}; settle it there`);
|
|
18805
|
-
if (row["containment"] != null && row["container"] != null && row["container_empty_at"] == null &&
|
|
18806
|
-
containerEmptiness(String(row["containment"]), String(row["container"]), process.platform, row["container_identity"] == null ? null : String(row["container_identity"])) === "populated") {
|
|
18807
|
-
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
18808
|
-
}
|
|
18809
|
-
}
|
|
18810
|
-
// A workspace still held by a live process refuses; one whose note can't
|
|
18811
|
-
// be read is settled by the approver's word, in the ledger.
|
|
18812
|
-
let workspace = false;
|
|
18813
|
-
if (run.worktree !== null) {
|
|
18814
|
-
try {
|
|
18815
|
-
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
18816
|
-
if (occupied.held)
|
|
18817
|
-
return alive(`run #${args.runId}'s workspace is still held by process ${occupied.by}`);
|
|
18818
|
-
}
|
|
18819
|
-
catch {
|
|
18820
|
-
workspace = !this.workspaceConfirmedByApprover(args.runId);
|
|
18821
|
-
}
|
|
18822
|
-
}
|
|
18823
|
-
const ref = this.refById(run.taskRef);
|
|
18824
|
-
if (workspace) {
|
|
18825
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
18826
|
-
action: "workspace settled by approver", outcome: run.worktree ?? "", source: "request", detail: args.why });
|
|
18827
|
-
}
|
|
18828
|
-
// A run that may have spawned before any witness was written has none
|
|
18829
|
-
// to settle: the approver's word is recorded as one ended witness.
|
|
18830
|
-
if (rows.length === 0 && run.providerStartedAt !== null && !this.processesSummarized(args.runId)) {
|
|
18831
|
-
const id = Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
18832
|
-
.run(args.runId, hostname(), now.toISOString(), currentBootId(), now.toISOString()).lastInsertRowid);
|
|
18833
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
18834
|
-
action: "process witness settled by approver", outcome: `witness ${id}`, source: "request", detail: args.why });
|
|
18835
|
-
this.settleQuiescentStops(now);
|
|
18836
|
-
return { ok: true, witnesses: [id], repeated: false };
|
|
18837
|
-
}
|
|
18838
|
-
if (open.length === 0) {
|
|
18839
|
-
if (workspace)
|
|
18840
|
-
this.settleQuiescentStops(now);
|
|
18841
|
-
return { ok: true, witnesses: [], repeated: !workspace };
|
|
18842
|
-
}
|
|
18843
|
-
const ids = open.map(row => Number(row["id"]));
|
|
18844
|
-
for (const id of ids)
|
|
18845
|
-
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND run = ? AND exited_at IS NULL").run(now.toISOString(), id, args.runId);
|
|
18846
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
18847
|
-
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
18848
|
-
compactSettledRunProcesses(this.db, now, args.runId);
|
|
18849
|
-
this.settleQuiescentStops(now);
|
|
18850
|
-
return { ok: true, witnesses: ids, repeated: false };
|
|
18851
|
-
});
|
|
18852
|
-
}
|
|
18853
19290
|
recordRunProcess(runId, pid, now, group = true, witness) {
|
|
18854
19291
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
18855
19292
|
throw new Error("a process witness needs a valid spawned PID");
|
|
@@ -22376,3 +22813,91 @@ export function laptopPortOf(store) {
|
|
|
22376
22813
|
export function recordLaptopPort(store, port, now) {
|
|
22377
22814
|
store.recordInstallationFact(LAPTOP_PORT_FACT, String(port), now);
|
|
22378
22815
|
}
|
|
22816
|
+
// ---- v148: results routing (docs/plans/results-routing.md) ------------------------------------------------------
|
|
22817
|
+
/** The Models rows of a project being deleted (project-delete.ts), through the same counter as every other Models
|
|
22818
|
+
* writer: each phase whose rows go moves its generation (pausing what depended on it), then the project's
|
|
22819
|
+
* results-routing rows go (route-store.ts forgetProjectRoutes). One of the routing scan's named phase_* writers
|
|
22820
|
+
* (route-hooks.ts). How many rows went. */
|
|
22821
|
+
export function forgetProjectModels(store, repo, now) {
|
|
22822
|
+
return store.transact(() => {
|
|
22823
|
+
const db = store.handle;
|
|
22824
|
+
let rows = 0;
|
|
22825
|
+
const phases = new Set(db.prepare(`SELECT phase FROM phase_config WHERE scope = ? UNION SELECT phase FROM phase_tier_config WHERE scope = ?
|
|
22826
|
+
UNION SELECT phase FROM phase_tier_alternate WHERE scope = ?`).all(repo, repo, repo).map(row => String(row["phase"])));
|
|
22827
|
+
rows += Number(db.prepare("DELETE FROM phase_config WHERE scope = ?").run(repo).changes);
|
|
22828
|
+
rows += Number(db.prepare("DELETE FROM phase_tier_config WHERE scope = ?").run(repo).changes);
|
|
22829
|
+
rows += Number(db.prepare("DELETE FROM phase_tier_alternate WHERE scope = ?").run(repo).changes);
|
|
22830
|
+
for (const phase of [...phases].sort())
|
|
22831
|
+
bumpModelsGeneration(store, repo, phase, now);
|
|
22832
|
+
return rows + forgetProjectRoutes(store, repo);
|
|
22833
|
+
});
|
|
22834
|
+
}
|
|
22835
|
+
// ---- Toolroll's own stop proof (stop-proof.ts) ------------------------------------------------------------------
|
|
22836
|
+
/** The ledger words of Toolroll's own stop proof. Nobody is asked to vouch that a build stopped: Toolroll looks at
|
|
22837
|
+
* this computer's processes itself, and a person's older recorded word (`run settle` before this) is still read. */
|
|
22838
|
+
export const STOP_PROOF_SETTLED = "run stop proven automatically";
|
|
22839
|
+
export const STOP_PROOF_BLOCKED = "automatic stop check blocked";
|
|
22840
|
+
export const STOP_PROOF_WORKSPACE_FREE = "workspace proven free automatically";
|
|
22841
|
+
/** Why Toolroll's own stop check is blocked for a run, in words that follow "run #N " (the stop fact's problem), or
|
|
22842
|
+
* null when its newest word is not a block (never checked, or proven since). */
|
|
22843
|
+
export function stopProofVerdictOf(store, runId) {
|
|
22844
|
+
const row = store.handle.prepare("SELECT action, outcome FROM action_ledger WHERE run_id = ? AND action IN (?, ?) ORDER BY id DESC LIMIT 1")
|
|
22845
|
+
.get(runId, STOP_PROOF_BLOCKED, STOP_PROOF_SETTLED);
|
|
22846
|
+
return row === undefined || row["action"] !== STOP_PROOF_BLOCKED ? null : String(row["outcome"]);
|
|
22847
|
+
}
|
|
22848
|
+
/** Record that Toolroll's own stop check could not prove a run stopped (`words` follow "run #N "; `detail` is the
|
|
22849
|
+
* evidence). The same block twice in a row is recorded once. True when recorded. */
|
|
22850
|
+
export function recordStopProofBlocked(store, runId, words, detail, now) {
|
|
22851
|
+
return store.transact(() => {
|
|
22852
|
+
if (stopProofVerdictOf(store, runId) === words)
|
|
22853
|
+
return false;
|
|
22854
|
+
const run = store.getRun(runId);
|
|
22855
|
+
const ref = run === null ? null : store.refById(run.taskRef);
|
|
22856
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId,
|
|
22857
|
+
action: STOP_PROOF_BLOCKED, outcome: words, source: "work", detail });
|
|
22858
|
+
return true;
|
|
22859
|
+
});
|
|
22860
|
+
}
|
|
22861
|
+
/**
|
|
22862
|
+
* Settle a finished run's unproven stop on Toolroll's own observation of this computer (stop-proof.ts): the caller
|
|
22863
|
+
* looked at every process here and found none of the run's. Applies only while the run's stop is still unproven for
|
|
22864
|
+
* exactly `cause` (re-read in this transaction): a pid-less spawn witness is recorded ended, a run with no witness gets
|
|
22865
|
+
* one ended witness, an unreadable workspace note is recorded proven free. The evidence goes in the ledger. Never
|
|
22866
|
+
* signals. True when this call settled it.
|
|
22867
|
+
*/
|
|
22868
|
+
export function settleRunStopByObservation(store, args, now) {
|
|
22869
|
+
return store.transact(() => {
|
|
22870
|
+
const fact = store.stopQuiescenceFact(args.runId);
|
|
22871
|
+
if (fact === null || fact.run !== args.runId || fact.cause !== args.cause || (fact.kind !== "unprovable" && fact.kind !== "unknown"))
|
|
22872
|
+
return false;
|
|
22873
|
+
const run = store.getRun(args.runId);
|
|
22874
|
+
if (run === null || run.outcome === null || run.finishedAt === null)
|
|
22875
|
+
return false;
|
|
22876
|
+
const db = store.handle;
|
|
22877
|
+
const ref = store.refById(run.taskRef);
|
|
22878
|
+
const at = now.toISOString();
|
|
22879
|
+
let what;
|
|
22880
|
+
if (args.cause === "incomplete-spawn") {
|
|
22881
|
+
const changed = db.prepare(`UPDATE run_process SET exited_at = ? WHERE run = ? AND exited_at IS NULL AND pid IS NULL
|
|
22882
|
+
AND containment IS NULL AND container IS NULL AND host = ?`).run(at, args.runId, hostname()).changes;
|
|
22883
|
+
if (Number(changed) === 0)
|
|
22884
|
+
return false;
|
|
22885
|
+
what = `${Number(changed)} process witness${Number(changed) === 1 ? "" : "es"} with no process ID recorded ended`;
|
|
22886
|
+
}
|
|
22887
|
+
else if (args.cause === "no-witness") {
|
|
22888
|
+
db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
22889
|
+
.run(args.runId, hostname(), at, currentBootId(), at);
|
|
22890
|
+
what = "its missing process witness recorded ended";
|
|
22891
|
+
}
|
|
22892
|
+
else {
|
|
22893
|
+
store.recordAction({ at, actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
22894
|
+
action: STOP_PROOF_WORKSPACE_FREE, outcome: run.worktree ?? "", source: "work", detail: args.evidence });
|
|
22895
|
+
what = "its folder recorded free";
|
|
22896
|
+
}
|
|
22897
|
+
store.recordAction({ at, actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
22898
|
+
action: STOP_PROOF_SETTLED, outcome: what, source: "work", detail: args.evidence });
|
|
22899
|
+
compactSettledRunProcesses(db, now, args.runId);
|
|
22900
|
+
store.settleQuiescentStops(now);
|
|
22901
|
+
return true;
|
|
22902
|
+
});
|
|
22903
|
+
}
|