toolroll 0.9.92 → 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 +19 -0
- 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/builder.js +21 -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/console-api.d.ts +2 -2
- package/dist/contracts/console-api.js +2 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- 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/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate.js +138 -18
- package/dist/project-delete.js +5 -1
- package/dist/project-knowledge.d.ts +6 -6
- 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 +13 -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-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- 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.js +3 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +7 -1
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +102 -20
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +70 -31
- package/dist/store.js +335 -95
- package/dist/surface.js +1 -1
- package/dist/task-outcome-cli.js +19 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +1 -0
- 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
|
@@ -1114,7 +1114,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1114
1114
|
// v148 adds, and changes no saved row (docs/plans/results-routing.md): the results-routing tables (V148_SCHEMA, new
|
|
1115
1115
|
// tables only) and a run's execution class, a revision's origin and a project's "Suggest cheaper models" switch
|
|
1116
1116
|
// (V148_ADDED_COLUMNS: saved rows read NULL, NULL and 1).
|
|
1117
|
-
|
|
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;
|
|
1118
1121
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1119
1122
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1120
1123
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1247,7 +1250,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1247
1250
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1248
1251
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1249
1252
|
*/
|
|
1250
|
-
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]);
|
|
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]);
|
|
1251
1254
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1252
1255
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1253
1256
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1650,6 +1653,229 @@ export const V148_ADDED_COLUMNS = Object.freeze({
|
|
|
1650
1653
|
});
|
|
1651
1654
|
/** The v148 migration's steps after v147's, in order (OpenOptions.migrationStep may stop after each). */
|
|
1652
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"]);
|
|
1653
1879
|
/**
|
|
1654
1880
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1655
1881
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4941,6 +5167,9 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4941
5167
|
for (const [column, { definition }] of Object.entries(V148_ADDED_COLUMNS["project"]))
|
|
4942
5168
|
addColumn(db, "project", column, definition);
|
|
4943
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");
|
|
4944
5173
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4945
5174
|
// nothing to reclaim.
|
|
4946
5175
|
if (!fresh)
|
|
@@ -7873,6 +8102,11 @@ export class Store {
|
|
|
7873
8102
|
this.db
|
|
7874
8103
|
.prepare("UPDATE notification SET resolved_at = ? WHERE dedupe_key LIKE ? AND resolved_at IS NULL")
|
|
7875
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"]));
|
|
7876
8110
|
// The approval fact (Telegram task updates): a NEW yes on these
|
|
7877
8111
|
// exact bytes. Re-sealing an approval that already stands on
|
|
7878
8112
|
// the same digest is the same state and says nothing.
|
|
@@ -18820,12 +19054,20 @@ export class Store {
|
|
|
18820
19054
|
* what kind of answer it is (needs-you.ts reads it): `alive` while
|
|
18821
19055
|
* something of the run may still be running, `elsewhere` when only
|
|
18822
19056
|
* another computer can tell, `unprovable` when nothing is known to run
|
|
18823
|
-
* but its exit was never recorded
|
|
18824
|
-
*
|
|
18825
|
-
*
|
|
18826
|
-
*
|
|
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. */
|
|
18827
19062
|
stopQuiescenceFact(runId) {
|
|
18828
|
-
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
|
+
};
|
|
18829
19071
|
const ids = this.ownedRunsOf(runId);
|
|
18830
19072
|
if (ids.length === 0)
|
|
18831
19073
|
return fact(runId, "open", `run #${runId} does not exist`);
|
|
@@ -18838,7 +19080,7 @@ export class Store {
|
|
|
18838
19080
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
18839
19081
|
let incompleteSpawn = false;
|
|
18840
19082
|
if (run.providerStartedAt !== null && witnesses.length === 0 && !this.processesSummarized(id)) {
|
|
18841
|
-
return
|
|
19083
|
+
return provable(id, `run #${id} may have spawned before its process witness was recorded; exit is unproven`, "no-witness");
|
|
18842
19084
|
}
|
|
18843
19085
|
for (const witness of witnesses) {
|
|
18844
19086
|
// A recorded group exit proves the whole group ended, not just its
|
|
@@ -18887,20 +19129,27 @@ export class Store {
|
|
|
18887
19129
|
return fact(id, "alive", `run #${id}'s workspace is still held by process ${occupied.by}`);
|
|
18888
19130
|
}
|
|
18889
19131
|
catch {
|
|
18890
|
-
// An unreadable workspace note is no proof either way;
|
|
18891
|
-
|
|
18892
|
-
|
|
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
|
+
}
|
|
18893
19138
|
}
|
|
18894
19139
|
}
|
|
18895
|
-
if (incompleteSpawn)
|
|
18896
|
-
|
|
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
|
+
}
|
|
18897
19145
|
}
|
|
18898
19146
|
return null;
|
|
18899
19147
|
}
|
|
18900
|
-
/**
|
|
18901
|
-
*
|
|
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). */
|
|
18902
19150
|
workspaceConfirmedByApprover(runId) {
|
|
18903
|
-
|
|
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;
|
|
18904
19153
|
}
|
|
18905
19154
|
reserveRunProcess(runId, now, group = true) {
|
|
18906
19155
|
return Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id) VALUES (?,?,?,?,?)")
|
|
@@ -18989,7 +19238,7 @@ export class Store {
|
|
|
18989
19238
|
* here, every other witness of the run has exited and none of the run's
|
|
18990
19239
|
* process groups is alive, it settles as never started, with a ledger
|
|
18991
19240
|
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
18992
|
-
*
|
|
19241
|
+
* Toolroll's own look (stop-proof.ts). Native witnesses keep their OS-only road. */
|
|
18993
19242
|
settleUnspawnedWitnesses(now, onlyRun) {
|
|
18994
19243
|
const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
18995
19244
|
WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
|
|
@@ -19038,84 +19287,6 @@ export class Store {
|
|
|
19038
19287
|
return [];
|
|
19039
19288
|
return pidless;
|
|
19040
19289
|
}
|
|
19041
|
-
/** `run settle` (an approver's last resort): every witness of a finished
|
|
19042
|
-
* run that cannot be proven either way is recorded settled under the
|
|
19043
|
-
* approver's reason, in the ledger. Refuses while anything of the run is
|
|
19044
|
-
* alive or could be checked only from another host. Never signals. */
|
|
19045
|
-
settleRunWitnessesByApprover(args, now) {
|
|
19046
|
-
return this.transact(() => {
|
|
19047
|
-
const run = this.getRun(args.runId);
|
|
19048
|
-
if (run === null)
|
|
19049
|
-
return { ok: false, reason: "unknown-run", detail: `no run #${args.runId}` };
|
|
19050
|
-
if (run.outcome === null)
|
|
19051
|
-
return { ok: false, reason: "still-running", detail: `run #${args.runId} is still open` };
|
|
19052
|
-
const alive = (detail) => ({ ok: false, reason: "alive", detail });
|
|
19053
|
-
if (ownedProcessCount(runOwnerTag(this, args.runId)) > 0)
|
|
19054
|
-
return alive(`run #${args.runId} still has an owned subprocess`);
|
|
19055
|
-
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(args.runId);
|
|
19056
|
-
const open = rows.filter(row => row["exited_at"] === null);
|
|
19057
|
-
for (const row of rows) {
|
|
19058
|
-
if (row["pid"] === null)
|
|
19059
|
-
continue;
|
|
19060
|
-
if (row["host"] !== hostname()) {
|
|
19061
|
-
if (row["exited_at"] === null)
|
|
19062
|
-
return alive(`run #${args.runId}'s process ${String(row["pid"])} belongs to ${String(row["host"])}; settle it there`);
|
|
19063
|
-
continue;
|
|
19064
|
-
}
|
|
19065
|
-
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt, exitedAt: row["exited_at"] }))
|
|
19066
|
-
return alive(`run #${args.runId}'s process ${String(row["pid"])} is still running`);
|
|
19067
|
-
}
|
|
19068
|
-
for (const row of open) {
|
|
19069
|
-
if (row["pid"] === null && row["host"] !== hostname())
|
|
19070
|
-
return alive(`run #${args.runId} has a witness from ${String(row["host"])}; settle it there`);
|
|
19071
|
-
if (row["containment"] != null && row["container"] != null && row["container_empty_at"] == null &&
|
|
19072
|
-
containerEmptiness(String(row["containment"]), String(row["container"]), process.platform, row["container_identity"] == null ? null : String(row["container_identity"])) === "populated") {
|
|
19073
|
-
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
19074
|
-
}
|
|
19075
|
-
}
|
|
19076
|
-
// A workspace still held by a live process refuses; one whose note can't
|
|
19077
|
-
// be read is settled by the approver's word, in the ledger.
|
|
19078
|
-
let workspace = false;
|
|
19079
|
-
if (run.worktree !== null) {
|
|
19080
|
-
try {
|
|
19081
|
-
const occupied = worktreeProcessOccupancy(run.worktree);
|
|
19082
|
-
if (occupied.held)
|
|
19083
|
-
return alive(`run #${args.runId}'s workspace is still held by process ${occupied.by}`);
|
|
19084
|
-
}
|
|
19085
|
-
catch {
|
|
19086
|
-
workspace = !this.workspaceConfirmedByApprover(args.runId);
|
|
19087
|
-
}
|
|
19088
|
-
}
|
|
19089
|
-
const ref = this.refById(run.taskRef);
|
|
19090
|
-
if (workspace) {
|
|
19091
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
19092
|
-
action: "workspace settled by approver", outcome: run.worktree ?? "", source: "request", detail: args.why });
|
|
19093
|
-
}
|
|
19094
|
-
// A run that may have spawned before any witness was written has none
|
|
19095
|
-
// to settle: the approver's word is recorded as one ended witness.
|
|
19096
|
-
if (rows.length === 0 && run.providerStartedAt !== null && !this.processesSummarized(args.runId)) {
|
|
19097
|
-
const id = Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
19098
|
-
.run(args.runId, hostname(), now.toISOString(), currentBootId(), now.toISOString()).lastInsertRowid);
|
|
19099
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
19100
|
-
action: "process witness settled by approver", outcome: `witness ${id}`, source: "request", detail: args.why });
|
|
19101
|
-
this.settleQuiescentStops(now);
|
|
19102
|
-
return { ok: true, witnesses: [id], repeated: false };
|
|
19103
|
-
}
|
|
19104
|
-
if (open.length === 0) {
|
|
19105
|
-
if (workspace)
|
|
19106
|
-
this.settleQuiescentStops(now);
|
|
19107
|
-
return { ok: true, witnesses: [], repeated: !workspace };
|
|
19108
|
-
}
|
|
19109
|
-
const ids = open.map(row => Number(row["id"]));
|
|
19110
|
-
for (const id of ids)
|
|
19111
|
-
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND run = ? AND exited_at IS NULL").run(now.toISOString(), id, args.runId);
|
|
19112
|
-
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
19113
|
-
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
19114
|
-
compactSettledRunProcesses(this.db, now, args.runId);
|
|
19115
|
-
this.settleQuiescentStops(now);
|
|
19116
|
-
return { ok: true, witnesses: ids, repeated: false };
|
|
19117
|
-
});
|
|
19118
|
-
}
|
|
19119
19290
|
recordRunProcess(runId, pid, now, group = true, witness) {
|
|
19120
19291
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
19121
19292
|
throw new Error("a process witness needs a valid spawned PID");
|
|
@@ -22661,3 +22832,72 @@ export function forgetProjectModels(store, repo, now) {
|
|
|
22661
22832
|
return rows + forgetProjectRoutes(store, repo);
|
|
22662
22833
|
});
|
|
22663
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
|
+
}
|
package/dist/surface.js
CHANGED
|
@@ -315,7 +315,7 @@ const GUIDE_ROWS = [
|
|
|
315
315
|
{ name: "days", takesValue: true, meaning: "also finished tasks untouched this many days (1–365, default 14)" },
|
|
316
316
|
{ name: "except", takesValue: true, meaning: "comma-separated tasks from the list to leave out" }],
|
|
317
317
|
notableReasons: ["usage", "not-an-approver", "empty"] },
|
|
318
|
-
operator("run settle", "
|
|
318
|
+
operator("run settle", "have Toolroll check again whether a finished run stopped: it looks at this computer's processes itself, stops only leftovers proven the run's, and settles the run when none is left (nobody vouches; --why is ignored)"),
|
|
319
319
|
operator("task resume", "resume the exact stopped attempt (--run <id>): refuses until it is quiescent, lifts only that stop's hold, approves nothing — the next pass re-proves the scope and inherits the draft with fresh proof"),
|
|
320
320
|
// ---- leases (runner surface) ----
|
|
321
321
|
{ invocation: "claim", synopsis: "take one ready, approved task; answers with a lease", audience: "agent", agentMayInvoke: true, mutation: "keyed",
|
package/dist/task-outcome-cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { requestResultChanges } from './result-actions.js';
|
|
|
10
10
|
import { commentSourceKey, revisionSourceOf } from './result-review.js';
|
|
11
11
|
import { envelopeJson } from './envelope.js';
|
|
12
12
|
import { completeAndOpenPullRequest, mergeAsPerson, MERGE_NEEDS_PERSON, newestPullRequestOf, publishingOf, pullRequestViewOf } from './pull-request-flow.js';
|
|
13
|
+
import { proveRunStopped } from './stop-proof.js';
|
|
13
14
|
export async function runTaskOutcomeCommand(action, positional, flags, context) {
|
|
14
15
|
const command = `task ${action}`;
|
|
15
16
|
const emit = (result, lines) => {
|
|
@@ -36,6 +37,10 @@ export async function runTaskOutcomeCommand(action, positional, flags, context)
|
|
|
36
37
|
if (credential.token === null && operator === null)
|
|
37
38
|
return fail('unauthenticated', 'Sign in with up, or supply --as and --token. Agents can use --token-file or --token-env with a scoped credential.');
|
|
38
39
|
const { store, now, evidenceRoot } = context;
|
|
40
|
+
// A result whose build Toolroll hasn't yet proven stopped: it looks now (stop-proof.ts), so completing never waits
|
|
41
|
+
// on a person to confirm it, only on Toolroll's own check (a refusal says "stopping" until it lands).
|
|
42
|
+
if (action === 'complete')
|
|
43
|
+
await proveTaskBuildsStopped(store, task, () => now);
|
|
39
44
|
return store.transact(() => {
|
|
40
45
|
const coordinator = credential.token === null ? null : authenticateCoordinator(store, credential.token);
|
|
41
46
|
if (coordinator !== null && !coordinator.ok)
|
|
@@ -172,3 +177,17 @@ export function pullRequestLines(view) {
|
|
|
172
177
|
return [];
|
|
173
178
|
return [` pull request: ${view.prUrl ?? 'opening'} · ${view.label}`, ` ${view.detail}`, ...(view.mergeCommit === null ? [] : [` merge commit: ${view.mergeCommit}`])];
|
|
174
179
|
}
|
|
180
|
+
/** Toolroll's own stop proof for every finished build of a task's family whose stop it can prove here. Best effort: a
|
|
181
|
+
* failure leaves the completion's own refusal to say what is still unproven. */
|
|
182
|
+
async function proveTaskBuildsStopped(store, task, clock) {
|
|
183
|
+
try {
|
|
184
|
+
const family = store.taskFamilyOf(task, null, true);
|
|
185
|
+
const refs = family === null ? [store.lookupRef(task)?.id].filter((one) => one !== undefined) : family.versions.map(version => version.refId);
|
|
186
|
+
for (const ref of refs)
|
|
187
|
+
for (const run of store.runsFor(ref)) {
|
|
188
|
+
if (run.outcome !== null && store.stopQuiescenceFact(run.id)?.cause !== undefined)
|
|
189
|
+
await proveRunStopped(store, run.id, clock);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch { /* the completion below refuses with what is still unproven */ }
|
|
193
|
+
}
|
|
@@ -24,7 +24,7 @@ export function updateStepsHtml(j, running, options = {}) {
|
|
|
24
24
|
const items = UPDATE_STEPS.map((step, i) => {
|
|
25
25
|
const state = j.phase === "complete" || i < last ? "done" : i > last ? "waiting" : failed ? "failed" : finished ? "waiting" : "now";
|
|
26
26
|
// Only where it adds something: which work it waits for, or what went wrong.
|
|
27
|
-
const detail = state === "now" && step === "draining" && j.waiting ? html `${j.waiting.on}.${j.waiting.action ? html `
|
|
27
|
+
const detail = state === "now" && step === "draining" && j.waiting ? html `${j.waiting.on}.${j.waiting.action ? html ` To have Toolroll check again, run <code class="k-mono">${j.waiting.action}</code>.` : ""}`
|
|
28
28
|
: (state === "now" && step === "draining") || (state === "failed" && j.phase === "rolling-back") ? html `${j.detail}` : "";
|
|
29
29
|
return settingsRecord({ title: STEP_WORDS[step], sub: detail, when: STEP_CHIP[state], attrs: { "data-step": step, "data-state": state } });
|
|
30
30
|
});
|
|
@@ -43,7 +43,7 @@ function outcomeHtml(j) {
|
|
|
43
43
|
// Stopped on a finished run it can't show has ended: what is in the way and the one command, nothing more.
|
|
44
44
|
if (j.phase === "refused" && j.waiting) {
|
|
45
45
|
return settingsSection({ id: "update-outcome", title: `Update to ${j.to.version} stopped: run #${j.waiting.run} is in the way`,
|
|
46
|
-
body: html `<div data-update-outcome="stopped">${infoLine(`${j.waiting.on}. New work resumed.`)}${j.waiting.action ? note(html `
|
|
46
|
+
body: html `<div data-update-outcome="stopped">${infoLine(`${j.waiting.on}. New work resumed.`)}${j.waiting.action ? note(html `To have Toolroll check again, run <code class="k-mono">${j.waiting.action}</code>, then update again.`) : note("Update again once it has stopped.")}</div>` });
|
|
47
47
|
}
|
|
48
48
|
const title = j.phase === "refused" ? `Didn't update to ${j.to.version}` : j.phase === "restored" ? `Update to ${j.to.version} didn't finish` : "The update needs attention";
|
|
49
49
|
return settingsSection({ id: "update-outcome", title, state: html ` ${chip("Failed", "failed")}`,
|
package/dist/toolroll-update.js
CHANGED
|
@@ -414,6 +414,7 @@ const HISTORY_RULES = {
|
|
|
414
414
|
// v147: an account's password flag reads set (1), a browser session reads full with no local binding or key, and the
|
|
415
415
|
// console record reads no key epoch.
|
|
416
416
|
// v148: a run's execution class and a revision's origin read none (NULL), and a project's suggestion switch reads on.
|
|
417
|
+
// v149 adds tables only (the learning-loops records): no saved row changes, so no rule is needed for it.
|
|
417
418
|
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS, V148_ADDED_COLUMNS))
|
|
418
419
|
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
419
420
|
};
|