row 271 (264d, #1776): schema step 3l to 3m, session_ops and message_cancels with 7 triggers; successor id pinned as a lowercase version 8 UUID for Huey (gizmoduck-0274)

This commit is contained in:
jason.woltje committed 2026-10-22 12:06:46 -05:00
1 parent 82ff4ca105
commit 74b5e36386
4 files changed
+95 -5

No files matched your search

+9 -3
View File
@@ -29,7 +29,12 @@ Answer, the operation envelope:
"state": "clearing", "start": null, "refusal": null, "at": "..." } "state": "clearing", "start": null, "refusal": null, "at": "..." }
``` ```
- `successor` is bound when the request is accepted, before any asynchronous work. - `successor` is bound when the request is accepted, before any asynchronous work. It is a
lowercase UUID, `xxxxxxxx-xxxx-8xxx-Vxxx-xxxxxxxxxxxx` with V one of `8`, `9`, `a`, `b` (RFC 9562
version 8, variant 10). It's the first 32 hex digits of sha256(business, thread, old session, op)
with those two nibbles set. It matches the UUID test every session selector uses
(cli/src/thread.mjs:41), so 264a's selectors and deep links take it unchanged (Huey's question,
gizmoduck-0274).
- `state`: - `state`:
- `clearing`: accepted. Sends and launches to the old session are fenced, and the drain or stop - `clearing`: accepted. Sends and launches to the old session are fenced, and the drain or stop
is in progress. is in progress.
@@ -139,8 +144,9 @@ alone can't show that no child is running.
- Check owner and grant (the `session.create` path), the revision, and that processing isn't - Check owner and grant (the `session.create` path), the revision, and that processing isn't
halted. halted.
- Bind the successor before any async work: - Bind the successor before any async work:
- The id is deterministic, from a sha256 of (business, thread, old session, op), shaped to - The id is deterministic, from a sha256 of (business, thread, old session, op), shaped as a
`ID_PATTERN` (vocabulary.mjs:199). lowercase version 8 UUID (the contract above). 264a's ids are UUIDs, and every selector
checks the UUID shape, so draft 2's `ID_PATTERN` shape is replaced.
- The name is `<old name>-N`, with N the next free number in that thread. If a user already - The name is `<old name>-N`, with N the next free number in that thread. If a user already
made a session called `<old>-N`, the next free N is taken. made a session called `<old>-N`, the next free N is taken.
- Both go on the `requested` row, so a repeat of the op returns the same id and name. - Both go on the `requested` row, so a repeat of the op returns the same id and name.
+37
View File
@@ -478,3 +478,40 @@ CREATE TABLE gitea_grants (
PRIMARY KEY (business, agent), PRIMARY KEY (business, agent),
FOREIGN KEY (business, agent) REFERENCES gitea_users(business, agent) FOREIGN KEY (business, agent) REFERENCES gitea_users(business, agent)
) STRICT; ) STRICT;
CREATE TABLE session_ops (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
business TEXT NOT NULL,
thread TEXT NOT NULL REFERENCES threads(id),
session TEXT NOT NULL REFERENCES sessions(id),
op TEXT NOT NULL,
stage TEXT NOT NULL CHECK (stage IN ('requested','drained','uncertain','exited','retired','failed','cancelled','start-held','start-started','start-refused')),
attempt INTEGER NOT NULL CHECK (CASE WHEN stage GLOB 'start-*' THEN attempt >= 1 ELSE attempt = 0 END),
work_attempt INTEGER CHECK (work_attempt IS NULL OR (stage GLOB 'start-*' AND work_attempt >= 1)),
successor TEXT NOT NULL,
successor_name TEXT NOT NULL,
run TEXT,
generation INTEGER CHECK (generation IS NULL OR generation >= 1),
digest TEXT,
refusal TEXT,
UNIQUE (thread, op, stage, attempt)
) STRICT;
CREATE TABLE message_cancels (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
message TEXT NOT NULL UNIQUE REFERENCES messages(id),
session TEXT NOT NULL REFERENCES sessions(id),
op TEXT NOT NULL
) STRICT;
CREATE INDEX session_ops_session ON session_ops (session, seq);
CREATE TRIGGER session_ops_no_update BEFORE UPDATE ON session_ops BEGIN SELECT RAISE(ABORT, 'session_ops is append-only'); END;
CREATE TRIGGER session_ops_no_delete BEFORE DELETE ON session_ops BEGIN SELECT RAISE(ABORT, 'session_ops is append-only'); END;
CREATE TRIGGER session_ops_no_replace BEFORE INSERT ON session_ops WHEN EXISTS (SELECT 1 FROM session_ops WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'session_ops is append-only'); END;
CREATE TRIGGER session_ops_one_open BEFORE INSERT ON session_ops
WHEN NEW.stage = 'requested' AND EXISTS (SELECT 1 FROM session_ops o WHERE o.session = NEW.session
AND o.seq = (SELECT max(seq) FROM session_ops WHERE thread = o.thread AND op = o.op)
AND o.stage IN ('requested','drained','uncertain','exited'))
BEGIN SELECT RAISE(ABORT, 'session has an open clear'); END;
CREATE TRIGGER message_cancels_no_update BEFORE UPDATE ON message_cancels BEGIN SELECT RAISE(ABORT, 'message_cancels is append-only'); END;
CREATE TRIGGER message_cancels_no_delete BEFORE DELETE ON message_cancels BEGIN SELECT RAISE(ABORT, 'message_cancels is append-only'); END;
CREATE TRIGGER message_cancels_no_replace BEFORE INSERT ON message_cancels WHEN EXISTS (SELECT 1 FROM message_cancels WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'message_cancels is append-only'); END;
+40
View File
@@ -0,0 +1,40 @@
-- Row 271 (264d, #1776, T1 item 7 D7/D8): clear context. session_ops is the clear journal, one row
-- per stage, and message_cancels holds a receipt for each message a clear cancelled. Only new
-- tables, an index and their triggers.
CREATE TABLE session_ops (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
business TEXT NOT NULL,
thread TEXT NOT NULL REFERENCES threads(id),
session TEXT NOT NULL REFERENCES sessions(id),
op TEXT NOT NULL,
stage TEXT NOT NULL CHECK (stage IN ('requested','drained','uncertain','exited','retired','failed','cancelled','start-held','start-started','start-refused')),
attempt INTEGER NOT NULL CHECK (CASE WHEN stage GLOB 'start-*' THEN attempt >= 1 ELSE attempt = 0 END),
work_attempt INTEGER CHECK (work_attempt IS NULL OR (stage GLOB 'start-*' AND work_attempt >= 1)),
successor TEXT NOT NULL,
successor_name TEXT NOT NULL,
run TEXT,
generation INTEGER CHECK (generation IS NULL OR generation >= 1),
digest TEXT,
refusal TEXT,
UNIQUE (thread, op, stage, attempt)
) STRICT;
CREATE TABLE message_cancels (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
message TEXT NOT NULL UNIQUE REFERENCES messages(id),
session TEXT NOT NULL REFERENCES sessions(id),
op TEXT NOT NULL
) STRICT;
CREATE INDEX session_ops_session ON session_ops (session, seq);
CREATE TRIGGER session_ops_no_update BEFORE UPDATE ON session_ops BEGIN SELECT RAISE(ABORT, 'session_ops is append-only'); END;
CREATE TRIGGER session_ops_no_delete BEFORE DELETE ON session_ops BEGIN SELECT RAISE(ABORT, 'session_ops is append-only'); END;
CREATE TRIGGER session_ops_no_replace BEFORE INSERT ON session_ops WHEN EXISTS (SELECT 1 FROM session_ops WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'session_ops is append-only'); END;
CREATE TRIGGER session_ops_one_open BEFORE INSERT ON session_ops
WHEN NEW.stage = 'requested' AND EXISTS (SELECT 1 FROM session_ops o WHERE o.session = NEW.session
AND o.seq = (SELECT max(seq) FROM session_ops WHERE thread = o.thread AND op = o.op)
AND o.stage IN ('requested','drained','uncertain','exited'))
BEGIN SELECT RAISE(ABORT, 'session has an open clear'); END;
CREATE TRIGGER message_cancels_no_update BEFORE UPDATE ON message_cancels BEGIN SELECT RAISE(ABORT, 'message_cancels is append-only'); END;
CREATE TRIGGER message_cancels_no_delete BEFORE DELETE ON message_cancels BEGIN SELECT RAISE(ABORT, 'message_cancels is append-only'); END;
CREATE TRIGGER message_cancels_no_replace BEFORE INSERT ON message_cancels WHEN EXISTS (SELECT 1 FROM message_cancels WHERE seq = NEW.seq) BEGIN SELECT RAISE(ABORT, 'message_cancels is append-only'); END;
+9 -2
View File
@@ -13,8 +13,8 @@ import { join, isAbsolute } from 'node:path';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8'); const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8');
export const SCHEMA_SHA256 = 'cd68512212ce8bf8f5631138b2e63a340fa7b940088430649bb7784af7df162a'; export const SCHEMA_SHA256 = '129a76db0fe9973cdea5a87f274941b4111c86730fba8a0cbe16e61a534525d0';
export const SCHEMA_VERSION = '3l'; export const SCHEMA_VERSION = '3m';
// Ordered, named offline steps. New schema changes append a step and keep prior DDL pinned. // Ordered, named offline steps. New schema changes append a step and keep prior DDL pinned.
const legacyDDL = readFileSync(new URL('./migrations/3b.sql', import.meta.url), 'utf8'); const legacyDDL = readFileSync(new URL('./migrations/3b.sql', import.meta.url), 'utf8');
const LEGACY_SHA256 = '179ffe356d4ff19a49b5ebad39b6c6bfd7771deb8e1b7c55b5e746f039d69e65'; const LEGACY_SHA256 = '179ffe356d4ff19a49b5ebad39b6c6bfd7771deb8e1b7c55b5e746f039d69e65';
@@ -72,6 +72,11 @@ const migrations = [{
from: '3k', from: '3k',
to: '3l', to: '3l',
sql: readFileSync(new URL('./migrations/3k-3l.sql', import.meta.url), 'utf8'), sql: readFileSync(new URL('./migrations/3k-3l.sql', import.meta.url), 'utf8'),
}, {
// Row 271 (#1776): clear context. The clear journal and the cancelled-message receipts. Only new tables.
from: '3l',
to: '3m',
sql: readFileSync(new URL('./migrations/3l-3m.sql', import.meta.url), 'utf8'),
}]; }];
const timedTables = [ const timedTables = [
'events', 'events',
@@ -89,6 +94,8 @@ const timedTables = [
'thread_opens', 'thread_opens',
'instance_names', 'instance_names',
'usage_records', 'usage_records',
'session_ops',
'message_cancels',
]; ];
const hex = (value) => createHash('sha256').update(value).digest('hex'); const hex = (value) => createHash('sha256').update(value).digest('hex');
if (hex(ddl) !== SCHEMA_SHA256) throw Error('schema-source-mismatch'); if (hex(ddl) !== SCHEMA_SHA256) throw Error('schema-source-mismatch');