@automagik/genie 4.260428.5 → 4.260428.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260428.5",
3
+ "version": "4.260428.7",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: the npm distribution is being soft-deprecated — the canonical install is `curl -fsSL https://get.automagik.dev/genie | bash` (cosign + SLSA verified). See https://automagik.dev/genie/security/distribution-sovereignty",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260428.5",
3
+ "version": "4.260428.7",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260428.5",
3
+ "version": "4.260428.7",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -0,0 +1,138 @@
1
+ -- 055_runtime_events_partition_drain.sql
2
+ -- Bug: genie_runtime_events_maintain_partitions never drained the DEFAULT
3
+ -- partition. Rows that landed in genie_runtime_events_default (e.g. inserts
4
+ -- after UTC midnight before the next maintenance call ran) permanently
5
+ -- blocked creation of new dated partitions, because PG validates that no
6
+ -- existing default-partition row would belong to the new partition. Symptom:
7
+ -- SQLSTATE 23514 "updated partition constraint for default partition
8
+ -- genie_runtime_events_default would be violated by some row" on every
9
+ -- subsequent `genie serve start`. Migration 038's docstring claimed the
10
+ -- nightly scheduler converted DEFAULT rows into named partitions; no such
11
+ -- code ever existed.
12
+ --
13
+ -- Fix:
14
+ -- 1. New helper genie_runtime_events_drain_default() detaches DEFAULT,
15
+ -- ensures dated partitions exist for every day represented by its rows,
16
+ -- re-inserts the rows (parent-routing them to the correct dated
17
+ -- partition) with notify-trigger suppression, truncates the now-empty
18
+ -- detached default, and re-attaches it as DEFAULT.
19
+ -- 2. genie_runtime_events_maintain_partitions calls drain_default() before
20
+ -- creating today..today+forward_days, so a stuck DEFAULT can no longer
21
+ -- poison subsequent partition creation.
22
+ -- 3. One-shot drain at migration time so existing installs unstick on the
23
+ -- first post-upgrade `genie serve start`.
24
+
25
+ CREATE OR REPLACE FUNCTION genie_runtime_events_drain_default()
26
+ RETURNS INTEGER AS $$
27
+ DECLARE
28
+ drained INTEGER := 0;
29
+ d_rec RECORD;
30
+ prev_role TEXT;
31
+ BEGIN
32
+ IF NOT EXISTS (
33
+ SELECT 1
34
+ FROM pg_class c
35
+ JOIN pg_namespace n ON n.oid = c.relnamespace
36
+ WHERE c.relname = 'genie_runtime_events_default'
37
+ AND n.nspname = current_schema()
38
+ ) THEN
39
+ RETURN 0;
40
+ END IF;
41
+
42
+ IF NOT EXISTS (SELECT 1 FROM genie_runtime_events_default LIMIT 1) THEN
43
+ RETURN 0;
44
+ END IF;
45
+
46
+ -- Detach so the re-insert below routes to dated partitions instead of
47
+ -- looping back into DEFAULT. Non-CONCURRENTLY form is transaction-safe.
48
+ EXECUTE 'ALTER TABLE genie_runtime_events DETACH PARTITION genie_runtime_events_default';
49
+
50
+ -- Ensure a dated partition exists for every day represented in the
51
+ -- detached default. create_partition is CREATE TABLE IF NOT EXISTS, so
52
+ -- pre-existing partitions are fine.
53
+ FOR d_rec IN
54
+ SELECT DISTINCT date_trunc('day', created_at)::DATE AS d
55
+ FROM genie_runtime_events_default
56
+ LOOP
57
+ PERFORM genie_runtime_events_create_partition(d_rec.d);
58
+ END LOOP;
59
+
60
+ -- Suppress AFTER INSERT triggers (notify_runtime_event_split, audit chain
61
+ -- triggers, etc.) for the drain. These rows already fired their notifies
62
+ -- on their original insert; re-firing would broadcast duplicates to every
63
+ -- LISTEN'er.
64
+ prev_role := current_setting('session_replication_role');
65
+ PERFORM set_config('session_replication_role', 'replica', true);
66
+
67
+ EXECUTE $sql$
68
+ INSERT INTO genie_runtime_events (
69
+ id, repo_path, subject, kind, source, agent, team, direction, peer,
70
+ text, data, thread_id, trace_id, parent_event_id, span_id, parent_span_id,
71
+ severity, schema_version, duration_ms, dedup_key, source_subsystem, created_at
72
+ )
73
+ OVERRIDING SYSTEM VALUE
74
+ SELECT id, repo_path, subject, kind, source, agent, team, direction, peer,
75
+ text, data, thread_id, trace_id, parent_event_id, span_id, parent_span_id,
76
+ severity, schema_version, duration_ms, dedup_key, source_subsystem, created_at
77
+ FROM genie_runtime_events_default
78
+ $sql$;
79
+
80
+ GET DIAGNOSTICS drained = ROW_COUNT;
81
+
82
+ PERFORM set_config('session_replication_role', prev_role, true);
83
+
84
+ EXECUTE 'TRUNCATE genie_runtime_events_default';
85
+ EXECUTE 'ALTER TABLE genie_runtime_events ATTACH PARTITION genie_runtime_events_default DEFAULT';
86
+
87
+ RETURN drained;
88
+ END;
89
+ $$ LANGUAGE plpgsql;
90
+
91
+ CREATE OR REPLACE FUNCTION genie_runtime_events_maintain_partitions(
92
+ forward_days INTEGER DEFAULT 2,
93
+ retention_days INTEGER DEFAULT 30
94
+ )
95
+ RETURNS JSONB AS $$
96
+ DECLARE
97
+ drained INTEGER;
98
+ created INTEGER := 0;
99
+ dropped INTEGER := 0;
100
+ i INTEGER;
101
+ BEGIN
102
+ -- Drain DEFAULT first so any rows that accumulated there (UTC-midnight
103
+ -- rollover between maintenance calls) get routed to their proper dated
104
+ -- partitions before we try to CREATE the new ones.
105
+ drained := genie_runtime_events_drain_default();
106
+
107
+ FOR i IN 0..forward_days LOOP
108
+ PERFORM genie_runtime_events_create_partition((CURRENT_DATE + i)::DATE);
109
+ created := created + 1;
110
+ END LOOP;
111
+ SELECT genie_runtime_events_drop_old_partitions(retention_days) INTO dropped;
112
+ RETURN jsonb_build_object(
113
+ 'created_or_present', created,
114
+ 'dropped', dropped,
115
+ 'drained_from_default', drained,
116
+ 'next_rotation_at', (CURRENT_DATE + 1)::TIMESTAMPTZ
117
+ );
118
+ END;
119
+ $$ LANGUAGE plpgsql;
120
+
121
+ -- Mirror the role grants from migration 041 for the new helper. Conditional
122
+ -- so this works on installs that ran 041 before events_admin existed.
123
+ DO $$
124
+ BEGIN
125
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'events_admin') THEN
126
+ EXECUTE 'GRANT EXECUTE ON FUNCTION genie_runtime_events_drain_default() TO events_admin';
127
+ END IF;
128
+ END$$;
129
+
130
+ DO $$
131
+ DECLARE
132
+ drained INTEGER;
133
+ BEGIN
134
+ drained := genie_runtime_events_drain_default();
135
+ IF drained > 0 THEN
136
+ RAISE NOTICE 'genie_runtime_events_default: drained % stuck row(s) into dated partitions', drained;
137
+ END IF;
138
+ END$$;
@@ -196,6 +196,53 @@ describe.skipIf(!DB_AVAILABLE)('Group 1 observability migrations', () => {
196
196
  expect(deleteErr?.message).toMatch(/append-only/);
197
197
  });
198
198
 
199
+ test('maintain_partitions drains rows stuck in DEFAULT (regression: SQLSTATE 23514)', async () => {
200
+ const sql = await getConnection();
201
+
202
+ // Pick a far-future date that no rolling-window partition covers.
203
+ // create_partition uses session-TZ midnight, so the date string suffices.
204
+ const stuckDate = '2099-12-31';
205
+
206
+ // Pre-clean any leftovers from a previous test run.
207
+ await sql`DELETE FROM genie_runtime_events WHERE kind = 'partition.drain.test'`;
208
+
209
+ // Insert a row whose created_at falls outside every dated partition. It
210
+ // must route to genie_runtime_events_default.
211
+ await sql`
212
+ INSERT INTO genie_runtime_events (repo_path, kind, source, agent, text, created_at)
213
+ VALUES ('test', 'partition.drain.test', 'test', 'test', 'stuck', ${`${stuckDate} 12:00:00+00`}::TIMESTAMPTZ)
214
+ `;
215
+
216
+ const beforeDefault = await sql<{ n: number }[]>`
217
+ SELECT count(*)::INT AS n FROM genie_runtime_events_default
218
+ WHERE kind = 'partition.drain.test'
219
+ `;
220
+ expect(beforeDefault[0].n).toBe(1);
221
+
222
+ const result = await sql<
223
+ Array<{ r: { created_or_present: number; drained_from_default: number } }>
224
+ >`SELECT genie_runtime_events_maintain_partitions(2, 30)::jsonb AS r`;
225
+ expect(result[0].r.drained_from_default).toBeGreaterThanOrEqual(1);
226
+
227
+ const afterDefault = await sql<{ n: number }[]>`
228
+ SELECT count(*)::INT AS n FROM genie_runtime_events_default
229
+ WHERE kind = 'partition.drain.test'
230
+ `;
231
+ expect(afterDefault[0].n).toBe(0);
232
+
233
+ const inDated = await sql<{ relname: string }[]>`
234
+ SELECT tableoid::regclass::TEXT AS relname
235
+ FROM genie_runtime_events
236
+ WHERE kind = 'partition.drain.test'
237
+ `;
238
+ expect(inDated.length).toBe(1);
239
+ expect(inDated[0].relname).toMatch(/genie_runtime_events_p20991231$/);
240
+
241
+ // Cleanup so the dated partition created above doesn't haunt the rolling
242
+ // window or the retention sweep on subsequent runs.
243
+ await sql`DELETE FROM genie_runtime_events WHERE kind = 'partition.drain.test'`;
244
+ });
245
+
199
246
  test('Group 1 migrations are idempotent on re-apply', async () => {
200
247
  // Re-run each migration body against the already-migrated schema and
201
248
  // confirm the guarded statements are all no-ops.