Package Exports
- @x12i/activix
Readme
@x12i/activix
Track start → complete / fail (and timeouts) for activities stored in MongoDB (via @x12i/xronox-store) or in a local playground folder (no database). Each record has root-level outer (input, output, metadata, optional cost) and optional inner[] sub-activities (each with input, output, metadata, optional cost, plus ISO timing), a runContext object (supply sessionId for correlation; if omitted, Activix warns and stores without it), primary key activityId, and lifecycle fields (status, startTime, endTime, duration) that Activix sets (integrators should not send those or parallel createdAt / updatedAt). See .docs/activity-structure.md. v6 layout: .docs/MIGRATION-v6.md. v5 runContext: .docs/MIGRATION-v5.md. Older: .docs/MIGRATION-v4.md. Maintainers: .docs/COMMUNICATING-RUNCONTEXT-V5.md.
Install
@x12i/xronox-store is a private dependency on npmjs. Make sure your npm auth token has access to the @x12i org (see .npmrc.example), then:
npm install @x12i/activixThe published package ships .docs/ (same as this repo). Dependents can link or copy from node_modules/@x12i/activix/.docs/run-context-object.md when documenting their own Activix integration.
Activation policy (important)
Activix is explicitly enabled by config.
- Enable Activix by constructing
new Activix(...)with either:- a pre-built
store, or collection/collections(and usuallymongoUrior Mongo URI env vars when you expect database storage).
- a pre-built
- Default
storageModeisautomatic(when omitted): the real backend is chosen onawait init()— one MongoDB connectivity check, thendatabaseorlocalplayground. Until then,storageBackendis'pending'. - Set
storageMode: 'database'to skip the probe and always use MongoDB (fail at store init if Mongo is down). SetstorageMode: 'local'for playground-only (see Storage modes). await Activix.create(options)is equivalent tonew Activix(options)thenawait ax.init()(convenience helper).- If your app does not construct Activix, treat it as disabled (do not silently auto-enable from ambient env).
- If Activix is enabled, missing required connection details should fail fast at app wiring/startup.
- Collection name is required application wiring. Every package must pass a package-owned, hard-coded
collectionorcollectionsvalue when constructing Activix. Do not pick runtime package collection names from.env. - Database name is not accepted in constructor options. Set DB once in
.env(ACTIVIX_DB_NAME, fallbackMONGO_AI_LOGS_DB, thenMONGO_LOGS_DB, thenMONGO_DB), otherwise Activix usesactivitix. - Diagnostic logs are error-only by default. This prevents feedback loops where the logging system reports routine Activix logging activity about activity logs. Set
ENABLE_ACTIVIX_LOGXER=trueto enable full Activix logxer/console diagnostics. When enabled, per-package level followsACTIVIX_LOGS_LEVEL(canonical; legacyACTIVIX_LOG_LEVELfallback); if both are unset, the logxer default iswarn. SetACTIVIX_LOGS_LEVEL=errorto return to errors-only while keeping logxer enabled. Injectedloggerinstances are also gated: with the enable flag off, onlylogger.erroris called. - Dependencies below Activix use their own log switches (documented here for convenience only; each package remains authoritative). To trace persistence end-to-end, enable each layer you need (see Diagnostic logging down the stack).
This keeps behavior deterministic across environments and avoids hidden coupling to process env.
Run context at runtime (not Activix constructor config)
runContext is per-run execution context, not something you define once in new Activix({ … }) or in static app configuration.
- Constructor options (
mongoUri,collection/collections,store, index definitions, storage mode, and similar) only configure where and how documents are stored. They do not carrysessionId,jobId,taskId, or other correlation fields for a specific request, message, or job. - On each write (
startRecord, and any follow-up that carries run context), pass therunContextobject that reflects this execution: usually taken from the inbound request or job envelope, optionally extended at each layer, and forwarded to downstream code. That is the object persisted under the configuredrunContextfield (default BSON keyrunContext; override withrunContextField). Full guide:.docs/run-context-object.md.
If you omit both runContext.sessionId and top-level sessionId, Activix does not invent a value—it logs a warning and stores the record without sessionId. For end-to-end correlation across services, supply sessionId (and other work-scope fields) from the true upstream of the run.
v5 naming: the correlation object is runContext only—there is no identity field, identityField config, or findRecordsByIdentity in the default public API (no backward-compatible aliases). Stray identity keys on writes are not read as the correlation envelope. v5 API summary is at the top of .docs/run-context-object.md. To announce this upgrade to teams, use .docs/COMMUNICATING-RUNCONTEXT-V5.md.
Diagnostic logging down the stack
Activix sits on xronox-store, which sits on xronox. Diagnostic output is layered: turning on Activix logs does not automatically enable store or engine logs. The table below is reference for Activix users only; do not treat it as instructions to modify downstream repos — pull specifics from each dependency when you need them.
| Layer | Package | Turn on | Notes |
|---|---|---|---|
| Activix | @x12i/activix |
Default: errors only. Full diagnostics: ENABLE_ACTIVIX_LOGXER=true; optional ACTIVIX_LOGS_LEVEL (error, warn, info, debug, etc.) |
Avoids logging-system noise about logs activity. isActivixDiagnosticLoggingEnabled() is true only when the enable flag is true and the level is not silenced. |
| Store | @x12i/xronox-store |
XRONOX_STORE_LOG=1 (or true / yes / on) |
Console lines prefixed [xronox-store]. Optional logger on XronoxStore; see xronox-store README — Logging. |
| Engine | @x12i/xronox |
XRONOX_VERBOSE=1 or XRONOX_LOGS=1 (1, true, or yes) |
Only XRONOX_* keys; generic DEBUG=1 does not enable xronox diagnostics. |
Example for a noisy trace of one persistence path:
ENABLE_ACTIVIX_LOGXER=true
ACTIVIX_LOGS_LEVEL=debug
XRONOX_STORE_LOG=1
XRONOX_VERBOSE=1Expanded reference (same scope — Activix docs only) and logs-gateway pointers: .docs/logging-stack.md.
Breaking change: no constructor DB override
mongoDb in new Activix(...) is intentionally unsupported.
Why:
- Prevent hidden per-component DB selection in code.
- Avoid accidental split-brain activity history (some services writing to one DB, others to another).
- Keep one operational source of truth in deployment/env config.
Quick start (single collection)
Use this when all activity records for this Activix instance live in one MongoDB collection. Pass the collection name once in the constructor; you do not pass it again on startRecord, completeRecord, and the other methods.
The collection name is part of the package's source-level ownership contract. Choose a stable package-owned name such as my-service-activities and hard-code it in your Activix initialization. Environment variables are for deployment settings like Mongo URI or database name, not for deciding which package-owned activity collection a runtime package writes to.
Default (automatic): try Mongo once on init(), else playground folder.
import {
Activix,
activixActivityIo,
activixOuterTier,
type ActivixRunContext,
} from '@x12i/activix';
const ax = new Activix({
mongoUri: process.env.MONGO_URI!,
collection: 'my-service-activities',
});
await ax.init(); // runs connectivity probe; ax.storageBackend is 'database' or 'local'
const runContext: ActivixRunContext = {
sessionId: '550e8400-e29b-41d4-a716-446655440000', // normally the id your upstream already assigned
jobId: 'job-123',
};
const { activityId } = await ax.startRecord({
runContext,
...activixActivityIo(
activixOuterTier({ kind: 'task', args: { id: 1 } }, null, { type: 'import' })
),
});
await ax.completeRecord(activityId, {
outer: { output: { result: 'ok' } },
});
await ax.close();Always MongoDB (no probe; same behavior as pre–automatic-default releases):
const ax = new Activix({
storageMode: 'database',
mongoUri: process.env.MONGO_URI!,
collection: 'my-service-activities',
});You can pass a full config object instead of a string if you need custom primary keys, indexes, or field names:
const ax = new Activix({
mongoUri: process.env.MONGO_URI!,
collection: {
name: 'my-service-activities',
indexes: [
{ keys: { 'runContext.jobId': 1 } },
{ keys: { 'runContext.sessionId': 1 } }, // helps findRecordsByRunContext({ sessionId })
],
},
});Defaults (v6)
| Item | Default |
|---|---|
| Primary key field | activityId (values like <primaryKeyPrefix><uuid>; default act-<uuid>) |
runContext object |
Field runContext (override with runContextField) with at least sessionId (UUID; you may pass runContext or top-level sessionId) |
outer / inner |
Root fields (no structure wrapper): outer requires input, output (may be null until complete), metadata, optional cost; optional inner is an array of step entries with input, output, metadata, optional cost, startedAt, endedAt, optional durationMs |
| Status / times | status, startTime, endTime, duration — numbers (Unix ms for start/end); set only by Activix |
| Mid-flight status | statusValues.inProgress default 'in_progress' (markInProgress) |
| Abandoned / stale | statusValues.timeout default 'timeout'; TTL staleRecordTTL (ms) vs startTime (reconcileAbandonedActivities / markStaleRecords) |
| Purge old rows | purgeOldRecords() — soft-purges (marks as purgedAt and hides) docs with startTimeField older than purgeRecordMaxAgeMs (default 7 days); optional per-call olderThanMs |
| Optional “last heard” | progressAtField — set on each markInProgress |
startRecord creates the primary key, writes it to both the configured primary-key field and root activityId, persists the row, and returns activityId. Pass that activityId into completeRecord, failRecord, markInProgress, patchRecord, and getRecord. Deprecated recordId on the result object is the same string.
Lifecycle: done, in-progress, failed, abandoned
| Intent | API |
|---|---|
Success (sets completed, endTime, duration) |
completeRecord(activityId, updates?) |
Failure (sets failed, error, endTime, duration) |
failRecord(activityId, error, updates?) |
Still running — “last we heard” (sets in_progress, optional touch field) |
markInProgress(activityId, updates?) |
Arbitrary fields, any status (including after completed / failed) |
patchRecord(activityId, fields) |
Optional per collection: progressAtField (e.g. lastHeardAt) — on each markInProgress, Activix sets it to Date.now(). Override the mid-flight label with statusValues.inProgress.
Persistence warnings
Pass onPersistenceWarning when you want a metrics/logging hook for failed activity writes:
const ax = new Activix({
collection: 'my-service-activities',
mongoUri: process.env.MONGO_URI!,
onPersistenceWarning: (warning) => {
metrics.increment(warning.event, {
collection: warning.collection,
phase: warning.phase,
});
},
});Activix calls this hook before it rethrows or swallows a persistence error. Normal lifecycle writes (startRecord, completeRecord, failRecord, markInProgress) still reject after the warning. patchRecord keeps its historical non-fatal behavior by default, but strictPersistence: true makes it rethrow after warning so CI/audit-sensitive runs can fail fast.
Warning events are activix.record.insert_failed and activix.record.update_failed. The payload includes collection, operation, phase, primary key, and an error summary. phase is resolved from top-level kind, then outer.metadata.kind, then outer.input.kind, else activix:record. When xronox-store reports clone diagnostics, Activix forwards the offending path/type summary under warning.clone.
Abandoned activities (started but never finished)
Call reconcileAbandonedActivities() on a timer (or from your job runner). It finds documents still in started whose startTime is older than a TTL and sets status to statusValues.timeout (default 'timeout'). Default TTL is constructor staleRecordTTL (ms); override per run with { ttlMs }. Same implementation as markStaleRecords() — use whichever name fits your docs.
Purging old records (soft purge)
Call purgeOldRecords(options?) on a schedule to soft-purge documents whose startTime is older than a threshold (any status, including stuck or finished).
Activix marks matching rows with purgedAt = Date.now() (or your purgeAtField). When Activix creates XronoxStore, it registers the same field as xronox-store visibility, so tombstoned rows are hidden from getByKey / readMany / updateMany (i.e. getRecord, findRecords, findRecordsByRunContext, stale reconciliation, and purge matching).
- Default age: constructor
purgeRecordMaxAgeMs, which defaults to 7 days (604800000ms). - Override per run:
purgeOldRecords({ olderThanMs: 24 * 60 * 60 * 1000 })(example: older than 1 day). - You can configure the marker field name per collection with
purgeAtField(default:purgedAt).
const ax = new Activix({
mongoUri: process.env.MONGO_URI!,
purgeRecordMaxAgeMs: 7 * 24 * 60 * 60 * 1000, // default; omit to use 1 week
});
await ax.init();
const purged = await ax.purgeOldRecords(); // or { collection: 'other-stream', olderThanMs: ... }Migrating from v1 (recordId / rec-)
If existing collections and indexes use recordId, set in config:
primaryKey: 'recordId', primaryKeyPrefix: 'rec-'
Cache-first reads (primary key)
When Activix creates the store, each collection’s cache is resolved with xronox-store resolveCacheConfig (defaults maxSize 10000, ttlMs 0) and your optional cache overrides. Writes go to the cache first, then Mongo. getRecord(id), completeRecord, markInProgress, patchRecord, and failRecord use the store’s per-key cache when the key is hot.
findRecords / findRecordsByRunContext use readMany. By default they do not merge the in-memory cache; pass { mergeCache: true } (xronox-store 1.2+) to union matching cached rows with DB results. After a write, getRecord(activityId) is still the simplest way to read the latest row for one id in-process.
Query by sessionId / runContext / status
Use findRecordsByRunContext(criteria, { collection?, limit?, sort? }) to load activities from the database by:
sessionId— matches<runContextField>.sessionId(default field pathrunContext.sessionId)runContext— extra exact matches on nested run context, e.g.{ tenant: 'acme' }→runContext.tenantstatus— exact match on your configured status field (e.g.'started','completed')
Criteria are combined with AND. You must pass at least one of: a non-empty sessionId, at least one runContext property, or a non-empty status. If top-level sessionId and runContext.sessionId both appear, they must be the same string or Activix throws.
// All activities in a session
const rows = await ax.findRecordsByRunContext({ sessionId: '550e8400-e29b-41d4-a716-446655440000' });
// Same, but only still-open ones
const open = await ax.findRecordsByRunContext({
sessionId: '550e8400-e29b-41d4-a716-446655440000',
status: 'started',
});
// By custom run-context fields (uses configured runContextField name, default `runContext`)
const forTenant = await ax.findRecordsByRunContext({
runContext: { tenant: 'acme', plan: 'pro' },
status: 'in_progress',
});For ad hoc Mongo shapes, use findRecords(rawFilter, …) instead. Consider an index on runContext.sessionId (and other queried run-context keys) in indexes on the collection config.
Exported type: FindByRunContextCriteria.
Runtime observability query client
Activix is the official owner of activity queryability for runtime observability. Packages that expose debug-only runtimeObjects should expose their package-owned Activix instance by reference as runtimeObjects.activixClient; they should not build local Mongo adapters, scrape playground files, or query xronox-store internals.
Use getJobActivities(input) when a parent package, playground, or debug UI needs all Activix activity information for a runtime job:
const result = await ax.getJobActivities({
jobId: 'job-123',
graphId: 'graph-main',
nodeId: 'node-a',
limit: 500,
});
console.log(result.jobId, result.graphRun, result.activities);Shape:
type ActivixQueryableClient = {
getJobActivities(input: {
jobId: string;
graphId?: string;
nodeId?: string;
limit?: number;
}): Promise<{
jobId: string;
graphRun?: unknown;
activities: unknown[];
}>;
};jobId is required and is matched inside the configured runContextField (default runContext.jobId). Optional graphId and nodeId filter the same run-context object. Activix queries every collection configured on the instance, returns full activity rows, sorts them into timeline order, and applies limit after merging collections.
The configured store is the source of truth: MongoDB/xronox-store in database mode, or the playground/local store in local mode or automatic fallback. Activix passes mergeCache: true so hot in-process rows can be merged with the store result where supported, but the cache is only a freshness layer, not an in-memory-only backend.
Full guide: .docs/runtime-observability-querying.md.
Run context contract for gateway integrations
This is the same runtime runContext object described in Run context at runtime (not Activix constructor config); gateway docs spell out the envelope for AI-shaped stacks.
For teams integrating through @athenices/ai-gateway, use the shared run-context envelope in:
.docs/run-context-object.md— whatrunContextis, layers,instance, examples, mistakes, v5 API, graphs/hooks appendix.docs/session-id-usage.md— happy-pathsessionIdfor a full run; nested layers should not replace an id they received; entry/direct-call behavior is product-defined.docs/activity-structure.md— rootouter/inner[]I/O (including optionalouter.cost/inner[].cost)
Recommended shape for cross-service correlation (see the doc for hierarchy vs optional executor):
runContext: {
sessionId: string,
// Work scope: add as you go deeper (examples — names are product-defined)
jobId?: string,
taskId?: string,
stepId?: string,
skillId?: string,
// Optional: which executor ran the work (agent / worker / replica)
instance?: {
instanceId: string,
type: string,
},
}Activix itself guarantees a non-empty runContext.sessionId; gateway-facing systems should preserve the fuller run-context object end-to-end.
Multiple collections
Use this when one service writes to several activity collections (for example separate streams per domain). List each collection in collections and set defaultCollection to the primary stream name (explicit in source — there is no implicit “first item” default). For non-default streams, pass collection on the method call.
const ax = new Activix({
mongoUri: process.env.MONGO_URI!,
collections: [
{ name: 'workflow-runs' },
{ name: 'import-jobs' },
],
defaultCollection: 'workflow-runs',
});
await ax.startRecord({ step: 1 }); // uses workflow-runs
await ax.startRecord({ file: 'a.csv' }, { collection: 'import-jobs' });Collection names always come from your application code (collection or collections in the constructor), so each app owns its own table names.
Jobs collection (job metadata)
If you want a dedicated collection for job-level metadata (separate from activity rows), you must wire it in when constructing Activix by adding it to the constructor’s collections array (recommended name: jobs) with primary key jobId, and set jobsCollection to that same name (or pass { collection: 'jobs' } on every job helper call). Activix does not assume a default jobs collection name.
Then use startJob() / endJob():
import { Activix } from '@x12i/activix';
const ax = new Activix({
mongoUri: process.env.MONGO_URI!,
collections: [
{ name: 'my-service-activities' }, // your activity stream(s)
{ name: 'jobs', primaryKey: 'jobId' }, // job metadata
],
defaultCollection: 'my-service-activities',
jobsCollection: 'jobs',
});
await ax.init();
await ax.startJob({
jobId: 'job-123',
description: 'Import customers from CRM',
startedAt: Date.now(),
identityObject: {
source: {
sourceType: 'mongo',
identifier: { property: '_id', value: '66f0...' },
objectType: 'customer',
},
},
// optional correlation envelope (stored under the collection’s configured `runContextField`)
runContext: { sessionId: '...', jobId: 'job-123' },
});
await ax.endJob({ jobId: 'job-123', endedAt: Date.now() });Fetch job metadata or list/search recent jobs:
const job = await ax.getJob('job-123'); // ActivixJobRecord | null
const jobs = await ax.listJobs({ limit: 50, searchText: 'customers' });Fetch job metadata + all activity rows for that job (across every configured activity collection):
const bundle = await ax.getJobBundle({ jobId: 'job-123' });
// bundle.job uses constructor jobsCollection (or pass { jobsCollection: 'jobs' } here)
// bundle.activities -> activity rows matched by runContext.jobId across all collectionsTypes exported by this package:
ActivixJobRecord—{ jobId, description, startedAt, endedAt?, status: 'in-progress' | 'ended', identityObject, runContext? }ActivixJobStatus
Collection legend / registry (activix-collections)
When Activix creates the store (you do not pass a custom store), it registers activix-collections automatically if it is not already in your collections list (primary key collectionName). After await init(), it inserts one legend document per configured collection (including the registry collection itself) when a row is missing. Rows use diagnostics.owner (recommended: npm package id) or collectionRegistry.owner as the owner stamp and a short auto-generated about line (override the template via collectionRegistry.aboutTemplate; use {name} as the collection-name placeholder).
Set collectionRegistry: false to disable both behaviors (used by the activix CLI). Custom store integrators must define the registry collection on the store and on Activix themselves if they want the same behavior.
If you need rich legend metadata (friendlyName, tags, custom about) for specific collections, list those names in collectionRegistry.skipAutoInsertForCollections and call initializeCollection(...) after init() for each — initializeCollection is insert-only and does nothing when a row already exists.
const ax = new Activix({
mongoUri: process.env.MONGO_URI!,
diagnostics: { owner: '@my-scope/my-service', component: 'worker' },
collections: [
{ name: 'my-service-activities' },
{ name: 'jobs', primaryKey: 'jobId' },
],
defaultCollection: 'my-service-activities',
jobsCollection: 'jobs',
collectionRegistry: {
skipAutoInsertForCollections: ['jobs'],
},
});
await ax.init();
await ax.initializeCollection({
collectionName: 'jobs',
friendlyName: 'Jobs',
tags: ['jobs'],
about: 'Job metadata records (one doc per jobId)',
owner: { package: '@my-scope/my-service', component: 'worker' },
});Use tags to control whether callers query ALL activity collections or just a subset:
- ALL (activities): tag every activity stream with
activities, then uselistActivityCollections()(this returns only legends taggedactivities). - Tag subsets: add tags like
['activities','ai','gateway']and filter by tags (still always constrained toactivities).
Example (Activities UI selects only activity streams, optionally narrowed):
const activityCollections = await ax.listActivityCollections();
// Narrow to AI gateway activity streams only:
const gatewayCollections = await ax.listActivityCollections({ tags: ['gateway'] });
// Or search by legend text:
const searched = await ax.listActivityCollections({ searchText: 'gateway' });Fetch legend data and configured collections:
const configured = ax.listConfiguredCollections(); // [{ name, primaryKey }, ...]
const legend = await ax.getCollectionLegend('jobs');
const legends = await ax.listCollectionLegends({ limit: 100, searchText: 'job', tags: ['jobs'] });One-time backfill for an existing database (creates missing legend rows only):
npm run legend:backfillTypes exported by this package:
ActivixCollectionLegendRecordActivixCollectionLegendOwner
Unique indexes and nulls
MongoDB unique indexes treat null like any other value. If a field (e.g. activityId) is indexed unique, do not persist null for it. Activix now generates the key before insert and writes it to both the configured primary-key field and root activityId so writes do not reach Mongo with activityId: null. runContext.sessionId is set only when you pass it (in runContext or as top-level sessionId); otherwise it is omitted. Prefer sparse unique indexes for optional fields, or omit the field instead of setting null.
Database name
When Activix creates the store for you, the MongoDB database name is resolved from environment only:
process.env.ACTIVIX_DB_NAMEprocess.env.MONGO_AI_LOGS_DBprocess.env.MONGO_LOGS_DBprocess.env.MONGO_DB- the string
activitix
You can use the same resolution in your own code with resolveActivixLogsDatabaseName() (exported from this package).
MongoDB URI for connection tests and default (automatic) init()
For testActivixMongoConnection() and for the automatic path inside await init() when you omit mongoUri, the URI is resolved by resolveActivixMongoUriFromEnv():
process.env.MONGO_LOGS_URI- then
process.env.MONGO_URI
There is no silent fallback to a default URI: if both are unset and you do not pass mongoUri, the health check returns { ok: false, reason: '…' } and automatic / default init() falls back to local playground storage.
MongoDB connection check (single attempt)
Use testActivixMongoConnection(options?) to verify connectivity once (no retry loop): open a client, ping the target database, close. Returns:
{ ok: true }if the ping succeeds{ ok: false, reason: string }if the URI is missing, the driver fails to load, or connect/ping throws
Options (all optional):
| Option | Role |
|---|---|
mongoUri |
Connection string; else env via resolveActivixMongoUriFromEnv() |
mongoDb |
Database name for ping; else resolveActivixLogsDatabaseName() |
serverSelectionTimeoutMS |
Server selection / connect timeout for that single attempt (default 5000) |
This package depends on the mongodb driver for this probe (also used indirectly by the xronox stack).
import {
testActivixMongoConnection,
resolveActivixMongoUriFromEnv,
resolveActivixLogsDatabaseName,
} from '@x12i/activix';
const check = await testActivixMongoConnection({
mongoUri: process.env.MONGO_URI,
mongoDb: resolveActivixLogsDatabaseName(),
});
if (!check.ok) {
console.error('MongoDB not usable:', check.reason);
}Activity persistence verification (no mongodb import in your app)
For smoke tests, CI, and operators, use Activix-owned helpers so gateway and application repos do not depend on the mongodb driver directly. Resolution matches Activix for URI via resolveActivixMongoUriFromEnv() and database via resolveActivixLogsDatabaseName(). resolveActivixPersistenceTarget and the snapshot helpers require a collection string in options (package-owned name in code). resolveActivixActivitiesCollectionName() is for CLI/smoke scripts only: it reads ACTIVIX_COLLECTION or MONGO_LOGS_COLLECTION and throws if neither is set (no default name).
To trace live writes through Activix → xronox-store → xronox, enable diagnostics per layer (separate env vars); see Diagnostic logging down the stack and .docs/logging-stack.md.
API (read-only; opens a short-lived client per call):
| Export | Role |
|---|---|
countActivixActivitiesInMongo(options) |
Document count ( options.collection required ) |
getActivixLatestActivitySummariesInMongo(limit, options) |
Latest limit rows by startTime descending |
getActivixActivityPersistenceSnapshotInMongo(limit, options) |
{ count, latest } in one connection |
summarizeActivixActivityForDiagnostics(doc, options?) |
Pure helper: same summary shape from any in-memory / exported document |
resolveActivixPersistenceTarget(options) |
Resolved URI, DB name, collection, and field names (collection required) |
By default, counts and queries exclude tombstoned rows (purgedAt non-null), consistent with typical Activix visibility. Pass { purgeAtField: false } to include them.
Each summary includes activityId, runContext.jobId when present, status, provider / model from top-level config or outer.metadata, hasResponse (whether outer.output is set), startTime, endTime. Override field names with the same options Activix uses (runContextField, primaryKeyField) if your collection config differs from defaults. Legacy rows with nested structure.outer are still summarized when outer is absent at the root.
import { getActivixActivityPersistenceSnapshotInMongo } from '@x12i/activix';
const { count, latest } = await getActivixActivityPersistenceSnapshotInMongo(5, {
collection: 'my-package-activities',
});
console.log({ count, latest });CLI (after npm install; binary activix):
npx activix verify --collection my-activities
npx activix verify --limit 10 --json --collection my-activities
# Or set ACTIVIX_COLLECTION or MONGO_LOGS_COLLECTION instead of --collection
npx activix verify --mongo-uri "$MONGO_URI" --include-tombstoned --collection my-activitiesStorage modes
How to set storageMode (all optional except you must pick a valid combination with store):
| Value | When to use |
|---|---|
| (omit) | Default = automatic. Same as storageMode: 'automatic'. |
'automatic' |
On await init(), run testActivixMongoConnection() once; Mongo reachable → XronoxStore; else → playground ('local'). storageBackend is 'pending' until init() finishes. |
'database' |
Always MongoDB — no probe; construct XronoxStore immediately. Use when you require Mongo or want failures only from store.init(), not from a separate ping. |
'local' |
Always playground folder — no MongoDB (see playground options). |
Custom store: storageMode is ignored for picking the implementation; Activix uses your store. storageBackend is 'local' for ActivixPlaygroundStore / ActivixMemoryStore, otherwise 'database'. Do not pass storageMode: 'local' together with a custom store.
storageBackend |
Meaning |
|---|---|
'pending' |
Automatic mode, init() not completed yet. |
'database' |
MongoDB (XronoxStore or compatible custom store). |
'local' |
Playground folder or in-memory custom store. |
Activix.create() (construct + init)
import { Activix } from '@x12i/activix';
// Same as: const ax = new Activix({ ... }); await ax.init(); return ax;
const ax = await Activix.create({
mongoUri: process.env.MONGO_URI,
collection: 'my-service-activities',
logger: console,
playground: { outputDir: 'playground', runId: 'my-run-id' },
});
// ax.storageBackend === 'database' or 'local' (never 'pending' here)Explicit storageMode examples
// Automatic (explicit; equivalent to omitting storageMode)
new Activix({ storageMode: 'automatic', collection: 'activities', mongoUri: process.env.MONGO_URI });
// Mongo only
new Activix({ storageMode: 'database', collection: 'activities', mongoUri: process.env.MONGO_URI! });
// Playground only
new Activix({ storageMode: 'local', collection: 'activities', playground: { outputDir: 'playground' } });Repeated Mongo init logs across packages
Initialization is per Activix instance. In storageMode: 'automatic', each instance performs:
- one Mongo connectivity probe (
testActivixMongoConnection) - one real store initialization (
store.init)
If your architecture creates multiple Activix instances (for example different packages or workers), repeated probe/init logs are expected and do not mean one instance is double-initializing. Treat this as an optimization concern only when instance creation frequency becomes expensive or side effects appear.
You can pass optional diagnostics metadata to make logs self-identifying:
new Activix({
collection: 'activities',
mongoUri: process.env.MONGO_URI,
diagnostics: {
owner: '@woroces/worox-graph',
component: 'ai-tasks-client',
instanceLabel: 'graph-executor',
workerId: process.pid.toString(),
},
});Troubleshooting repeated init logs
- Enable Activix and store diagnostics (
ENABLE_ACTIVIX_LOGXER=true,ACTIVIX_LOGS_LEVEL=debug,XRONOX_STORE_LOG=1). - Group logs by
activixInstanceId. - Verify each instance shows one
activix.init.mongo_probe.start+ oneactivix.init.store_init.start. - If repeated logs share the same
activixInstanceIdwith risinginitCallCount, inspect caller lifecycle. - If repeated logs have different
activixInstanceIdvalues, behavior is expected for multi-instance architecture.
Local playground layout
With storageMode: 'local' (or after automatic fallback), Activix uses ActivixPlaygroundStore under playground.outputDir (default "playground"). Optional playground.runId appears in the Markdown header.
Typical artifacts:
report.md— human-readable activity timeline (also written onclose())activities.jsonl— one JSON line per persisted mutationcollections/<name>/records/<pk>.json— snapshot per primary keymeta.json— metadata including dispatch sequence for numbered sidecarsNN-<activityKey>-request.json/-response.json— when rows carry playground work shapes (work:request/fullRequest,work:response/fullOutput, etc.)
Helpers on Activix when using built-in playground storage:
getPlaygroundStore()—ActivixPlaygroundStore | nullgetPlaygroundMarkdown()— current timeline as Markdown, ornullwritePlaygroundReport(targetPath?)— writereport.md(default: under the playground root); returns absolute path ornull
For in-memory-only testing without the filesystem, you can still construct a custom ActivixMemoryStore and pass store (see Pre-built store).
Pre-built store
If you already have a XronoxStore, pass store (and usually skipStoreInit: true if it is already initialized). Use the same primaryKey / primaryKeyPrefix in both the store’s collections and Activix’s collection / collections config so inserts and reads agree.
For soft-purge behavior to match Activix, each store collection must set visibility with field equal to Activix’s purgeAtField (default purgedAt) and the same mode (default hiddenIfNonNull). Otherwise Activix will still write the purge timestamp, but getRecord / findRecords will not hide those rows unless the store applies visibility.
Documentation
- Contributor / extract / CI / troubleshooting:
.docs/README.md - Full specification:
.docs/activix.spec.md
Scripts
npm run build— ESM + CJS outputsnpm test— main lifecycle integration tests (MongoDB +.env)npm run test:single— single-collectionconstructor modenpm run test:options— database name resolution and constructor validation (no MongoDB)npm run test:local— playgroundstorageMode: 'local'(temp dir; no MongoDB)npm run test:patch/test:stale/test:gaps/test:spy/test:purge— focused integration suitesnpm run test:all— everything above
Tests compile to .tests-out/ as ESM and import dist/index.js. See .docs/build-and-test.md.
CI
After this tree is the Git repo root, GitHub Actions runs .github/workflows/ci.yml (build; enable integration tests when NODE_AUTH_TOKEN, Mongo secrets, and a DB are configured).
License
Athenix License