pilotswarm-sdk 0.1.35 → 0.2.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/dist/agent-loader.d.ts +9 -0
- package/dist/agent-loader.d.ts.map +1 -1
- package/dist/agent-loader.js +3 -0
- package/dist/agent-loader.js.map +1 -1
- package/dist/child-notifications.d.ts +10 -0
- package/dist/child-notifications.d.ts.map +1 -1
- package/dist/child-notifications.js +15 -1
- package/dist/child-notifications.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -20
- package/dist/client.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +321 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +132 -8
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +142 -6
- package/dist/cms.js.map +1 -1
- package/dist/duroxide-schema-migration.d.ts +19 -0
- package/dist/duroxide-schema-migration.d.ts.map +1 -0
- package/dist/duroxide-schema-migration.js +122 -0
- package/dist/duroxide-schema-migration.js.map +1 -0
- package/dist/facts-migrations.d.ts.map +1 -1
- package/dist/facts-migrations.js +923 -0
- package/dist/facts-migrations.js.map +1 -1
- package/dist/facts-store.d.ts +278 -28
- package/dist/facts-store.d.ts.map +1 -1
- package/dist/facts-store.js +262 -32
- package/dist/facts-store.js.map +1 -1
- package/dist/facts-tools.d.ts +8 -1
- package/dist/facts-tools.d.ts.map +1 -1
- package/dist/facts-tools.js +458 -25
- package/dist/facts-tools.js.map +1 -1
- package/dist/graph-store.d.ts +243 -0
- package/dist/graph-store.d.ts.map +1 -0
- package/dist/graph-store.js +63 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/graph-tools.d.ts +47 -0
- package/dist/graph-tools.d.ts.map +1 -0
- package/dist/graph-tools.js +554 -0
- package/dist/graph-tools.js.map +1 -0
- package/dist/horizon-env.d.ts +61 -0
- package/dist/horizon-env.d.ts.map +1 -0
- package/dist/horizon-env.js +120 -0
- package/dist/horizon-env.js.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect-tools.d.ts +2 -2
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +303 -6
- package/dist/inspect-tools.js.map +1 -1
- package/dist/knowledge-index.d.ts +33 -2
- package/dist/knowledge-index.d.ts.map +1 -1
- package/dist/knowledge-index.js +90 -19
- package/dist/knowledge-index.js.map +1 -1
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +61 -2
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +81 -3
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +97 -20
- package/dist/management-client.js.map +1 -1
- package/dist/orchestration/agents.d.ts +4 -1
- package/dist/orchestration/agents.d.ts.map +1 -1
- package/dist/orchestration/agents.js +27 -4
- package/dist/orchestration/agents.js.map +1 -1
- package/dist/orchestration/index.d.ts +2 -2
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts +3 -0
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +6 -1
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/queue.d.ts.map +1 -1
- package/dist/orchestration/queue.js +10 -1
- package/dist/orchestration/queue.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/state.d.ts +1 -0
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +1 -0
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts +2 -2
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +42 -12
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +4 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +1 -1
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_52/agents.d.ts +38 -0
- package/dist/orchestration_1_0_52/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/agents.js +735 -0
- package/dist/orchestration_1_0_52/agents.js.map +1 -0
- package/dist/orchestration_1_0_52/index.d.ts +24 -0
- package/dist/orchestration_1_0_52/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/index.js +13 -0
- package/dist/orchestration_1_0_52/index.js.map +1 -0
- package/dist/orchestration_1_0_52/lifecycle.d.ts +38 -0
- package/dist/orchestration_1_0_52/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/lifecycle.js +504 -0
- package/dist/orchestration_1_0_52/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_52/queue.d.ts +7 -0
- package/dist/orchestration_1_0_52/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/queue.js +635 -0
- package/dist/orchestration_1_0_52/queue.js.map +1 -0
- package/dist/orchestration_1_0_52/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_52/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/runtime.js +190 -0
- package/dist/orchestration_1_0_52/runtime.js.map +1 -0
- package/dist/orchestration_1_0_52/state.d.ts +124 -0
- package/dist/orchestration_1_0_52/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/state.js +104 -0
- package/dist/orchestration_1_0_52/state.js.map +1 -0
- package/dist/orchestration_1_0_52/turn.d.ts +6 -0
- package/dist/orchestration_1_0_52/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/turn.js +834 -0
- package/dist/orchestration_1_0_52/turn.js.map +1 -0
- package/dist/orchestration_1_0_52/utils.d.ts +22 -0
- package/dist/orchestration_1_0_52/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_52/utils.js +226 -0
- package/dist/orchestration_1_0_52/utils.js.map +1 -0
- package/dist/resourcemgr-tools.d.ts +2 -2
- package/dist/resourcemgr-tools.d.ts.map +1 -1
- package/dist/resourcemgr-tools.js +1 -1
- package/dist/resourcemgr-tools.js.map +1 -1
- package/dist/session-dumper.d.ts +2 -2
- package/dist/session-dumper.d.ts.map +1 -1
- package/dist/session-dumper.js.map +1 -1
- package/dist/session-manager.d.ts +8 -2
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +86 -4
- package/dist/session-manager.js.map +1 -1
- package/dist/session-messages.d.ts +2 -2
- package/dist/session-messages.d.ts.map +1 -1
- package/dist/session-messages.js +0 -6
- package/dist/session-messages.js.map +1 -1
- package/dist/session-proxy.d.ts +47 -2
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +79 -59
- package/dist/session-proxy.js.map +1 -1
- package/dist/storage-config.d.ts +62 -0
- package/dist/storage-config.d.ts.map +1 -0
- package/dist/storage-config.js +138 -0
- package/dist/storage-config.js.map +1 -0
- package/dist/storage-providers.d.ts +25 -0
- package/dist/storage-providers.d.ts.map +1 -0
- package/dist/storage-providers.js +150 -0
- package/dist/storage-providers.js.map +1 -0
- package/dist/sweeper-tools.d.ts +2 -2
- package/dist/sweeper-tools.d.ts.map +1 -1
- package/dist/system-agents.d.ts +2 -2
- package/dist/system-agents.d.ts.map +1 -1
- package/dist/types.d.ts +103 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +5 -4
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +108 -33
- package/dist/worker.js.map +1 -1
- package/package.json +3 -1
- package/plugins/mgmt/agents/agent-tuner.agent.md +56 -4
- package/plugins/mgmt/agents/facts-manager.agent.md +81 -3
- package/plugins/mgmt/skills/graph-debug/SKILL.md +133 -0
package/dist/facts-migrations.js
CHANGED
|
@@ -31,6 +31,36 @@ export function FACTS_MIGRATIONS(schema) {
|
|
|
31
31
|
name: "facts_read_unrestricted",
|
|
32
32
|
sql: migration_0004_facts_read_unrestricted(schema),
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
version: "0005",
|
|
36
|
+
name: "facts_read_scope_keys",
|
|
37
|
+
sql: migration_0005_facts_read_scope_keys(schema),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
version: "0006",
|
|
41
|
+
name: "crawl_queue",
|
|
42
|
+
sql: migration_0006_crawl_queue(schema),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
version: "0007",
|
|
46
|
+
name: "minimal_crawl_queue",
|
|
47
|
+
sql: migration_0007_minimal_crawl_queue(schema),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
version: "0008",
|
|
51
|
+
name: "batch_store_pattern_delete",
|
|
52
|
+
sql: migration_0008_batch_store_pattern_delete(schema),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
version: "0009",
|
|
56
|
+
name: "unified_store_delete_api",
|
|
57
|
+
sql: migration_0009_unified_store_delete_api(schema),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
version: "0010",
|
|
61
|
+
name: "soft_delete_etag",
|
|
62
|
+
sql: migration_0010_soft_delete_etag(schema),
|
|
63
|
+
},
|
|
34
64
|
];
|
|
35
65
|
}
|
|
36
66
|
// ─── Migration 0001: Baseline ────────────────────────────────────
|
|
@@ -437,4 +467,897 @@ END;
|
|
|
437
467
|
$$ LANGUAGE plpgsql;
|
|
438
468
|
`;
|
|
439
469
|
}
|
|
470
|
+
// ─── Migration 0005: facts_read_facts exposes scope_key + accepts scopeKeys ──
|
|
471
|
+
function migration_0005_facts_read_scope_keys(schema) {
|
|
472
|
+
const s = `"${schema}"`;
|
|
473
|
+
const t = `${s}.facts`;
|
|
474
|
+
return `
|
|
475
|
+
-- 0005_facts_read_scope_keys (enhancedfactstore 02 §1c): the read proc now
|
|
476
|
+
-- (a) RETURNS the canonical scope_key (so graph 'evidence' arrays can reference
|
|
477
|
+
-- a real fact and resolve back), and
|
|
478
|
+
-- (b) accepts an optional p_scope_keys TEXT[] for bulk read-by-key — the way
|
|
479
|
+
-- graph evidence resolves into facts. ACL still applies; the scope_keys
|
|
480
|
+
-- filter only narrows. Additive + idempotent.
|
|
481
|
+
--
|
|
482
|
+
-- The return shape changes (adds scope_key), so the previous 9-arg proc must be
|
|
483
|
+
-- DROPped before recreating.
|
|
484
|
+
|
|
485
|
+
DROP FUNCTION IF EXISTS ${s}.facts_read_facts(TEXT, TEXT, TEXT[], TEXT, TEXT[], TEXT, TEXT, INT, BOOLEAN) CASCADE;
|
|
486
|
+
|
|
487
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_read_facts(
|
|
488
|
+
p_scope TEXT,
|
|
489
|
+
p_reader_session_id TEXT,
|
|
490
|
+
p_granted_ids TEXT[],
|
|
491
|
+
p_key_pattern TEXT,
|
|
492
|
+
p_tags TEXT[],
|
|
493
|
+
p_session_id TEXT,
|
|
494
|
+
p_agent_id TEXT,
|
|
495
|
+
p_limit INT,
|
|
496
|
+
p_unrestricted BOOLEAN DEFAULT FALSE,
|
|
497
|
+
p_scope_keys TEXT[] DEFAULT NULL
|
|
498
|
+
) RETURNS TABLE (
|
|
499
|
+
scope_key TEXT,
|
|
500
|
+
key TEXT,
|
|
501
|
+
value JSONB,
|
|
502
|
+
agent_id TEXT,
|
|
503
|
+
session_id TEXT,
|
|
504
|
+
shared BOOLEAN,
|
|
505
|
+
tags TEXT[],
|
|
506
|
+
created_at TIMESTAMPTZ,
|
|
507
|
+
updated_at TIMESTAMPTZ
|
|
508
|
+
) AS $$
|
|
509
|
+
DECLARE
|
|
510
|
+
base_sql TEXT;
|
|
511
|
+
where_clauses TEXT[] := ARRAY[]::TEXT[];
|
|
512
|
+
final_sql TEXT;
|
|
513
|
+
BEGIN
|
|
514
|
+
base_sql := 'SELECT f.scope_key, f.key, f.value, f.agent_id, f.session_id, f.shared, f.tags, f.created_at, f.updated_at FROM ${t} f WHERE ';
|
|
515
|
+
|
|
516
|
+
IF p_unrestricted THEN
|
|
517
|
+
-- Bypass visibility entirely; only optional filters apply.
|
|
518
|
+
where_clauses := array_append(where_clauses, 'TRUE');
|
|
519
|
+
ELSIF p_scope = 'shared' THEN
|
|
520
|
+
where_clauses := array_append(where_clauses, 'f.shared = TRUE');
|
|
521
|
+
ELSIF p_scope = 'session' THEN
|
|
522
|
+
IF p_reader_session_id IS NULL THEN
|
|
523
|
+
RETURN;
|
|
524
|
+
END IF;
|
|
525
|
+
where_clauses := array_append(where_clauses,
|
|
526
|
+
'f.shared = FALSE AND f.session_id = ' || quote_literal(p_reader_session_id));
|
|
527
|
+
ELSIF p_reader_session_id IS NOT NULL THEN
|
|
528
|
+
-- "accessible" or "descendants"
|
|
529
|
+
DECLARE
|
|
530
|
+
vis_parts TEXT[] := ARRAY[
|
|
531
|
+
'f.shared = TRUE',
|
|
532
|
+
'(f.shared = FALSE AND f.session_id = ' || quote_literal(p_reader_session_id) || ')'
|
|
533
|
+
];
|
|
534
|
+
BEGIN
|
|
535
|
+
IF p_granted_ids IS NOT NULL AND array_length(p_granted_ids, 1) > 0 THEN
|
|
536
|
+
vis_parts := array_append(vis_parts,
|
|
537
|
+
'(f.shared = FALSE AND f.session_id = ANY(' || quote_literal(p_granted_ids)::TEXT || '::TEXT[]))');
|
|
538
|
+
END IF;
|
|
539
|
+
where_clauses := array_append(where_clauses, '(' || array_to_string(vis_parts, ' OR ') || ')');
|
|
540
|
+
END;
|
|
541
|
+
ELSE
|
|
542
|
+
where_clauses := array_append(where_clauses, 'f.shared = TRUE');
|
|
543
|
+
END IF;
|
|
544
|
+
|
|
545
|
+
-- Bulk read-by-key (narrows within the visibility filter above). A non-null
|
|
546
|
+
-- but EMPTY array matches nothing (caller asked for zero facts), so it can
|
|
547
|
+
-- only narrow — never widen — what the visibility clause already permits.
|
|
548
|
+
IF p_scope_keys IS NOT NULL THEN
|
|
549
|
+
where_clauses := array_append(where_clauses,
|
|
550
|
+
'f.scope_key = ANY(' || quote_literal(p_scope_keys)::TEXT || '::TEXT[])');
|
|
551
|
+
END IF;
|
|
552
|
+
|
|
553
|
+
-- Optional filters
|
|
554
|
+
IF p_key_pattern IS NOT NULL THEN
|
|
555
|
+
where_clauses := array_append(where_clauses,
|
|
556
|
+
'f.key LIKE ' || quote_literal(p_key_pattern));
|
|
557
|
+
END IF;
|
|
558
|
+
IF p_tags IS NOT NULL AND array_length(p_tags, 1) > 0 THEN
|
|
559
|
+
where_clauses := array_append(where_clauses,
|
|
560
|
+
'f.tags @> ' || quote_literal(p_tags)::TEXT || '::TEXT[]');
|
|
561
|
+
END IF;
|
|
562
|
+
IF p_session_id IS NOT NULL THEN
|
|
563
|
+
where_clauses := array_append(where_clauses,
|
|
564
|
+
'f.session_id = ' || quote_literal(p_session_id));
|
|
565
|
+
END IF;
|
|
566
|
+
IF p_agent_id IS NOT NULL THEN
|
|
567
|
+
where_clauses := array_append(where_clauses,
|
|
568
|
+
'f.agent_id = ' || quote_literal(p_agent_id));
|
|
569
|
+
END IF;
|
|
570
|
+
|
|
571
|
+
final_sql := base_sql || array_to_string(where_clauses, ' AND ')
|
|
572
|
+
|| ' ORDER BY f.updated_at DESC LIMIT ' || p_limit;
|
|
573
|
+
|
|
574
|
+
RETURN QUERY EXECUTE final_sql;
|
|
575
|
+
END;
|
|
576
|
+
$$ LANGUAGE plpgsql;
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
// ─── Migration 0006: crawl queue (base-store, vanilla PG — no extension) ─────
|
|
580
|
+
function migration_0006_crawl_queue(schema) {
|
|
581
|
+
const s = `"${schema}"`;
|
|
582
|
+
const t = `${s}.facts`;
|
|
583
|
+
return `
|
|
584
|
+
-- 0006_crawl_queue (enhancedfactstore 07 D3): the facts↔graph crawl bridge lives
|
|
585
|
+
-- on the BASE facts store as plain facts-table bookkeeping — so a base-Postgres
|
|
586
|
+
-- deployment can feed a separate GraphStore harvester. Vanilla PG, no extension.
|
|
587
|
+
--
|
|
588
|
+
-- - content_hash : trigger-maintained md5 of (key, value); the receipt
|
|
589
|
+
-- key for the read→mark race guard.
|
|
590
|
+
-- - last_crawled_at : NULL ⇒ pending crawl. Reset to NULL whenever content
|
|
591
|
+
-- changes (the facts_touch trigger).
|
|
592
|
+
-- - facts_read_uncrawled / facts_mark_crawled : the harvester work queue.
|
|
593
|
+
--
|
|
594
|
+
-- Additive + idempotent. Inert unless a graph harvester runs.
|
|
595
|
+
|
|
596
|
+
ALTER TABLE ${t} ADD COLUMN IF NOT EXISTS content_hash TEXT;
|
|
597
|
+
ALTER TABLE ${t} ADD COLUMN IF NOT EXISTS last_crawled_at TIMESTAMPTZ;
|
|
598
|
+
|
|
599
|
+
-- Backfill content_hash for rows that predate this migration. last_crawled_at
|
|
600
|
+
-- stays NULL (pending) — correct: nothing has crawled them yet.
|
|
601
|
+
UPDATE ${t}
|
|
602
|
+
SET content_hash = md5(coalesce(key, '') || E'\\x1f' || coalesce(value::text, ''))
|
|
603
|
+
WHERE content_hash IS NULL;
|
|
604
|
+
|
|
605
|
+
-- Work-queue index: only the pending rows.
|
|
606
|
+
CREATE INDEX IF NOT EXISTS idx_${schema}_facts_uncrawled
|
|
607
|
+
ON ${t} (id) WHERE last_crawled_at IS NULL;
|
|
608
|
+
|
|
609
|
+
-- Write-resets-pending-state: recompute content_hash on every write; when it
|
|
610
|
+
-- CHANGES, reset last_crawled_at → NULL so a harvester re-crawls. Identical
|
|
611
|
+
-- content writes change nothing (so re-storing an unchanged fact is a no-op for
|
|
612
|
+
-- the queue).
|
|
613
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_touch() RETURNS trigger
|
|
614
|
+
LANGUAGE plpgsql AS $$
|
|
615
|
+
DECLARE
|
|
616
|
+
new_hash TEXT;
|
|
617
|
+
BEGIN
|
|
618
|
+
new_hash := md5(coalesce(NEW.key, '') || E'\\x1f' || coalesce(NEW.value::text, ''));
|
|
619
|
+
-- Keep content_hash authoritative on every write (defends against any
|
|
620
|
+
-- direct content_hash tampering by other code paths).
|
|
621
|
+
NEW.content_hash := new_hash;
|
|
622
|
+
-- Re-queue (reset crawl stamp) ONLY when the embeddable content changed.
|
|
623
|
+
IF TG_OP = 'INSERT' OR new_hash IS DISTINCT FROM OLD.content_hash THEN
|
|
624
|
+
NEW.last_crawled_at := NULL;
|
|
625
|
+
END IF;
|
|
626
|
+
RETURN NEW;
|
|
627
|
+
END $$;
|
|
628
|
+
|
|
629
|
+
DROP TRIGGER IF EXISTS facts_touch ON ${t};
|
|
630
|
+
CREATE TRIGGER facts_touch
|
|
631
|
+
BEFORE INSERT OR UPDATE ON ${t}
|
|
632
|
+
FOR EACH ROW EXECUTE FUNCTION ${s}.facts_touch();
|
|
633
|
+
|
|
634
|
+
-- Harvester work queue: pending facts (last_crawled_at IS NULL), across ALL
|
|
635
|
+
-- scopes (privileged), optionally narrowed by a key prefix. Returns the
|
|
636
|
+
-- content_hash so the caller can produce a mark receipt.
|
|
637
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_read_uncrawled(
|
|
638
|
+
p_ns_prefix TEXT,
|
|
639
|
+
p_limit INT
|
|
640
|
+
) RETURNS TABLE (
|
|
641
|
+
scope_key TEXT,
|
|
642
|
+
key TEXT,
|
|
643
|
+
value JSONB,
|
|
644
|
+
agent_id TEXT,
|
|
645
|
+
session_id TEXT,
|
|
646
|
+
shared BOOLEAN,
|
|
647
|
+
tags TEXT[],
|
|
648
|
+
created_at TIMESTAMPTZ,
|
|
649
|
+
updated_at TIMESTAMPTZ,
|
|
650
|
+
content_hash TEXT
|
|
651
|
+
) LANGUAGE sql STABLE AS $$
|
|
652
|
+
SELECT f.scope_key, f.key, f.value, f.agent_id, f.session_id, f.shared,
|
|
653
|
+
f.tags, f.created_at, f.updated_at, f.content_hash
|
|
654
|
+
FROM ${t} f
|
|
655
|
+
WHERE f.last_crawled_at IS NULL
|
|
656
|
+
AND (p_ns_prefix IS NULL OR starts_with(f.key, p_ns_prefix))
|
|
657
|
+
ORDER BY f.id
|
|
658
|
+
LIMIT p_limit;
|
|
659
|
+
$$;
|
|
660
|
+
|
|
661
|
+
-- Stamp last_crawled_at = now(), but ONLY where content_hash still matches the
|
|
662
|
+
-- supplied receipt (a fact edited mid-crawl keeps last_crawled_at = NULL and
|
|
663
|
+
-- re-enters the queue). Mismatches are skipped, not errors.
|
|
664
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_mark_crawled(p_stamps JSONB)
|
|
665
|
+
RETURNS TABLE (marked INT, skipped INT)
|
|
666
|
+
LANGUAGE sql AS $$
|
|
667
|
+
WITH stamps AS (
|
|
668
|
+
SELECT e->>'scopeKey' AS scope_key, e->>'contentHash' AS content_hash
|
|
669
|
+
FROM jsonb_array_elements(p_stamps) e
|
|
670
|
+
),
|
|
671
|
+
upd AS (
|
|
672
|
+
UPDATE ${t} f
|
|
673
|
+
SET last_crawled_at = now()
|
|
674
|
+
FROM stamps s
|
|
675
|
+
WHERE f.scope_key = s.scope_key
|
|
676
|
+
AND f.content_hash = s.content_hash
|
|
677
|
+
RETURNING f.scope_key
|
|
678
|
+
)
|
|
679
|
+
SELECT (SELECT count(*) FROM upd)::int AS marked,
|
|
680
|
+
((SELECT count(*) FROM stamps) - (SELECT count(*) FROM upd))::int AS skipped;
|
|
681
|
+
$$;
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
function migration_0007_minimal_crawl_queue(schema) {
|
|
685
|
+
const s = `"${schema}"`;
|
|
686
|
+
const t = `${s}.facts`;
|
|
687
|
+
return `
|
|
688
|
+
-- 0007_minimal_crawl_queue: remove content_hash from the base facts final
|
|
689
|
+
-- schema. Crawl state is intentionally minimal: last_crawled_at NULL means
|
|
690
|
+
-- pending, and facts_mark_crawled accepts scopeKey-only receipts.
|
|
691
|
+
|
|
692
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_touch() RETURNS trigger
|
|
693
|
+
LANGUAGE plpgsql AS $$
|
|
694
|
+
BEGIN
|
|
695
|
+
IF TG_OP = 'INSERT' OR NEW.key IS DISTINCT FROM OLD.key OR NEW.value IS DISTINCT FROM OLD.value THEN
|
|
696
|
+
NEW.last_crawled_at := NULL;
|
|
697
|
+
END IF;
|
|
698
|
+
RETURN NEW;
|
|
699
|
+
END $$;
|
|
700
|
+
|
|
701
|
+
ALTER TABLE ${t} DROP COLUMN IF EXISTS content_hash;
|
|
702
|
+
|
|
703
|
+
DROP FUNCTION IF EXISTS ${s}.facts_read_uncrawled(TEXT, INT);
|
|
704
|
+
|
|
705
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_read_uncrawled(
|
|
706
|
+
p_ns_prefix TEXT,
|
|
707
|
+
p_limit INT
|
|
708
|
+
) RETURNS TABLE (
|
|
709
|
+
scope_key TEXT,
|
|
710
|
+
key TEXT,
|
|
711
|
+
value JSONB,
|
|
712
|
+
agent_id TEXT,
|
|
713
|
+
session_id TEXT,
|
|
714
|
+
shared BOOLEAN,
|
|
715
|
+
tags TEXT[],
|
|
716
|
+
created_at TIMESTAMPTZ,
|
|
717
|
+
updated_at TIMESTAMPTZ
|
|
718
|
+
) LANGUAGE sql STABLE AS $$
|
|
719
|
+
SELECT f.scope_key, f.key, f.value, f.agent_id, f.session_id, f.shared,
|
|
720
|
+
f.tags, f.created_at, f.updated_at
|
|
721
|
+
FROM ${t} f
|
|
722
|
+
WHERE f.last_crawled_at IS NULL
|
|
723
|
+
AND (p_ns_prefix IS NULL OR starts_with(f.key, p_ns_prefix))
|
|
724
|
+
ORDER BY f.id
|
|
725
|
+
LIMIT p_limit;
|
|
726
|
+
$$;
|
|
727
|
+
|
|
728
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_mark_crawled(p_stamps JSONB)
|
|
729
|
+
RETURNS TABLE (marked INT, skipped INT)
|
|
730
|
+
LANGUAGE sql AS $$
|
|
731
|
+
WITH stamps AS (
|
|
732
|
+
SELECT e->>'scopeKey' AS scope_key
|
|
733
|
+
FROM jsonb_array_elements(p_stamps) e
|
|
734
|
+
),
|
|
735
|
+
upd AS (
|
|
736
|
+
UPDATE ${t} f
|
|
737
|
+
SET last_crawled_at = now()
|
|
738
|
+
FROM stamps s
|
|
739
|
+
WHERE f.scope_key = s.scope_key
|
|
740
|
+
AND f.last_crawled_at IS NULL
|
|
741
|
+
RETURNING f.scope_key
|
|
742
|
+
)
|
|
743
|
+
SELECT (SELECT count(*) FROM upd)::int AS marked,
|
|
744
|
+
((SELECT count(*) FROM stamps) - (SELECT count(*) FROM upd))::int AS skipped;
|
|
745
|
+
$$;
|
|
746
|
+
`;
|
|
747
|
+
}
|
|
748
|
+
function migration_0008_batch_store_pattern_delete(schema) {
|
|
749
|
+
const s = `"${schema}"`;
|
|
750
|
+
const t = `${s}.facts`;
|
|
751
|
+
return `
|
|
752
|
+
-- 0008_batch_store_pattern_delete: batch fact ingestion and explicit pattern
|
|
753
|
+
-- deletes for the base FactStore. Stored procedures stay the only write/delete
|
|
754
|
+
-- data-access surface.
|
|
755
|
+
|
|
756
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_store_facts(p_facts JSONB)
|
|
757
|
+
RETURNS INT
|
|
758
|
+
LANGUAGE sql AS $$
|
|
759
|
+
WITH input AS (
|
|
760
|
+
SELECT
|
|
761
|
+
e->>'scopeKey' AS scope_key,
|
|
762
|
+
e->>'key' AS key,
|
|
763
|
+
e->'value' AS value,
|
|
764
|
+
e->>'agentId' AS agent_id,
|
|
765
|
+
e->>'sessionId' AS session_id,
|
|
766
|
+
coalesce((e->>'shared')::boolean, false) AS shared,
|
|
767
|
+
coalesce((e->>'transient')::boolean, true) AS transient,
|
|
768
|
+
coalesce(ARRAY(SELECT jsonb_array_elements_text(e->'tags')), '{}'::text[]) AS tags
|
|
769
|
+
FROM jsonb_array_elements(p_facts) e
|
|
770
|
+
), upserted AS (
|
|
771
|
+
INSERT INTO ${t}
|
|
772
|
+
(scope_key, key, value, agent_id, session_id, shared, transient, tags)
|
|
773
|
+
SELECT scope_key, key, value, agent_id, session_id, shared, transient, tags
|
|
774
|
+
FROM input
|
|
775
|
+
ON CONFLICT (scope_key) DO UPDATE SET
|
|
776
|
+
value = EXCLUDED.value,
|
|
777
|
+
agent_id = EXCLUDED.agent_id,
|
|
778
|
+
session_id = EXCLUDED.session_id,
|
|
779
|
+
shared = EXCLUDED.shared,
|
|
780
|
+
transient = EXCLUDED.transient,
|
|
781
|
+
tags = EXCLUDED.tags,
|
|
782
|
+
updated_at = now()
|
|
783
|
+
RETURNING 1
|
|
784
|
+
)
|
|
785
|
+
SELECT count(*)::int FROM upserted;
|
|
786
|
+
$$;
|
|
787
|
+
|
|
788
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_store_fact(
|
|
789
|
+
p_scope_key TEXT,
|
|
790
|
+
p_key TEXT,
|
|
791
|
+
p_value JSONB,
|
|
792
|
+
p_agent_id TEXT,
|
|
793
|
+
p_session_id TEXT,
|
|
794
|
+
p_shared BOOLEAN,
|
|
795
|
+
p_transient BOOLEAN,
|
|
796
|
+
p_tags TEXT[]
|
|
797
|
+
) RETURNS VOID AS $$
|
|
798
|
+
BEGIN
|
|
799
|
+
PERFORM ${s}.facts_store_facts(jsonb_build_array(jsonb_build_object(
|
|
800
|
+
'scopeKey', p_scope_key,
|
|
801
|
+
'key', p_key,
|
|
802
|
+
'value', p_value,
|
|
803
|
+
'agentId', p_agent_id,
|
|
804
|
+
'sessionId', p_session_id,
|
|
805
|
+
'shared', p_shared,
|
|
806
|
+
'transient', p_transient,
|
|
807
|
+
'tags', coalesce(p_tags, '{}'::text[])
|
|
808
|
+
)));
|
|
809
|
+
END;
|
|
810
|
+
$$ LANGUAGE plpgsql;
|
|
811
|
+
|
|
812
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_delete_facts(
|
|
813
|
+
p_key_pattern TEXT,
|
|
814
|
+
p_scope TEXT,
|
|
815
|
+
p_session_id TEXT,
|
|
816
|
+
p_unrestricted BOOLEAN DEFAULT FALSE
|
|
817
|
+
) RETURNS BIGINT AS $$
|
|
818
|
+
DECLARE
|
|
819
|
+
deleted_count BIGINT;
|
|
820
|
+
BEGIN
|
|
821
|
+
IF p_key_pattern IS NULL OR p_key_pattern = '' THEN
|
|
822
|
+
RAISE EXCEPTION 'facts_delete_facts requires a key pattern';
|
|
823
|
+
END IF;
|
|
824
|
+
IF p_scope NOT IN ('session', 'shared', 'all') THEN
|
|
825
|
+
RAISE EXCEPTION 'facts_delete_facts scope must be session, shared, or all';
|
|
826
|
+
END IF;
|
|
827
|
+
IF p_scope = 'all' AND p_unrestricted IS DISTINCT FROM TRUE THEN
|
|
828
|
+
RAISE EXCEPTION 'facts_delete_facts scope=all requires unrestricted=true';
|
|
829
|
+
END IF;
|
|
830
|
+
IF p_scope = 'session' AND p_session_id IS NULL THEN
|
|
831
|
+
RAISE EXCEPTION 'facts_delete_facts scope=session requires sessionId';
|
|
832
|
+
END IF;
|
|
833
|
+
|
|
834
|
+
DELETE FROM ${t} f
|
|
835
|
+
WHERE f.key LIKE p_key_pattern
|
|
836
|
+
AND (
|
|
837
|
+
(p_scope = 'shared' AND f.shared = TRUE)
|
|
838
|
+
OR (p_scope = 'session' AND f.shared = FALSE AND f.session_id = p_session_id)
|
|
839
|
+
OR (p_scope = 'all' AND p_unrestricted = TRUE)
|
|
840
|
+
);
|
|
841
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
842
|
+
RETURN deleted_count;
|
|
843
|
+
END;
|
|
844
|
+
$$ LANGUAGE plpgsql;
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
function migration_0009_unified_store_delete_api(schema) {
|
|
848
|
+
const s = `"${schema}"`;
|
|
849
|
+
const t = `${s}.facts`;
|
|
850
|
+
return `
|
|
851
|
+
-- 0009_unified_store_delete_api: collapse plural stored procedure names back
|
|
852
|
+
-- into the singular store/delete API. Batch store is facts_store_fact(jsonb);
|
|
853
|
+
-- pattern delete is facts_delete_fact(key, pattern, scope, sessionId, unrestricted).
|
|
854
|
+
|
|
855
|
+
DROP FUNCTION IF EXISTS ${s}.facts_store_fact(JSONB);
|
|
856
|
+
DROP FUNCTION IF EXISTS ${s}.facts_store_fact(TEXT, TEXT, JSONB, TEXT, TEXT, BOOLEAN, BOOLEAN, TEXT[]);
|
|
857
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_store_fact(p_facts JSONB)
|
|
858
|
+
RETURNS INT
|
|
859
|
+
LANGUAGE sql AS $$
|
|
860
|
+
WITH input AS (
|
|
861
|
+
SELECT
|
|
862
|
+
e->>'scopeKey' AS scope_key,
|
|
863
|
+
e->>'key' AS key,
|
|
864
|
+
e->'value' AS value,
|
|
865
|
+
e->>'agentId' AS agent_id,
|
|
866
|
+
e->>'sessionId' AS session_id,
|
|
867
|
+
coalesce((e->>'shared')::boolean, false) AS shared,
|
|
868
|
+
coalesce((e->>'transient')::boolean, true) AS transient,
|
|
869
|
+
coalesce(ARRAY(SELECT jsonb_array_elements_text(e->'tags')), '{}'::text[]) AS tags
|
|
870
|
+
FROM jsonb_array_elements(p_facts) e
|
|
871
|
+
), upserted AS (
|
|
872
|
+
INSERT INTO ${t}
|
|
873
|
+
(scope_key, key, value, agent_id, session_id, shared, transient, tags)
|
|
874
|
+
SELECT scope_key, key, value, agent_id, session_id, shared, transient, tags
|
|
875
|
+
FROM input
|
|
876
|
+
ON CONFLICT (scope_key) DO UPDATE SET
|
|
877
|
+
value = EXCLUDED.value,
|
|
878
|
+
agent_id = EXCLUDED.agent_id,
|
|
879
|
+
session_id = EXCLUDED.session_id,
|
|
880
|
+
shared = EXCLUDED.shared,
|
|
881
|
+
transient = EXCLUDED.transient,
|
|
882
|
+
tags = EXCLUDED.tags,
|
|
883
|
+
updated_at = now()
|
|
884
|
+
RETURNING 1
|
|
885
|
+
)
|
|
886
|
+
SELECT count(*)::int FROM upserted;
|
|
887
|
+
$$;
|
|
888
|
+
|
|
889
|
+
DROP FUNCTION IF EXISTS ${s}.facts_delete_fact(TEXT);
|
|
890
|
+
DROP FUNCTION IF EXISTS ${s}.facts_delete_fact(TEXT, BOOLEAN, TEXT, TEXT, BOOLEAN);
|
|
891
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_delete_fact(
|
|
892
|
+
p_key_or_pattern TEXT,
|
|
893
|
+
p_pattern BOOLEAN DEFAULT FALSE,
|
|
894
|
+
p_scope TEXT DEFAULT NULL,
|
|
895
|
+
p_session_id TEXT DEFAULT NULL,
|
|
896
|
+
p_unrestricted BOOLEAN DEFAULT FALSE
|
|
897
|
+
) RETURNS BIGINT AS $$
|
|
898
|
+
DECLARE
|
|
899
|
+
deleted_count BIGINT;
|
|
900
|
+
v_scope TEXT;
|
|
901
|
+
BEGIN
|
|
902
|
+
IF p_pattern THEN
|
|
903
|
+
IF p_key_or_pattern IS NULL OR p_key_or_pattern = '' THEN
|
|
904
|
+
RAISE EXCEPTION 'facts_delete_fact pattern mode requires key';
|
|
905
|
+
END IF;
|
|
906
|
+
v_scope := coalesce(p_scope, 'session');
|
|
907
|
+
IF v_scope NOT IN ('session', 'shared', 'all') THEN
|
|
908
|
+
RAISE EXCEPTION 'facts_delete_fact scope must be session, shared, or all';
|
|
909
|
+
END IF;
|
|
910
|
+
IF v_scope = 'all' AND p_unrestricted IS DISTINCT FROM TRUE THEN
|
|
911
|
+
RAISE EXCEPTION 'facts_delete_fact scope=all requires unrestricted=true';
|
|
912
|
+
END IF;
|
|
913
|
+
IF v_scope = 'session' AND p_session_id IS NULL THEN
|
|
914
|
+
RAISE EXCEPTION 'facts_delete_fact scope=session requires sessionId';
|
|
915
|
+
END IF;
|
|
916
|
+
|
|
917
|
+
DELETE FROM ${t} f
|
|
918
|
+
WHERE f.key LIKE p_key_or_pattern
|
|
919
|
+
AND (
|
|
920
|
+
(v_scope = 'shared' AND f.shared = TRUE)
|
|
921
|
+
OR (v_scope = 'session' AND f.shared = FALSE AND f.session_id = p_session_id)
|
|
922
|
+
OR (v_scope = 'all' AND p_unrestricted = TRUE)
|
|
923
|
+
);
|
|
924
|
+
ELSE
|
|
925
|
+
DELETE FROM ${t} WHERE scope_key = p_key_or_pattern;
|
|
926
|
+
END IF;
|
|
927
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
928
|
+
RETURN deleted_count;
|
|
929
|
+
END;
|
|
930
|
+
$$ LANGUAGE plpgsql;
|
|
931
|
+
|
|
932
|
+
DROP FUNCTION IF EXISTS ${s}.facts_store_facts(JSONB);
|
|
933
|
+
DROP FUNCTION IF EXISTS ${s}.facts_delete_facts(TEXT, TEXT, TEXT, BOOLEAN);
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
function migration_0010_soft_delete_etag(schema) {
|
|
937
|
+
const s = `"${schema}"`;
|
|
938
|
+
const t = `${s}.facts`;
|
|
939
|
+
return `
|
|
940
|
+
-- 0010_soft_delete_etag: soft-delete facts and protect crawl mark writes with
|
|
941
|
+
-- a per-row etag. The facts store stays graph-agnostic: deleted facts remain in
|
|
942
|
+
-- the crawl queue until a crawler marks them, or until the Facts Manager TTL
|
|
943
|
+
-- purge removes expired tombstones.
|
|
944
|
+
|
|
945
|
+
ALTER TABLE ${t} ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
|
946
|
+
ALTER TABLE ${t} ADD COLUMN IF NOT EXISTS etag BIGINT NOT NULL DEFAULT 0;
|
|
947
|
+
|
|
948
|
+
CREATE INDEX IF NOT EXISTS idx_${schema}_facts_tombstones
|
|
949
|
+
ON ${t} (deleted_at) WHERE deleted_at IS NOT NULL;
|
|
950
|
+
|
|
951
|
+
-- Crawl-relevant state changes bump the etag and re-enter the crawl queue.
|
|
952
|
+
-- Embedding state does not exist in the base store, so only crawl bookkeeping is
|
|
953
|
+
-- handled here. last_crawled_at-only writes (facts_mark_crawled) do not bump etag.
|
|
954
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_touch() RETURNS trigger
|
|
955
|
+
LANGUAGE plpgsql AS $$
|
|
956
|
+
BEGIN
|
|
957
|
+
IF TG_OP = 'INSERT' THEN
|
|
958
|
+
NEW.etag := COALESCE(NEW.etag, 0) + 1;
|
|
959
|
+
NEW.last_crawled_at := NULL;
|
|
960
|
+
ELSIF NEW.key IS DISTINCT FROM OLD.key
|
|
961
|
+
OR NEW.value IS DISTINCT FROM OLD.value
|
|
962
|
+
OR NEW.deleted_at IS DISTINCT FROM OLD.deleted_at THEN
|
|
963
|
+
NEW.etag := COALESCE(OLD.etag, 0) + 1;
|
|
964
|
+
NEW.last_crawled_at := NULL;
|
|
965
|
+
END IF;
|
|
966
|
+
RETURN NEW;
|
|
967
|
+
END $$;
|
|
968
|
+
|
|
969
|
+
DROP FUNCTION IF EXISTS ${s}.facts_store_fact(JSONB);
|
|
970
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_store_fact(p_facts JSONB)
|
|
971
|
+
RETURNS INT
|
|
972
|
+
LANGUAGE sql AS $$
|
|
973
|
+
WITH input AS (
|
|
974
|
+
SELECT
|
|
975
|
+
e->>'scopeKey' AS scope_key,
|
|
976
|
+
e->>'key' AS key,
|
|
977
|
+
e->'value' AS value,
|
|
978
|
+
e->>'agentId' AS agent_id,
|
|
979
|
+
e->>'sessionId' AS session_id,
|
|
980
|
+
coalesce((e->>'shared')::boolean, false) AS shared,
|
|
981
|
+
coalesce((e->>'transient')::boolean, true) AS transient,
|
|
982
|
+
coalesce(ARRAY(SELECT jsonb_array_elements_text(e->'tags')), '{}'::text[]) AS tags
|
|
983
|
+
FROM jsonb_array_elements(p_facts) e
|
|
984
|
+
), upserted AS (
|
|
985
|
+
INSERT INTO ${t}
|
|
986
|
+
(scope_key, key, value, agent_id, session_id, shared, transient, tags)
|
|
987
|
+
SELECT scope_key, key, value, agent_id, session_id, shared, transient, tags
|
|
988
|
+
FROM input
|
|
989
|
+
ON CONFLICT (scope_key) DO UPDATE SET
|
|
990
|
+
key = EXCLUDED.key,
|
|
991
|
+
value = EXCLUDED.value,
|
|
992
|
+
agent_id = EXCLUDED.agent_id,
|
|
993
|
+
session_id = EXCLUDED.session_id,
|
|
994
|
+
shared = EXCLUDED.shared,
|
|
995
|
+
transient = EXCLUDED.transient,
|
|
996
|
+
tags = EXCLUDED.tags,
|
|
997
|
+
deleted_at = NULL,
|
|
998
|
+
updated_at = now()
|
|
999
|
+
RETURNING 1
|
|
1000
|
+
)
|
|
1001
|
+
SELECT count(*)::int FROM upserted;
|
|
1002
|
+
$$;
|
|
1003
|
+
|
|
1004
|
+
DROP FUNCTION IF EXISTS ${s}.facts_delete_fact(TEXT, BOOLEAN, TEXT, TEXT, BOOLEAN);
|
|
1005
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_delete_fact(
|
|
1006
|
+
p_key_or_pattern TEXT,
|
|
1007
|
+
p_pattern BOOLEAN DEFAULT FALSE,
|
|
1008
|
+
p_scope TEXT DEFAULT NULL,
|
|
1009
|
+
p_session_id TEXT DEFAULT NULL,
|
|
1010
|
+
p_unrestricted BOOLEAN DEFAULT FALSE
|
|
1011
|
+
) RETURNS BIGINT AS $$
|
|
1012
|
+
DECLARE
|
|
1013
|
+
deleted_count BIGINT;
|
|
1014
|
+
v_scope TEXT;
|
|
1015
|
+
BEGIN
|
|
1016
|
+
IF p_pattern THEN
|
|
1017
|
+
IF p_key_or_pattern IS NULL OR p_key_or_pattern = '' THEN
|
|
1018
|
+
RAISE EXCEPTION 'facts_delete_fact pattern mode requires key';
|
|
1019
|
+
END IF;
|
|
1020
|
+
v_scope := coalesce(p_scope, 'session');
|
|
1021
|
+
IF v_scope NOT IN ('session', 'shared', 'all') THEN
|
|
1022
|
+
RAISE EXCEPTION 'facts_delete_fact scope must be session, shared, or all';
|
|
1023
|
+
END IF;
|
|
1024
|
+
IF v_scope = 'all' AND p_unrestricted IS DISTINCT FROM TRUE THEN
|
|
1025
|
+
RAISE EXCEPTION 'facts_delete_fact scope=all requires unrestricted=true';
|
|
1026
|
+
END IF;
|
|
1027
|
+
IF v_scope = 'session' AND p_session_id IS NULL THEN
|
|
1028
|
+
RAISE EXCEPTION 'facts_delete_fact scope=session requires sessionId';
|
|
1029
|
+
END IF;
|
|
1030
|
+
|
|
1031
|
+
UPDATE ${t} f
|
|
1032
|
+
SET deleted_at = now(), updated_at = now()
|
|
1033
|
+
WHERE f.deleted_at IS NULL
|
|
1034
|
+
AND f.key LIKE p_key_or_pattern
|
|
1035
|
+
AND (
|
|
1036
|
+
(v_scope = 'shared' AND f.shared = TRUE)
|
|
1037
|
+
OR (v_scope = 'session' AND f.shared = FALSE AND f.session_id = p_session_id)
|
|
1038
|
+
OR (v_scope = 'all' AND p_unrestricted = TRUE)
|
|
1039
|
+
);
|
|
1040
|
+
ELSE
|
|
1041
|
+
UPDATE ${t}
|
|
1042
|
+
SET deleted_at = now(), updated_at = now()
|
|
1043
|
+
WHERE scope_key = p_key_or_pattern
|
|
1044
|
+
AND deleted_at IS NULL;
|
|
1045
|
+
END IF;
|
|
1046
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
1047
|
+
RETURN deleted_count;
|
|
1048
|
+
END;
|
|
1049
|
+
$$ LANGUAGE plpgsql;
|
|
1050
|
+
|
|
1051
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_delete_session_facts(
|
|
1052
|
+
p_session_id TEXT
|
|
1053
|
+
) RETURNS BIGINT AS $$
|
|
1054
|
+
DECLARE
|
|
1055
|
+
deleted_count BIGINT;
|
|
1056
|
+
BEGIN
|
|
1057
|
+
UPDATE ${t}
|
|
1058
|
+
SET deleted_at = now(), updated_at = now()
|
|
1059
|
+
WHERE session_id = p_session_id
|
|
1060
|
+
AND shared = FALSE
|
|
1061
|
+
AND deleted_at IS NULL;
|
|
1062
|
+
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
1063
|
+
RETURN deleted_count;
|
|
1064
|
+
END;
|
|
1065
|
+
$$ LANGUAGE plpgsql;
|
|
1066
|
+
|
|
1067
|
+
DROP FUNCTION IF EXISTS ${s}.facts_read_facts(TEXT, TEXT, TEXT[], TEXT, TEXT[], TEXT, TEXT, INT, BOOLEAN, TEXT[]) CASCADE;
|
|
1068
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_read_facts(
|
|
1069
|
+
p_scope TEXT,
|
|
1070
|
+
p_reader_session_id TEXT,
|
|
1071
|
+
p_granted_ids TEXT[],
|
|
1072
|
+
p_key_pattern TEXT,
|
|
1073
|
+
p_tags TEXT[],
|
|
1074
|
+
p_session_id TEXT,
|
|
1075
|
+
p_agent_id TEXT,
|
|
1076
|
+
p_limit INT,
|
|
1077
|
+
p_unrestricted BOOLEAN DEFAULT FALSE,
|
|
1078
|
+
p_scope_keys TEXT[] DEFAULT NULL
|
|
1079
|
+
) RETURNS TABLE (
|
|
1080
|
+
scope_key TEXT,
|
|
1081
|
+
key TEXT,
|
|
1082
|
+
value JSONB,
|
|
1083
|
+
agent_id TEXT,
|
|
1084
|
+
session_id TEXT,
|
|
1085
|
+
shared BOOLEAN,
|
|
1086
|
+
tags TEXT[],
|
|
1087
|
+
created_at TIMESTAMPTZ,
|
|
1088
|
+
updated_at TIMESTAMPTZ,
|
|
1089
|
+
deleted_at TIMESTAMPTZ,
|
|
1090
|
+
etag BIGINT
|
|
1091
|
+
) AS $$
|
|
1092
|
+
DECLARE
|
|
1093
|
+
base_sql TEXT;
|
|
1094
|
+
where_clauses TEXT[] := ARRAY[]::TEXT[];
|
|
1095
|
+
final_sql TEXT;
|
|
1096
|
+
BEGIN
|
|
1097
|
+
base_sql := 'SELECT f.scope_key, f.key, f.value, f.agent_id, f.session_id, f.shared, f.tags, f.created_at, f.updated_at, f.deleted_at, f.etag FROM ${t} f WHERE f.deleted_at IS NULL AND ';
|
|
1098
|
+
|
|
1099
|
+
IF p_unrestricted THEN
|
|
1100
|
+
where_clauses := array_append(where_clauses, 'TRUE');
|
|
1101
|
+
ELSIF p_scope = 'shared' THEN
|
|
1102
|
+
where_clauses := array_append(where_clauses, 'f.shared = TRUE');
|
|
1103
|
+
ELSIF p_scope = 'session' THEN
|
|
1104
|
+
IF p_reader_session_id IS NULL THEN
|
|
1105
|
+
RETURN;
|
|
1106
|
+
END IF;
|
|
1107
|
+
where_clauses := array_append(where_clauses,
|
|
1108
|
+
'f.shared = FALSE AND f.session_id = ' || quote_literal(p_reader_session_id));
|
|
1109
|
+
ELSIF p_reader_session_id IS NOT NULL THEN
|
|
1110
|
+
DECLARE
|
|
1111
|
+
vis_parts TEXT[] := ARRAY[
|
|
1112
|
+
'f.shared = TRUE',
|
|
1113
|
+
'(f.shared = FALSE AND f.session_id = ' || quote_literal(p_reader_session_id) || ')'
|
|
1114
|
+
];
|
|
1115
|
+
BEGIN
|
|
1116
|
+
IF p_granted_ids IS NOT NULL AND array_length(p_granted_ids, 1) > 0 THEN
|
|
1117
|
+
vis_parts := array_append(vis_parts,
|
|
1118
|
+
'(f.shared = FALSE AND f.session_id = ANY(' || quote_literal(p_granted_ids)::TEXT || '::TEXT[]))');
|
|
1119
|
+
END IF;
|
|
1120
|
+
where_clauses := array_append(where_clauses, '(' || array_to_string(vis_parts, ' OR ') || ')');
|
|
1121
|
+
END;
|
|
1122
|
+
ELSE
|
|
1123
|
+
where_clauses := array_append(where_clauses, 'f.shared = TRUE');
|
|
1124
|
+
END IF;
|
|
1125
|
+
|
|
1126
|
+
IF p_scope_keys IS NOT NULL THEN
|
|
1127
|
+
where_clauses := array_append(where_clauses,
|
|
1128
|
+
'f.scope_key = ANY(' || quote_literal(p_scope_keys)::TEXT || '::TEXT[])');
|
|
1129
|
+
END IF;
|
|
1130
|
+
IF p_key_pattern IS NOT NULL THEN
|
|
1131
|
+
where_clauses := array_append(where_clauses,
|
|
1132
|
+
'f.key LIKE ' || quote_literal(p_key_pattern));
|
|
1133
|
+
END IF;
|
|
1134
|
+
IF p_tags IS NOT NULL AND array_length(p_tags, 1) > 0 THEN
|
|
1135
|
+
where_clauses := array_append(where_clauses,
|
|
1136
|
+
'f.tags @> ' || quote_literal(p_tags)::TEXT || '::TEXT[]');
|
|
1137
|
+
END IF;
|
|
1138
|
+
IF p_session_id IS NOT NULL THEN
|
|
1139
|
+
where_clauses := array_append(where_clauses,
|
|
1140
|
+
'f.session_id = ' || quote_literal(p_session_id));
|
|
1141
|
+
END IF;
|
|
1142
|
+
IF p_agent_id IS NOT NULL THEN
|
|
1143
|
+
where_clauses := array_append(where_clauses,
|
|
1144
|
+
'f.agent_id = ' || quote_literal(p_agent_id));
|
|
1145
|
+
END IF;
|
|
1146
|
+
|
|
1147
|
+
final_sql := base_sql || array_to_string(where_clauses, ' AND ')
|
|
1148
|
+
|| ' ORDER BY f.updated_at DESC LIMIT ' || p_limit;
|
|
1149
|
+
|
|
1150
|
+
RETURN QUERY EXECUTE final_sql;
|
|
1151
|
+
END;
|
|
1152
|
+
$$ LANGUAGE plpgsql;
|
|
1153
|
+
|
|
1154
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_get_session_facts_stats(
|
|
1155
|
+
p_session_id TEXT
|
|
1156
|
+
) RETURNS TABLE (
|
|
1157
|
+
namespace TEXT,
|
|
1158
|
+
fact_count BIGINT,
|
|
1159
|
+
total_value_bytes BIGINT,
|
|
1160
|
+
oldest_created_at TIMESTAMPTZ,
|
|
1161
|
+
newest_updated_at TIMESTAMPTZ
|
|
1162
|
+
) AS $$
|
|
1163
|
+
BEGIN
|
|
1164
|
+
RETURN QUERY
|
|
1165
|
+
SELECT
|
|
1166
|
+
${s}.facts_namespace_for_key(f.key) AS namespace,
|
|
1167
|
+
COUNT(*)::BIGINT AS fact_count,
|
|
1168
|
+
COALESCE(SUM(pg_column_size(f.value)), 0)::BIGINT AS total_value_bytes,
|
|
1169
|
+
MIN(f.created_at) AS oldest_created_at,
|
|
1170
|
+
MAX(f.updated_at) AS newest_updated_at
|
|
1171
|
+
FROM ${t} f
|
|
1172
|
+
WHERE f.session_id = p_session_id
|
|
1173
|
+
AND f.shared = FALSE
|
|
1174
|
+
AND f.deleted_at IS NULL
|
|
1175
|
+
GROUP BY 1
|
|
1176
|
+
ORDER BY fact_count DESC, namespace;
|
|
1177
|
+
END;
|
|
1178
|
+
$$ LANGUAGE plpgsql;
|
|
1179
|
+
|
|
1180
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_get_facts_stats_for_sessions(
|
|
1181
|
+
p_session_ids TEXT[]
|
|
1182
|
+
) RETURNS TABLE (
|
|
1183
|
+
namespace TEXT,
|
|
1184
|
+
fact_count BIGINT,
|
|
1185
|
+
total_value_bytes BIGINT,
|
|
1186
|
+
oldest_created_at TIMESTAMPTZ,
|
|
1187
|
+
newest_updated_at TIMESTAMPTZ
|
|
1188
|
+
) AS $$
|
|
1189
|
+
BEGIN
|
|
1190
|
+
RETURN QUERY
|
|
1191
|
+
SELECT
|
|
1192
|
+
${s}.facts_namespace_for_key(f.key) AS namespace,
|
|
1193
|
+
COUNT(*)::BIGINT AS fact_count,
|
|
1194
|
+
COALESCE(SUM(pg_column_size(f.value)), 0)::BIGINT AS total_value_bytes,
|
|
1195
|
+
MIN(f.created_at) AS oldest_created_at,
|
|
1196
|
+
MAX(f.updated_at) AS newest_updated_at
|
|
1197
|
+
FROM ${t} f
|
|
1198
|
+
WHERE f.session_id = ANY(p_session_ids)
|
|
1199
|
+
AND f.shared = FALSE
|
|
1200
|
+
AND f.deleted_at IS NULL
|
|
1201
|
+
GROUP BY 1
|
|
1202
|
+
ORDER BY fact_count DESC, namespace;
|
|
1203
|
+
END;
|
|
1204
|
+
$$ LANGUAGE plpgsql;
|
|
1205
|
+
|
|
1206
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_get_shared_facts_stats()
|
|
1207
|
+
RETURNS TABLE (
|
|
1208
|
+
namespace TEXT,
|
|
1209
|
+
fact_count BIGINT,
|
|
1210
|
+
total_value_bytes BIGINT,
|
|
1211
|
+
oldest_created_at TIMESTAMPTZ,
|
|
1212
|
+
newest_updated_at TIMESTAMPTZ
|
|
1213
|
+
) AS $$
|
|
1214
|
+
BEGIN
|
|
1215
|
+
RETURN QUERY
|
|
1216
|
+
SELECT
|
|
1217
|
+
${s}.facts_namespace_for_key(f.key) AS namespace,
|
|
1218
|
+
COUNT(*)::BIGINT AS fact_count,
|
|
1219
|
+
COALESCE(SUM(pg_column_size(f.value)), 0)::BIGINT AS total_value_bytes,
|
|
1220
|
+
MIN(f.created_at) AS oldest_created_at,
|
|
1221
|
+
MAX(f.updated_at) AS newest_updated_at
|
|
1222
|
+
FROM ${t} f
|
|
1223
|
+
WHERE f.shared = TRUE
|
|
1224
|
+
AND f.deleted_at IS NULL
|
|
1225
|
+
GROUP BY 1
|
|
1226
|
+
ORDER BY fact_count DESC, namespace;
|
|
1227
|
+
END;
|
|
1228
|
+
$$ LANGUAGE plpgsql;
|
|
1229
|
+
|
|
1230
|
+
DROP FUNCTION IF EXISTS ${s}.facts_read_uncrawled(TEXT, INT);
|
|
1231
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_read_uncrawled(
|
|
1232
|
+
p_ns_prefix TEXT,
|
|
1233
|
+
p_limit INT
|
|
1234
|
+
) RETURNS TABLE (
|
|
1235
|
+
scope_key TEXT,
|
|
1236
|
+
key TEXT,
|
|
1237
|
+
value JSONB,
|
|
1238
|
+
agent_id TEXT,
|
|
1239
|
+
session_id TEXT,
|
|
1240
|
+
shared BOOLEAN,
|
|
1241
|
+
tags TEXT[],
|
|
1242
|
+
created_at TIMESTAMPTZ,
|
|
1243
|
+
updated_at TIMESTAMPTZ,
|
|
1244
|
+
deleted_at TIMESTAMPTZ,
|
|
1245
|
+
etag BIGINT
|
|
1246
|
+
) LANGUAGE sql STABLE AS $$
|
|
1247
|
+
SELECT f.scope_key, f.key, f.value, f.agent_id, f.session_id, f.shared,
|
|
1248
|
+
f.tags, f.created_at, f.updated_at, f.deleted_at, f.etag
|
|
1249
|
+
FROM ${t} f
|
|
1250
|
+
WHERE f.last_crawled_at IS NULL
|
|
1251
|
+
AND (p_ns_prefix IS NULL OR starts_with(f.key, p_ns_prefix))
|
|
1252
|
+
ORDER BY f.id
|
|
1253
|
+
LIMIT p_limit;
|
|
1254
|
+
$$;
|
|
1255
|
+
|
|
1256
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_mark_crawled(p_stamps JSONB)
|
|
1257
|
+
RETURNS TABLE (marked INT, skipped INT)
|
|
1258
|
+
LANGUAGE sql AS $$
|
|
1259
|
+
WITH stamps AS (
|
|
1260
|
+
SELECT e->>'scopeKey' AS scope_key,
|
|
1261
|
+
CASE WHEN (e->>'etag') ~ '^[0-9]+$' THEN (e->>'etag')::BIGINT ELSE NULL END AS etag
|
|
1262
|
+
FROM jsonb_array_elements(p_stamps) e
|
|
1263
|
+
), upd AS (
|
|
1264
|
+
UPDATE ${t} f
|
|
1265
|
+
SET last_crawled_at = now()
|
|
1266
|
+
FROM stamps s
|
|
1267
|
+
WHERE f.scope_key = s.scope_key
|
|
1268
|
+
AND f.last_crawled_at IS NULL
|
|
1269
|
+
AND f.etag = s.etag
|
|
1270
|
+
RETURNING f.scope_key
|
|
1271
|
+
)
|
|
1272
|
+
SELECT (SELECT count(*) FROM upd)::int AS marked,
|
|
1273
|
+
((SELECT count(*) FROM stamps) - (SELECT count(*) FROM upd))::int AS skipped;
|
|
1274
|
+
$$;
|
|
1275
|
+
|
|
1276
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_purge_expired(
|
|
1277
|
+
p_ttl_seconds INT DEFAULT 21600,
|
|
1278
|
+
p_limit INT DEFAULT 1000
|
|
1279
|
+
) RETURNS BIGINT AS $$
|
|
1280
|
+
DECLARE
|
|
1281
|
+
purged BIGINT;
|
|
1282
|
+
BEGIN
|
|
1283
|
+
WITH candidates AS (
|
|
1284
|
+
SELECT id
|
|
1285
|
+
FROM ${t}
|
|
1286
|
+
WHERE deleted_at IS NOT NULL
|
|
1287
|
+
AND (
|
|
1288
|
+
last_crawled_at IS NOT NULL
|
|
1289
|
+
OR deleted_at < now() - make_interval(secs => GREATEST(p_ttl_seconds, 0))
|
|
1290
|
+
)
|
|
1291
|
+
-- Prefer already-reconciled tombstones within a batch so a lagging (not
|
|
1292
|
+
-- dead) crawler's unreconciled tombstones get maximum time before the TTL
|
|
1293
|
+
-- backstop reclaims them and strands their graph evidence.
|
|
1294
|
+
ORDER BY (last_crawled_at IS NULL), deleted_at, id
|
|
1295
|
+
LIMIT GREATEST(p_limit, 1)
|
|
1296
|
+
FOR UPDATE SKIP LOCKED
|
|
1297
|
+
), del AS (
|
|
1298
|
+
DELETE FROM ${t} f
|
|
1299
|
+
USING candidates c
|
|
1300
|
+
WHERE f.id = c.id
|
|
1301
|
+
RETURNING 1
|
|
1302
|
+
)
|
|
1303
|
+
SELECT count(*) INTO purged FROM del;
|
|
1304
|
+
RETURN COALESCE(purged, 0);
|
|
1305
|
+
END;
|
|
1306
|
+
$$ LANGUAGE plpgsql;
|
|
1307
|
+
|
|
1308
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_tombstone_stats(
|
|
1309
|
+
p_ttl_seconds INT DEFAULT 21600
|
|
1310
|
+
) RETURNS TABLE (
|
|
1311
|
+
pending_total BIGINT,
|
|
1312
|
+
unreconciled BIGINT,
|
|
1313
|
+
ttl_blocked BIGINT,
|
|
1314
|
+
oldest_unreconciled_age_seconds DOUBLE PRECISION,
|
|
1315
|
+
reconciled_unswept BIGINT
|
|
1316
|
+
) LANGUAGE sql STABLE AS $$
|
|
1317
|
+
SELECT
|
|
1318
|
+
count(*) FILTER (WHERE deleted_at IS NOT NULL)::BIGINT AS pending_total,
|
|
1319
|
+
count(*) FILTER (WHERE deleted_at IS NOT NULL AND last_crawled_at IS NULL)::BIGINT AS unreconciled,
|
|
1320
|
+
count(*) FILTER (
|
|
1321
|
+
WHERE deleted_at IS NOT NULL
|
|
1322
|
+
AND last_crawled_at IS NULL
|
|
1323
|
+
AND deleted_at >= now() - make_interval(secs => GREATEST(p_ttl_seconds, 0))
|
|
1324
|
+
)::BIGINT AS ttl_blocked,
|
|
1325
|
+
EXTRACT(EPOCH FROM (now() - MIN(deleted_at) FILTER (
|
|
1326
|
+
WHERE deleted_at IS NOT NULL AND last_crawled_at IS NULL
|
|
1327
|
+
)))::DOUBLE PRECISION AS oldest_unreconciled_age_seconds,
|
|
1328
|
+
count(*) FILTER (WHERE deleted_at IS NOT NULL AND last_crawled_at IS NOT NULL)::BIGINT AS reconciled_unswept
|
|
1329
|
+
FROM ${t};
|
|
1330
|
+
$$;
|
|
1331
|
+
|
|
1332
|
+
CREATE OR REPLACE FUNCTION ${s}.facts_force_purge(
|
|
1333
|
+
p_cutoff TIMESTAMPTZ,
|
|
1334
|
+
p_only_unreconciled BOOLEAN DEFAULT FALSE,
|
|
1335
|
+
p_key_prefix TEXT DEFAULT NULL,
|
|
1336
|
+
p_limit INT DEFAULT 1000
|
|
1337
|
+
) RETURNS BIGINT AS $$
|
|
1338
|
+
DECLARE
|
|
1339
|
+
purged BIGINT;
|
|
1340
|
+
BEGIN
|
|
1341
|
+
WITH candidates AS (
|
|
1342
|
+
SELECT id
|
|
1343
|
+
FROM ${t}
|
|
1344
|
+
WHERE deleted_at IS NOT NULL
|
|
1345
|
+
AND deleted_at < p_cutoff
|
|
1346
|
+
AND (p_only_unreconciled IS DISTINCT FROM TRUE OR last_crawled_at IS NULL)
|
|
1347
|
+
AND (p_key_prefix IS NULL OR starts_with(key, p_key_prefix))
|
|
1348
|
+
ORDER BY deleted_at, id
|
|
1349
|
+
LIMIT GREATEST(p_limit, 1)
|
|
1350
|
+
FOR UPDATE SKIP LOCKED
|
|
1351
|
+
), del AS (
|
|
1352
|
+
DELETE FROM ${t} f
|
|
1353
|
+
USING candidates c
|
|
1354
|
+
WHERE f.id = c.id
|
|
1355
|
+
RETURNING 1
|
|
1356
|
+
)
|
|
1357
|
+
SELECT count(*) INTO purged FROM del;
|
|
1358
|
+
RETURN COALESCE(purged, 0);
|
|
1359
|
+
END;
|
|
1360
|
+
$$ LANGUAGE plpgsql;
|
|
1361
|
+
`;
|
|
1362
|
+
}
|
|
440
1363
|
//# sourceMappingURL=facts-migrations.js.map
|