@stackbone/cli 0.6.2 → 0.7.0
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/CHANGELOG.md +163 -0
- package/main.js +20538 -8590
- package/migrations/0094_rag_document_versions.sql +61 -0
- package/migrations/0095_rag_document_version_counters.sql +22 -0
- package/migrations/0096_trigger_subscriptions_target_kind.sql +29 -0
- package/migrations/0097_voice_settings.sql +34 -0
- package/migrations/0098_model_selections_provider_id.sql +23 -0
- package/migrations/0099_memory.sql +35 -0
- package/migrations/0100_memory_call_run.sql +9 -0
- package/migrations/0101_memory_call_run_no_fk.sql +5 -0
- package/migrations/0102_voice_recordings.sql +31 -0
- package/migrations/0103_rag_collection_embeddings.sql +21 -0
- package/migrations/0104_rag_collection_rebuilds.sql +33 -0
- package/migrations/0105_rag_rebuild_manifest_size.sql +15 -0
- package/migrations/0106_memory_agent_scope.sql +16 -0
- package/migrations/0107_memory_settings.sql +7 -0
- package/migrations/0108_memory_identity_bindings.sql +13 -0
- package/migrations/0109_trigger_subscriptions_conversation_key.sql +17 -0
- package/migrations/0110_concurrency_limits.sql +52 -0
- package/migrations/0111_concurrency_slots_now_idx.sql +21 -0
- package/migrations/0112_concurrency_max_hold_override.sql +15 -0
- package/migrations/0113_trigger_subscriptions_debounce_ms.sql +13 -0
- package/package.json +2 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +223 -85
- package/stackbone-cli-0.7.0.tgz +0 -0
- package/voice-worker/main.js +2150 -0
- package/voice-worker/main.js.map +7 -0
- package/voice-worker/package.json +18 -0
- package/stackbone-cli-0.6.2.tgz +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- 0094_rag_document_versions.sql — RAG document versions (STA3-8)
|
|
2
|
+
-- Every version of a document is its own row, with its own `source` (its storage
|
|
3
|
+
-- key) and its own chunks, so rollback flips a flag and re-embeds nothing. The
|
|
4
|
+
-- versions of one document share a `document_key`; the agent reads only the
|
|
5
|
+
-- `is_active` one.
|
|
6
|
+
--
|
|
7
|
+
-- A document that existed before this is its own version 1, published, keyed by
|
|
8
|
+
-- its `source`.
|
|
9
|
+
|
|
10
|
+
ALTER TABLE stackbone_platform.rag_documents
|
|
11
|
+
ADD COLUMN IF NOT EXISTS document_key text,
|
|
12
|
+
ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 1,
|
|
13
|
+
ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
|
|
14
|
+
|
|
15
|
+
-- Two overlapping ingests of one source could leave two rows before PLA-2327's
|
|
16
|
+
-- lock. The newest keeps the source as its key; the older ones become documents
|
|
17
|
+
-- of their own, still published, so nothing the agent reads changes and the
|
|
18
|
+
-- unique index below can build. Deleting them would lose data.
|
|
19
|
+
UPDATE stackbone_platform.rag_documents d
|
|
20
|
+
SET document_key = CASE WHEN ranked.rn = 1 THEN d.source ELSE d.source || '#' || d.id END
|
|
21
|
+
FROM (
|
|
22
|
+
SELECT id, row_number() OVER (
|
|
23
|
+
PARTITION BY collection_id, source ORDER BY created_at DESC, id DESC
|
|
24
|
+
) AS rn
|
|
25
|
+
FROM stackbone_platform.rag_documents
|
|
26
|
+
WHERE document_key IS NULL
|
|
27
|
+
) ranked
|
|
28
|
+
WHERE d.id = ranked.id;
|
|
29
|
+
|
|
30
|
+
-- Postgres cannot default one column to another. A workspace on an SDK older
|
|
31
|
+
-- than this migration inserts without the key, so the trigger fills it in
|
|
32
|
+
-- instead of failing every ingest on NOT NULL.
|
|
33
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.rag_documents_default_key()
|
|
34
|
+
RETURNS trigger
|
|
35
|
+
LANGUAGE plpgsql
|
|
36
|
+
AS $$
|
|
37
|
+
BEGIN
|
|
38
|
+
NEW.document_key := COALESCE(NEW.document_key, NEW.source);
|
|
39
|
+
RETURN NEW;
|
|
40
|
+
END;
|
|
41
|
+
$$;
|
|
42
|
+
|
|
43
|
+
DROP TRIGGER IF EXISTS rag_documents_default_key ON stackbone_platform.rag_documents;
|
|
44
|
+
CREATE TRIGGER rag_documents_default_key
|
|
45
|
+
BEFORE INSERT ON stackbone_platform.rag_documents
|
|
46
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.rag_documents_default_key();
|
|
47
|
+
|
|
48
|
+
ALTER TABLE stackbone_platform.rag_documents
|
|
49
|
+
ALTER COLUMN document_key SET NOT NULL;
|
|
50
|
+
|
|
51
|
+
-- At most one published version per document. Non-deferrable, so publishing is
|
|
52
|
+
-- two statements (unpublish the siblings, then publish the target): one swapping
|
|
53
|
+
-- UPDATE can reach the target before the sibling and fail on the check.
|
|
54
|
+
CREATE UNIQUE INDEX IF NOT EXISTS rag_documents_active_key_uq
|
|
55
|
+
ON stackbone_platform.rag_documents (collection_id, document_key)
|
|
56
|
+
WHERE is_active;
|
|
57
|
+
|
|
58
|
+
-- Covers unpublished versions too: a writer that misses the key lock and hands
|
|
59
|
+
-- out a version number twice fails here instead of leaving two v2s behind.
|
|
60
|
+
CREATE UNIQUE INDEX IF NOT EXISTS rag_documents_key_version_uq
|
|
61
|
+
ON stackbone_platform.rag_documents (collection_id, document_key, version);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- 0095_rag_document_version_counters.sql — a version number is never reused (STA3-8 D11)
|
|
2
|
+
-- The last version number each document handed out. Numbering from the rows
|
|
3
|
+
-- left (`max(version) + 1`) gave a deleted draft's number to the next upload,
|
|
4
|
+
-- and the activity log then showed "deleted v3" next to a live, different v3.
|
|
5
|
+
-- A new version takes this counter or the rows' highest version, whichever is
|
|
6
|
+
-- greater, plus one, under the document's key lock. Deleting the whole document
|
|
7
|
+
-- drops its row; deleting one version leaves it alone.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.rag_document_version_counters (
|
|
10
|
+
collection_id uuid NOT NULL REFERENCES stackbone_platform.rag_collections (id) ON DELETE CASCADE,
|
|
11
|
+
document_key text NOT NULL,
|
|
12
|
+
last_version integer NOT NULL,
|
|
13
|
+
PRIMARY KEY (collection_id, document_key)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- A document from before this starts from the highest version it still holds:
|
|
17
|
+
-- a number deleted before 0095 cannot be told apart from one never handed out.
|
|
18
|
+
INSERT INTO stackbone_platform.rag_document_version_counters (collection_id, document_key, last_version)
|
|
19
|
+
SELECT collection_id, document_key, max(version)
|
|
20
|
+
FROM stackbone_platform.rag_documents
|
|
21
|
+
GROUP BY collection_id, document_key
|
|
22
|
+
ON CONFLICT (collection_id, document_key) DO NOTHING;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- 0096_trigger_subscriptions_target_kind.sql — a link's destination carries a kind (PLA-967)
|
|
2
|
+
-- A connector trigger is about to start a deep agent as well as a workflow (PLA-951),
|
|
3
|
+
-- and `workflow_name` (0021) gave the destination a name and no kind. The column
|
|
4
|
+
-- becomes `target_name` beside a `target_kind`, the same pair `eval_suites` uses
|
|
5
|
+
-- (0038). Every link that existed before reads as a workflow link through the default.
|
|
6
|
+
--
|
|
7
|
+
-- No CHECK on `target_kind`, as `eval_suites.target_kind` (0038) and `runs.entity_kind`
|
|
8
|
+
-- (0037): a box running an older build must be able to store a kind it cannot run
|
|
9
|
+
-- rather than reject the write. The register endpoint refuses what it cannot start.
|
|
10
|
+
--
|
|
11
|
+
-- `RENAME COLUMN` has no `IF EXISTS`, and a DBA who applies the platform SQL by hand
|
|
12
|
+
-- (`STACKBONE_SKIP_MIGRATIONS=1`) can run it twice, so the rename is guarded on the
|
|
13
|
+
-- old column still being there.
|
|
14
|
+
|
|
15
|
+
DO $$
|
|
16
|
+
BEGIN
|
|
17
|
+
IF EXISTS (
|
|
18
|
+
SELECT 1 FROM information_schema.columns
|
|
19
|
+
WHERE table_schema = 'stackbone_platform'
|
|
20
|
+
AND table_name = 'trigger_subscriptions'
|
|
21
|
+
AND column_name = 'workflow_name'
|
|
22
|
+
) THEN
|
|
23
|
+
ALTER TABLE stackbone_platform.trigger_subscriptions
|
|
24
|
+
RENAME COLUMN workflow_name TO target_name;
|
|
25
|
+
END IF;
|
|
26
|
+
END $$;
|
|
27
|
+
|
|
28
|
+
ALTER TABLE stackbone_platform.trigger_subscriptions
|
|
29
|
+
ADD COLUMN IF NOT EXISTS target_kind text NOT NULL DEFAULT 'workflow';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- 0096_voice_settings.sql — the voice settings an operator changed from Studio
|
|
2
|
+
-- for one deep agent (FDE-62 S5).
|
|
3
|
+
--
|
|
4
|
+
-- A row holds ONLY the fields Studio changed, as one JSON object in the shape of
|
|
5
|
+
-- `voiceSettingsSchema` (`@stackbone/validators`, `studio-voice.ts`): top-level
|
|
6
|
+
-- leaves, the `vad` / `turn` / `interruptions` groups with only their changed
|
|
7
|
+
-- leaves, `keyterms` whole, and `greeting: null` as a declared "wait for the
|
|
8
|
+
-- caller". The box lays it over the agent's code and the box defaults leaf by
|
|
9
|
+
-- leaf (Studio > code > box default, FDE-62 D2) each time it reads the agent or
|
|
10
|
+
-- dispatches a call, so a saved change reaches the next call with nothing
|
|
11
|
+
-- restarted.
|
|
12
|
+
--
|
|
13
|
+
-- Partial on purpose, never the full effective value: a leaf Studio did not
|
|
14
|
+
-- touch keeps following the code, so a redeploy that changes it still lands.
|
|
15
|
+
-- Resetting a field removes its leaf, and resetting the last one deletes the
|
|
16
|
+
-- row, so "no row" means "nothing changed in Studio".
|
|
17
|
+
--
|
|
18
|
+
-- NO VERSIONS and no `updated_by`, unlike 0073: the activity log records who
|
|
19
|
+
-- changed what and when, which is the history this table does not keep.
|
|
20
|
+
--
|
|
21
|
+
-- Identity is the deep agent's name alone. Only a deep agent takes a call, so
|
|
22
|
+
-- there is no `owner_kind` beside it. `owner_name` is free text for the reason
|
|
23
|
+
-- 0047 gives for a prompt's owner.
|
|
24
|
+
--
|
|
25
|
+
-- Deliberately NO CHECK on `overrides`, following 0073: a box running an older
|
|
26
|
+
-- build must still be able to STORE what a newer one wrote, and the rules
|
|
27
|
+
-- (bounds, the known model lists, the cross-field checks) change with the build.
|
|
28
|
+
-- The route parses a body before it writes, and the merge
|
|
29
|
+
-- (`resolveEffectiveVoiceSettings`) reads only the leaves its build knows.
|
|
30
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.voice_settings (
|
|
31
|
+
owner_name text PRIMARY KEY,
|
|
32
|
+
overrides jsonb NOT NULL,
|
|
33
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
34
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- 0098_model_selections_provider_id.sql — the provider an operator picked next
|
|
2
|
+
-- to the model, for a box that runs more than one (STA3-17, decision D10).
|
|
3
|
+
--
|
|
4
|
+
-- Two providers may serve the same model id. A pick that names only the id is
|
|
5
|
+
-- ambiguous there, so the pick also names the provider that serves it. The SDK
|
|
6
|
+
-- composes `<providerId>::<modelId>` when it reads a pick; this column is where
|
|
7
|
+
-- the provider half lives.
|
|
8
|
+
--
|
|
9
|
+
-- Its own column, not a `b::gpt-4o` string in `model_id`: every box reader of
|
|
10
|
+
-- `model_id` would then have to split it (D10).
|
|
11
|
+
--
|
|
12
|
+
-- NULLABLE, with no default and no backfill. Every pick saved before this file
|
|
13
|
+
-- named no provider, and "no provider" keeps today's resolution: the box sends
|
|
14
|
+
-- the id to the one provider that serves it, or refuses it as ambiguous when two
|
|
15
|
+
-- do. Free text with NO CHECK and no foreign key (providers live in system
|
|
16
|
+
-- secrets, not a table), for the same reason 0073 has none on `model_id`: the
|
|
17
|
+
-- provider may be removed while a pick still names it, and that fails at run
|
|
18
|
+
-- time, not at write.
|
|
19
|
+
--
|
|
20
|
+
-- The PRIMARY KEY stays (owner_kind, owner_name, key): one pick per key, whatever
|
|
21
|
+
-- provider serves it.
|
|
22
|
+
ALTER TABLE stackbone_platform.model_selections
|
|
23
|
+
ADD COLUMN IF NOT EXISTS provider_id text;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- 0099_memory.sql — per-person long-term facts and call history (FDE-68)
|
|
2
|
+
-- Memory is platform-owned box state: it follows RAG into the
|
|
3
|
+
-- stackbone_platform schema so every deployed workspace receives it.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.memory_calls (
|
|
6
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
7
|
+
user_id text NOT NULL,
|
|
8
|
+
started_at timestamptz NOT NULL,
|
|
9
|
+
duration_seconds integer CHECK (duration_seconds >= 0),
|
|
10
|
+
outcome text CHECK (outcome IS NULL OR char_length(outcome) BETWEEN 1 AND 100),
|
|
11
|
+
summary text NOT NULL CHECK (char_length(summary) BETWEEN 1 AND 600),
|
|
12
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE INDEX IF NOT EXISTS memory_calls_user_started_idx
|
|
16
|
+
ON stackbone_platform.memory_calls (user_id, started_at DESC, created_at DESC);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.memory_facts (
|
|
19
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
20
|
+
user_id text NOT NULL,
|
|
21
|
+
content text NOT NULL CHECK (char_length(content) BETWEEN 1 AND 500),
|
|
22
|
+
insertion_order bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
|
|
23
|
+
created_call_id uuid REFERENCES stackbone_platform.memory_calls(id) ON DELETE SET NULL,
|
|
24
|
+
updated_call_id uuid REFERENCES stackbone_platform.memory_calls(id) ON DELETE SET NULL,
|
|
25
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS memory_facts_user_idx
|
|
30
|
+
ON stackbone_platform.memory_facts (user_id);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.memory_user_revisions (
|
|
33
|
+
user_id text PRIMARY KEY,
|
|
34
|
+
revision bigint NOT NULL DEFAULT 0 CHECK (revision >= 0)
|
|
35
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- A memory record keeps the run that owns the full transcript without copying
|
|
2
|
+
-- transcript content into memory itself.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE stackbone_platform.memory_calls
|
|
5
|
+
ADD COLUMN IF NOT EXISTS run_id uuid
|
|
6
|
+
REFERENCES stackbone_platform.runs(id) ON DELETE SET NULL;
|
|
7
|
+
|
|
8
|
+
CREATE INDEX IF NOT EXISTS memory_calls_run_idx
|
|
9
|
+
ON stackbone_platform.memory_calls (run_id);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- 0097_voice_recordings.sql — the recording of each voice call (FDE-62 S12).
|
|
2
|
+
--
|
|
3
|
+
-- One row per ROOM, keyed on it: a room is one call (`POST /api/voice/rooms`
|
|
4
|
+
-- opens a fresh `call-<agent>-<8 hex>` every time), and the box starts one
|
|
5
|
+
-- room-composite egress for it. A session can hold several calls, so
|
|
6
|
+
-- `session_id` is indexed, newest first, for the read Studio's session screen makes.
|
|
7
|
+
--
|
|
8
|
+
-- `storage_key` is relative to the agent's scope in the box bucket, the key the
|
|
9
|
+
-- Storage Browser shows; the egress writes it under the `<agentId>/` prefix.
|
|
10
|
+
--
|
|
11
|
+
-- `egress_id` is LiveKit's id for the egress, null until LiveKit accepts the
|
|
12
|
+
-- start. The status moves past `recording` lazily, when someone reads the row:
|
|
13
|
+
-- the box asks LiveKit for that egress then, because no webhook can reach a dev
|
|
14
|
+
-- box. A row still without one after the join window means the start never
|
|
15
|
+
-- went through (the room was never joined, or the box restarted mid-retry).
|
|
16
|
+
--
|
|
17
|
+
-- Deliberately NO foreign key to `sessions`: the row is written when the room
|
|
18
|
+
-- opens, and the call's first turn creates the session row later, the race 0019
|
|
19
|
+
-- describes.
|
|
20
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.voice_recordings (
|
|
21
|
+
room text PRIMARY KEY,
|
|
22
|
+
session_id uuid NOT NULL,
|
|
23
|
+
storage_key text NOT NULL,
|
|
24
|
+
status text NOT NULL CHECK (status IN ('recording', 'ready', 'failed')),
|
|
25
|
+
egress_id text NULL,
|
|
26
|
+
started_at timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
ended_at timestamptz NULL
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX IF NOT EXISTS voice_recordings_session_id_started_at_idx
|
|
31
|
+
ON stackbone_platform.voice_recordings (session_id, started_at DESC);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- STA3-18: preserve the meaning of historical vectors and document versions
|
|
2
|
+
-- while new collections can choose another provider, model and dimension.
|
|
3
|
+
ALTER TABLE stackbone_platform.rag_collections
|
|
4
|
+
ADD COLUMN IF NOT EXISTS embedding_provider text NOT NULL DEFAULT 'default',
|
|
5
|
+
ADD COLUMN IF NOT EXISTS embedding_model text NOT NULL DEFAULT 'openai/text-embedding-3-small',
|
|
6
|
+
ADD COLUMN IF NOT EXISTS embedding_dimension integer NOT NULL DEFAULT 1536
|
|
7
|
+
CONSTRAINT rag_collections_embedding_dimension_check CHECK (embedding_dimension BETWEEN 1 AND 16000),
|
|
8
|
+
ADD COLUMN IF NOT EXISTS embedding_revision integer NOT NULL DEFAULT 1
|
|
9
|
+
CONSTRAINT rag_collections_embedding_revision_check CHECK (embedding_revision > 0),
|
|
10
|
+
ADD COLUMN IF NOT EXISTS embedding_state text NOT NULL DEFAULT 'ready'
|
|
11
|
+
CONSTRAINT rag_collections_embedding_state_check CHECK (embedding_state IN ('ready', 'rebuilding', 'failed')),
|
|
12
|
+
ADD CONSTRAINT rag_collections_embedding_identity_check
|
|
13
|
+
CHECK (length(trim(embedding_provider)) > 0 AND length(trim(embedding_model)) > 0);
|
|
14
|
+
|
|
15
|
+
DROP INDEX IF EXISTS stackbone_platform.rag_chunks_embedding_hnsw;
|
|
16
|
+
ALTER TABLE stackbone_platform.rag_chunks ALTER COLUMN embedding TYPE vector;
|
|
17
|
+
|
|
18
|
+
-- Other dimensions are initialized under the shared index-creation lock.
|
|
19
|
+
CREATE INDEX IF NOT EXISTS rag_chunks_embedding_1536_hnsw
|
|
20
|
+
ON stackbone_platform.rag_chunks USING hnsw ((embedding::vector(1536)) vector_cosine_ops)
|
|
21
|
+
WHERE vector_dims(embedding) = 1536;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
CREATE TABLE stackbone_platform.rag_collection_rebuilds (
|
|
2
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
collection_id uuid NOT NULL REFERENCES stackbone_platform.rag_collections(id) ON DELETE CASCADE,
|
|
4
|
+
target_provider text NOT NULL CHECK (length(trim(target_provider)) > 0),
|
|
5
|
+
target_model text NOT NULL CHECK (length(trim(target_model)) > 0),
|
|
6
|
+
target_dimension integer NOT NULL CHECK (target_dimension BETWEEN 1 AND 16000),
|
|
7
|
+
target_revision integer NOT NULL CHECK (target_revision > 0),
|
|
8
|
+
attempt integer NOT NULL DEFAULT 1 CHECK (attempt > 0),
|
|
9
|
+
attempt_token uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
10
|
+
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'failed', 'succeeded')),
|
|
11
|
+
error text,
|
|
12
|
+
run_id text,
|
|
13
|
+
world_run_id text,
|
|
14
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
15
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
16
|
+
finished_at timestamptz,
|
|
17
|
+
UNIQUE (collection_id, target_revision)
|
|
18
|
+
);
|
|
19
|
+
CREATE UNIQUE INDEX rag_collection_rebuilds_active_collection_uq
|
|
20
|
+
ON stackbone_platform.rag_collection_rebuilds(collection_id)
|
|
21
|
+
WHERE status IN ('pending', 'running', 'failed');
|
|
22
|
+
|
|
23
|
+
CREATE TABLE stackbone_platform.rag_collection_rebuild_items (
|
|
24
|
+
rebuild_id uuid NOT NULL REFERENCES stackbone_platform.rag_collection_rebuilds(id) ON DELETE CASCADE,
|
|
25
|
+
document_id uuid NOT NULL REFERENCES stackbone_platform.rag_documents(id) ON DELETE CASCADE,
|
|
26
|
+
ordinal integer NOT NULL CHECK (ordinal >= 0),
|
|
27
|
+
document_snapshot jsonb NOT NULL CHECK (jsonb_typeof(document_snapshot) = 'object'),
|
|
28
|
+
source_kind text NOT NULL CHECK (source_kind IN ('original', 'chunks')),
|
|
29
|
+
prepared_chunks jsonb NOT NULL CHECK (jsonb_typeof(prepared_chunks) = 'array' AND jsonb_array_length(prepared_chunks) > 0),
|
|
30
|
+
completed_at timestamptz,
|
|
31
|
+
PRIMARY KEY (rebuild_id, document_id),
|
|
32
|
+
UNIQUE (rebuild_id, ordinal)
|
|
33
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Keep the original manifest size when a missing document cascades its item.
|
|
2
|
+
ALTER TABLE stackbone_platform.rag_collection_rebuilds
|
|
3
|
+
ADD COLUMN manifest_size integer NOT NULL DEFAULT 0;
|
|
4
|
+
|
|
5
|
+
UPDATE stackbone_platform.rag_collection_rebuilds r
|
|
6
|
+
SET manifest_size = (
|
|
7
|
+
SELECT count(*)::integer FROM stackbone_platform.rag_collection_rebuild_items i
|
|
8
|
+
WHERE i.rebuild_id = r.id
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
ALTER TABLE stackbone_platform.rag_collection_rebuilds
|
|
12
|
+
ADD CONSTRAINT rag_collection_rebuilds_manifest_size_check CHECK (manifest_size >= 0);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX rag_collection_rebuild_items_document_idx
|
|
15
|
+
ON stackbone_platform.rag_collection_rebuild_items(document_id);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- FDE-87: legacy rows have no reliable agent owner; the author approved erasure.
|
|
2
|
+
DELETE FROM stackbone_platform.memory_facts;
|
|
3
|
+
DELETE FROM stackbone_platform.memory_calls;
|
|
4
|
+
DELETE FROM stackbone_platform.memory_user_revisions;
|
|
5
|
+
|
|
6
|
+
ALTER TABLE stackbone_platform.memory_facts ADD COLUMN agent_id text NOT NULL;
|
|
7
|
+
ALTER TABLE stackbone_platform.memory_calls ADD COLUMN agent_id text NOT NULL;
|
|
8
|
+
ALTER TABLE stackbone_platform.memory_user_revisions ADD COLUMN agent_id text NOT NULL;
|
|
9
|
+
ALTER TABLE stackbone_platform.memory_user_revisions DROP CONSTRAINT memory_user_revisions_pkey;
|
|
10
|
+
ALTER TABLE stackbone_platform.memory_user_revisions ADD PRIMARY KEY (agent_id, user_id);
|
|
11
|
+
|
|
12
|
+
DROP INDEX stackbone_platform.memory_facts_user_idx;
|
|
13
|
+
CREATE INDEX memory_facts_user_idx ON stackbone_platform.memory_facts (agent_id, user_id);
|
|
14
|
+
DROP INDEX stackbone_platform.memory_calls_user_started_idx;
|
|
15
|
+
CREATE INDEX memory_calls_user_started_idx ON stackbone_platform.memory_calls
|
|
16
|
+
(agent_id, user_id, started_at DESC, created_at DESC);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
CREATE TABLE stackbone_platform.memory_settings (
|
|
2
|
+
agent_id text PRIMARY KEY,
|
|
3
|
+
overrides jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
4
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
5
|
+
updated_by text,
|
|
6
|
+
CONSTRAINT memory_settings_overrides_object CHECK (jsonb_typeof(overrides) = 'object')
|
|
7
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE stackbone_platform.memory_identity_bindings (
|
|
2
|
+
agent_id text PRIMARY KEY,
|
|
3
|
+
identity_key text NOT NULL CHECK (char_length(identity_key) BETWEEN 1 AND 100),
|
|
4
|
+
source text NOT NULL CHECK (source IN ('code', 'studio', 'legacy')),
|
|
5
|
+
fixed_at timestamptz NOT NULL DEFAULT now()
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
INSERT INTO stackbone_platform.memory_identity_bindings (agent_id, identity_key, source)
|
|
9
|
+
SELECT agent_id, 'customerId', 'legacy' FROM (
|
|
10
|
+
SELECT agent_id FROM stackbone_platform.memory_facts
|
|
11
|
+
UNION SELECT agent_id FROM stackbone_platform.memory_calls
|
|
12
|
+
UNION SELECT agent_id FROM stackbone_platform.memory_user_revisions
|
|
13
|
+
) AS existing_agents;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- 0109_trigger_subscriptions_conversation_key.sql — an agent link's conversation key (PLA-971)
|
|
2
|
+
-- A JSONata expression evaluated against each inbound event. Events that yield the
|
|
3
|
+
-- same value continue one agent session; an event that yields nothing usable starts a
|
|
4
|
+
-- fresh one. Nullable: NULL is "every event opens its own session", which every link
|
|
5
|
+
-- that existed before reads as.
|
|
6
|
+
--
|
|
7
|
+
-- No CHECK tying it to `target_kind = 'agent'`, as `target_kind` itself (0096): a box
|
|
8
|
+
-- running an older build must be able to store a row it cannot run rather than reject
|
|
9
|
+
-- the write. The validators and the subscriptions router refuse a conversation key on
|
|
10
|
+
-- a workflow link, and a correlation key on an agent link.
|
|
11
|
+
--
|
|
12
|
+
-- 0109 and not 0106: STA3-18 took 0103-0105, and open PR #785 (FDE-87) also claims
|
|
13
|
+
-- 0103-0105 and has to renumber into 0106-0108. Gaps are legal
|
|
14
|
+
-- (`scripts/check-migration-numbers.mjs`).
|
|
15
|
+
|
|
16
|
+
ALTER TABLE stackbone_platform.trigger_subscriptions
|
|
17
|
+
ADD COLUMN IF NOT EXISTS conversation_key text;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- 0110_concurrency_limits.sql — named concurrency limits for workflows.
|
|
2
|
+
--
|
|
3
|
+
-- A workflow takes a slot (`acquireSlot`) before it starts scarce work, and
|
|
4
|
+
-- another workflow gives it back (`releaseSlot`) when that work ends. When no
|
|
5
|
+
-- slot is free, the run waits on a World hook; the box resumes the hook when it
|
|
6
|
+
-- grants a slot.
|
|
7
|
+
--
|
|
8
|
+
-- `concurrency_limiters`: one row per limit the workspace declares, synced from
|
|
9
|
+
-- the manifest at boot. `capacity` and `max_hold` are what the code declares;
|
|
10
|
+
-- `capacity_override` is what an operator saves in Studio, and wins when set.
|
|
11
|
+
--
|
|
12
|
+
-- `concurrency_slots`: one row per slot requested. `waiting` is in the queue,
|
|
13
|
+
-- `granted` is given to the run but not yet confirmed by it, `held` is
|
|
14
|
+
-- confirmed. Only `granted` and `held` count against the capacity.
|
|
15
|
+
-- `claim_deadline` bounds how long a granted slot waits for its run to confirm;
|
|
16
|
+
-- `expires_at` is the `max_hold` cut-off, after which the box takes the slot back.
|
|
17
|
+
-- `hook_token` is UNIQUE so a retried enqueue step finds its row instead of
|
|
18
|
+
-- queueing twice. `label` is an id the creator chooses, never personal data.
|
|
19
|
+
--
|
|
20
|
+
-- Every write locks the limiter row first (`SELECT ... FOR UPDATE`), then
|
|
21
|
+
-- touches its slots: one lock order, so READ COMMITTED is enough.
|
|
22
|
+
--
|
|
23
|
+
-- Deliberately NO foreign key from `run_id` to `runs`: the run row is projected
|
|
24
|
+
-- in the background by the run sweep and can land after the slot row.
|
|
25
|
+
-- Deliberately NO foreign key from `limiter` to `concurrency_limiters` either:
|
|
26
|
+
-- the boot sync keeps a limiter with slots, and the store refuses an unknown one.
|
|
27
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.concurrency_limiters (
|
|
28
|
+
name text PRIMARY KEY,
|
|
29
|
+
capacity integer NOT NULL CHECK (capacity > 0),
|
|
30
|
+
max_hold interval NOT NULL,
|
|
31
|
+
capacity_override integer NULL CHECK (capacity_override > 0),
|
|
32
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.concurrency_slots (
|
|
36
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
37
|
+
limiter text NOT NULL,
|
|
38
|
+
state text NOT NULL CHECK (state IN ('waiting', 'granted', 'held')),
|
|
39
|
+
run_id uuid NULL,
|
|
40
|
+
hook_token text NOT NULL UNIQUE,
|
|
41
|
+
label text NULL,
|
|
42
|
+
enqueued_at timestamptz NOT NULL DEFAULT now(),
|
|
43
|
+
granted_at timestamptz NULL,
|
|
44
|
+
claim_deadline timestamptz NULL,
|
|
45
|
+
expires_at timestamptz NULL
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS concurrency_slots_limiter_state_enqueued_at_idx
|
|
49
|
+
ON stackbone_platform.concurrency_slots (limiter, state, enqueued_at);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS concurrency_slots_run_id_idx
|
|
52
|
+
ON stackbone_platform.concurrency_slots (run_id);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- 0111_concurrency_slots_now_idx.sql — the indexes the Insights "now" metrics
|
|
2
|
+
-- over concurrency slots are read through.
|
|
3
|
+
--
|
|
4
|
+
-- Two metrics read `concurrency_slots` (0110) on every query, never from a
|
|
5
|
+
-- cache (the box runtime's `insights/engine/catalog/now-metrics.ts`):
|
|
6
|
+
--
|
|
7
|
+
-- - `slots_in_use`: slots `granted` or `held`, the ones that count against a
|
|
8
|
+
-- limit's capacity, by `limiter`;
|
|
9
|
+
-- - `slots_waiting`: slots `waiting` in the queue, by `limiter`.
|
|
10
|
+
--
|
|
11
|
+
-- As in 0086, each index is partial on the metric's predicate, spelled as the
|
|
12
|
+
-- metric spells it so the planner proves the index covers the read, and holds
|
|
13
|
+
-- the column its property reads, so a count by limit is an index-only scan over
|
|
14
|
+
-- the rows the metric counts and no others.
|
|
15
|
+
CREATE INDEX IF NOT EXISTS concurrency_slots_in_use_now_idx
|
|
16
|
+
ON stackbone_platform.concurrency_slots (limiter)
|
|
17
|
+
WHERE state IN ('granted', 'held');
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS concurrency_slots_waiting_now_idx
|
|
20
|
+
ON stackbone_platform.concurrency_slots (limiter)
|
|
21
|
+
WHERE state = 'waiting';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 0112_concurrency_max_hold_override.sql — an operator override for a limit's
|
|
2
|
+
-- max hold, next to the capacity override 0110 already carries.
|
|
3
|
+
--
|
|
4
|
+
-- `max_hold` is what the code declares, synced from the manifest at boot;
|
|
5
|
+
-- `max_hold_override` is what an operator saves in Studio or with
|
|
6
|
+
-- `stackbone limits max-hold`, and wins when set. The boot sync never touches it.
|
|
7
|
+
--
|
|
8
|
+
-- The effective hold, `coalesce(max_hold_override, max_hold)`, is read once,
|
|
9
|
+
-- when a slot is granted, and stamped into `concurrency_slots.expires_at`.
|
|
10
|
+
-- Changing the override never rewrites `expires_at` of a slot already granted
|
|
11
|
+
-- or held: lowering it does not take back running work, raising it does not
|
|
12
|
+
-- extend a slot granted before the change.
|
|
13
|
+
ALTER TABLE stackbone_platform.concurrency_limiters
|
|
14
|
+
ADD COLUMN IF NOT EXISTS max_hold_override interval NULL
|
|
15
|
+
CHECK (max_hold_override > interval '0');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 0113_trigger_subscriptions_debounce_ms.sql — an agent link's wait before it answers (STA3-25)
|
|
2
|
+
-- Milliseconds the box waits after an event for more events of the same conversation,
|
|
3
|
+
-- so a burst of chat messages from one customer gets one answer. Nullable: NULL is
|
|
4
|
+
-- "answer each event as it arrives", which every link that existed before reads as.
|
|
5
|
+
--
|
|
6
|
+
-- No CHECK on the range or tying it to `target_kind = 'agent'` and a conversation key,
|
|
7
|
+
-- as `conversation_key` (0109): a box running an older build must be able to store a
|
|
8
|
+
-- row it cannot run rather than reject the write. The validators and the subscriptions
|
|
9
|
+
-- router refuse a wait outside 1..10000, on a workflow link, and on an agent link
|
|
10
|
+
-- without a conversation key.
|
|
11
|
+
|
|
12
|
+
ALTER TABLE stackbone_platform.trigger_subscriptions
|
|
13
|
+
ADD COLUMN IF NOT EXISTS debounce_ms integer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackbone/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"ioredis": "^5.10.1",
|
|
33
33
|
"jose": "^6.2.3",
|
|
34
34
|
"libsodium-wrappers": "^0.8.4",
|
|
35
|
+
"livekit-server-sdk": "2.19.1",
|
|
35
36
|
"open": "^11.0.0",
|
|
36
37
|
"openai": "^6.42.0",
|
|
37
38
|
"pg": "^8.20.0",
|