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.
Files changed (50) hide show
  1. package/dist/index.js +3112 -1534
  2. package/dist/mcp-server/index.js +12 -12
  3. package/dist/migrations/091-skills-sop-columns.sql +18 -0
  4. package/dist/migrations/092-operational-pillars.sql +55 -0
  5. package/dist/migrations/093-seed-operational-pillars.sql +176 -0
  6. package/dist/migrations/094-person-models.sql +58 -0
  7. package/dist/migrations/095-transition-engine.sql +52 -0
  8. package/dist/migrations/096-work-router.sql +95 -0
  9. package/dist/migrations/097-human-growth.sql +58 -0
  10. package/dist/migrations/098-observation-layer.sql +5 -0
  11. package/dist/migrations/099-collective-intelligence.sql +5 -0
  12. package/dist/migrations/100-agent-model-policy.sql +32 -0
  13. package/dist/migrations/101-llm-calls.sql +36 -0
  14. package/dist/migrations/102-team-member-guide.sql +14 -0
  15. package/dist/migrations/103-fix-person-models-fk.sql +89 -0
  16. package/dist/migrations/104-fix-person-observations-fk.sql +48 -0
  17. package/dist/migrations/105-onboarding-plans.sql +56 -0
  18. package/dist/migrations/106-deliverable-actor-link.sql +29 -0
  19. package/dist/migrations/107-code-skills.sql +20 -0
  20. package/dist/migrations/108-archive-procedure-skills.sql +29 -0
  21. package/dist/migrations/109-workspace-default-fs-paths.sql +19 -0
  22. package/dist/migrations/110-task-state-ttl.sql +28 -0
  23. package/dist/migrations/111-conversation-status.sql +25 -0
  24. package/dist/migrations/112-deliverables-created-at-iso.sql +40 -0
  25. package/dist/migrations/113-permission-requests.sql +36 -0
  26. package/dist/migrations/114-llm-calls-tool-telemetry.sql +32 -0
  27. package/dist/migrations/115-trigger-watchdog.sql +46 -0
  28. package/dist/migrations/116-self-findings.sql +46 -0
  29. package/dist/migrations/117-experiment-validations.sql +64 -0
  30. package/dist/migrations/118-validation-rollback.sql +33 -0
  31. package/dist/migrations/119-runtime-config-overrides.sql +44 -0
  32. package/dist/migrations/120-business-vitals.sql +44 -0
  33. package/dist/migrations/121-x-contact-events.sql +42 -0
  34. package/dist/migrations/122-video-jobs.sql +52 -0
  35. package/dist/migrations/123-insight-distiller.sql +68 -0
  36. package/dist/migrations/124-x-dm-messages.sql +55 -0
  37. package/dist/migrations/125-x-dm-messages-bodies.sql +40 -0
  38. package/dist/migrations/126-x-dm-signals.sql +52 -0
  39. package/dist/migrations/127-x-dm-contact-linking.sql +36 -0
  40. package/dist/migrations/128-attribution-view.sql +59 -0
  41. package/dist/migrations/129-x-posted-log.sql +36 -0
  42. package/dist/migrations/130-patches-attempted-log.sql +44 -0
  43. package/dist/scrapling-server/requirements.txt +3 -0
  44. package/dist/scrapling-server/server.py +224 -0
  45. package/dist/web/assets/index-Bp9CoQ8c.css +1 -0
  46. package/dist/web/assets/index-C5xtuLcg.js +102 -0
  47. package/dist/web/index.html +2 -2
  48. package/package.json +9 -4
  49. package/dist/web/assets/index-B2PzvKIq.js +0 -100
  50. 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;