ohwow 0.6.9 → 0.9.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/dist/index.js +3112 -1534
- package/dist/mcp-server/index.js +12 -12
- package/dist/migrations/091-skills-sop-columns.sql +18 -0
- package/dist/migrations/092-operational-pillars.sql +55 -0
- package/dist/migrations/093-seed-operational-pillars.sql +176 -0
- package/dist/migrations/094-person-models.sql +58 -0
- package/dist/migrations/095-transition-engine.sql +52 -0
- package/dist/migrations/096-work-router.sql +95 -0
- package/dist/migrations/097-human-growth.sql +58 -0
- package/dist/migrations/098-observation-layer.sql +5 -0
- package/dist/migrations/099-collective-intelligence.sql +5 -0
- package/dist/migrations/100-agent-model-policy.sql +32 -0
- package/dist/migrations/101-llm-calls.sql +36 -0
- package/dist/migrations/102-team-member-guide.sql +14 -0
- package/dist/migrations/103-fix-person-models-fk.sql +89 -0
- package/dist/migrations/104-fix-person-observations-fk.sql +48 -0
- package/dist/migrations/105-onboarding-plans.sql +56 -0
- package/dist/migrations/106-deliverable-actor-link.sql +29 -0
- package/dist/migrations/107-code-skills.sql +20 -0
- package/dist/migrations/108-archive-procedure-skills.sql +29 -0
- package/dist/migrations/109-workspace-default-fs-paths.sql +19 -0
- package/dist/migrations/110-task-state-ttl.sql +28 -0
- package/dist/migrations/111-conversation-status.sql +25 -0
- package/dist/migrations/112-deliverables-created-at-iso.sql +40 -0
- package/dist/migrations/113-permission-requests.sql +36 -0
- package/dist/migrations/114-llm-calls-tool-telemetry.sql +32 -0
- package/dist/migrations/115-trigger-watchdog.sql +46 -0
- package/dist/migrations/116-self-findings.sql +46 -0
- package/dist/migrations/117-experiment-validations.sql +64 -0
- package/dist/migrations/118-validation-rollback.sql +33 -0
- package/dist/migrations/119-runtime-config-overrides.sql +44 -0
- package/dist/migrations/120-business-vitals.sql +44 -0
- package/dist/migrations/121-x-contact-events.sql +42 -0
- package/dist/migrations/122-video-jobs.sql +52 -0
- package/dist/migrations/123-insight-distiller.sql +68 -0
- package/dist/migrations/124-x-dm-messages.sql +55 -0
- package/dist/migrations/125-x-dm-messages-bodies.sql +40 -0
- package/dist/migrations/126-x-dm-signals.sql +52 -0
- package/dist/migrations/127-x-dm-contact-linking.sql +36 -0
- package/dist/migrations/128-attribution-view.sql +59 -0
- package/dist/migrations/129-x-posted-log.sql +36 -0
- package/dist/migrations/130-patches-attempted-log.sql +44 -0
- package/dist/scrapling-server/requirements.txt +3 -0
- package/dist/scrapling-server/server.py +224 -0
- package/dist/web/assets/index-Bp9CoQ8c.css +1 -0
- package/dist/web/assets/index-C5xtuLcg.js +102 -0
- package/dist/web/index.html +2 -2
- package/package.json +9 -4
- package/dist/web/assets/index-B2PzvKIq.js +0 -100
- package/dist/web/assets/index-DZAi92e-.css +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Normalize agent_workforce_deliverables.created_at to ISO-8601 with a
|
|
2
|
+
-- trailing Z, so lexicographic comparison against JS `.toISOString()`
|
|
3
|
+
-- filter values in list_deliverables works correctly.
|
|
4
|
+
--
|
|
5
|
+
-- Background: the schema default is `datetime('now')`, which outputs
|
|
6
|
+
-- `YYYY-MM-DD HH:MM:SS` (space separator, no milliseconds, no Z).
|
|
7
|
+
-- One insert path (deliverables-recorder.ts) explicitly writes
|
|
8
|
+
-- `new Date().toISOString()` so those rows land as
|
|
9
|
+
-- `YYYY-MM-DDTHH:MM:SS.mmmZ`. Two other insert paths (saveDeliverable,
|
|
10
|
+
-- task-completion.ts) omit created_at entirely and fall back to the
|
|
11
|
+
-- default. That produces a mixed table: 24 rows in ISO-with-Z, 50 rows
|
|
12
|
+
-- in SQL-default.
|
|
13
|
+
--
|
|
14
|
+
-- list_deliverables' `since` filter calls `.gte('created_at', iso)`
|
|
15
|
+
-- where `iso` is always ISO-with-Z. SQLite compares lexicographically.
|
|
16
|
+
-- Position 10 of the string decides: space (0x20) < 'T' (0x54), so
|
|
17
|
+
-- every SQL-default row silently sorts BEFORE any ISO-with-Z filter,
|
|
18
|
+
-- and gets excluded even when it's chronologically newer. Found during
|
|
19
|
+
-- the M0.21 self-bench moonshot — ohwow reported 25 deliverables in
|
|
20
|
+
-- the 24h window when the real answer was 38. 13 rows lost to format
|
|
21
|
+
-- drift.
|
|
22
|
+
--
|
|
23
|
+
-- Fix:
|
|
24
|
+
-- 1. Backfill every row whose created_at is missing a T or Z to ISO.
|
|
25
|
+
-- `strftime('%Y-%m-%dT%H:%M:%f', col) || 'Z'` produces
|
|
26
|
+
-- `2026-04-13T19:46:18.000Z` regardless of the original shape.
|
|
27
|
+
-- 2. Do the same for updated_at since the same inserts leave it with
|
|
28
|
+
-- the SQL default.
|
|
29
|
+
-- 3. Use LIKE filters so re-running the migration is a no-op.
|
|
30
|
+
--
|
|
31
|
+
-- The downstream insert sites (saveDeliverable, task-completion.ts)
|
|
32
|
+
-- are fixed in the same commit so new rows land in ISO from here on.
|
|
33
|
+
|
|
34
|
+
UPDATE agent_workforce_deliverables
|
|
35
|
+
SET created_at = strftime('%Y-%m-%dT%H:%M:%f', created_at) || 'Z'
|
|
36
|
+
WHERE created_at NOT LIKE '%T%Z';
|
|
37
|
+
|
|
38
|
+
UPDATE agent_workforce_deliverables
|
|
39
|
+
SET updated_at = strftime('%Y-%m-%dT%H:%M:%f', updated_at) || 'Z'
|
|
40
|
+
WHERE updated_at IS NOT NULL AND updated_at NOT LIKE '%T%Z';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 113: Filesystem permission requests
|
|
3
|
+
--
|
|
4
|
+
-- When an agent's filesystem/bash call hits FileAccessGuard the task now
|
|
5
|
+
-- throws PermissionDeniedError and lands in needs_approval instead of
|
|
6
|
+
-- returning an error string that the model hallucinates around. These
|
|
7
|
+
-- columns back that flow:
|
|
8
|
+
--
|
|
9
|
+
-- approval_reason — typed discriminator on needs_approval rows.
|
|
10
|
+
-- 'permission_denied' is the new value the
|
|
11
|
+
-- approval routing writes; existing deliverable
|
|
12
|
+
-- and verifier-escalation paths can migrate to
|
|
13
|
+
-- it incrementally.
|
|
14
|
+
-- permission_request — JSON describing the denied call (tool name,
|
|
15
|
+
-- attempted path, suggestedExact, suggestedParent,
|
|
16
|
+
-- guardReason, iteration, timestamp).
|
|
17
|
+
-- permission_grants — JSON array of ephemeral "approve once" paths
|
|
18
|
+
-- carried on a resumed child task. Read by
|
|
19
|
+
-- resolveTaskCapabilities and union'd into the
|
|
20
|
+
-- FileAccessGuard so the child can write without
|
|
21
|
+
-- persisting a row in agent_file_access_paths.
|
|
22
|
+
-- resumed_from_task_id — backref so the UI can thread resume chains
|
|
23
|
+
-- when the operator approves a denied task.
|
|
24
|
+
-- =====================================================================
|
|
25
|
+
|
|
26
|
+
-- @statement
|
|
27
|
+
ALTER TABLE agent_workforce_tasks ADD COLUMN approval_reason TEXT;
|
|
28
|
+
-- @statement
|
|
29
|
+
ALTER TABLE agent_workforce_tasks ADD COLUMN permission_request TEXT;
|
|
30
|
+
-- @statement
|
|
31
|
+
ALTER TABLE agent_workforce_tasks ADD COLUMN permission_grants TEXT;
|
|
32
|
+
-- @statement
|
|
33
|
+
ALTER TABLE agent_workforce_tasks ADD COLUMN resumed_from_task_id TEXT;
|
|
34
|
+
-- @statement
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_approval_reason
|
|
36
|
+
ON agent_workforce_tasks(approval_reason);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 114: LLM call tool-use telemetry
|
|
3
|
+
--
|
|
4
|
+
-- Adds per-row "did this model actually call tools on this call" signal
|
|
5
|
+
-- to llm_calls. The agent ReAct loops (react-loop.ts, model-router-loop.ts)
|
|
6
|
+
-- will start writing rows here after commit B wires them up — today the
|
|
7
|
+
-- table only has runLlmCall and orchestrator chat, so agent iterations
|
|
8
|
+
-- are invisible to telemetry.
|
|
9
|
+
--
|
|
10
|
+
-- Columns:
|
|
11
|
+
-- tool_call_count — integer count of tool_calls in the model response
|
|
12
|
+
-- for this single LLM call. NULL when not meaningful
|
|
13
|
+
-- (e.g. a generation purpose call that wasn't offered
|
|
14
|
+
-- tools). 0 is a real value — means the model was
|
|
15
|
+
-- offered tools and chose not to call any.
|
|
16
|
+
-- task_shape — 'work' when looksLikeToolWork(taskInput) is true
|
|
17
|
+
-- at the call site, 'chat' when it's false, NULL when
|
|
18
|
+
-- the call site doesn't have a task input (ad-hoc
|
|
19
|
+
-- /api/llm, orchestrator chat without a task, etc.).
|
|
20
|
+
--
|
|
21
|
+
-- Enables E1's selector self-healing: query the rolling tool-call rate
|
|
22
|
+
-- per (model, task_shape='work') and auto-demote models with <40% rate
|
|
23
|
+
-- from the FAST agent tier.
|
|
24
|
+
-- =====================================================================
|
|
25
|
+
|
|
26
|
+
-- @statement
|
|
27
|
+
ALTER TABLE llm_calls ADD COLUMN tool_call_count INTEGER;
|
|
28
|
+
-- @statement
|
|
29
|
+
ALTER TABLE llm_calls ADD COLUMN task_shape TEXT;
|
|
30
|
+
-- @statement
|
|
31
|
+
CREATE INDEX IF NOT EXISTS llm_calls_model_shape_idx
|
|
32
|
+
ON llm_calls(model, task_shape, created_at DESC);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 115: Trigger watchdog columns
|
|
3
|
+
--
|
|
4
|
+
-- Experiment E2: give the daemon a way to notice that a scheduled
|
|
5
|
+
-- trigger has been silently miscarrying. Without this, the diary
|
|
6
|
+
-- trigger shipped silent failures for 2+ weeks before anyone checked
|
|
7
|
+
-- — the trigger fired, the task failed, and nothing aggregated the
|
|
8
|
+
-- outcome at the trigger level.
|
|
9
|
+
--
|
|
10
|
+
-- Columns:
|
|
11
|
+
-- local_triggers.last_succeeded_at — ISO timestamp, set on every
|
|
12
|
+
-- task completion that
|
|
13
|
+
-- traces back to this trigger.
|
|
14
|
+
-- local_triggers.consecutive_failures — int counter. Incremented on
|
|
15
|
+
-- every failed/needs_approval
|
|
16
|
+
-- outcome; reset to 0 on
|
|
17
|
+
-- success. Crossing the
|
|
18
|
+
-- threshold (default 3) emits
|
|
19
|
+
-- a trigger_stuck activity row
|
|
20
|
+
-- so the operator's existing
|
|
21
|
+
-- activity surface picks it up
|
|
22
|
+
-- without new UI.
|
|
23
|
+
-- agent_workforce_tasks.source_trigger_id — FK-ish back-link so the
|
|
24
|
+
-- task finalization hook can
|
|
25
|
+
-- find which trigger to
|
|
26
|
+
-- update without walking the
|
|
27
|
+
-- title prefix or parsing
|
|
28
|
+
-- action_result JSON.
|
|
29
|
+
--
|
|
30
|
+
-- The resumed child task spawned by ohwow_approve_permission_request
|
|
31
|
+
-- inherits source_trigger_id from its parent so a successful resume
|
|
32
|
+
-- also resets the trigger counter.
|
|
33
|
+
-- =====================================================================
|
|
34
|
+
|
|
35
|
+
-- @statement
|
|
36
|
+
ALTER TABLE local_triggers ADD COLUMN last_succeeded_at TEXT;
|
|
37
|
+
-- @statement
|
|
38
|
+
ALTER TABLE local_triggers ADD COLUMN consecutive_failures INTEGER DEFAULT 0;
|
|
39
|
+
-- @statement
|
|
40
|
+
ALTER TABLE agent_workforce_tasks ADD COLUMN source_trigger_id TEXT;
|
|
41
|
+
-- @statement
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_source_trigger
|
|
43
|
+
ON agent_workforce_tasks(source_trigger_id);
|
|
44
|
+
-- @statement
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_triggers_consecutive_failures
|
|
46
|
+
ON local_triggers(consecutive_failures DESC) WHERE consecutive_failures > 0;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 116: self_findings — structured ledger for self-experimentation
|
|
3
|
+
--
|
|
4
|
+
-- Phase 1 of the self-improvement loop. Every experiment run by the
|
|
5
|
+
-- ExperimentRunner writes a row here: what was tested, what the verdict
|
|
6
|
+
-- was, what intervention (if any) was applied, and what the evidence
|
|
7
|
+
-- looked like. This becomes:
|
|
8
|
+
-- 1. The ground-truth record the next experiment reads before running
|
|
9
|
+
-- so the system doesn't re-investigate things it already knows.
|
|
10
|
+
-- 2. The feedback substrate: E1's demotion cache, E2's trigger
|
|
11
|
+
-- watchdog, the upcoming canary suite, etc. all write findings so
|
|
12
|
+
-- every future Claude session (and every agent's own planning) can
|
|
13
|
+
-- query a uniform "what do we know about ourselves?" surface.
|
|
14
|
+
-- 3. The input for the eventual meta-loop that picks the next
|
|
15
|
+
-- experiment to run based on what's unknown or drifting.
|
|
16
|
+
--
|
|
17
|
+
-- Nothing writes here yet after this migration — the writers land in
|
|
18
|
+
-- commit Phase1-B as part of the ExperimentRunner and its wrapper
|
|
19
|
+
-- experiments around E1/E2. This migration is the shape-only slice.
|
|
20
|
+
-- =====================================================================
|
|
21
|
+
|
|
22
|
+
-- @statement
|
|
23
|
+
CREATE TABLE IF NOT EXISTS self_findings (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
experiment_id TEXT NOT NULL,
|
|
26
|
+
category TEXT NOT NULL,
|
|
27
|
+
subject TEXT,
|
|
28
|
+
hypothesis TEXT,
|
|
29
|
+
verdict TEXT NOT NULL CHECK (verdict IN ('pass', 'warning', 'fail', 'error')),
|
|
30
|
+
summary TEXT NOT NULL,
|
|
31
|
+
evidence TEXT NOT NULL DEFAULT '{}',
|
|
32
|
+
intervention_applied TEXT,
|
|
33
|
+
ran_at TEXT NOT NULL,
|
|
34
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'superseded', 'revoked')),
|
|
36
|
+
superseded_by TEXT,
|
|
37
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
+
);
|
|
39
|
+
-- @statement
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_findings_experiment ON self_findings(experiment_id, ran_at DESC);
|
|
41
|
+
-- @statement
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_findings_category ON self_findings(category, ran_at DESC);
|
|
43
|
+
-- @statement
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_findings_verdict ON self_findings(verdict, ran_at DESC) WHERE status = 'active';
|
|
45
|
+
-- @statement
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_findings_subject ON self_findings(subject, ran_at DESC) WHERE subject IS NOT NULL;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 117: experiment_validations — accountability for interventions
|
|
3
|
+
--
|
|
4
|
+
-- Phase 3 of the self-improvement loop. Every time an Experiment's
|
|
5
|
+
-- intervene() mutates system state, the runner enqueues a validation
|
|
6
|
+
-- row to be processed ~15 minutes later. At validation time the
|
|
7
|
+
-- experiment's validate() hook reads the stored baseline, measures
|
|
8
|
+
-- current state, and returns held | failed | inconclusive. The outcome
|
|
9
|
+
-- lands as a self_findings row with category='validation' so queries
|
|
10
|
+
-- can trace "what the system decided and whether it was right."
|
|
11
|
+
--
|
|
12
|
+
-- Without this table, an intervention vanishes into history the moment
|
|
13
|
+
-- it's applied — there's no way to tell tomorrow whether yesterday's
|
|
14
|
+
-- stale-task-cleanup actually unblocked the queue or the queue filled
|
|
15
|
+
-- up again with new zombies. The validation step is the feedback loop
|
|
16
|
+
-- that makes every intervention a measurable claim instead of a
|
|
17
|
+
-- fire-and-forget side effect.
|
|
18
|
+
--
|
|
19
|
+
-- Columns:
|
|
20
|
+
-- intervention_finding_id — the self_findings row that carried the
|
|
21
|
+
-- original intervention_applied blob.
|
|
22
|
+
-- experiment_id — the experiment that owns the validate()
|
|
23
|
+
-- hook. The runner looks it up in the live
|
|
24
|
+
-- registry at validation time.
|
|
25
|
+
-- baseline — JSON snapshot captured from the
|
|
26
|
+
-- intervention's details. This is what the
|
|
27
|
+
-- validate() function gets as its first
|
|
28
|
+
-- argument.
|
|
29
|
+
-- validate_at — ISO timestamp when the runner should fire
|
|
30
|
+
-- the validation. Indexed so the due-query
|
|
31
|
+
-- stays cheap.
|
|
32
|
+
-- status — pending | completed | skipped | error
|
|
33
|
+
-- ('skipped' = experiment no longer has
|
|
34
|
+
-- validate() by the time the row is due)
|
|
35
|
+
-- outcome — held | failed | inconclusive — null until
|
|
36
|
+
-- validation fires.
|
|
37
|
+
-- outcome_finding_id — self_findings row the validation wrote.
|
|
38
|
+
-- =====================================================================
|
|
39
|
+
|
|
40
|
+
-- @statement
|
|
41
|
+
CREATE TABLE IF NOT EXISTS experiment_validations (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
intervention_finding_id TEXT NOT NULL,
|
|
44
|
+
experiment_id TEXT NOT NULL,
|
|
45
|
+
baseline TEXT NOT NULL DEFAULT '{}',
|
|
46
|
+
validate_at TEXT NOT NULL,
|
|
47
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
48
|
+
CHECK (status IN ('pending', 'completed', 'skipped', 'error')),
|
|
49
|
+
outcome TEXT CHECK (outcome IS NULL OR outcome IN ('held', 'failed', 'inconclusive')),
|
|
50
|
+
outcome_finding_id TEXT,
|
|
51
|
+
error_message TEXT,
|
|
52
|
+
completed_at TEXT,
|
|
53
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
54
|
+
);
|
|
55
|
+
-- @statement
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_validations_due
|
|
57
|
+
ON experiment_validations(validate_at)
|
|
58
|
+
WHERE status = 'pending';
|
|
59
|
+
-- @statement
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_validations_experiment
|
|
61
|
+
ON experiment_validations(experiment_id, created_at DESC);
|
|
62
|
+
-- @statement
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_validations_intervention
|
|
64
|
+
ON experiment_validations(intervention_finding_id);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 118: Rollback tracking on experiment_validations
|
|
3
|
+
--
|
|
4
|
+
-- Phase 5-A: close the validation feedback loop by letting the runner
|
|
5
|
+
-- auto-revert failed interventions. When validate() returns
|
|
6
|
+
-- outcome='failed' and the experiment exposes a rollback() hook, the
|
|
7
|
+
-- runner calls it, writes a rollback finding, and stamps the
|
|
8
|
+
-- validation row so queries can distinguish "failed but reverted"
|
|
9
|
+
-- from "failed and still bad."
|
|
10
|
+
--
|
|
11
|
+
-- Columns:
|
|
12
|
+
-- rolled_back — 1 when a rollback ran successfully, else 0.
|
|
13
|
+
-- Default 0 so legacy rows show as not-rolled-back.
|
|
14
|
+
-- rollback_finding_id — FK to the self_findings row the runner wrote
|
|
15
|
+
-- for the rollback. Pair with outcome_finding_id
|
|
16
|
+
-- to get the full "validation said fail,
|
|
17
|
+
-- rollback said X" trail.
|
|
18
|
+
-- rolled_back_at — ISO timestamp when the rollback ran.
|
|
19
|
+
--
|
|
20
|
+
-- No schema change for Experiment implementations that don't need
|
|
21
|
+
-- rollback — the hook is optional on the interface.
|
|
22
|
+
-- =====================================================================
|
|
23
|
+
|
|
24
|
+
-- @statement
|
|
25
|
+
ALTER TABLE experiment_validations ADD COLUMN rolled_back INTEGER DEFAULT 0;
|
|
26
|
+
-- @statement
|
|
27
|
+
ALTER TABLE experiment_validations ADD COLUMN rollback_finding_id TEXT;
|
|
28
|
+
-- @statement
|
|
29
|
+
ALTER TABLE experiment_validations ADD COLUMN rolled_back_at TEXT;
|
|
30
|
+
-- @statement
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_validations_rolled_back
|
|
32
|
+
ON experiment_validations(rolled_back, validate_at DESC)
|
|
33
|
+
WHERE rolled_back = 1;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 119: runtime_config_overrides — reversible config at runtime
|
|
3
|
+
--
|
|
4
|
+
-- Phase 5-B: key-value store for config values that an experiment
|
|
5
|
+
-- can change at runtime and roll back if validation fails. Used by
|
|
6
|
+
-- the upcoming tuner experiments (Phase 5-C) that adjust thresholds
|
|
7
|
+
-- like STALE_THRESHOLD_MS based on observed ledger patterns.
|
|
8
|
+
--
|
|
9
|
+
-- Design
|
|
10
|
+
-- ------
|
|
11
|
+
-- Every entry has:
|
|
12
|
+
-- - key — opaque string, by convention namespaced with a
|
|
13
|
+
-- dot (e.g. "stale_task_cleanup.threshold_ms")
|
|
14
|
+
-- - value — JSON-serialized value, parsed by the consumer
|
|
15
|
+
-- - set_by — experiment_id that wrote this entry (for audit)
|
|
16
|
+
-- - finding_id — the finding row that captured the decision,
|
|
17
|
+
-- so rollbacks can link back to the original
|
|
18
|
+
-- intervention
|
|
19
|
+
-- - set_at — ISO timestamp
|
|
20
|
+
--
|
|
21
|
+
-- Consumers pattern:
|
|
22
|
+
-- const threshold = await getRuntimeConfig(db, 'stale_task_cleanup.threshold_ms', DEFAULT);
|
|
23
|
+
-- Writers pattern (inside intervene):
|
|
24
|
+
-- await setRuntimeConfig(db, 'key', newValue, { setBy: exp.id, findingId });
|
|
25
|
+
-- Rollback pattern (inside rollback):
|
|
26
|
+
-- await deleteRuntimeConfig(db, 'key'); // reverts to caller's default
|
|
27
|
+
--
|
|
28
|
+
-- A module-level cache mirrors the table so hot-path reads don't hit
|
|
29
|
+
-- SQLite. Cache is refreshed on daemon boot + every 60s + on every
|
|
30
|
+
-- set/delete (local invalidation).
|
|
31
|
+
-- =====================================================================
|
|
32
|
+
|
|
33
|
+
-- @statement
|
|
34
|
+
CREATE TABLE IF NOT EXISTS runtime_config_overrides (
|
|
35
|
+
key TEXT PRIMARY KEY,
|
|
36
|
+
value TEXT NOT NULL,
|
|
37
|
+
set_by TEXT,
|
|
38
|
+
finding_id TEXT,
|
|
39
|
+
set_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
40
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
41
|
+
);
|
|
42
|
+
-- @statement
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_runtime_config_set_by
|
|
44
|
+
ON runtime_config_overrides(set_by, set_at DESC);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 120: business_vitals — time-series of operator business signals
|
|
3
|
+
--
|
|
4
|
+
-- Week-1 "Heart": give the homeostasis controller something to read when
|
|
5
|
+
-- deciding whether the runtime is producing more value than it costs.
|
|
6
|
+
-- Each row is one snapshot of the operator's business at time ts.
|
|
7
|
+
--
|
|
8
|
+
-- Columns are all nullable (except ts / source) so partial snapshots
|
|
9
|
+
-- land cleanly: a workspace with no Stripe key still accumulates
|
|
10
|
+
-- daily_cost_cents rows; a workspace with Stripe but no active-user
|
|
11
|
+
-- tracker still gets MRR.
|
|
12
|
+
--
|
|
13
|
+
-- Units:
|
|
14
|
+
-- mrr — monthly recurring revenue, cents
|
|
15
|
+
-- arr — annualized recurring revenue, cents (= mrr * 12 when
|
|
16
|
+
-- not derived from a separate feed)
|
|
17
|
+
-- active_users — count of distinct users active in the trailing window
|
|
18
|
+
-- daily_cost_cents — sum of agent_workforce_tasks.cost_cents for the
|
|
19
|
+
-- local day of ts (UTC)
|
|
20
|
+
-- runway_days — cash-on-hand / burn_rate when both are known
|
|
21
|
+
-- source — producer of this row: "stripe", "manual", "import",
|
|
22
|
+
-- "tasks_aggregate", etc. Never a business-specific
|
|
23
|
+
-- name. New producers just add their own string.
|
|
24
|
+
-- =====================================================================
|
|
25
|
+
|
|
26
|
+
-- @statement
|
|
27
|
+
CREATE TABLE IF NOT EXISTS business_vitals (
|
|
28
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
29
|
+
workspace_id TEXT NOT NULL,
|
|
30
|
+
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
31
|
+
mrr INTEGER,
|
|
32
|
+
arr INTEGER,
|
|
33
|
+
active_users INTEGER,
|
|
34
|
+
daily_cost_cents INTEGER,
|
|
35
|
+
runway_days REAL,
|
|
36
|
+
source TEXT NOT NULL,
|
|
37
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
+
);
|
|
39
|
+
-- @statement
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_business_vitals_workspace_ts
|
|
41
|
+
ON business_vitals(workspace_id, ts DESC);
|
|
42
|
+
-- @statement
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_business_vitals_source
|
|
44
|
+
ON business_vitals(source, ts DESC);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- 121-x-contact-events.sql
|
|
2
|
+
-- Layer-4 sales loop: substrate for attributing X signal → contact → revenue.
|
|
3
|
+
--
|
|
4
|
+
-- The contact_events table was introduced in 001 with columns tuned for
|
|
5
|
+
-- free-text CRM notes (event_type, title, description, agent_id, task_id,
|
|
6
|
+
-- metadata, created_at). The sales-loop flow needs a structured-event
|
|
7
|
+
-- shape (kind, source, payload JSON, occurred_at) that can encode the
|
|
8
|
+
-- funnel: x:seen, x:reached, x:replied, x:qualified, dm:received,
|
|
9
|
+
-- demo:booked, plan:paid. Rather than rename or migrate data, we grow
|
|
10
|
+
-- the new columns alongside the legacy ones so both consumer styles
|
|
11
|
+
-- coexist. Legacy CRM tools keep writing event_type/metadata; the
|
|
12
|
+
-- sales-loop path writes kind/payload/occurred_at.
|
|
13
|
+
--
|
|
14
|
+
-- All statements are idempotent. init.ts splits on `-- @statement` and
|
|
15
|
+
-- swallows "duplicate column" errors so re-runs against a partially
|
|
16
|
+
-- applied DB are safe.
|
|
17
|
+
--
|
|
18
|
+
-- Adds:
|
|
19
|
+
-- - outreach_token + never_sync on contacts (privacy + attribution).
|
|
20
|
+
-- - kind/source/payload/occurred_at on contact_events (funnel shape).
|
|
21
|
+
-- - contact_id + source_event_id on revenue_entries (attribution join).
|
|
22
|
+
|
|
23
|
+
ALTER TABLE agent_workforce_contacts ADD COLUMN outreach_token TEXT;
|
|
24
|
+
-- @statement
|
|
25
|
+
ALTER TABLE agent_workforce_contacts ADD COLUMN never_sync INTEGER NOT NULL DEFAULT 0;
|
|
26
|
+
-- @statement
|
|
27
|
+
ALTER TABLE agent_workforce_contact_events ADD COLUMN kind TEXT;
|
|
28
|
+
-- @statement
|
|
29
|
+
ALTER TABLE agent_workforce_contact_events ADD COLUMN source TEXT;
|
|
30
|
+
-- @statement
|
|
31
|
+
ALTER TABLE agent_workforce_contact_events ADD COLUMN payload TEXT DEFAULT '{}';
|
|
32
|
+
-- @statement
|
|
33
|
+
ALTER TABLE agent_workforce_contact_events ADD COLUMN occurred_at TEXT;
|
|
34
|
+
-- @statement
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_contact_events_workspace_kind
|
|
36
|
+
ON agent_workforce_contact_events(workspace_id, kind, occurred_at);
|
|
37
|
+
-- @statement
|
|
38
|
+
ALTER TABLE agent_workforce_revenue_entries ADD COLUMN contact_id TEXT REFERENCES agent_workforce_contacts(id);
|
|
39
|
+
-- @statement
|
|
40
|
+
ALTER TABLE agent_workforce_revenue_entries ADD COLUMN source_event_id TEXT REFERENCES agent_workforce_contact_events(id);
|
|
41
|
+
-- @statement
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_revenue_contact ON agent_workforce_revenue_entries(contact_id);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- 122-video-jobs.sql
|
|
2
|
+
-- Tracking table for deterministic video renders driven by the
|
|
3
|
+
-- video_generation skill (src/execution/skills/video_generation.ts).
|
|
4
|
+
--
|
|
5
|
+
-- Each job corresponds to one VideoSpec → MP4 pipeline invocation. The
|
|
6
|
+
-- spec_hash column lets us dedupe: if a prior job with the same spec
|
|
7
|
+
-- hash is already 'done', callers can short-circuit and reuse that MP4
|
|
8
|
+
-- instead of re-rendering.
|
|
9
|
+
--
|
|
10
|
+
-- Checkpoints live in a child table so a crashed daemon can resume from
|
|
11
|
+
-- the last completed stage without losing earlier work (rendered voice,
|
|
12
|
+
-- generated music, timing solver output).
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS video_jobs (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
workspace_id TEXT NOT NULL,
|
|
17
|
+
spec_hash TEXT NOT NULL,
|
|
18
|
+
spec_path TEXT NOT NULL,
|
|
19
|
+
status TEXT NOT NULL CHECK (status IN (
|
|
20
|
+
'pending','preparing','resolving','rendering','storing',
|
|
21
|
+
'done','failed','canceled'
|
|
22
|
+
)),
|
|
23
|
+
progress REAL NOT NULL DEFAULT 0,
|
|
24
|
+
stage TEXT,
|
|
25
|
+
error TEXT,
|
|
26
|
+
output_path TEXT,
|
|
27
|
+
size_bytes INTEGER,
|
|
28
|
+
duration_frames INTEGER,
|
|
29
|
+
duration_ms INTEGER,
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
31
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
32
|
+
);
|
|
33
|
+
-- @statement
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_video_jobs_workspace_status
|
|
35
|
+
ON video_jobs(workspace_id, status);
|
|
36
|
+
-- @statement
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_video_jobs_spec_hash
|
|
38
|
+
ON video_jobs(spec_hash);
|
|
39
|
+
-- @statement
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_video_jobs_created
|
|
41
|
+
ON video_jobs(created_at DESC);
|
|
42
|
+
-- @statement
|
|
43
|
+
CREATE TABLE IF NOT EXISTS video_job_checkpoints (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
job_id TEXT NOT NULL REFERENCES video_jobs(id) ON DELETE CASCADE,
|
|
46
|
+
stage TEXT NOT NULL,
|
|
47
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
48
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
49
|
+
);
|
|
50
|
+
-- @statement
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_vjc_job_stage
|
|
52
|
+
ON video_job_checkpoints(job_id, stage);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Migration 123: insight distiller — novelty baselines + feedback ledger
|
|
3
|
+
--
|
|
4
|
+
-- Piece 1 of the "surprise-first self-observation" bundle. Raw findings
|
|
5
|
+
-- keep flowing into self_findings on the 5s reactive reschedule; this
|
|
6
|
+
-- migration adds the two tables that let the system tell "the 500th
|
|
7
|
+
-- identical repetition" apart from "an unusual thing just happened":
|
|
8
|
+
--
|
|
9
|
+
-- self_observation_baselines
|
|
10
|
+
-- One row per (experiment_id, subject) that accumulates a rolling
|
|
11
|
+
-- running mean + stddev over an optional numeric evidence field
|
|
12
|
+
-- (`tracked_field`) via Welford's algorithm. Also tracks first-seen
|
|
13
|
+
-- timestamp, sample count, last verdict, and consecutive fail
|
|
14
|
+
-- count — enough to answer "have we seen this before?" and "has
|
|
15
|
+
-- the verdict been stuck for a while?" without scanning the
|
|
16
|
+
-- ledger. Findings-store writes this row alongside every insert
|
|
17
|
+
-- and mixes the resulting novelty score into the finding's
|
|
18
|
+
-- evidence.__novelty so the distiller can rank by surprise.
|
|
19
|
+
--
|
|
20
|
+
-- self_insight_feedback
|
|
21
|
+
-- Operator / agent feedback ledger: accepted / rejected /
|
|
22
|
+
-- deferred / applied actions taken on a specific finding, keyed
|
|
23
|
+
-- by finding_id. Closes the loop so the strategist and
|
|
24
|
+
-- experiment-author can eventually learn which suggestions
|
|
25
|
+
-- actually landed well. Nothing writes here yet — the REST +
|
|
26
|
+
-- MCP surfaces for recording feedback come in a later piece;
|
|
27
|
+
-- this migration is the shape-only slice.
|
|
28
|
+
-- =====================================================================
|
|
29
|
+
|
|
30
|
+
-- @statement
|
|
31
|
+
CREATE TABLE IF NOT EXISTS self_observation_baselines (
|
|
32
|
+
experiment_id TEXT NOT NULL,
|
|
33
|
+
subject TEXT NOT NULL,
|
|
34
|
+
first_seen_at TEXT NOT NULL,
|
|
35
|
+
last_seen_at TEXT NOT NULL,
|
|
36
|
+
sample_count INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
tracked_field TEXT,
|
|
38
|
+
running_mean REAL,
|
|
39
|
+
running_m2 REAL,
|
|
40
|
+
last_value REAL,
|
|
41
|
+
last_verdict TEXT,
|
|
42
|
+
consecutive_fails INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
44
|
+
PRIMARY KEY (experiment_id, subject)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- @statement
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_baselines_last_seen
|
|
49
|
+
ON self_observation_baselines(last_seen_at DESC);
|
|
50
|
+
|
|
51
|
+
-- @statement
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_baselines_consecutive_fails
|
|
53
|
+
ON self_observation_baselines(consecutive_fails DESC)
|
|
54
|
+
WHERE consecutive_fails > 0;
|
|
55
|
+
|
|
56
|
+
-- @statement
|
|
57
|
+
CREATE TABLE IF NOT EXISTS self_insight_feedback (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
finding_id TEXT NOT NULL,
|
|
60
|
+
action TEXT NOT NULL CHECK (action IN ('accepted','rejected','deferred','applied')),
|
|
61
|
+
actor TEXT NOT NULL,
|
|
62
|
+
rationale TEXT,
|
|
63
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- @statement
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_insight_feedback_finding
|
|
68
|
+
ON self_insight_feedback(finding_id, created_at DESC);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- 124-x-dm-messages.sql — store the result of XDmPollerScheduler ticks.
|
|
2
|
+
--
|
|
3
|
+
-- The poller calls listDmsViaBrowser hourly and writes the inbox
|
|
4
|
+
-- summaries here. Two tables: a current-state thread row (one per
|
|
5
|
+
-- conversation_pair) and an append-only observation log keyed by the
|
|
6
|
+
-- preview text's hash so we don't re-insert when nothing changed.
|
|
7
|
+
--
|
|
8
|
+
-- Why two tables: thread row supports "show me my inbox" queries
|
|
9
|
+
-- without scanning history; observations supports "what changed when"
|
|
10
|
+
-- queries used by future findings/triage. Both are write-light — DMs
|
|
11
|
+
-- are low-volume.
|
|
12
|
+
--
|
|
13
|
+
-- Dedup key on observations is (workspace_id, conversation_pair,
|
|
14
|
+
-- preview_hash). The poller computes preview_hash = sha1 of the
|
|
15
|
+
-- preview text, so identical previews observed across ticks collapse
|
|
16
|
+
-- to one row. New text from the same correspondent inserts a new
|
|
17
|
+
-- observation and bumps the thread's last_seen_at + last_preview.
|
|
18
|
+
--
|
|
19
|
+
-- No FK to agent_workforce_contacts: contact linking layers on later
|
|
20
|
+
-- (the operator must approve the link via the approval-queue path).
|
|
21
|
+
-- Storing handle / pair without a FK keeps the ingest tick cheap and
|
|
22
|
+
-- doesn't gate it on CRM state.
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS x_dm_threads (
|
|
25
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
26
|
+
workspace_id TEXT NOT NULL,
|
|
27
|
+
conversation_pair TEXT NOT NULL,
|
|
28
|
+
primary_name TEXT,
|
|
29
|
+
last_preview TEXT,
|
|
30
|
+
last_preview_hash TEXT,
|
|
31
|
+
has_unread INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
observation_count INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
raw_meta TEXT,
|
|
36
|
+
UNIQUE(workspace_id, conversation_pair)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_x_dm_threads_workspace
|
|
40
|
+
ON x_dm_threads(workspace_id, last_seen_at DESC);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS x_dm_observations (
|
|
43
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
44
|
+
workspace_id TEXT NOT NULL,
|
|
45
|
+
conversation_pair TEXT NOT NULL,
|
|
46
|
+
primary_name TEXT,
|
|
47
|
+
preview_text TEXT NOT NULL,
|
|
48
|
+
preview_hash TEXT NOT NULL,
|
|
49
|
+
has_unread INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
observed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
51
|
+
UNIQUE(workspace_id, conversation_pair, preview_hash)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_x_dm_obs_pair
|
|
55
|
+
ON x_dm_observations(workspace_id, conversation_pair, observed_at DESC);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- 125-x-dm-messages-bodies.sql — add per-message storage to the DM ingest.
|
|
2
|
+
--
|
|
3
|
+
-- Migration 124 introduced thread + observation tables that captured
|
|
4
|
+
-- only inbox-level previews (one row per (pair, preview_hash)). That
|
|
5
|
+
-- left the actual message bodies invisible — we stored the gloss the
|
|
6
|
+
-- inbox shows, not the conversation. Live DOM probe (2026-04-16,
|
|
7
|
+
-- scripts/probe-x-dm-dom.mjs) confirmed each message has a stable
|
|
8
|
+
-- per-conversation UUID exposed via `data-testid="message-<uuid>"`,
|
|
9
|
+
-- which is the right dedup key for body-level ingest.
|
|
10
|
+
--
|
|
11
|
+
-- Direction comes from the bubble's bg-primary (outbound) vs
|
|
12
|
+
-- bg-gray-50 (inbound) class — X never exposes a sender id in the DM
|
|
13
|
+
-- DOM, so this is the most reliable signal short of authenticated API
|
|
14
|
+
-- access.
|
|
15
|
+
--
|
|
16
|
+
-- Note that we still don't store an absolute timestamp: X inlines the
|
|
17
|
+
-- "x minutes ago" / "6:49 AM" tooltip into the message text and never
|
|
18
|
+
-- exposes a machine-readable datetime here. observed_at (when the
|
|
19
|
+
-- poller saw it) is the closest available stamp.
|
|
20
|
+
|
|
21
|
+
CREATE TABLE IF NOT EXISTS x_dm_messages (
|
|
22
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
23
|
+
workspace_id TEXT NOT NULL,
|
|
24
|
+
conversation_pair TEXT NOT NULL,
|
|
25
|
+
message_id TEXT NOT NULL,
|
|
26
|
+
direction TEXT NOT NULL CHECK (direction IN ('outbound', 'inbound', 'unknown')),
|
|
27
|
+
text TEXT,
|
|
28
|
+
is_media INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
observed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
30
|
+
UNIQUE(workspace_id, message_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_x_dm_messages_pair
|
|
34
|
+
ON x_dm_messages(workspace_id, conversation_pair, observed_at DESC);
|
|
35
|
+
|
|
36
|
+
-- Bring the threads table forward with one denormalized field so the
|
|
37
|
+
-- inbox query doesn't need a join to show the latest message body.
|
|
38
|
+
ALTER TABLE x_dm_threads ADD COLUMN last_message_id TEXT;
|
|
39
|
+
ALTER TABLE x_dm_threads ADD COLUMN last_message_text TEXT;
|
|
40
|
+
ALTER TABLE x_dm_threads ADD COLUMN last_message_direction TEXT;
|