@intentface/latch-drizzle 0.9.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 +21 -0
- package/README.md +36 -0
- package/dist/adapter.d.ts +18 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +33 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapter.pg.d.ts +10 -0
- package/dist/adapter.pg.d.ts.map +1 -0
- package/dist/adapter.pg.js +18 -0
- package/dist/adapter.pg.js.map +1 -0
- package/dist/adapter.shared.d.ts +55 -0
- package/dist/adapter.shared.d.ts.map +1 -0
- package/dist/adapter.shared.js +527 -0
- package/dist/adapter.shared.js.map +1 -0
- package/dist/agent-store.d.ts +34 -0
- package/dist/agent-store.d.ts.map +1 -0
- package/dist/agent-store.js +236 -0
- package/dist/agent-store.js.map +1 -0
- package/dist/agent-store.smoke.d.ts +2 -0
- package/dist/agent-store.smoke.d.ts.map +1 -0
- package/dist/agent-store.smoke.js +77 -0
- package/dist/agent-store.smoke.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/limit-store.d.ts +5 -0
- package/dist/limit-store.d.ts.map +1 -0
- package/dist/limit-store.js +7 -0
- package/dist/limit-store.js.map +1 -0
- package/dist/limit-store.pg.d.ts +5 -0
- package/dist/limit-store.pg.d.ts.map +1 -0
- package/dist/limit-store.pg.js +7 -0
- package/dist/limit-store.pg.js.map +1 -0
- package/dist/limit-store.shared.d.ts +14 -0
- package/dist/limit-store.shared.d.ts.map +1 -0
- package/dist/limit-store.shared.js +60 -0
- package/dist/limit-store.shared.js.map +1 -0
- package/dist/limit-store.smoke.shared.d.ts +7 -0
- package/dist/limit-store.smoke.shared.d.ts.map +1 -0
- package/dist/limit-store.smoke.shared.js +33 -0
- package/dist/limit-store.smoke.shared.js.map +1 -0
- package/dist/memory-job-store.d.ts +5 -0
- package/dist/memory-job-store.d.ts.map +1 -0
- package/dist/memory-job-store.js +7 -0
- package/dist/memory-job-store.js.map +1 -0
- package/dist/memory-job-store.pg.d.ts +5 -0
- package/dist/memory-job-store.pg.d.ts.map +1 -0
- package/dist/memory-job-store.pg.js +7 -0
- package/dist/memory-job-store.pg.js.map +1 -0
- package/dist/memory-job-store.shared.d.ts +23 -0
- package/dist/memory-job-store.shared.d.ts.map +1 -0
- package/dist/memory-job-store.shared.js +294 -0
- package/dist/memory-job-store.shared.js.map +1 -0
- package/dist/memory-job-store.smoke.shared.d.ts +18 -0
- package/dist/memory-job-store.smoke.shared.d.ts.map +1 -0
- package/dist/memory-job-store.smoke.shared.js +149 -0
- package/dist/memory-job-store.smoke.shared.js.map +1 -0
- package/dist/migrate.d.ts +10 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +202 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrate.pg.d.ts +3 -0
- package/dist/migrate.pg.d.ts.map +1 -0
- package/dist/migrate.pg.js +120 -0
- package/dist/migrate.pg.js.map +1 -0
- package/dist/run-lease.smoke.shared.d.ts +13 -0
- package/dist/run-lease.smoke.shared.d.ts.map +1 -0
- package/dist/run-lease.smoke.shared.js +65 -0
- package/dist/run-lease.smoke.shared.js.map +1 -0
- package/dist/schedule.smoke.shared.d.ts +16 -0
- package/dist/schedule.smoke.shared.d.ts.map +1 -0
- package/dist/schedule.smoke.shared.js +104 -0
- package/dist/schedule.smoke.shared.js.map +1 -0
- package/dist/schema.d.ts +2348 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +225 -0
- package/dist/schema.js.map +1 -0
- package/dist/schema.pg.d.ts +2215 -0
- package/dist/schema.pg.d.ts.map +1 -0
- package/dist/schema.pg.js +197 -0
- package/dist/schema.pg.js.map +1 -0
- package/dist/secret-store.d.ts +11 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/secret-store.js +11 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/secret-store.pg.d.ts +6 -0
- package/dist/secret-store.pg.d.ts.map +1 -0
- package/dist/secret-store.pg.js +7 -0
- package/dist/secret-store.pg.js.map +1 -0
- package/dist/secret-store.shared.d.ts +25 -0
- package/dist/secret-store.shared.d.ts.map +1 -0
- package/dist/secret-store.shared.js +67 -0
- package/dist/secret-store.shared.js.map +1 -0
- package/dist/smoke.d.ts +2 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +108 -0
- package/dist/smoke.js.map +1 -0
- package/dist/smoke.pg.d.ts +2 -0
- package/dist/smoke.pg.d.ts.map +1 -0
- package/dist/smoke.pg.js +107 -0
- package/dist/smoke.pg.js.map +1 -0
- package/package.json +62 -0
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** Create the v1 tables. No migration tooling needed to get running. */
|
|
2
|
+
const DDL = [
|
|
3
|
+
`CREATE TABLE IF NOT EXISTS latch_chats (
|
|
4
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL,
|
|
5
|
+
title TEXT, kind TEXT NOT NULL DEFAULT 'user',
|
|
6
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
7
|
+
`CREATE INDEX IF NOT EXISTS latch_chats_owner_idx ON latch_chats (owner)`,
|
|
8
|
+
// NOTE: composite PK — message ids are only unique within a chat, and the
|
|
9
|
+
// UNIQUE (chat_id, seq) guards concurrent appenders. Databases created by an
|
|
10
|
+
// older version of this DDL (global `id` PK, non-unique index) can't take the
|
|
11
|
+
// UNIQUE index in place (the old writer restarted `seq` per turn, so a chat
|
|
12
|
+
// may hold duplicate seqs). `migrate(client)` heals them first via
|
|
13
|
+
// `healLegacyMessages` (rebuild + re-sequence); the exec-only `migrateWith`
|
|
14
|
+
// path (no query seam) still needs that one-off rebuild done by the host.
|
|
15
|
+
`CREATE TABLE IF NOT EXISTS latch_messages (
|
|
16
|
+
id TEXT NOT NULL, chat_id TEXT NOT NULL, run_id TEXT, seq INTEGER NOT NULL,
|
|
17
|
+
role TEXT NOT NULL, parts TEXT NOT NULL, metadata TEXT, created_at INTEGER NOT NULL,
|
|
18
|
+
PRIMARY KEY (chat_id, id))`,
|
|
19
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS latch_messages_chat_seq_idx ON latch_messages (chat_id, seq)`,
|
|
20
|
+
`CREATE TABLE IF NOT EXISTS latch_runs (
|
|
21
|
+
id TEXT PRIMARY KEY, chat_id TEXT NOT NULL, agent TEXT NOT NULL, status TEXT NOT NULL,
|
|
22
|
+
identity TEXT,
|
|
23
|
+
lease_owner TEXT, lease_expires_at INTEGER, fencing_token INTEGER, heartbeat_at INTEGER,
|
|
24
|
+
attempt INTEGER, usage_input_tokens INTEGER,
|
|
25
|
+
usage_output_tokens INTEGER, usage_cache_read_tokens INTEGER, usage_cache_write_tokens INTEGER,
|
|
26
|
+
cost_total REAL, kind TEXT, tools_hash TEXT, error TEXT,
|
|
27
|
+
started_at INTEGER NOT NULL, ended_at INTEGER)`,
|
|
28
|
+
`CREATE INDEX IF NOT EXISTS latch_runs_chat_idx ON latch_runs (chat_id)`,
|
|
29
|
+
`CREATE INDEX IF NOT EXISTS latch_runs_reaper_idx ON latch_runs (status, lease_expires_at)`,
|
|
30
|
+
// One active run per chat (see ChatBusyError). Existing DBs may hold several
|
|
31
|
+
// active rows for one chat (pre-invariant concurrent turns, never-reaped T1
|
|
32
|
+
// crashes): keep the newest, mark the rest errored — then the index can exist.
|
|
33
|
+
`UPDATE latch_runs SET status = 'errored', error = 'superseded: duplicate active run',
|
|
34
|
+
lease_owner = NULL, fencing_token = COALESCE(fencing_token, 0) + 1
|
|
35
|
+
WHERE status = 'active' AND EXISTS (
|
|
36
|
+
SELECT 1 FROM latch_runs n
|
|
37
|
+
WHERE n.chat_id = latch_runs.chat_id AND n.status = 'active'
|
|
38
|
+
AND (n.started_at > latch_runs.started_at
|
|
39
|
+
OR (n.started_at = latch_runs.started_at AND n.id > latch_runs.id)))`,
|
|
40
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS latch_runs_one_active_per_chat ON latch_runs (chat_id) WHERE status = 'active'`,
|
|
41
|
+
`CREATE TABLE IF NOT EXISTS latch_usage (
|
|
42
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
43
|
+
model TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL,
|
|
44
|
+
cache_read_tokens INTEGER, cache_write_tokens INTEGER,
|
|
45
|
+
cost REAL NOT NULL, created_at INTEGER NOT NULL)`,
|
|
46
|
+
`CREATE INDEX IF NOT EXISTS latch_usage_owner_idx ON latch_usage (owner)`,
|
|
47
|
+
`CREATE TABLE IF NOT EXISTS latch_secrets (
|
|
48
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, key TEXT NOT NULL,
|
|
49
|
+
ciphertext TEXT NOT NULL, iv TEXT NOT NULL, wrapped_dek TEXT NOT NULL, dek_iv TEXT NOT NULL,
|
|
50
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
51
|
+
`CREATE INDEX IF NOT EXISTS latch_secrets_owner_idx ON latch_secrets (owner)`,
|
|
52
|
+
// Same shape latch-platform's libsql agent store creates (base table +
|
|
53
|
+
// idempotent ALTERs), so either writer can own an existing DB.
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS latch_agents (
|
|
55
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, name TEXT NOT NULL,
|
|
56
|
+
title TEXT, description TEXT, model_id TEXT NOT NULL, instructions TEXT NOT NULL,
|
|
57
|
+
connections TEXT NOT NULL, tools TEXT, exec TEXT, subagents TEXT, attached_evals TEXT,
|
|
58
|
+
max_steps INTEGER,
|
|
59
|
+
scope TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
60
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS latch_agents_owner_name ON latch_agents (owner, name)`,
|
|
61
|
+
`CREATE TABLE IF NOT EXISTS latch_schedules (
|
|
62
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL, identity TEXT,
|
|
63
|
+
cron TEXT, timezone TEXT, prompt TEXT NOT NULL, delivery TEXT,
|
|
64
|
+
next_run_at INTEGER NOT NULL, last_run_at INTEGER,
|
|
65
|
+
on_parked TEXT, last_chat_id TEXT,
|
|
66
|
+
enabled INTEGER NOT NULL, lease_owner TEXT, lease_expires_at INTEGER,
|
|
67
|
+
created_at INTEGER NOT NULL)`,
|
|
68
|
+
`CREATE INDEX IF NOT EXISTS latch_schedules_owner_idx ON latch_schedules (owner)`,
|
|
69
|
+
`CREATE INDEX IF NOT EXISTS latch_schedules_due_idx ON latch_schedules (enabled, next_run_at)`,
|
|
70
|
+
// Memory sweep state (see @intentface/latch-memory). Lease columns mirror
|
|
71
|
+
// latch_schedules; next_eligible_at always advances so nothing hot-loops.
|
|
72
|
+
// (The sweep index on latch_chats lives in ALTERS: it references the `kind`
|
|
73
|
+
// column, which pre-existing DBs only gain from the ALTER below.)
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS latch_memory_chat_state (
|
|
75
|
+
chat_id TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL, scope_key TEXT NOT NULL,
|
|
76
|
+
last_extracted_seq INTEGER NOT NULL DEFAULT -1, last_extracted_key TEXT,
|
|
77
|
+
next_eligible_at INTEGER NOT NULL DEFAULT 0, failures INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
lease_owner TEXT, lease_expires_at INTEGER,
|
|
79
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
80
|
+
`CREATE INDEX IF NOT EXISTS latch_memory_chat_state_scope_idx ON latch_memory_chat_state (scope_key)`,
|
|
81
|
+
`CREATE TABLE IF NOT EXISTS latch_memory_scope_state (
|
|
82
|
+
scope_key TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL, connection TEXT NOT NULL,
|
|
83
|
+
identity TEXT, has_new INTEGER NOT NULL DEFAULT 0, pending_episodes TEXT,
|
|
84
|
+
consolidation_watermark TEXT, last_consolidated_at INTEGER,
|
|
85
|
+
next_eligible_at INTEGER NOT NULL DEFAULT 0, failures INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
lease_owner TEXT, lease_expires_at INTEGER,
|
|
87
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
|
|
88
|
+
`CREATE INDEX IF NOT EXISTS latch_memory_scope_due_idx ON latch_memory_scope_state (has_new, next_eligible_at)`,
|
|
89
|
+
`CREATE TABLE IF NOT EXISTS latch_memory_usage (
|
|
90
|
+
scope_key TEXT NOT NULL, memory_id TEXT NOT NULL,
|
|
91
|
+
usage_count INTEGER NOT NULL DEFAULT 0, last_used_at INTEGER, created_at INTEGER NOT NULL,
|
|
92
|
+
PRIMARY KEY (scope_key, memory_id))`,
|
|
93
|
+
`CREATE TABLE IF NOT EXISTS latch_limit_counters (
|
|
94
|
+
key TEXT NOT NULL, window_start INTEGER NOT NULL, window_ms INTEGER NOT NULL,
|
|
95
|
+
turns INTEGER NOT NULL DEFAULT 0, tokens INTEGER NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0,
|
|
96
|
+
updated_at INTEGER NOT NULL,
|
|
97
|
+
PRIMARY KEY (key, window_start, window_ms))`,
|
|
98
|
+
// The prune predicate (`window_start + window_ms < ?`) as an expression index.
|
|
99
|
+
`CREATE INDEX IF NOT EXISTS idx_limit_counters_end ON latch_limit_counters (window_start + window_ms)`,
|
|
100
|
+
];
|
|
101
|
+
/** Idempotent column adds for DBs created before a column existed. */
|
|
102
|
+
const ALTERS = [
|
|
103
|
+
`ALTER TABLE latch_schedules ADD COLUMN timezone TEXT`,
|
|
104
|
+
`ALTER TABLE latch_schedules ADD COLUMN delivery TEXT`,
|
|
105
|
+
`ALTER TABLE latch_schedules ADD COLUMN on_parked TEXT`,
|
|
106
|
+
`ALTER TABLE latch_schedules ADD COLUMN last_chat_id TEXT`,
|
|
107
|
+
`ALTER TABLE latch_runs ADD COLUMN usage_cache_read_tokens INTEGER`,
|
|
108
|
+
`ALTER TABLE latch_runs ADD COLUMN usage_cache_write_tokens INTEGER`,
|
|
109
|
+
`ALTER TABLE latch_runs ADD COLUMN kind TEXT`,
|
|
110
|
+
`ALTER TABLE latch_runs ADD COLUMN tools_hash TEXT`,
|
|
111
|
+
`ALTER TABLE latch_usage ADD COLUMN cache_read_tokens INTEGER`,
|
|
112
|
+
`ALTER TABLE latch_usage ADD COLUMN cache_write_tokens INTEGER`,
|
|
113
|
+
`ALTER TABLE latch_chats ADD COLUMN kind TEXT NOT NULL DEFAULT 'user'`,
|
|
114
|
+
`ALTER TABLE latch_memory_chat_state ADD COLUMN last_extracted_key TEXT`,
|
|
115
|
+
`ALTER TABLE latch_agents ADD COLUMN attached_evals TEXT`,
|
|
116
|
+
// AFTER the kind ALTER above — a pre-kind DB can't index the column earlier.
|
|
117
|
+
`CREATE INDEX IF NOT EXISTS latch_chats_sweep_idx ON latch_chats (kind, updated_at)`,
|
|
118
|
+
];
|
|
119
|
+
/**
|
|
120
|
+
* "Column already exists", wherever a driver put the message (top level or the
|
|
121
|
+
* `cause` chain). Matches only the duplicate-COLUMN forms — sqlite/libsql
|
|
122
|
+
* "duplicate column name", Postgres `column "x" of relation "y" already exists`
|
|
123
|
+
* — so an unrelated schema failure that merely says "already exists" is still
|
|
124
|
+
* propagated.
|
|
125
|
+
*/
|
|
126
|
+
function isColumnExists(error) {
|
|
127
|
+
let e = error;
|
|
128
|
+
for (let depth = 0; e && depth < 5; depth++) {
|
|
129
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
130
|
+
if (/duplicate column|column .*already exists/i.test(message))
|
|
131
|
+
return true;
|
|
132
|
+
e = e instanceof Error ? e.cause : undefined;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run the schema against any executor — a driver-agnostic seam so a consumer on
|
|
138
|
+
* a non-libsql driver (e.g. better-sqlite3's `db.run`) can create the tables
|
|
139
|
+
* without pulling in a libsql `Client`. `exec` runs one statement; ALTERs that
|
|
140
|
+
* fail because the column already exists are swallowed.
|
|
141
|
+
*/
|
|
142
|
+
export async function migrateWith(exec) {
|
|
143
|
+
for (const stmt of DDL)
|
|
144
|
+
await exec(stmt);
|
|
145
|
+
for (const stmt of ALTERS) {
|
|
146
|
+
try {
|
|
147
|
+
await exec(stmt);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// Idempotency only: swallow "column already exists" (sqlite/libsql say
|
|
151
|
+
// "duplicate column name"), propagate everything else (locked DB, failed
|
|
152
|
+
// write) so callers don't proceed against an outdated schema. Some
|
|
153
|
+
// drivers surface the message on a wrapped cause (drizzle's better-sqlite3
|
|
154
|
+
// binding throws a DrizzleError whose `cause` is the SqliteError), so
|
|
155
|
+
// look down the cause chain, not just at the top-level message.
|
|
156
|
+
if (!isColumnExists(error))
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* One-off upgrade for DBs created before `latch_messages` moved to a composite
|
|
163
|
+
* `(chat_id, id)` PK + `UNIQUE (chat_id, seq)`. The old DDL used a global `id`
|
|
164
|
+
* PK and a NON-unique `(chat_id, seq)` index, and the old writer restarted
|
|
165
|
+
* `seq` per turn — so an existing chat can hold duplicate `(chat_id, seq)` rows,
|
|
166
|
+
* which makes the new UNIQUE index fail to create. Rebuild once into the correct
|
|
167
|
+
* shape, re-sequencing `seq` densely per chat (order preserved: by old seq, then
|
|
168
|
+
* time), then swap. No-op on a fresh DB (table absent → the DDL builds it right)
|
|
169
|
+
* or an already-migrated DB (composite PK present). Columns are unchanged, so
|
|
170
|
+
* the copy is loss-free. libsql-only (needs a query to detect the legacy shape);
|
|
171
|
+
* the exec-only `migrateWith` path can't run it.
|
|
172
|
+
*/
|
|
173
|
+
async function healLegacyMessages(client) {
|
|
174
|
+
const existing = await client.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='latch_messages'");
|
|
175
|
+
const ddl = existing.rows[0]?.sql;
|
|
176
|
+
if (!ddl)
|
|
177
|
+
return; // fresh DB — the create DDL builds the right shape
|
|
178
|
+
if (/PRIMARY\s+KEY\s*\(\s*chat_id\s*,\s*id\s*\)/i.test(ddl))
|
|
179
|
+
return; // already upgraded
|
|
180
|
+
// Atomic rebuild: correct-shape table, densely re-sequenced seq per chat.
|
|
181
|
+
await client.batch([
|
|
182
|
+
`CREATE TABLE latch_messages_v2 (
|
|
183
|
+
id TEXT NOT NULL, chat_id TEXT NOT NULL, run_id TEXT, seq INTEGER NOT NULL,
|
|
184
|
+
role TEXT NOT NULL, parts TEXT NOT NULL, metadata TEXT, created_at INTEGER NOT NULL,
|
|
185
|
+
PRIMARY KEY (chat_id, id))`,
|
|
186
|
+
// Re-sequence by true chronology — `created_at`, then insertion order
|
|
187
|
+
// (`rowid`). NOT by old `seq`: the legacy writer restarted `seq` per turn,
|
|
188
|
+
// so ordering by seq would interleave turns (all seq=0s, then all seq=1s).
|
|
189
|
+
`INSERT INTO latch_messages_v2 (id, chat_id, run_id, seq, role, parts, metadata, created_at)
|
|
190
|
+
SELECT id, chat_id, run_id,
|
|
191
|
+
ROW_NUMBER() OVER (PARTITION BY chat_id ORDER BY created_at, rowid) - 1,
|
|
192
|
+
role, parts, metadata, created_at
|
|
193
|
+
FROM latch_messages`,
|
|
194
|
+
`DROP TABLE latch_messages`,
|
|
195
|
+
`ALTER TABLE latch_messages_v2 RENAME TO latch_messages`,
|
|
196
|
+
], "write");
|
|
197
|
+
}
|
|
198
|
+
export async function migrate(client) {
|
|
199
|
+
await healLegacyMessages(client);
|
|
200
|
+
await migrateWith((s) => client.execute(s));
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.pg.d.ts","sourceRoot":"","sources":["../src/migrate.pg.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAoHtD,wBAAsB,eAAe,CAAC,EAAE,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
const DDL = [
|
|
3
|
+
`CREATE TABLE IF NOT EXISTS latch_chats (
|
|
4
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL,
|
|
5
|
+
title TEXT, kind TEXT NOT NULL DEFAULT 'user',
|
|
6
|
+
created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL)`,
|
|
7
|
+
`CREATE INDEX IF NOT EXISTS latch_chats_owner_idx ON latch_chats (owner)`,
|
|
8
|
+
// NOTE: composite PK — message ids are only unique within a chat, and the
|
|
9
|
+
// UNIQUE (chat_id, seq) guards concurrent appenders. Databases created by an
|
|
10
|
+
// older version of this DDL (global `id` PK, non-unique index) are NOT
|
|
11
|
+
// upgraded by migratePostgres (it only creates missing tables / adds
|
|
12
|
+
// columns) — they need a one-off constraint swap / table rebuild.
|
|
13
|
+
`CREATE TABLE IF NOT EXISTS latch_messages (
|
|
14
|
+
id TEXT NOT NULL, chat_id TEXT NOT NULL, run_id TEXT, seq INTEGER NOT NULL,
|
|
15
|
+
role TEXT NOT NULL, parts JSONB NOT NULL, metadata JSONB, created_at BIGINT NOT NULL,
|
|
16
|
+
PRIMARY KEY (chat_id, id))`,
|
|
17
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS latch_messages_chat_seq_idx ON latch_messages (chat_id, seq)`,
|
|
18
|
+
`CREATE TABLE IF NOT EXISTS latch_runs (
|
|
19
|
+
id TEXT PRIMARY KEY, chat_id TEXT NOT NULL, agent TEXT NOT NULL, status TEXT NOT NULL,
|
|
20
|
+
identity JSONB,
|
|
21
|
+
lease_owner TEXT, lease_expires_at BIGINT, fencing_token INTEGER, heartbeat_at BIGINT,
|
|
22
|
+
attempt INTEGER, usage_input_tokens INTEGER,
|
|
23
|
+
usage_output_tokens INTEGER, usage_cache_read_tokens INTEGER, usage_cache_write_tokens INTEGER,
|
|
24
|
+
cost_total DOUBLE PRECISION, kind TEXT, tools_hash TEXT, error TEXT,
|
|
25
|
+
started_at BIGINT NOT NULL, ended_at BIGINT)`,
|
|
26
|
+
`CREATE INDEX IF NOT EXISTS latch_runs_chat_idx ON latch_runs (chat_id)`,
|
|
27
|
+
`CREATE INDEX IF NOT EXISTS latch_runs_reaper_idx ON latch_runs (status, lease_expires_at)`,
|
|
28
|
+
// One active run per chat (see ChatBusyError). Existing DBs may hold several
|
|
29
|
+
// active rows for one chat (pre-invariant concurrent turns, never-reaped T1
|
|
30
|
+
// crashes): keep the newest, mark the rest errored — then the index can exist.
|
|
31
|
+
`UPDATE latch_runs SET status = 'errored', error = 'superseded: duplicate active run',
|
|
32
|
+
lease_owner = NULL, fencing_token = COALESCE(fencing_token, 0) + 1
|
|
33
|
+
WHERE status = 'active' AND EXISTS (
|
|
34
|
+
SELECT 1 FROM latch_runs n
|
|
35
|
+
WHERE n.chat_id = latch_runs.chat_id AND n.status = 'active'
|
|
36
|
+
AND (n.started_at > latch_runs.started_at
|
|
37
|
+
OR (n.started_at = latch_runs.started_at AND n.id > latch_runs.id)))`,
|
|
38
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS latch_runs_one_active_per_chat ON latch_runs (chat_id) WHERE status = 'active'`,
|
|
39
|
+
`CREATE TABLE IF NOT EXISTS latch_usage (
|
|
40
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
41
|
+
model TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL,
|
|
42
|
+
cache_read_tokens INTEGER, cache_write_tokens INTEGER,
|
|
43
|
+
cost DOUBLE PRECISION NOT NULL, created_at BIGINT NOT NULL)`,
|
|
44
|
+
`CREATE INDEX IF NOT EXISTS latch_usage_owner_idx ON latch_usage (owner)`,
|
|
45
|
+
`CREATE TABLE IF NOT EXISTS latch_secrets (
|
|
46
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, key TEXT NOT NULL,
|
|
47
|
+
ciphertext TEXT NOT NULL, iv TEXT NOT NULL, wrapped_dek TEXT NOT NULL, dek_iv TEXT NOT NULL,
|
|
48
|
+
created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL)`,
|
|
49
|
+
`CREATE INDEX IF NOT EXISTS latch_secrets_owner_idx ON latch_secrets (owner)`,
|
|
50
|
+
`CREATE TABLE IF NOT EXISTS latch_agents (
|
|
51
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, name TEXT NOT NULL,
|
|
52
|
+
title TEXT, description TEXT, model_id TEXT NOT NULL, instructions TEXT NOT NULL,
|
|
53
|
+
connections JSONB NOT NULL, tools JSONB, exec JSONB, subagents JSONB, attached_evals JSONB,
|
|
54
|
+
max_steps INTEGER,
|
|
55
|
+
scope TEXT NOT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL)`,
|
|
56
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS latch_agents_owner_name ON latch_agents (owner, name)`,
|
|
57
|
+
`CREATE TABLE IF NOT EXISTS latch_schedules (
|
|
58
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL, identity JSONB,
|
|
59
|
+
cron TEXT, timezone TEXT, prompt TEXT NOT NULL, delivery JSONB,
|
|
60
|
+
next_run_at BIGINT NOT NULL, last_run_at BIGINT,
|
|
61
|
+
on_parked TEXT, last_chat_id TEXT,
|
|
62
|
+
enabled INTEGER NOT NULL, lease_owner TEXT, lease_expires_at BIGINT,
|
|
63
|
+
created_at BIGINT NOT NULL)`,
|
|
64
|
+
`CREATE INDEX IF NOT EXISTS latch_schedules_owner_idx ON latch_schedules (owner)`,
|
|
65
|
+
`CREATE INDEX IF NOT EXISTS latch_schedules_due_idx ON latch_schedules (enabled, next_run_at)`,
|
|
66
|
+
// Memory sweep state (see @intentface/latch-memory). Lease columns mirror
|
|
67
|
+
// latch_schedules; next_eligible_at always advances so nothing hot-loops.
|
|
68
|
+
// (The sweep index on latch_chats lives in ALTERS: it references the `kind`
|
|
69
|
+
// column, which pre-existing DBs only gain from the ALTER below.)
|
|
70
|
+
`CREATE TABLE IF NOT EXISTS latch_memory_chat_state (
|
|
71
|
+
chat_id TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL, scope_key TEXT NOT NULL,
|
|
72
|
+
last_extracted_seq INTEGER NOT NULL DEFAULT -1, last_extracted_key TEXT,
|
|
73
|
+
next_eligible_at BIGINT NOT NULL DEFAULT 0, failures INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
lease_owner TEXT, lease_expires_at BIGINT,
|
|
75
|
+
created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL)`,
|
|
76
|
+
`CREATE INDEX IF NOT EXISTS latch_memory_chat_state_scope_idx ON latch_memory_chat_state (scope_key)`,
|
|
77
|
+
`CREATE TABLE IF NOT EXISTS latch_memory_scope_state (
|
|
78
|
+
scope_key TEXT PRIMARY KEY, owner TEXT NOT NULL, agent TEXT NOT NULL, connection TEXT NOT NULL,
|
|
79
|
+
identity JSONB, has_new INTEGER NOT NULL DEFAULT 0, pending_episodes JSONB,
|
|
80
|
+
consolidation_watermark TEXT, last_consolidated_at BIGINT,
|
|
81
|
+
next_eligible_at BIGINT NOT NULL DEFAULT 0, failures INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
lease_owner TEXT, lease_expires_at BIGINT,
|
|
83
|
+
created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL)`,
|
|
84
|
+
`CREATE INDEX IF NOT EXISTS latch_memory_scope_due_idx ON latch_memory_scope_state (has_new, next_eligible_at)`,
|
|
85
|
+
`CREATE TABLE IF NOT EXISTS latch_memory_usage (
|
|
86
|
+
scope_key TEXT NOT NULL, memory_id TEXT NOT NULL,
|
|
87
|
+
usage_count INTEGER NOT NULL DEFAULT 0, last_used_at BIGINT, created_at BIGINT NOT NULL,
|
|
88
|
+
PRIMARY KEY (scope_key, memory_id))`,
|
|
89
|
+
`CREATE TABLE IF NOT EXISTS latch_limit_counters (
|
|
90
|
+
key TEXT NOT NULL, window_start BIGINT NOT NULL, window_ms BIGINT NOT NULL,
|
|
91
|
+
turns INTEGER NOT NULL DEFAULT 0, tokens BIGINT NOT NULL DEFAULT 0, cost DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
92
|
+
updated_at BIGINT NOT NULL,
|
|
93
|
+
PRIMARY KEY (key, window_start, window_ms))`,
|
|
94
|
+
`CREATE INDEX IF NOT EXISTS idx_limit_counters_end ON latch_limit_counters ((window_start + window_ms))`,
|
|
95
|
+
];
|
|
96
|
+
/** Idempotent column adds for DBs created before a column existed. */
|
|
97
|
+
const ALTERS = [
|
|
98
|
+
`ALTER TABLE latch_schedules ADD COLUMN IF NOT EXISTS timezone TEXT`,
|
|
99
|
+
`ALTER TABLE latch_schedules ADD COLUMN IF NOT EXISTS delivery JSONB`,
|
|
100
|
+
`ALTER TABLE latch_schedules ADD COLUMN IF NOT EXISTS on_parked TEXT`,
|
|
101
|
+
`ALTER TABLE latch_schedules ADD COLUMN IF NOT EXISTS last_chat_id TEXT`,
|
|
102
|
+
`ALTER TABLE latch_runs ADD COLUMN IF NOT EXISTS usage_cache_read_tokens INTEGER`,
|
|
103
|
+
`ALTER TABLE latch_runs ADD COLUMN IF NOT EXISTS usage_cache_write_tokens INTEGER`,
|
|
104
|
+
`ALTER TABLE latch_runs ADD COLUMN IF NOT EXISTS kind TEXT`,
|
|
105
|
+
`ALTER TABLE latch_runs ADD COLUMN IF NOT EXISTS tools_hash TEXT`,
|
|
106
|
+
`ALTER TABLE latch_usage ADD COLUMN IF NOT EXISTS cache_read_tokens INTEGER`,
|
|
107
|
+
`ALTER TABLE latch_usage ADD COLUMN IF NOT EXISTS cache_write_tokens INTEGER`,
|
|
108
|
+
`ALTER TABLE latch_chats ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'user'`,
|
|
109
|
+
`ALTER TABLE latch_memory_chat_state ADD COLUMN IF NOT EXISTS last_extracted_key TEXT`,
|
|
110
|
+
`ALTER TABLE latch_agents ADD COLUMN IF NOT EXISTS attached_evals JSONB`,
|
|
111
|
+
// AFTER the kind ALTER above — a pre-kind DB can't index the column earlier.
|
|
112
|
+
`CREATE INDEX IF NOT EXISTS latch_chats_sweep_idx ON latch_chats (kind, updated_at)`,
|
|
113
|
+
];
|
|
114
|
+
export async function migratePostgres(db) {
|
|
115
|
+
for (const stmt of DDL)
|
|
116
|
+
await db.execute(sql.raw(stmt));
|
|
117
|
+
for (const stmt of ALTERS)
|
|
118
|
+
await db.execute(sql.raw(stmt));
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=migrate.pg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.pg.js","sourceRoot":"","sources":["../src/migrate.pg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAGlC,MAAM,GAAG,GAAG;IACV;;;6DAG2D;IAC3D,yEAAyE;IACzE,0EAA0E;IAC1E,6EAA6E;IAC7E,uEAAuE;IACvE,qEAAqE;IACrE,kEAAkE;IAClE;;;gCAG8B;IAC9B,gGAAgG;IAChG;;;;;;;kDAOgD;IAChD,wEAAwE;IACxE,2FAA2F;IAC3F,6EAA6E;IAC7E,4EAA4E;IAC5E,+EAA+E;IAC/E;;;;;;iFAM+E;IAC/E,kHAAkH;IAClH;;;;iEAI+D;IAC/D,yEAAyE;IACzE;;;6DAG2D;IAC3D,6EAA6E;IAC7E;;;;;kFAKgF;IAChF,yFAAyF;IACzF;;;;;;iCAM+B;IAC/B,iFAAiF;IACjF,8FAA8F;IAC9F,0EAA0E;IAC1E,0EAA0E;IAC1E,4EAA4E;IAC5E,kEAAkE;IAClE;;;;;6DAK2D;IAC3D,qGAAqG;IACrG;;;;;;6DAM2D;IAC3D,+GAA+G;IAC/G;;;yCAGuC;IACvC;;;;iDAI+C;IAC/C,wGAAwG;CACzG,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,GAAG;IACb,oEAAoE;IACpE,qEAAqE;IACrE,qEAAqE;IACrE,wEAAwE;IACxE,iFAAiF;IACjF,kFAAkF;IAClF,2DAA2D;IAC3D,iEAAiE;IACjE,4EAA4E;IAC5E,6EAA6E;IAC7E,oFAAoF;IACpF,sFAAsF;IACtF,wEAAwE;IACxE,6EAA6E;IAC7E,oFAAoF;CACrF,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EAA6B;IACjE,KAAK,MAAM,IAAI,IAAI,GAAG;QAAE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,KAAK,MAAM,IAAI,IAAI,MAAM;QAAE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StorageAdapter } from "@intentface/latch-core";
|
|
2
|
+
/**
|
|
3
|
+
* Dialect-shared smoke for the one-active-run-per-chat invariant: a live run
|
|
4
|
+
* refuses a second (durable and non-durable), an expired lease is superseded by
|
|
5
|
+
* the next turn, a paused run doesn't block, and a superseded crash can't be
|
|
6
|
+
* reclaimed by the sweep.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runLeaseSmoke<P>(args: {
|
|
9
|
+
store: StorageAdapter<P>;
|
|
10
|
+
principal: P;
|
|
11
|
+
assert: (cond: boolean, msg: string) => void;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=run-lease.smoke.shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-lease.smoke.shared.d.ts","sourceRoot":"","sources":["../src/run-lease.smoke.shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG7D;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE;IAC3C,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACzB,SAAS,EAAE,CAAC,CAAC;IACb,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DhB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isChatBusyError } from "@intentface/latch-core";
|
|
2
|
+
/**
|
|
3
|
+
* Dialect-shared smoke for the one-active-run-per-chat invariant: a live run
|
|
4
|
+
* refuses a second (durable and non-durable), an expired lease is superseded by
|
|
5
|
+
* the next turn, a paused run doesn't block, and a superseded crash can't be
|
|
6
|
+
* reclaimed by the sweep.
|
|
7
|
+
*/
|
|
8
|
+
export async function runLeaseSmoke(args) {
|
|
9
|
+
const { store, principal: me, assert } = args;
|
|
10
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
const busy = async (fn) => {
|
|
12
|
+
try {
|
|
13
|
+
await fn();
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return isChatBusyError(e) ? e.retryAfterMs : -1;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// Durable: a live lease refuses a second claim, with the lease's remaining time.
|
|
21
|
+
const chat = await store.createChat(me, { agent: "lease" });
|
|
22
|
+
const live = await store.claimRun({ principal: me, chatId: chat.id, agent: "lease", owner: "n1", ttlMs: 60_000 });
|
|
23
|
+
const retry = await busy(() => store.claimRun({ principal: me, chatId: chat.id, agent: "lease", owner: "n2", ttlMs: 60_000 }));
|
|
24
|
+
assert(retry !== null && retry > 0 && retry <= 60_000, `run lease: second claim refused with retryAfterMs (${retry})`);
|
|
25
|
+
assert((await busy(() => store.createRun(me, { chatId: chat.id, agent: "lease" }))) !== null, "run lease: T1 createRun refused too");
|
|
26
|
+
// Paused (awaiting_input) does not block: a decision opens a new run.
|
|
27
|
+
await store.updateRun(live.id, { status: "awaiting_input" });
|
|
28
|
+
const next = await store.claimRun({ principal: me, chatId: chat.id, agent: "lease", owner: "n1", ttlMs: 60_000 });
|
|
29
|
+
assert(next.id !== live.id, "run lease: a paused run does not block the chat");
|
|
30
|
+
await store.updateRun(next.id, { status: "completed", endedAt: Date.now() });
|
|
31
|
+
// Expired lease: the next turn supersedes the crashed run (errored, token bumped) instead of waiting.
|
|
32
|
+
const crashed = await store.claimRun({ principal: me, chatId: chat.id, agent: "lease", owner: "n1", ttlMs: 1 });
|
|
33
|
+
await sleep(5);
|
|
34
|
+
const fresh = await store.claimRun({ principal: me, chatId: chat.id, agent: "lease", owner: "n2", ttlMs: 60_000 });
|
|
35
|
+
const old = (await store.getRun(crashed.id));
|
|
36
|
+
assert(old.status === "errored" && /superseded/.test(old.error ?? ""), "run lease: expired lease superseded by the next turn");
|
|
37
|
+
assert((old.fencingToken ?? 0) > (crashed.fencingToken ?? 0), "run lease: superseded run's fencing token bumped");
|
|
38
|
+
// …and the sweep can no longer reclaim it while the chat has a newer live run.
|
|
39
|
+
assert((await store.reclaimRun(crashed.id, "n3", 60_000)) === null, "run lease: superseded run is not reclaimable");
|
|
40
|
+
await store.updateRun(fresh.id, { status: "completed", endedAt: Date.now() });
|
|
41
|
+
// openTurn: run + message together; a refused turn persists NOTHING.
|
|
42
|
+
const msg = (id) => ({
|
|
43
|
+
id,
|
|
44
|
+
role: "user",
|
|
45
|
+
parts: [{ type: "text", text: id }],
|
|
46
|
+
metadata: { visibility: "user", createdAt: Date.now() },
|
|
47
|
+
});
|
|
48
|
+
const t = await store.createChat(me, { agent: "lease" });
|
|
49
|
+
const opened = await store.openTurn(me, { chatId: t.id, agent: "lease", messages: [msg("u1")], lease: { owner: "n1", ttlMs: 60_000 } });
|
|
50
|
+
assert(opened.status === "active" && opened.leaseOwner === "n1", "openTurn: opens a leased run");
|
|
51
|
+
const refused = await busy(() => store.openTurn(me, { chatId: t.id, agent: "lease", messages: [msg("u2")] }));
|
|
52
|
+
assert(refused !== null && refused > 0, "openTurn: busy chat refused with retryAfterMs");
|
|
53
|
+
const ids = (await store.loadMessages(me, t.id)).map((m) => m.id);
|
|
54
|
+
assert(ids.length === 1 && ids[0] === "u1", `openTurn: refused message not persisted (${ids.join(",")})`);
|
|
55
|
+
// Racing openTurns: exactly one wins, exactly one message lands.
|
|
56
|
+
const t2 = await store.createChat(me, { agent: "lease" });
|
|
57
|
+
const race = await Promise.allSettled(["a", "b", "c"].map((id) => store.openTurn(me, { chatId: t2.id, agent: "lease", messages: [msg(id)] })));
|
|
58
|
+
const won = race.filter((r) => r.status === "fulfilled").length;
|
|
59
|
+
const persisted = (await store.loadMessages(me, t2.id)).length;
|
|
60
|
+
assert(won === 1 && persisted === 1, `openTurn: one of three racing turns wins, one message persisted (${won}/${persisted})`);
|
|
61
|
+
// Another chat is unaffected.
|
|
62
|
+
const other = await store.createChat(me, { agent: "lease" });
|
|
63
|
+
assert((await busy(() => store.createRun(me, { chatId: other.id, agent: "lease" }))) === null, "run lease: other chats unaffected");
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=run-lease.smoke.shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-lease.smoke.shared.js","sourceRoot":"","sources":["../src/run-lease.smoke.shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StorageAdapter } from "@intentface/latch-core";
|
|
2
|
+
/**
|
|
3
|
+
* Schedule persistence, run against BOTH dialects.
|
|
4
|
+
*
|
|
5
|
+
* `delivery` is the host's opaque channel target: `jsonb` on Postgres,
|
|
6
|
+
* json-in-text on sqlite. It has to survive create → list → claim identically
|
|
7
|
+
* on both, or a scheduled push works in dev and silently stops delivering in
|
|
8
|
+
* production. `bumpSchedule` ("run now") is owner-fenced and enabled-only.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scheduleSmoke<P>({ store, me, other, assert, }: {
|
|
11
|
+
store: StorageAdapter<P>;
|
|
12
|
+
me: P;
|
|
13
|
+
other: P;
|
|
14
|
+
assert: (cond: boolean, msg: string) => void;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=schedule.smoke.shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.smoke.shared.d.ts","sourceRoot":"","sources":["../src/schedule.smoke.shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,EACrC,KAAK,EACL,EAAE,EACF,KAAK,EACL,MAAM,GACP,EAAE;IACD,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACzB,EAAE,EAAE,CAAC,CAAC;IACN,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC,IAAI,CAAC,CAuKhB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule persistence, run against BOTH dialects.
|
|
3
|
+
*
|
|
4
|
+
* `delivery` is the host's opaque channel target: `jsonb` on Postgres,
|
|
5
|
+
* json-in-text on sqlite. It has to survive create → list → claim identically
|
|
6
|
+
* on both, or a scheduled push works in dev and silently stops delivering in
|
|
7
|
+
* production. `bumpSchedule` ("run now") is owner-fenced and enabled-only.
|
|
8
|
+
*/
|
|
9
|
+
export async function scheduleSmoke({ store, me, other, assert, }) {
|
|
10
|
+
const delivery = { kind: "telegram", botId: "bot-1", nested: { epoch: 2 } };
|
|
11
|
+
const soon = Date.now() + 60 * 60 * 1000;
|
|
12
|
+
const created = await store.createSchedule(me, {
|
|
13
|
+
agent: "briefer",
|
|
14
|
+
cron: "0 8 * * 1-5",
|
|
15
|
+
timezone: "Europe/Helsinki",
|
|
16
|
+
prompt: "morning brief",
|
|
17
|
+
nextRunAt: soon,
|
|
18
|
+
delivery,
|
|
19
|
+
});
|
|
20
|
+
assert(JSON.stringify(created.delivery) === JSON.stringify(delivery), "schedule returns the delivery blob it was created with");
|
|
21
|
+
const listed = await store.listSchedules(me);
|
|
22
|
+
assert(JSON.stringify(listed[0]?.delivery) === JSON.stringify(delivery), "delivery survives a list round trip");
|
|
23
|
+
assert((await store.listSchedules(other)).length === 0, "schedules are owner-isolated");
|
|
24
|
+
// A schedule with no target stays undefined rather than becoming null/{}.
|
|
25
|
+
const plain = await store.createSchedule(me, {
|
|
26
|
+
agent: "briefer",
|
|
27
|
+
cron: null,
|
|
28
|
+
prompt: "no delivery",
|
|
29
|
+
nextRunAt: soon,
|
|
30
|
+
});
|
|
31
|
+
assert(plain.delivery === undefined, "absent delivery reads back as undefined");
|
|
32
|
+
// onParked round-trips; absent = undefined (the runtime defaults it to skip).
|
|
33
|
+
assert(plain.onParked === undefined, "absent onParked reads back as undefined");
|
|
34
|
+
const alwaysFire = await store.createSchedule(me, {
|
|
35
|
+
agent: "briefer",
|
|
36
|
+
cron: "0 8 * * *",
|
|
37
|
+
prompt: "fire regardless",
|
|
38
|
+
nextRunAt: soon,
|
|
39
|
+
onParked: "fire",
|
|
40
|
+
});
|
|
41
|
+
assert(alwaysFire.onParked === "fire", "onParked survives a create round trip");
|
|
42
|
+
await store.deleteSchedule(me, alwaysFire.id);
|
|
43
|
+
// Not due yet.
|
|
44
|
+
assert((await store.claimDueSchedules(Date.now(), 30_000, "node-1")).length === 0, "a future schedule isn't claimed");
|
|
45
|
+
// Another owner can't arm it, and is told so rather than getting a silent no-op.
|
|
46
|
+
assert((await store.bumpSchedule(other, created.id)) === false, "bumpSchedule reports false for another owner");
|
|
47
|
+
assert((await store.claimDueSchedules(Date.now(), 30_000, "node-1")).length === 0, "bumpSchedule is owner-fenced");
|
|
48
|
+
assert((await store.bumpSchedule(me, "sch_does_not_exist")) === false, "bumpSchedule reports false for an unknown id");
|
|
49
|
+
assert((await store.bumpSchedule(me, created.id)) === true, "bumpSchedule reports the arm");
|
|
50
|
+
const claimed = await store.claimDueSchedules(Date.now(), 30_000, "node-1");
|
|
51
|
+
assert(claimed.some((s) => s.id === created.id), "bumpSchedule makes it due on the next tick");
|
|
52
|
+
const claimedOne = claimed.find((s) => s.id === created.id);
|
|
53
|
+
assert(JSON.stringify(claimedOne.delivery) === JSON.stringify(delivery), "delivery survives the claim the cron reads it through");
|
|
54
|
+
assert(JSON.stringify(claimedOne.identity) === JSON.stringify(me), "claim carries the identity blob (to rebuild the principal)");
|
|
55
|
+
// The double-fire contract, in two halves, on a simulated clock (`now` is a
|
|
56
|
+
// parameter everywhere, so no sleeping). The row is currently leased by
|
|
57
|
+
// node-1 (30s, from the claim above) with nextRunAt=0 from the bump.
|
|
58
|
+
//
|
|
59
|
+
// First the hazard: the claim lease is stamped once and never renewed, so an
|
|
60
|
+
// EXPIRED lease alone does not protect an occurrence — the row is
|
|
61
|
+
// re-claimable the moment the TTL lapses, same owner or not. This is exactly
|
|
62
|
+
// why the runtime must consume the occurrence before firing anything slow.
|
|
63
|
+
const t1 = Date.now() + 60_000; // node-1's 30s lease has expired by here
|
|
64
|
+
await store.claimDueSchedules(t1, 0, "node-1"); // re-claim, already-expired lease
|
|
65
|
+
assert((await store.claimDueSchedules(t1 + 1, 30_000, "node-2")).some((s) => s.id === created.id), "an expired claim lease alone does NOT protect an occurrence (contract, not a bug)");
|
|
66
|
+
// Second, the protection: once the occurrence is CONSUMED (nextRunAt
|
|
67
|
+
// advanced past now), no claimer can take it again — lease state is
|
|
68
|
+
// irrelevant. This is the write the runtime issues between claim and fire.
|
|
69
|
+
await store.completeSchedule(created.id, { nextRunAt: t1 + 24 * 60 * 60 * 1000, lastRunAt: t1, enabled: true }, "node-2");
|
|
70
|
+
assert(!(await store.claimDueSchedules(t1 + 60_000, 30_000, "node-3")).some((s) => s.id === created.id), "a claimed-and-advanced occurrence is not re-claimable, even with the lease expired");
|
|
71
|
+
// Spend it, then confirm a finished schedule can't be re-armed.
|
|
72
|
+
await store.bumpSchedule(me, created.id); // re-arm so the one-shot spend below is reachable
|
|
73
|
+
const t2 = t1 + 120_000; // past node-2's completeSchedule-cleared lease either way
|
|
74
|
+
await store.claimDueSchedules(t2, 30_000, "node-1");
|
|
75
|
+
await store.completeSchedule(created.id, { nextRunAt: t2, lastRunAt: t2, enabled: false }, "node-1");
|
|
76
|
+
assert((await store.bumpSchedule(me, created.id)) === false, "bumpSchedule reports false for a spent one-shot");
|
|
77
|
+
assert(!(await store.claimDueSchedules(Date.now(), 30_000, "node-2")).some((s) => s.id === created.id), "a disabled schedule can't be resurrected by bumping");
|
|
78
|
+
// noteScheduleFire stamps the fire's chat — fenced by the OCCURRENCE: it
|
|
79
|
+
// only lands while last_run_at still holds the value this fire's
|
|
80
|
+
// completeSchedule wrote, so a delayed stamp from an older overlapping fire
|
|
81
|
+
// (runScheduleNow during a long turn) can't overwrite a newer one's.
|
|
82
|
+
const stampMe = await store.createSchedule(me, {
|
|
83
|
+
agent: "briefer",
|
|
84
|
+
cron: null,
|
|
85
|
+
prompt: "stamp test",
|
|
86
|
+
nextRunAt: 0,
|
|
87
|
+
});
|
|
88
|
+
assert(stampMe.lastChatId == null, "lastChatId starts unset");
|
|
89
|
+
const tFire = Date.now() + 300_000; // past every lease/claim time used above
|
|
90
|
+
await store.claimDueSchedules(tFire, 30_000, "node-1");
|
|
91
|
+
await store.completeSchedule(stampMe.id, { nextRunAt: tFire, lastRunAt: tFire, enabled: false }, "node-1");
|
|
92
|
+
await store.noteScheduleFire?.(stampMe.id, { lastChatId: "chat_fire_1", firedAt: tFire });
|
|
93
|
+
let stamped = (await store.listSchedules(me)).find((s) => s.id === stampMe.id);
|
|
94
|
+
assert(stamped?.lastChatId === "chat_fire_1", "noteScheduleFire stamps lastChatId");
|
|
95
|
+
// An OLDER fire's delayed stamp carries its own (stale) firedAt — must no-op.
|
|
96
|
+
await store.noteScheduleFire?.(stampMe.id, { lastChatId: "chat_stale", firedAt: tFire - 1 });
|
|
97
|
+
stamped = (await store.listSchedules(me)).find((s) => s.id === stampMe.id);
|
|
98
|
+
assert(stamped?.lastChatId === "chat_fire_1", "a stale fire's stamp (older occurrence) no-ops");
|
|
99
|
+
await store.deleteSchedule(me, stampMe.id);
|
|
100
|
+
await store.deleteSchedule(me, created.id);
|
|
101
|
+
await store.deleteSchedule(me, plain.id);
|
|
102
|
+
assert((await store.listSchedules(me)).length === 0, "schedules delete cleanly");
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=schedule.smoke.shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.smoke.shared.js","sourceRoot":"","sources":["../src/schedule.smoke.shared.ts"],"names":[],"mappings":"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"}
|