marketing_metric_daily (19th table, system-written, child of the campaign)
holds per-day raw counts and unique actors per stage and event type,
recomputed after every outcome-event insert, update and delete, plus a
Rebuild Results campaign action. MeasurementReport sums the rollup and
counts campaign-wide unique actors with a backend COUNT_DISTINCT instead
of loading every event. MarketingOutcomeImporter lets hosts plug in
analytics sources, run by the Import Outcomes campaign action through
the normal (source, external id) dedupe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maxConcurrentAgentRuns and maxModelCallsPerMinute were per-JVM, in memory.
Preflight now fails stale RUNNING runs (no heartbeat for maxRunSeconds
plus 60s) and counts RUNNING runs across all products; the per-minute
limit counts model-call steps in the last minute and waits instead of
failing. Runs record heartbeatAt (new column) on each model call. The
policy's maxConcurrentAgentRuns override is now honoured.
The count-then-start window remains racy; the ConditionalUpdateAction
claim from qqq#1178 (QQQ 4.1.0-RC.7) closes it in a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests for the required checks, and the fixes they drove:
- Unauthorized access is denied with core permission enforcement on.
Preparation, approval, execution and the agent need separate permissions.
The optional product security key bounds what users and the agent's tools
see.
- Drafts are made without publication authority. The agent has no approve,
schedule or send tool. A draft that cites a non-released claim gets a
blocker finding, which stops approval.
- Approval is invalidated by any approved-field edit, including a schedule
change. Status and approval rows can't be forged.
- Pause, resume and cancel. Duplicate scheduling and runs are refused.
Ambiguous outcomes are reconciled or decided by a person and never retried
blindly. Repeats are deduplicated by the idempotency key. Retries happen
only when the request was certainly not sent. Expired leases need
reconciliation.
- At execution time: the kill switch, the schedule, consent and suppression,
the bulk limit, channel AI-content rules, and unconfigured transports.
- Outcome events are deduplicated by (source, external id). Raw events and
unique actors are reported separately, and uninstrumented metrics show as
"Not measured". CRM import is idempotent, and leads are qualified only by
an explicit rule.
- Missing provider: the run fails with a plain reason and manual work still
runs. Provider errors are retried or failed by kind, and no secret appears
in errors. Budgets, the concurrency limit, cancellation and the model-call
limit are enforced.
- Persistence across a restart, on the RDBMS module over a file-backed H2
database created from the shipped DDL.
- The OpenAI-compatible provider is tested against a local HTTP fixture.
Fixes:
- Service entry points run as trusted internal work after their own
permission check, the same way QQQ trusts nested calls. Agent tools still
force PermissionEnforcement.ENFORCE.
- Content status and authorType are managed rather than required, and
productId is derived.
- Schedules are kept to the second, so stored values hash the same as
approved ones on every database.
- The Result screen lists skipped records with their reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first version of qbit-marketing. It manages products and their evidence,
campaigns, content with revision history, exact human approval, durable
execution through transports, measurement, and a marketing agent. Everything
lives in 18 standard marketing_* tables inside the host's QQQ app.
- Model: products, audiences, evidence sources, claims (released / planned /
unverified / disputed), campaigns, channels with their rules, content and
immutable revisions, approvals, executions and attempts, test deliveries,
agent runs and steps, findings, metrics, outcome events and the policy.
scripts/generate-model.py generates the entities, enums and DDL from one
spec.
- Approval binds a hash of the text, destination, recipient scope, schedule
and channel. Any edit invalidates it, sends the content back to draft and
blocks pending executions. System-written tables are read-only and are
guarded so that only Marketing's services write them.
- Execution: idempotency keys, leases, retries with backoff only when the
request was certainly not sent, and no blind retry of ambiguous outcomes
(reconcile, or let a person decide). Every send re-checks the kill switch,
approval, schedule, channel, transport, AI-content rules, rate limit,
consent and suppression. A controlled test transport and a QQQ messaging
email transport are provided.
- Agent: qbit-assistant's model-provider SPI (Anthropic, scripted) plus an
OpenAI-compatible provider. Narrow typed tools run as the user with
permission enforcement, inside the untrusted-data envelope, and with
redaction. The tools can draft and record findings but cannot approve or
send. Runs are bounded and recorded with their usage and cost.
- CRM: a narrow gateway SPI with a qbit-crm adapter. Results are imported as
outcome events, and leads are qualified only by a campaign's explicit rule.
- UI: the app home dashboard, record widgets, and streamed-ETL record actions;
the nav holds only the main and settings tables.
- Starter data: QQQ, gclo and MarkdownViewr for Windows, verified 2026-10-12.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>