@mastra/libsql 1.23.1-alpha.0 → 1.23.1-alpha.2

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.
@@ -3,7 +3,7 @@ name: mastra-libsql
3
3
  description: Documentation for @mastra/libsql. Use when working with @mastra/libsql APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/libsql"
6
- version: "1.23.1-alpha.0"
6
+ version: "1.23.1-alpha.2"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.23.1-alpha.0",
2
+ "version": "1.23.1-alpha.2",
3
3
  "package": "@mastra/libsql",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -170,6 +170,43 @@ export const supportAgent = new Agent({
170
170
 
171
171
  Title generation runs asynchronously after the agent responds and doesn't affect response time.
172
172
 
173
+ ### Streaming the generated title
174
+
175
+ By default title generation runs in the background and the run's stream doesn't wait for it, so HTTP clients only see the title on their next thread fetch. Set `emitEvent: true` to deliver the title on the run's stream instead: the stream waits for the title and emits a transient `data-thread-title` chunk before `finish`.
176
+
177
+ ```typescript
178
+ import { Agent } from '@mastra/core/agent'
179
+ import { Memory } from '@mastra/memory'
180
+
181
+ export const supportAgent = new Agent({
182
+ id: 'support-agent',
183
+ name: 'Support agent',
184
+ instructions: 'Answer customer support questions.',
185
+ model: 'openai/gpt-5.6-sol',
186
+ memory: new Memory({
187
+ options: {
188
+ generateTitle: {
189
+ emitEvent: true,
190
+ },
191
+ },
192
+ }),
193
+ })
194
+ ```
195
+
196
+ Stream consumers receive the chunk with the persisted title, so a chat UI can rename its thread list entry without polling:
197
+
198
+ ```typescript
199
+ for await (const chunk of stream.fullStream) {
200
+ if (chunk.type === 'data-thread-title') {
201
+ renameThreadInSidebar(chunk.data.threadId, chunk.data.title)
202
+ }
203
+ }
204
+ ```
205
+
206
+ The chunk is transient: it's delivered to stream consumers but never persisted as part of the conversation's messages. Because the stream waits for the title, `emitEvent` delays the stream's `finish` on the turn that generates the title: the first turn of a thread, or a later turn when `minMessages` sets a higher threshold. Leave it off to keep title generation fully non-blocking.
207
+
208
+ > **Note:** `emitEvent` applies to `stream()` runs. `generate()` returns JSON and can't carry the chunk, so it keeps non-blocking title generation. [Durable and evented agents](https://mastra.ai/docs/harness/durable-agents) don't emit this chunk yet. In all of these cases the title is still generated and persisted.
209
+
173
210
  To optimize cost or behavior, provide a smaller [`model`](https://mastra.ai/models) and custom `instructions`:
174
211
 
175
212
  ```typescript
@@ -41,9 +41,13 @@ GitHub Signals requires:
41
41
  - A Mastra storage adapter with memory and notification support. The provider stores each subscription in the thread's metadata, so the thread must already exist.
42
42
  - The `gitcrawl` command on `PATH`, configured to access the repositories you want to monitor. The provider runs `gitcrawl sync` and reads its SQLite database.
43
43
  - The `sqlite3` command on `PATH`.
44
- - The [GitHub CLI](https://cli.github.com/) installed and authenticated. The provider uses `gh api` to check whether comment authors have access to the repository before notifying the agent.
44
+ - The [GitHub CLI](https://cli.github.com/) installed and authenticated. The provider uses real `gh api` responses to check repository permissions and GitHub App ownership before notifying the agent.
45
45
 
46
- By default, comments from users with `admin`, `maintain`, or `write` access can trigger notifications. CodeRabbit and Devin bot comments are also allowed. Configure `authorizedPermissions`, `authorizedBots`, or `ignoredBots` when you need different rules.
46
+ By default, comments from users with `admin`, `maintain`, or `write` access can trigger notifications. CodeRabbit and Devin bot comments are explicitly allowed.
47
+
48
+ Bot authorization checks `ignoredBots` first, using an exact case-insensitive login match. Other bots can trigger notifications when they appear in `authorizedBots`, when the GitHub App is owned by a user whose repository permission appears in `authorizedPermissions`, or when the app is owned by the organization that owns the repository. The default authorized permissions are `admin`, `maintain`, and `write`.
49
+
50
+ Failed, inaccessible, malformed, and unsupported app-owner lookups deny the bot comment. Successful app-owner lookups are cached for 24 hours; failed lookups aren't cached.
47
51
 
48
52
  ## Agent and subscription
49
53
 
@@ -51,7 +51,7 @@ export const agent = new Agent({
51
51
 
52
52
  **options.observationalMemory** (`boolean | ObservationalMemoryOptions`): Enable Observational Memory for long-context agentic memory. Set to true for defaults, or pass a config object to customize token budgets, models, and scope. See Observational Memory reference for configuration details.
53
53
 
54
- **options.generateTitle** (`boolean | { model: DynamicArgument<MastraLanguageModel>; instructions?: DynamicArgument<string> }`): Controls automatic thread title generation from the conversation transcript. Can be a boolean or an object with custom model and instructions.
54
+ **options.generateTitle** (`boolean | { model?: DynamicArgument<MastraModelConfig>; instructions?: DynamicArgument<string>; minMessages?: number; emitEvent?: boolean }`): Controls automatic thread title generation from the conversation transcript. Accepts a boolean or an object with a custom model (any MastraModelConfig: a model instance, a "provider/model" ID, or an OpenAI-compatible config; defaults to the agent's own model), custom instructions, a minimum message count, and emitEvent. With emitEvent: true the run's stream waits for the title and emits it as a transient data-thread-title chunk before finish (durable and evented agents persist the title but don't emit the chunk yet).
55
55
 
56
56
  ## Returns
57
57
 
@@ -6,11 +6,37 @@
6
6
 
7
7
  Because storage grows without bound by default, Mastra provides an opt-in, age-based retention system. Declare per-table `maxAge` policies in the `retention` config, then call `storage.prune()` to delete rows older than their configured age. Unconfigured data is kept forever, so behavior doesn't change until you opt in.
8
8
 
9
- `prune()` deletes rows. It caps growth and is safe to run against large tables (batched, bounded, resumable, cancellable). It never reclaims disk: on SQLite/libSQL the freed pages are reused by future writes so the file stops growing, but handing disk back to the OS (for example a `VACUUM`) is left to the underlying database and the operator to manage.
9
+ `prune()` deletes rows in bounded batches. Runs are resumable and cancellable, so you can limit how much work each maintenance window performs. Pruning doesn't reclaim disk space by itself. Use the database-specific maintenance guidance below when you need to return freed space to the operating system.
10
10
 
11
11
  Retention covers **growth tables** only: tables that accumulate rows unbounded as a side effect of normal operation (conversation history, telemetry, job and run records, schedule fire history, event feeds). User-authored artifacts and config (agents, skills, workspaces, prompt blocks, datasets, schedule definitions, channel installations, and so on) grow with user intent and are edited or deleted explicitly, so they're not valid retention keys.
12
12
 
13
- The reference implementations are [libSQL](https://mastra.ai/integrations/databases/libsql), [PostgreSQL](https://mastra.ai/integrations/databases/postgresql), and [MongoDB](https://mastra.ai/integrations/databases/mongodb). Other adapters keep rows forever until they implement retention.
13
+ Storage adapters use the shared core retention contract for `prune()`, or a database-native mechanism when that better matches the backend.
14
+
15
+ | Adapter | Mechanism | Retention support |
16
+ | -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | libSQL | `prune()` | All supported growth domains |
18
+ | PostgreSQL | `prune()` | All supported growth domains. V-next observability drops expired partitions or chunks |
19
+ | MongoDB | `prune()` or native TTL | All supported growth domains. Native TTL indexes are also available |
20
+ | DuckDB | `prune()` | Observability spans, metrics, logs, scores, and feedback |
21
+ | MySQL | `prune()` | Observability spans |
22
+ | Microsoft SQL Server | `prune()` | Observability spans |
23
+ | Oracle Database | `prune()` | Observability spans and logs |
24
+ | Amazon Aurora DSQL | `prune()` | Observability spans |
25
+ | Google Cloud Spanner | `prune()` | Observability spans, plus metrics when metrics storage is enabled |
26
+ | ClickHouse | Native TTL | Observability spans, metrics, logs, scores, and feedback. When all five signals have finite retention, deletion-request records expire after the longest signal retention plus 30 days |
27
+
28
+ ## Storage-specific maintenance
29
+
30
+ | Adapter | Maintenance guidance |
31
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | SQLite and libSQL | Freed pages are reused by future writes, which stops the database file from growing. Reclaiming disk space requires database-level maintenance. |
33
+ | DuckDB | For file-backed stores, run `CHECKPOINT` after pruning to reclaim deleted rows in storage. DuckDB's `VACUUM` doesn't reclaim deleted rows. |
34
+
35
+ ## Schedule pruning
36
+
37
+ Run `prune()` from a scheduler or maintenance worker, not from application startup or shutdown hooks. For deployments that share a database, prefer a single active scheduler or worker for pruning.
38
+
39
+ Prefer lower-traffic periods when pruning large tables. Use `maxBatches`, `maxRows`, and `pauseMs` to bound each run, and pass an `AbortSignal` when the maintenance process needs to stop promptly. These recommendations apply to adapters that expose `prune()`. ClickHouse applies its native time to live (TTL) policy within the database.
14
40
 
15
41
  ## Usage example
16
42
 
@@ -94,7 +120,8 @@ Each domain specifies its age-prunable tables and the timestamp column that anch
94
120
  > - Experiments prune as whole units: an aged experiment's result rows are deleted together with it (results cascade with their parent), so a run is never left partially deleted. Retention doesn't have a separate `results` key.
95
121
  > - For `schedules`, the growth table is the fire history (`schedule_triggers`, one row per fire): schedule definitions are config and aren't pruned.
96
122
  > - On PostgreSQL, timestamp anchors use the timezone-aware mirror columns (for example `createdAtZ`, `completedAtZ`).
97
- > - LibSQL and PostgreSQL support all domains above except `harness`, which PostgreSQL doesn't implement. MongoDB supports all except `threadState` and `harness`.
123
+ > - DuckDB observability stores append-only events for all five signals. Its `spans` policy uses the event `timestamp` column rather than `startedAt`.
124
+ > - LibSQL and PostgreSQL support all domains above except `harness`, which PostgreSQL doesn't implement. MongoDB supports all except `threadState` and `harness`. DuckDB, MySQL, Microsoft SQL Server, Oracle Database, Amazon Aurora DSQL, and Google Cloud Spanner currently support retention only in their `observability` domains, with the signal coverage shown in the support matrix.
98
125
  > - The v-next PostgreSQL observability domain stores signal events in day-partitioned tables (`spans`, `metrics`, `logs`, `scores`, `feedback`). For it, `prune()` drops whole day partitions (or TimescaleDB chunks) that are entirely older than the cutoff instead of deleting rows: effective level of detail is one day, and a partition is only dropped once its entire day is past `maxAge`. `PruneResult.deleted` reports the number of rows in the dropped partitions.
99
126
 
100
127
  ## Methods
@@ -109,7 +136,7 @@ Deletes rows older than their configured `maxAge` across every domain that has a
109
136
 
110
137
  Pass `options.retention` to replace the configured policies for that call only: for example to skip a domain (keep chat history) or prune more aggressively than the standing config. The store's configured `retention` is unchanged.
111
138
 
112
- Anchor-column indexes are created lazily on the first `prune()` call for each table with a policy (never at `init()`) so deployments that don't configure retention pay no extra index write or disk overhead. The first prune of an existing large table pays a one-time index build. Subsequent prunes reuse the index.
139
+ Adapters that use anchor-column indexes create them lazily on the first `prune()` call for each table with a policy (never at `init()`) so deployments that don't configure retention pay no extra index write or disk overhead. The first prune of an existing large table pays a one-time index build. Subsequent prunes reuse the index. DuckDB uses its built-in zone maps instead of creating retention indexes.
113
140
 
114
141
  ```typescript
115
142
  const results = await storage.prune({
@@ -177,6 +204,31 @@ async function retentionTick() {
177
204
 
178
205
  You can also cancel a long-running prune with an `AbortSignal`: the loop stops between batches and returns partial results with `done: false`, so the next run resumes cleanly.
179
206
 
207
+ ## ClickHouse native TTL
208
+
209
+ ClickHouse observability storage uses native table TTLs instead of `prune()`. Configure retention as days per signal. `init()` applies the TTLs to new and existing tables and skips `ALTER TABLE` statements when the configured TTL is already present.
210
+
211
+ For deployments that need to update TTL configuration without running the full initialization path, call `applyRetention()` on the v-next observability store:
212
+
213
+ ```typescript
214
+ import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
215
+
216
+ const observability = new ObservabilityStorageClickhouseVNext({
217
+ client,
218
+ retention: {
219
+ tracing: 30,
220
+ logs: 7,
221
+ metrics: 14,
222
+ scores: 90,
223
+ feedback: 60,
224
+ },
225
+ })
226
+
227
+ await observability.applyRetention()
228
+ ```
229
+
230
+ Deletion requests are retained long enough to keep enforcing erasure after signal rows expire. Mastra applies a TTL to `mastra_deletion_requests` only when tracing, logs, metrics, scores, and feedback all have finite retention. The deletion-request TTL is the longest of those periods plus 30 days. For example, if score retention is the longest period at 90 days, deletion requests expire after 120 days. When any signal is unbounded, deletion requests remain unbounded because trace deletion requests cover rows across all five signals.
231
+
180
232
  ## MongoDB TTL indexes (alternative to prune)
181
233
 
182
234
  MongoDB offers native [TTL (Time-To-Live) indexes](https://www.mongodb.com/docs/manual/core/index-ttl/) that automatically delete expired documents without requiring manual `prune()` calls. This is a database-level feature that runs as a background thread.
package/dist/index.cjs CHANGED
@@ -2873,13 +2873,6 @@ var AgentsLibSQL = class extends _mastra_core_storage.AgentsStorage {
2873
2873
  };
2874
2874
  //#endregion
2875
2875
  //#region src/storage/retention.ts
2876
- const DEFAULT_BATCH_SIZE = 1e3;
2877
- /**
2878
- * Lazily create the single-column anchor index a prune target relies on.
2879
- * Called from the prune path (not init) so only deployments that actually
2880
- * configure retention pay the index's write/disk overhead. Best-effort: a
2881
- * failure is logged and pruning proceeds (correct, just slower).
2882
- */
2883
2876
  async function ensureAnchorIndex(db, target, logger) {
2884
2877
  if (!target.indexed) return;
2885
2878
  try {
@@ -2892,134 +2885,32 @@ async function ensureAnchorIndex(db, target, logger) {
2892
2885
  logger?.warn?.(`Failed to ensure retention index on ${target.table}(${target.column}):`, error);
2893
2886
  }
2894
2887
  }
2895
- async function sleep(ms, signal) {
2896
- if (ms <= 0 || signal?.aborted) return;
2897
- await new Promise((resolve) => {
2898
- const timer = setTimeout(() => {
2899
- signal?.removeEventListener("abort", onAbort);
2900
- resolve();
2901
- }, ms);
2902
- const onAbort = () => {
2903
- clearTimeout(timer);
2904
- resolve();
2905
- };
2906
- signal?.addEventListener("abort", onAbort, { once: true });
2907
- });
2908
- }
2909
- /**
2910
- * Runs the bounded, batched, cancellable delete loop for a set of tables in the
2911
- * given order (callers pass children before parents for cascade-safe pruning),
2912
- * and returns one {@link PruneResult} per table.
2913
- *
2914
- * The loop:
2915
- * - deletes in chunks of `batchSize` (default 1000), each its own statement;
2916
- * - stops a table's loop when a batch deletes fewer rows than requested (drained),
2917
- * or when `maxBatches`/`maxRows` is hit, or the `signal` aborts — the latter
2918
- * three leave `done: false` so the caller can resume;
2919
- * - pauses `pauseMs` between batches when set, to avoid starving live traffic.
2920
- *
2921
- * `prune()` only deletes rows; it never reclaims disk. Freed pages are reused
2922
- * by future writes so the file stops growing. Handing disk back to the OS is
2923
- * left to the underlying database and the operator to manage.
2924
- */
2925
- /** Convert a policy's `maxAge` into a cutoff bound matching the anchor's storage type. */
2926
2888
  function cutoffFor(policy, anchorType, now = Date.now()) {
2927
- const cutoffMs = now - (0, _mastra_core_storage.parseDuration)(policy.maxAge);
2889
+ const cutoffMs = (0, _mastra_core_storage.retentionCutoffMs)(policy, now);
2928
2890
  return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs).toISOString();
2929
2891
  }
2930
- /**
2931
- * Run the bounded/cancellable batched-delete loop for a single logical target,
2932
- * delegating the actual delete of up to `limit` rows to `deleteBatch`. Returns
2933
- * `{ deleted, done }`; `done: false` means the loop stopped on a bound or the
2934
- * abort signal and eligible rows may remain.
2935
- */
2936
- async function runBatchedDelete({ deleteBatch, batchSize, options }) {
2937
- if (!Number.isSafeInteger(batchSize) || batchSize <= 0) throw new Error(`retention batchSize must be a positive integer; received ${batchSize}`);
2938
- let deleted = 0;
2939
- let batches = 0;
2940
- while (true) {
2941
- if (options?.signal?.aborted) return {
2942
- deleted,
2943
- done: false
2944
- };
2945
- if (options?.maxBatches !== void 0 && batches >= options.maxBatches) return {
2946
- deleted,
2947
- done: false
2948
- };
2949
- let limit = batchSize;
2950
- if (options?.maxRows !== void 0) {
2951
- const remaining = options.maxRows - deleted;
2952
- if (remaining <= 0) return {
2953
- deleted,
2954
- done: false
2955
- };
2956
- limit = Math.min(limit, remaining);
2957
- }
2958
- const affected = await deleteBatch(limit);
2959
- deleted += affected;
2960
- batches += 1;
2961
- if (affected < limit) return {
2962
- deleted,
2963
- done: true
2964
- };
2965
- if (options?.pauseMs) await sleep(options.pauseMs, options.signal);
2966
- }
2967
- }
2968
- async function runPrune({ db, domain, targets, options, logger }) {
2969
- const results = [];
2970
- const now = Date.now();
2971
- for (const target of targets) {
2972
- if (options?.signal?.aborted) {
2973
- results.push({
2974
- domain,
2975
- table: target.table,
2976
- deleted: 0,
2977
- done: false
2978
- });
2979
- continue;
2980
- }
2981
- await ensureAnchorIndex(db, target, logger);
2982
- const cutoff = cutoffFor(target.policy, target.anchorType, now);
2983
- const { deleted, done } = await runBatchedDelete({
2984
- deleteBatch: (limit) => db.pruneBatch({
2985
- tableName: target.table,
2986
- column: target.column,
2987
- cutoff,
2988
- limit
2989
- }),
2990
- batchSize: target.policy.batchSize ?? DEFAULT_BATCH_SIZE,
2991
- options
2992
- });
2993
- results.push({
2994
- domain,
2995
- table: target.table,
2996
- deleted,
2997
- done
2998
- });
2999
- }
3000
- return results;
2892
+ const runBatchedDelete = _mastra_core_storage.runRetentionBatches;
2893
+ function runPrune({ db, domain, targets, options, logger }) {
2894
+ return (0, _mastra_core_storage.executeRetentionPrune)({
2895
+ domain,
2896
+ targets,
2897
+ options,
2898
+ beforeTarget: (target) => ensureAnchorIndex(db, target, logger),
2899
+ cutoffFor: (target, now) => cutoffFor(target.policy, target.anchorType ?? "timestamp", now),
2900
+ deleteBatch: (target, cutoff, limit) => db.pruneBatch({
2901
+ tableName: target.table,
2902
+ column: target.column,
2903
+ cutoff,
2904
+ limit
2905
+ })
2906
+ });
3001
2907
  }
3002
- /**
3003
- * Resolve a domain's `{ tableKey: policy }` map plus its descriptor into an
3004
- * ordered list of {@link PruneTarget}s. `order` lists table keys children-first
3005
- * so cascade-dependent rows are removed before their parents. Table keys not in
3006
- * `policies` are skipped (unset = keep forever).
3007
- */
3008
2908
  function resolveTargets({ policies, descriptor, order }) {
3009
- const targets = [];
3010
- for (const key of order) {
3011
- const policy = policies[key];
3012
- const entry = descriptor[key];
3013
- if (!policy || !entry) continue;
3014
- targets.push({
3015
- table: entry.table,
3016
- column: entry.column,
3017
- anchorType: entry.anchorType ?? "timestamp",
3018
- indexed: entry.indexed ?? true,
3019
- policy
3020
- });
3021
- }
3022
- return targets;
2909
+ return (0, _mastra_core_storage.resolveRetentionTargets)({
2910
+ policies,
2911
+ descriptor,
2912
+ order
2913
+ });
3023
2914
  }
3024
2915
  //#endregion
3025
2916
  //#region src/storage/domains/background-tasks/index.ts
@@ -13612,7 +13503,7 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends _mastra_core_storage.Workflo
13612
13503
  }
13613
13504
  }, "deleteWorkflowRunById");
13614
13505
  }
13615
- async listWorkflowRuns({ workflowName, fromDate, toDate, page, perPage, resourceId, status } = {}) {
13506
+ async listWorkflowRuns({ workflowName, fromDate, toDate, page, perPage, resourceId, threadId, status } = {}) {
13616
13507
  try {
13617
13508
  const conditions = [];
13618
13509
  const args = [];
@@ -13636,6 +13527,10 @@ var WorkflowsLibSQL = class WorkflowsLibSQL extends _mastra_core_storage.Workflo
13636
13527
  conditions.push("resourceId = ?");
13637
13528
  args.push(resourceId);
13638
13529
  } else this.logger.warn(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
13530
+ if (threadId) {
13531
+ conditions.push(`(EXISTS (SELECT 1 FROM json_each(snapshot, '$.context') AS je WHERE json_extract(je.value, '$.status') = 'suspended' AND json_extract(je.value, '$.suspendPayload.__streamState.messageList.memoryInfo.threadId') = ?) OR json_extract(snapshot, '$.context.input.messageListState.memoryInfo.threadId') = ?)`);
13532
+ args.push(threadId, threadId);
13533
+ }
13639
13534
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
13640
13535
  let total = 0;
13641
13536
  const usePagination = typeof perPage === "number" && typeof page === "number";