loomrail 0.1.0-alpha.1
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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +266 -0
- package/apps/cli/dist/index.js +11154 -0
- package/apps/cli/migrations/0001_initial.sql +99 -0
- package/apps/cli/migrations/0002_mock_workflow.sql +169 -0
- package/apps/cli/migrations/0003_budget_pause_recovery.sql +154 -0
- package/apps/cli/migrations/0004_pipeline_started_budget_backfill.sql +72 -0
- package/apps/cli/migrations/0005_acceptance_evidence.sql +129 -0
- package/apps/cli/migrations/0006_session_handoff.sql +166 -0
- package/apps/cli/migrations/0007_pack_share_backoff.sql +11 -0
- package/apps/cli/migrations/0008_stage_attempt_counter_backfill.sql +169 -0
- package/apps/cli/migrations/0009_session_window_occupancy.sql +44 -0
- package/apps/cli/migrations/0010_provider_session_pid.sql +5 -0
- package/apps/cli/migrations/0011_work_item_workspaces.sql +94 -0
- package/apps/cli/migrations/0012_registered_repository_projects.sql +54 -0
- package/apps/cli/migrations/0013_stage_attempt_result_tree.sql +166 -0
- package/apps/cli/migrations/0014_live_evidence_provider.sql +49 -0
- package/apps/web/dist/assets/index-DjwvCSSY.js +5 -0
- package/apps/web/dist/assets/index-DjwvCSSY.js.map +1 -0
- package/apps/web/dist/assets/index-Z88TqCIf.css +1 -0
- package/apps/web/dist/assets/radix-vendor-CRdHXIPR.js +42 -0
- package/apps/web/dist/assets/radix-vendor-CRdHXIPR.js.map +1 -0
- package/apps/web/dist/assets/react-vendor-BicOiEU0.js +10 -0
- package/apps/web/dist/assets/react-vendor-BicOiEU0.js.map +1 -0
- package/apps/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
- package/apps/web/dist/assets/tanstack-vendor-ec08LVZ_.js +2 -0
- package/apps/web/dist/assets/tanstack-vendor-ec08LVZ_.js.map +1 -0
- package/apps/web/dist/assets/validation-vendor-CGrOSWd3.js +65 -0
- package/apps/web/dist/assets/validation-vendor-CGrOSWd3.js.map +1 -0
- package/apps/web/dist/favicon.svg +5 -0
- package/apps/web/dist/index.html +60 -0
- package/apps/web/dist/theme-bootstrap.js +46 -0
- package/fixtures/projects/api-service-b/README.md +3 -0
- package/fixtures/projects/api-service-b/loomrail-fixture.json +6 -0
- package/fixtures/projects/web-app-a/README.md +3 -0
- package/fixtures/projects/web-app-a/loomrail-fixture.json +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
-- Spec docs/plans/07-a1-session-handoff-spec.ru.md §4.2, §4.4, §6.5.
|
|
2
|
+
--
|
|
3
|
+
-- Adds durable storage for the session-handoff loop: the recipe used to assemble each provider
|
|
4
|
+
-- session's context pack, the sessions themselves, and the checkpoints an agent publishes mid-
|
|
5
|
+
-- session. Also widens the `events` CHECK to admit the five new session-handoff event types, and
|
|
6
|
+
-- adds the unproductive-session counter to `stage_attempts` (§6.5: it must survive a daemon
|
|
7
|
+
-- restart, so it lives in the database rather than in daemon memory).
|
|
8
|
+
|
|
9
|
+
CREATE TABLE provider_sessions (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
12
|
+
stage_attempt_id TEXT NOT NULL REFERENCES stage_attempts(id) ON DELETE RESTRICT,
|
|
13
|
+
ordinal INTEGER NOT NULL CHECK (ordinal > 0),
|
|
14
|
+
status TEXT NOT NULL CHECK (status IN ('RUNNING', 'ENDED')),
|
|
15
|
+
end_reason TEXT CHECK (
|
|
16
|
+
end_reason IS NULL OR
|
|
17
|
+
end_reason IN ('COMPLETED', 'HANDOFF', 'CONTEXT_EXHAUSTED', 'INTERRUPTED', 'CANCELLED')
|
|
18
|
+
),
|
|
19
|
+
handoff_requested_at TEXT,
|
|
20
|
+
started_at TEXT NOT NULL,
|
|
21
|
+
ended_at TEXT,
|
|
22
|
+
version INTEGER NOT NULL CHECK (version > 0),
|
|
23
|
+
UNIQUE (stage_attempt_id, ordinal),
|
|
24
|
+
-- Mirrors providerSessionSchema's two refines: an ENDED session carries both an end reason and
|
|
25
|
+
-- an end timestamp, and a RUNNING one carries neither.
|
|
26
|
+
CHECK ((status = 'ENDED') = (end_reason IS NOT NULL)),
|
|
27
|
+
CHECK ((status = 'ENDED') = (ended_at IS NOT NULL))
|
|
28
|
+
) STRICT;
|
|
29
|
+
|
|
30
|
+
-- The link to ProviderSession is owned here, one-directional: a two-way FK between this table and
|
|
31
|
+
-- provider_sessions would be circular (spec §6.1 step 4 writes both in one transaction, and each
|
|
32
|
+
-- side's NOT NULL FK would need the other to exist first). UNIQUE enforces the 1:1 the design
|
|
33
|
+
-- assumes -- one recipe per session -- in the schema, not by convention. The StageAttempt is
|
|
34
|
+
-- reachable through the session; a second path to it here would be a second thing that can
|
|
35
|
+
-- disagree with the first.
|
|
36
|
+
CREATE TABLE context_pack_recipes (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
39
|
+
provider_session_id TEXT NOT NULL UNIQUE REFERENCES provider_sessions(id) ON DELETE RESTRICT,
|
|
40
|
+
template_id TEXT NOT NULL,
|
|
41
|
+
template_version INTEGER NOT NULL CHECK (template_version > 0),
|
|
42
|
+
spec_source TEXT NOT NULL CHECK (spec_source = 'WORKFLOW_TEMPLATE'),
|
|
43
|
+
-- [{ id, sources: [{ kind, id, version }], bytes }] -- see contextPackRecipeSectionSchema.
|
|
44
|
+
sections_json TEXT NOT NULL CHECK (json_valid(sections_json)),
|
|
45
|
+
-- [{ id, reason }] -- see contextPackRecipeOmittedSectionSchema.
|
|
46
|
+
omitted_json TEXT NOT NULL CHECK (json_valid(omitted_json)),
|
|
47
|
+
content_hash TEXT NOT NULL CHECK (content_hash LIKE 'sha256:%'),
|
|
48
|
+
estimated_tokens INTEGER NOT NULL CHECK (estimated_tokens >= 0),
|
|
49
|
+
budget_tokens INTEGER NOT NULL CHECK (budget_tokens > 0),
|
|
50
|
+
estimate_quality TEXT NOT NULL
|
|
51
|
+
CHECK (estimate_quality IN ('ACTUAL', 'PROVIDER_ESTIMATE', 'LOOMRAIL_ESTIMATE')),
|
|
52
|
+
created_at TEXT NOT NULL
|
|
53
|
+
) STRICT;
|
|
54
|
+
|
|
55
|
+
CREATE TABLE checkpoints (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
58
|
+
stage_attempt_id TEXT NOT NULL REFERENCES stage_attempts(id) ON DELETE RESTRICT,
|
|
59
|
+
provider_session_id TEXT NOT NULL REFERENCES provider_sessions(id) ON DELETE RESTRICT,
|
|
60
|
+
ordinal INTEGER NOT NULL CHECK (ordinal > 0),
|
|
61
|
+
summary TEXT NOT NULL CHECK (length(summary) BETWEEN 1 AND 4000),
|
|
62
|
+
completed_json TEXT NOT NULL CHECK (json_valid(completed_json)),
|
|
63
|
+
remaining_json TEXT NOT NULL CHECK (json_valid(remaining_json)),
|
|
64
|
+
dead_ends_json TEXT NOT NULL CHECK (json_valid(dead_ends_json)),
|
|
65
|
+
open_questions_json TEXT NOT NULL CHECK (json_valid(open_questions_json)),
|
|
66
|
+
created_at TEXT NOT NULL,
|
|
67
|
+
UNIQUE (provider_session_id, ordinal)
|
|
68
|
+
) STRICT;
|
|
69
|
+
|
|
70
|
+
-- §6.5: the counter lives in state, not in daemon memory, because a daemon restart is itself a
|
|
71
|
+
-- normal end of a session -- a counter held in memory would be reset by the very event it exists
|
|
72
|
+
-- to guard against.
|
|
73
|
+
ALTER TABLE stage_attempts
|
|
74
|
+
ADD COLUMN unproductive_sessions INTEGER NOT NULL DEFAULT 0
|
|
75
|
+
CHECK (unproductive_sessions >= 0);
|
|
76
|
+
|
|
77
|
+
CREATE INDEX provider_sessions_attempt_ordinal_idx
|
|
78
|
+
ON provider_sessions(stage_attempt_id, ordinal);
|
|
79
|
+
CREATE INDEX checkpoints_attempt_created_idx
|
|
80
|
+
ON checkpoints(stage_attempt_id, created_at DESC, id);
|
|
81
|
+
|
|
82
|
+
CREATE TRIGGER checkpoints_are_append_only_update
|
|
83
|
+
BEFORE UPDATE ON checkpoints
|
|
84
|
+
BEGIN
|
|
85
|
+
SELECT RAISE(ABORT, 'checkpoints are append-only');
|
|
86
|
+
END;
|
|
87
|
+
|
|
88
|
+
CREATE TRIGGER checkpoints_are_append_only_delete
|
|
89
|
+
BEFORE DELETE ON checkpoints
|
|
90
|
+
BEGIN
|
|
91
|
+
SELECT RAISE(ABORT, 'checkpoints are append-only');
|
|
92
|
+
END;
|
|
93
|
+
|
|
94
|
+
CREATE TRIGGER context_pack_recipes_are_append_only_update
|
|
95
|
+
BEFORE UPDATE ON context_pack_recipes
|
|
96
|
+
BEGIN
|
|
97
|
+
SELECT RAISE(ABORT, 'context pack recipes are append-only');
|
|
98
|
+
END;
|
|
99
|
+
|
|
100
|
+
CREATE TRIGGER context_pack_recipes_are_append_only_delete
|
|
101
|
+
BEFORE DELETE ON context_pack_recipes
|
|
102
|
+
BEGIN
|
|
103
|
+
SELECT RAISE(ABORT, 'context pack recipes are append-only');
|
|
104
|
+
END;
|
|
105
|
+
|
|
106
|
+
-- `events` bakes its allowed types into a CHECK, so widening it requires a rebuild: drop the
|
|
107
|
+
-- triggers and indexes, rename the table aside, recreate it with the widened list, copy every row
|
|
108
|
+
-- preserving `sequence`, drop the renamed table, then recreate the indexes and the append-only
|
|
109
|
+
-- triggers. This mirrors 0005_acceptance_evidence.sql's rebuild of the same table.
|
|
110
|
+
DROP TRIGGER events_are_append_only_update;
|
|
111
|
+
DROP TRIGGER events_are_append_only_delete;
|
|
112
|
+
DROP INDEX events_project_sequence_idx;
|
|
113
|
+
DROP INDEX events_aggregate_sequence_idx;
|
|
114
|
+
ALTER TABLE events RENAME TO events_v5;
|
|
115
|
+
|
|
116
|
+
CREATE TABLE events (
|
|
117
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
+
id TEXT NOT NULL UNIQUE,
|
|
119
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
120
|
+
type TEXT NOT NULL CHECK (
|
|
121
|
+
type IN (
|
|
122
|
+
'PROJECT_REGISTERED', 'WORK_ITEM_CREATED', 'WORK_ITEM_UPDATED', 'WORK_ITEM_STATE_CHANGED',
|
|
123
|
+
'PIPELINE_STARTED', 'STAGE_ATTEMPT_CHANGED', 'HUMAN_REQUEST_OPENED',
|
|
124
|
+
'HUMAN_REQUEST_RESOLVED', 'USAGE_RECORDED', 'BUDGET_THRESHOLD_REACHED',
|
|
125
|
+
'PIPELINE_PAUSED', 'PIPELINE_RESUMED', 'PIPELINE_CANCELLED',
|
|
126
|
+
'BUDGET_OVERRIDE_APPROVED', 'RECOVERY_REPORT_CREATED', 'PIPELINE_COMPLETED',
|
|
127
|
+
'EVIDENCE_ARTIFACT_RECORDED', 'ACCEPTANCE_REQUESTED', 'ACCEPTANCE_RESOLVED',
|
|
128
|
+
'PROVIDER_SESSION_STARTED', 'CONTEXT_HANDOFF_REQUESTED', 'CHECKPOINT_PUBLISHED',
|
|
129
|
+
'PROVIDER_SESSION_ENDED', 'CONTEXT_FLOOR_EXCEEDED'
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
aggregate_type TEXT NOT NULL CHECK (aggregate_type IN ('PROJECT', 'WORK_ITEM')),
|
|
133
|
+
aggregate_id TEXT NOT NULL,
|
|
134
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
|
|
135
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('HUMAN', 'SYSTEM')),
|
|
136
|
+
actor_id TEXT NOT NULL,
|
|
137
|
+
occurred_at TEXT NOT NULL,
|
|
138
|
+
correlation_id TEXT NOT NULL,
|
|
139
|
+
data_json TEXT NOT NULL CHECK (json_valid(data_json))
|
|
140
|
+
) STRICT;
|
|
141
|
+
|
|
142
|
+
INSERT INTO events (
|
|
143
|
+
sequence, id, schema_version, type, aggregate_type, aggregate_id, project_id,
|
|
144
|
+
actor_type, actor_id, occurred_at, correlation_id, data_json
|
|
145
|
+
)
|
|
146
|
+
SELECT
|
|
147
|
+
sequence, id, schema_version, type, aggregate_type, aggregate_id, project_id,
|
|
148
|
+
actor_type, actor_id, occurred_at, correlation_id, data_json
|
|
149
|
+
FROM events_v5;
|
|
150
|
+
|
|
151
|
+
DROP TABLE events_v5;
|
|
152
|
+
|
|
153
|
+
CREATE INDEX events_project_sequence_idx ON events(project_id, sequence);
|
|
154
|
+
CREATE INDEX events_aggregate_sequence_idx ON events(aggregate_id, sequence);
|
|
155
|
+
|
|
156
|
+
CREATE TRIGGER events_are_append_only_update
|
|
157
|
+
BEFORE UPDATE ON events
|
|
158
|
+
BEGIN
|
|
159
|
+
SELECT RAISE(ABORT, 'events are append-only');
|
|
160
|
+
END;
|
|
161
|
+
|
|
162
|
+
CREATE TRIGGER events_are_append_only_delete
|
|
163
|
+
BEFORE DELETE ON events
|
|
164
|
+
BEGIN
|
|
165
|
+
SELECT RAISE(ABORT, 'events are append-only');
|
|
166
|
+
END;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Spec docs/plans/07-a1-session-handoff-spec.ru.md §7 (the mis-estimated-pack branch).
|
|
2
|
+
--
|
|
3
|
+
-- When a provider rejects a pack Loomrail judged as fitting, the pack share for the next session of
|
|
4
|
+
-- the same StageAttempt drops by a fixed step; after one such retry the attempt asks the owner
|
|
5
|
+
-- instead of narrowing blindly. That "one retry" has to survive a daemon restart for the same
|
|
6
|
+
-- reason `unproductive_sessions` does (§6.4 makes a restart the ordinary end of a session), so the
|
|
7
|
+
-- count lives here rather than in daemon memory.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE stage_attempts
|
|
10
|
+
ADD COLUMN pack_share_backoffs INTEGER NOT NULL DEFAULT 0
|
|
11
|
+
CHECK (pack_share_backoffs >= 0);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
-- Spec docs/plans/07-a1-session-handoff-spec.ru.md §6.5, §7.
|
|
2
|
+
--
|
|
3
|
+
-- Migrations 0006 and 0007 added `unproductive_sessions` and `pack_share_backoffs` as columns with
|
|
4
|
+
-- a DEFAULT, so every stored StageAttempt *row* came through the upgrade correct. The matching
|
|
5
|
+
-- fields on `stageAttemptSchema` are required and the schema is `.strict()`, and that entity is
|
|
6
|
+
-- embedded verbatim in the payloads of PIPELINE_STARTED, STAGE_ATTEMPT_CHANGED, PIPELINE_PAUSED,
|
|
7
|
+
-- PIPELINE_RESUMED, PIPELINE_CANCELLED, BUDGET_OVERRIDE_APPROVED, RECOVERY_REPORT_CREATED,
|
|
8
|
+
-- ACCEPTANCE_REQUESTED, ACCEPTANCE_RESOLVED and PIPELINE_COMPLETED -- none of which 0006 rewrote:
|
|
9
|
+
-- its rebuild of `events` copied `data_json` verbatim. Every payload written before this milestone
|
|
10
|
+
-- therefore fails `domainEventSchema.parse` inside `eventFromRow`, which makes the whole activity
|
|
11
|
+
-- timeline and the events endpoint unreadable on a database that predates A1. The board keeps
|
|
12
|
+
-- loading because snapshots are read from rows, which is exactly why no test caught it: every test
|
|
13
|
+
-- starts from an empty database, so every stored payload already has the post-A1 shape.
|
|
14
|
+
--
|
|
15
|
+
-- The same applies to `commands.result_json`, which is replayed through `stateCommandResultSchema`
|
|
16
|
+
-- when a command id is reused, so a pre-A1 receipt would fail its replay in the same way.
|
|
17
|
+
--
|
|
18
|
+
-- This is the same class of repair as 0004_pipeline_started_budget_backfill.sql, and follows it:
|
|
19
|
+
-- backfill the missing fields with the value the column default already gave the row (0), guard so
|
|
20
|
+
-- the pass is idempotent and touches nothing already correct, and drop and recreate the
|
|
21
|
+
-- append-only triggers around the writes as 0004 and 0005 both do.
|
|
22
|
+
--
|
|
23
|
+
-- Unlike 0004 the paths cannot be spelled out literally: a StageAttempt appears at the top level of
|
|
24
|
+
-- a command result *and* inside `$.events[i].data`, and `events` is an array whose length varies by
|
|
25
|
+
-- command type (RECONCILE_WORKFLOWS emits one report per interrupted run). So the paths are found
|
|
26
|
+
-- with json_tree and folded over with a recursive CTE, one json_insert per occurrence. Adding
|
|
27
|
+
-- object keys never shifts an array index, so the fold order does not matter.
|
|
28
|
+
--
|
|
29
|
+
-- `$.stage IS NOT NULL` narrows the match to actual StageAttempt objects: the key name alone is the
|
|
30
|
+
-- convention this backfill leans on, and requiring a field only a StageAttempt has keeps the pass
|
|
31
|
+
-- from stamping counters onto some future payload that reuses the name for something else.
|
|
32
|
+
--
|
|
33
|
+
-- The guard is "either counter absent", not "unproductiveSessions absent". A database written by a
|
|
34
|
+
-- build between 0006 (which added `unproductive_sessions`) and 0007 (which added
|
|
35
|
+
-- `pack_share_backoffs`) holds payloads carrying the first field and not the second; keying the
|
|
36
|
+
-- guard on the first alone would skip exactly those, and 0008 is recorded as applied afterwards, so
|
|
37
|
+
-- there is no second pass -- that history would stay unreadable for good. `json_insert` rather than
|
|
38
|
+
-- `json_set` for the same reason: on a half-legacy payload the counter that is already there is a
|
|
39
|
+
-- real count, and json_set would flatten it back to 0.
|
|
40
|
+
--
|
|
41
|
+
-- The occurrence list is materialised into a TEMP TABLE indexed on (sequence, n) rather than left
|
|
42
|
+
-- as a CTE. A non-materialised CTE is re-evaluated per fold step -- json_tree over every row again,
|
|
43
|
+
-- once per occurrence -- which made the pass quadratic in history size: measured on synthetic
|
|
44
|
+
-- legacy histories, 2000 events took 9.3s and 2000 events plus 2000 four-occurrence command
|
|
45
|
+
-- receipts took 89s, with no progress output, inside the BEGIN IMMEDIATE of a startup path the
|
|
46
|
+
-- owner cannot skip. The temp table is the same set of rows; only the number of times it is
|
|
47
|
+
-- computed changes.
|
|
48
|
+
|
|
49
|
+
DROP TRIGGER events_are_append_only_update;
|
|
50
|
+
|
|
51
|
+
CREATE TEMP TABLE legacy_event_attempt (
|
|
52
|
+
sequence INTEGER NOT NULL,
|
|
53
|
+
path TEXT NOT NULL,
|
|
54
|
+
n INTEGER NOT NULL
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
INSERT INTO legacy_event_attempt (sequence, path, n)
|
|
58
|
+
SELECT events.sequence, tree.fullkey,
|
|
59
|
+
ROW_NUMBER() OVER (PARTITION BY events.sequence ORDER BY tree.fullkey)
|
|
60
|
+
FROM events, json_tree(events.data_json) AS tree
|
|
61
|
+
WHERE tree.key IN ('stageAttempt', 'previousStageAttempt')
|
|
62
|
+
AND tree.type = 'object'
|
|
63
|
+
AND json_type(tree.value, '$.stage') IS NOT NULL
|
|
64
|
+
AND (json_type(tree.value, '$.unproductiveSessions') IS NULL
|
|
65
|
+
OR json_type(tree.value, '$.packShareBackoffs') IS NULL);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX legacy_event_attempt_fold_idx ON legacy_event_attempt (sequence, n);
|
|
68
|
+
|
|
69
|
+
CREATE TEMP TABLE backfilled_event (
|
|
70
|
+
sequence INTEGER PRIMARY KEY,
|
|
71
|
+
data_json TEXT NOT NULL
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
INSERT INTO backfilled_event (sequence, data_json)
|
|
75
|
+
WITH RECURSIVE
|
|
76
|
+
backfilled(sequence, n, data_json) AS (
|
|
77
|
+
SELECT events.sequence, 0, events.data_json
|
|
78
|
+
FROM events
|
|
79
|
+
WHERE events.sequence IN (SELECT sequence FROM legacy_event_attempt)
|
|
80
|
+
UNION ALL
|
|
81
|
+
SELECT backfilled.sequence, legacy_event_attempt.n,
|
|
82
|
+
json_insert(backfilled.data_json,
|
|
83
|
+
legacy_event_attempt.path || '.unproductiveSessions', 0,
|
|
84
|
+
legacy_event_attempt.path || '.packShareBackoffs', 0)
|
|
85
|
+
FROM backfilled
|
|
86
|
+
JOIN legacy_event_attempt
|
|
87
|
+
ON legacy_event_attempt.sequence = backfilled.sequence
|
|
88
|
+
AND legacy_event_attempt.n = backfilled.n + 1
|
|
89
|
+
)
|
|
90
|
+
SELECT backfilled.sequence, backfilled.data_json
|
|
91
|
+
FROM backfilled
|
|
92
|
+
JOIN (SELECT sequence, MAX(n) AS n FROM legacy_event_attempt GROUP BY sequence) AS folded
|
|
93
|
+
ON folded.sequence = backfilled.sequence
|
|
94
|
+
AND folded.n = backfilled.n;
|
|
95
|
+
|
|
96
|
+
UPDATE events
|
|
97
|
+
SET data_json = (SELECT data_json FROM backfilled_event WHERE backfilled_event.sequence = events.sequence)
|
|
98
|
+
WHERE events.sequence IN (SELECT sequence FROM backfilled_event);
|
|
99
|
+
|
|
100
|
+
DROP TABLE backfilled_event;
|
|
101
|
+
DROP TABLE legacy_event_attempt;
|
|
102
|
+
|
|
103
|
+
CREATE TRIGGER events_are_append_only_update
|
|
104
|
+
BEFORE UPDATE ON events
|
|
105
|
+
BEGIN
|
|
106
|
+
SELECT RAISE(ABORT, 'events are append-only');
|
|
107
|
+
END;
|
|
108
|
+
|
|
109
|
+
DROP TRIGGER commands_are_append_only_update;
|
|
110
|
+
|
|
111
|
+
CREATE TEMP TABLE legacy_command_attempt (
|
|
112
|
+
command_id TEXT NOT NULL,
|
|
113
|
+
path TEXT NOT NULL,
|
|
114
|
+
n INTEGER NOT NULL
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
INSERT INTO legacy_command_attempt (command_id, path, n)
|
|
118
|
+
SELECT commands.command_id, tree.fullkey,
|
|
119
|
+
ROW_NUMBER() OVER (PARTITION BY commands.command_id ORDER BY tree.fullkey)
|
|
120
|
+
FROM commands, json_tree(commands.result_json) AS tree
|
|
121
|
+
WHERE tree.key IN ('stageAttempt', 'previousStageAttempt')
|
|
122
|
+
AND tree.type = 'object'
|
|
123
|
+
AND json_type(tree.value, '$.stage') IS NOT NULL
|
|
124
|
+
AND (json_type(tree.value, '$.unproductiveSessions') IS NULL
|
|
125
|
+
OR json_type(tree.value, '$.packShareBackoffs') IS NULL);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX legacy_command_attempt_fold_idx ON legacy_command_attempt (command_id, n);
|
|
128
|
+
|
|
129
|
+
CREATE TEMP TABLE backfilled_command (
|
|
130
|
+
command_id TEXT PRIMARY KEY,
|
|
131
|
+
result_json TEXT NOT NULL
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
INSERT INTO backfilled_command (command_id, result_json)
|
|
135
|
+
WITH RECURSIVE
|
|
136
|
+
backfilled(command_id, n, result_json) AS (
|
|
137
|
+
SELECT commands.command_id, 0, commands.result_json
|
|
138
|
+
FROM commands
|
|
139
|
+
WHERE commands.command_id IN (SELECT command_id FROM legacy_command_attempt)
|
|
140
|
+
UNION ALL
|
|
141
|
+
SELECT backfilled.command_id, legacy_command_attempt.n,
|
|
142
|
+
json_insert(backfilled.result_json,
|
|
143
|
+
legacy_command_attempt.path || '.unproductiveSessions', 0,
|
|
144
|
+
legacy_command_attempt.path || '.packShareBackoffs', 0)
|
|
145
|
+
FROM backfilled
|
|
146
|
+
JOIN legacy_command_attempt
|
|
147
|
+
ON legacy_command_attempt.command_id = backfilled.command_id
|
|
148
|
+
AND legacy_command_attempt.n = backfilled.n + 1
|
|
149
|
+
)
|
|
150
|
+
SELECT backfilled.command_id, backfilled.result_json
|
|
151
|
+
FROM backfilled
|
|
152
|
+
JOIN (SELECT command_id, MAX(n) AS n FROM legacy_command_attempt GROUP BY command_id) AS folded
|
|
153
|
+
ON folded.command_id = backfilled.command_id
|
|
154
|
+
AND folded.n = backfilled.n;
|
|
155
|
+
|
|
156
|
+
UPDATE commands
|
|
157
|
+
SET result_json = (
|
|
158
|
+
SELECT result_json FROM backfilled_command WHERE backfilled_command.command_id = commands.command_id
|
|
159
|
+
)
|
|
160
|
+
WHERE commands.command_id IN (SELECT command_id FROM backfilled_command);
|
|
161
|
+
|
|
162
|
+
DROP TABLE backfilled_command;
|
|
163
|
+
DROP TABLE legacy_command_attempt;
|
|
164
|
+
|
|
165
|
+
CREATE TRIGGER commands_are_append_only_update
|
|
166
|
+
BEFORE UPDATE ON commands
|
|
167
|
+
BEGIN
|
|
168
|
+
SELECT RAISE(ABORT, 'commands are append-only');
|
|
169
|
+
END;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- Spec docs/plans/07-a1-session-handoff-spec.ru.md §6.2: window occupancy is saved.
|
|
2
|
+
--
|
|
3
|
+
-- It was not. Below the handoff threshold `REQUEST_CONTEXT_HANDOFF` decided NO_ACTION and wrote
|
|
4
|
+
-- nothing, so the only occupancy that survived a session was the single reading that crossed the
|
|
5
|
+
-- threshold, and only as a field inside the CONTEXT_HANDOFF_REQUESTED audit event.
|
|
6
|
+
-- LIST_PROVIDER_SESSIONS then rebuilt a displayable number by scanning that event log, which is
|
|
7
|
+
-- exactly the separation of current state from audit that AGENTS.md forbids collapsing.
|
|
8
|
+
--
|
|
9
|
+
-- These four columns give the reading a home in current state. What they hold is the HIGHEST
|
|
10
|
+
-- occupancy the session has been observed at, not its current one: that is the figure that answers
|
|
11
|
+
-- "how full did this session get", which is what explains a cut after the fact, and the write in
|
|
12
|
+
-- REQUEST_CONTEXT_HANDOFF enforces it rather than trusting the order reports arrive in. All four
|
|
13
|
+
-- are nullable together: a session that has never been measured has no occupancy at all, and that
|
|
14
|
+
-- is a different fact from a session measured at zero.
|
|
15
|
+
|
|
16
|
+
ALTER TABLE provider_sessions
|
|
17
|
+
ADD COLUMN context_used_tokens INTEGER
|
|
18
|
+
CHECK (context_used_tokens IS NULL OR context_used_tokens >= 0);
|
|
19
|
+
|
|
20
|
+
ALTER TABLE provider_sessions
|
|
21
|
+
ADD COLUMN context_window_tokens INTEGER
|
|
22
|
+
CHECK (context_window_tokens IS NULL OR context_window_tokens > 0);
|
|
23
|
+
|
|
24
|
+
ALTER TABLE provider_sessions
|
|
25
|
+
ADD COLUMN context_usage_quality TEXT
|
|
26
|
+
CHECK (
|
|
27
|
+
context_usage_quality IS NULL OR
|
|
28
|
+
context_usage_quality IN ('ACTUAL', 'PROVIDER_ESTIMATE', 'LOOMRAIL_ESTIMATE')
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- Mirrors the two-sided CHECKs 0006 wrote for this table's status/end_reason pair, and carries the
|
|
32
|
+
-- whole-row invariant because it is the last column added: contextWindowUsageSchema is one value
|
|
33
|
+
-- with three parts plus the moment it was reported, so all four are present or all four are absent
|
|
34
|
+
-- -- never a used count with no window to measure it against. `usedTokens <= windowTokens` is the
|
|
35
|
+
-- schema's own refine and stays there; SQLite would accept it here too, but a stored row that
|
|
36
|
+
-- predates the constraint cannot be re-checked by ALTER TABLE, so the parse on read is the honest
|
|
37
|
+
-- place for it.
|
|
38
|
+
ALTER TABLE provider_sessions
|
|
39
|
+
ADD COLUMN context_usage_reported_at TEXT
|
|
40
|
+
CHECK (
|
|
41
|
+
(context_usage_reported_at IS NULL) = (context_used_tokens IS NULL) AND
|
|
42
|
+
(context_usage_reported_at IS NULL) = (context_window_tokens IS NULL) AND
|
|
43
|
+
(context_usage_reported_at IS NULL) = (context_usage_quality IS NULL)
|
|
44
|
+
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- The process a RUNNING session is driving, so a daemon that died without killing it can find that
|
|
2
|
+
-- process on the next start. Nullable on purpose: a session that never reached `spawn` has no
|
|
3
|
+
-- process, and that is a different fact from "a process whose pid is 0".
|
|
4
|
+
ALTER TABLE provider_sessions
|
|
5
|
+
ADD COLUMN process_pid INTEGER CHECK (process_pid IS NULL OR process_pid > 0);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
-- The Git worktree a WorkItem is edited in (spec D1/D6, docs/plans/14-e1-workspace-execution-
|
|
2
|
+
-- implementation-plan.ru.md Задача 7). `work_item_id` is UNIQUE: the workspace belongs to the
|
|
3
|
+
-- WorkItem, not to any one StageAttempt, and a second row for the same WorkItem would mean two
|
|
4
|
+
-- writers past the lease. That invariant belongs to the schema, not to a convention callers are
|
|
5
|
+
-- trusted to follow.
|
|
6
|
+
--
|
|
7
|
+
-- `base_commit`/`snapshot_commit` are nullable, not optional-with-a-default: an empty repository
|
|
8
|
+
-- genuinely has no HEAD, and a workspace cut from a clean working copy genuinely carried nothing
|
|
9
|
+
-- forward, so there is no snapshot commit to name (spec §2.9/§2.12, see
|
|
10
|
+
-- packages/contracts/src/workspace.ts).
|
|
11
|
+
--
|
|
12
|
+
-- `lease_holder` names the StageAttempt currently allowed to write, or no one (spec D6): the
|
|
13
|
+
-- workspace outlives any single attempt, so this is a lease held for an attempt's duration, not an
|
|
14
|
+
-- owner. It is acquired by a single `UPDATE ... WHERE lease_holder IS NULL` -- see
|
|
15
|
+
-- ACQUIRE_WORKSPACE_LEASE in src/index.ts -- so the check and the claim are one atomic statement
|
|
16
|
+
-- rather than a read followed by a write.
|
|
17
|
+
CREATE TABLE work_item_workspaces (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
schema_version INTEGER NOT NULL,
|
|
20
|
+
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
21
|
+
work_item_id TEXT NOT NULL UNIQUE REFERENCES work_items(id),
|
|
22
|
+
branch TEXT NOT NULL,
|
|
23
|
+
worktree_path TEXT NOT NULL,
|
|
24
|
+
base_commit TEXT,
|
|
25
|
+
snapshot_commit TEXT,
|
|
26
|
+
status TEXT NOT NULL CHECK (status IN ('READY', 'ORPHANED', 'REMOVED')),
|
|
27
|
+
lease_holder TEXT REFERENCES stage_attempts(id),
|
|
28
|
+
created_at TEXT NOT NULL,
|
|
29
|
+
version INTEGER NOT NULL CHECK (version > 0)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- `events` bakes its allowed types into a CHECK, so widening it for WORK_ITEM_WORKSPACE_CREATED and
|
|
33
|
+
-- WORK_ITEM_WORKSPACE_ORPHANED requires a rebuild: drop the triggers and indexes, rename the table
|
|
34
|
+
-- aside, recreate it with the widened list, copy every row preserving `sequence`, drop the renamed
|
|
35
|
+
-- table, then recreate the indexes and the append-only triggers. This mirrors
|
|
36
|
+
-- 0006_session_handoff.sql's rebuild of the same table.
|
|
37
|
+
DROP TRIGGER events_are_append_only_update;
|
|
38
|
+
DROP TRIGGER events_are_append_only_delete;
|
|
39
|
+
DROP INDEX events_project_sequence_idx;
|
|
40
|
+
DROP INDEX events_aggregate_sequence_idx;
|
|
41
|
+
ALTER TABLE events RENAME TO events_v10;
|
|
42
|
+
|
|
43
|
+
CREATE TABLE events (
|
|
44
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
id TEXT NOT NULL UNIQUE,
|
|
46
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
47
|
+
type TEXT NOT NULL CHECK (
|
|
48
|
+
type IN (
|
|
49
|
+
'PROJECT_REGISTERED', 'WORK_ITEM_CREATED', 'WORK_ITEM_UPDATED', 'WORK_ITEM_STATE_CHANGED',
|
|
50
|
+
'PIPELINE_STARTED', 'STAGE_ATTEMPT_CHANGED', 'HUMAN_REQUEST_OPENED',
|
|
51
|
+
'HUMAN_REQUEST_RESOLVED', 'USAGE_RECORDED', 'BUDGET_THRESHOLD_REACHED',
|
|
52
|
+
'PIPELINE_PAUSED', 'PIPELINE_RESUMED', 'PIPELINE_CANCELLED',
|
|
53
|
+
'BUDGET_OVERRIDE_APPROVED', 'RECOVERY_REPORT_CREATED', 'PIPELINE_COMPLETED',
|
|
54
|
+
'EVIDENCE_ARTIFACT_RECORDED', 'ACCEPTANCE_REQUESTED', 'ACCEPTANCE_RESOLVED',
|
|
55
|
+
'PROVIDER_SESSION_STARTED', 'CONTEXT_HANDOFF_REQUESTED', 'CHECKPOINT_PUBLISHED',
|
|
56
|
+
'PROVIDER_SESSION_ENDED', 'CONTEXT_FLOOR_EXCEEDED',
|
|
57
|
+
'WORK_ITEM_WORKSPACE_CREATED', 'WORK_ITEM_WORKSPACE_ORPHANED'
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
aggregate_type TEXT NOT NULL CHECK (aggregate_type IN ('PROJECT', 'WORK_ITEM')),
|
|
61
|
+
aggregate_id TEXT NOT NULL,
|
|
62
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
|
|
63
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('HUMAN', 'SYSTEM')),
|
|
64
|
+
actor_id TEXT NOT NULL,
|
|
65
|
+
occurred_at TEXT NOT NULL,
|
|
66
|
+
correlation_id TEXT NOT NULL,
|
|
67
|
+
data_json TEXT NOT NULL CHECK (json_valid(data_json))
|
|
68
|
+
) STRICT;
|
|
69
|
+
|
|
70
|
+
INSERT INTO events (
|
|
71
|
+
sequence, id, schema_version, type, aggregate_type, aggregate_id, project_id,
|
|
72
|
+
actor_type, actor_id, occurred_at, correlation_id, data_json
|
|
73
|
+
)
|
|
74
|
+
SELECT
|
|
75
|
+
sequence, id, schema_version, type, aggregate_type, aggregate_id, project_id,
|
|
76
|
+
actor_type, actor_id, occurred_at, correlation_id, data_json
|
|
77
|
+
FROM events_v10;
|
|
78
|
+
|
|
79
|
+
DROP TABLE events_v10;
|
|
80
|
+
|
|
81
|
+
CREATE INDEX events_project_sequence_idx ON events(project_id, sequence);
|
|
82
|
+
CREATE INDEX events_aggregate_sequence_idx ON events(aggregate_id, sequence);
|
|
83
|
+
|
|
84
|
+
CREATE TRIGGER events_are_append_only_update
|
|
85
|
+
BEFORE UPDATE ON events
|
|
86
|
+
BEGIN
|
|
87
|
+
SELECT RAISE(ABORT, 'events are append-only');
|
|
88
|
+
END;
|
|
89
|
+
|
|
90
|
+
CREATE TRIGGER events_are_append_only_delete
|
|
91
|
+
BEFORE DELETE ON events
|
|
92
|
+
BEGIN
|
|
93
|
+
SELECT RAISE(ABORT, 'events are append-only');
|
|
94
|
+
END;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Spec docs/plans/13-e1-workspace-execution-spec.ru.md §4: a Project is a local Git repository the
|
|
2
|
+
-- owner registered by path, and the two bundled fixtures are only the cheap ground the first run
|
|
3
|
+
-- starts from. Until now `projects.fixture_id` said the opposite -- `NOT NULL UNIQUE CHECK
|
|
4
|
+
-- (fixture_id IN ('web-app-a', 'api-service-b'))` -- so the table could hold nothing but the two
|
|
5
|
+
-- demos, and the owner could run a live agent only against Loomrail's own fixtures.
|
|
6
|
+
--
|
|
7
|
+
-- After this migration `fixture_id` is nullable and unrestricted: a Project registered by path
|
|
8
|
+
-- genuinely has no fixture, and NULL records that fact rather than omitting it. It stays UNIQUE,
|
|
9
|
+
-- which in SQLite constrains only non-NULL values -- every NULL is distinct in a UNIQUE index --
|
|
10
|
+
-- so the two demos still cannot each be registered twice while any number of path-registered
|
|
11
|
+
-- Projects coexist. `packages/contracts/src/work-management.ts` makes `projectSchema.fixtureId`
|
|
12
|
+
-- `.nullable()` for the same reason, and on the same grounds.
|
|
13
|
+
--
|
|
14
|
+
-- SQLite cannot alter a CHECK, so this is a table rebuild, the same shape 0006_session_handoff.sql
|
|
15
|
+
-- and 0011_work_item_workspaces.sql use for `events`. `projects` is harder than `events` was:
|
|
16
|
+
-- nothing references `events`, while thirteen tables reference `projects(id)`, twelve of them
|
|
17
|
+
-- ON DELETE RESTRICT. Two things follow, and both are why this migration is declared
|
|
18
|
+
-- `rebuildsAReferencedTable` in src/migrations.ts and therefore runs with `PRAGMA foreign_keys` off
|
|
19
|
+
-- -- SQLite's own documented procedure for this change, checked at the end of the same transaction
|
|
20
|
+
-- by `PRAGMA foreign_key_check`:
|
|
21
|
+
--
|
|
22
|
+
-- * With foreign keys ON, `ALTER TABLE projects RENAME TO ...` rewrites all thirteen
|
|
23
|
+
-- `REFERENCES projects(id)` clauses to name the renamed-aside table, and the database would end
|
|
24
|
+
-- up pointing at a table this migration just dropped. With them off, SQLite leaves those
|
|
25
|
+
-- clauses alone, so they still name `projects` -- which the last statement here restores.
|
|
26
|
+
-- * With foreign keys ON, `DROP TABLE projects` performs an implicit DELETE of every row, and
|
|
27
|
+
-- ON DELETE RESTRICT refuses it immediately. RESTRICT is the one action `defer_foreign_keys`
|
|
28
|
+
-- cannot defer, so deferring is not a way around this either (both were tried).
|
|
29
|
+
--
|
|
30
|
+
-- Every existing row is carried across column by column: the owner's real database has Projects in
|
|
31
|
+
-- it, and this migration preserves them exactly. test/local-state.integration.test.ts asserts both
|
|
32
|
+
-- halves -- the rows survive, and `PRAGMA foreign_key_check` is empty afterwards.
|
|
33
|
+
CREATE TABLE projects_v12 (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
|
|
36
|
+
fixture_id TEXT UNIQUE,
|
|
37
|
+
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 200),
|
|
38
|
+
repository_path TEXT NOT NULL UNIQUE,
|
|
39
|
+
status TEXT NOT NULL CHECK (status IN ('ACTIVE', 'ARCHIVED')),
|
|
40
|
+
version INTEGER NOT NULL CHECK (version > 0),
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
updated_at TEXT NOT NULL
|
|
43
|
+
) STRICT;
|
|
44
|
+
|
|
45
|
+
INSERT INTO projects_v12 (
|
|
46
|
+
id, workspace_id, fixture_id, name, repository_path, status, version, created_at, updated_at
|
|
47
|
+
)
|
|
48
|
+
SELECT
|
|
49
|
+
id, workspace_id, fixture_id, name, repository_path, status, version, created_at, updated_at
|
|
50
|
+
FROM projects;
|
|
51
|
+
|
|
52
|
+
DROP TABLE projects;
|
|
53
|
+
|
|
54
|
+
ALTER TABLE projects_v12 RENAME TO projects;
|