@salesforce/sfdx-agent-harness-mastra 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.57.0] - 2026-08-28
7
+
8
+ ### Fixes
9
+ - **harness-mastra,agent-sdk**: resilient thread enumeration + exception-safe destroyChatSession @W-24014120@ ([#774](https://github.com/forcedotcom/agentic-dx/pull/774))
10
+
11
+ ## [0.56.0] - 2026-08-27
12
+
13
+ _No changes — released alongside dependent packages._
14
+
6
15
  ## [0.55.0] - 2026-08-27
7
16
 
8
17
  _No changes — released alongside dependent packages._
@@ -393,8 +393,35 @@ export declare class MastraHarness implements MastraAgentHarness {
393
393
  * @requirements
394
394
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
395
395
  * - MUST query threads scoped to this agent and return their IDs.
396
+ * - MUST enumerate resiliently: a single thread whose persisted `metadata` is
397
+ * corrupt MUST NOT suppress the agent's other thread IDs.
398
+ *
399
+ * Resilience rationale (spike agent-offline-failure-modes #11, W-24014120):
400
+ * `memory.listThreads` builds its SELECT as `json(metadata)` over every row, so one
401
+ * malformed-JSON row makes SQLite throw `SQLITE_ERROR: malformed JSON`; Mastra's
402
+ * catch swallows that and returns an EMPTY list — silently un-restoring every thread
403
+ * on the agent (the "one bad thread bricks the whole agent" symptom). We instead
404
+ * enumerate with a column-only `SELECT id` (both `id` and `resourceId` are plain
405
+ * TEXT, never parsed), which a corrupt `metadata` on another row cannot break. The
406
+ * raw query couples to Mastra's `mastra_threads` schema, so it is fallback-guarded —
407
+ * but ONLY for the failures the fallback is meant to survive:
408
+ * - the libsql client isn't reachable (a future Mastra internals change), or
409
+ * - the raw query hits a schema-shape drift (`no such table` / `no such column`).
410
+ * Both degrade to the high-level `listThreads` API with a warning. Any OTHER failure
411
+ * (I/O error, corrupt/locked DB — a genuine whole-store fault) is rethrown unchanged,
412
+ * so the SDK's boot-restore full-failure invariant fires rather than papering over an
413
+ * unreadable store as healthy-but-empty (see `isRecoverableThreadEnumerationError`).
396
414
  */
397
415
  getThreadIds(agentId: string): Promise<string[]>;
416
+ /**
417
+ * Degraded thread enumeration via Mastra's high-level `listThreads` API, used when the
418
+ * resilient raw `SELECT id` path is unavailable for a recoverable reason (client absent
419
+ * or schema-shape drift). Logs a warning so the degradation is observable. NOTE: unlike
420
+ * the raw path, `listThreads` is itself vulnerable to a corrupt `metadata` row (the very
421
+ * defect `getThreadIds` exists to route around) — this is a best-effort fallback for the
422
+ * Mastra-rename scenario, not a second line of defense against metadata corruption.
423
+ */
424
+ private listThreadIdsFallback;
398
425
  /**
399
426
  * @requirements
400
427
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
@@ -131,6 +131,27 @@ export function renderSessionContextSystemText(context) {
131
131
  * API surface (src/index.ts).
132
132
  */
133
133
  const LAST_MESSAGES_WINDOW = 40;
134
+ /**
135
+ * Mastra's thread table (`@mastra/core`'s `TABLE_THREADS`). `getThreadIds` enumerates
136
+ * it with a column-only `SELECT id` to stay immune to a single corrupt `metadata` row
137
+ * (see the resilient-enumeration path in `getThreadIds`). Coupling to this name is the
138
+ * price of that immunity; the raw query is fallback-guarded so a future Mastra rename
139
+ * degrades to the high-level API rather than crashing enumeration.
140
+ */
141
+ const MASTRA_THREADS_TABLE = 'mastra_threads';
142
+ /**
143
+ * Decides whether a raw `getThreadIds` enumeration failure is a *recoverable* schema-shape
144
+ * drift (our column-only `SELECT id FROM mastra_threads` references a table/column a future
145
+ * Mastra renamed — libsql reports `no such table` / `no such column`), in which case the
146
+ * harness degrades to the high-level `listThreads` API. Every other failure (I/O error,
147
+ * corrupt or locked DB — a genuine whole-store fault) is deliberately NOT recoverable: it
148
+ * MUST propagate so the SDK's boot-restore full-failure invariant fires and the agent
149
+ * surfaces the fault rather than coming up silently empty. (spike #11, W-24014120)
150
+ */
151
+ function isRecoverableThreadEnumerationError(err) {
152
+ const message = getErrorMessage(err).toLowerCase();
153
+ return message.includes('no such table') || message.includes('no such column');
154
+ }
134
155
  /**
135
156
  * Maximum flattened-transcript length (in characters) sent to the model in a
136
157
  * single summarization call. Transcripts above this size are split into
@@ -1240,13 +1261,59 @@ export class MastraHarness {
1240
1261
  * @requirements
1241
1262
  * - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
1242
1263
  * - MUST query threads scoped to this agent and return their IDs.
1264
+ * - MUST enumerate resiliently: a single thread whose persisted `metadata` is
1265
+ * corrupt MUST NOT suppress the agent's other thread IDs.
1266
+ *
1267
+ * Resilience rationale (spike agent-offline-failure-modes #11, W-24014120):
1268
+ * `memory.listThreads` builds its SELECT as `json(metadata)` over every row, so one
1269
+ * malformed-JSON row makes SQLite throw `SQLITE_ERROR: malformed JSON`; Mastra's
1270
+ * catch swallows that and returns an EMPTY list — silently un-restoring every thread
1271
+ * on the agent (the "one bad thread bricks the whole agent" symptom). We instead
1272
+ * enumerate with a column-only `SELECT id` (both `id` and `resourceId` are plain
1273
+ * TEXT, never parsed), which a corrupt `metadata` on another row cannot break. The
1274
+ * raw query couples to Mastra's `mastra_threads` schema, so it is fallback-guarded —
1275
+ * but ONLY for the failures the fallback is meant to survive:
1276
+ * - the libsql client isn't reachable (a future Mastra internals change), or
1277
+ * - the raw query hits a schema-shape drift (`no such table` / `no such column`).
1278
+ * Both degrade to the high-level `listThreads` API with a warning. Any OTHER failure
1279
+ * (I/O error, corrupt/locked DB — a genuine whole-store fault) is rethrown unchanged,
1280
+ * so the SDK's boot-restore full-failure invariant fires rather than papering over an
1281
+ * unreadable store as healthy-but-empty (see `isRecoverableThreadEnumerationError`).
1243
1282
  */
1244
1283
  async getThreadIds(agentId) {
1245
1284
  const memory = await this.getMemoryOrThrow(agentId);
1246
- const { threads } = await memory.listThreads({
1247
- filter: { resourceId: this.agentResourceId(agentId) },
1248
- perPage: false,
1249
- });
1285
+ const resourceId = this.agentResourceId(agentId);
1286
+ const client = this.storage.client;
1287
+ if (typeof client?.execute !== 'function') {
1288
+ return this.listThreadIdsFallback(agentId, memory, resourceId, 'LibSQLStore client is not accessible');
1289
+ }
1290
+ try {
1291
+ const { rows } = await client.execute({
1292
+ sql: `SELECT id FROM ${MASTRA_THREADS_TABLE} WHERE resourceId = ?`,
1293
+ args: [resourceId],
1294
+ });
1295
+ return rows.map((row) => String(row['id']));
1296
+ }
1297
+ catch (err) {
1298
+ if (!isRecoverableThreadEnumerationError(err)) {
1299
+ throw err;
1300
+ }
1301
+ return this.listThreadIdsFallback(agentId, memory, resourceId, getErrorMessage(err));
1302
+ }
1303
+ }
1304
+ /**
1305
+ * Degraded thread enumeration via Mastra's high-level `listThreads` API, used when the
1306
+ * resilient raw `SELECT id` path is unavailable for a recoverable reason (client absent
1307
+ * or schema-shape drift). Logs a warning so the degradation is observable. NOTE: unlike
1308
+ * the raw path, `listThreads` is itself vulnerable to a corrupt `metadata` row (the very
1309
+ * defect `getThreadIds` exists to route around) — this is a best-effort fallback for the
1310
+ * Mastra-rename scenario, not a second line of defense against metadata corruption.
1311
+ */
1312
+ async listThreadIdsFallback(agentId, memory, resourceId, reason) {
1313
+ this.buses
1314
+ .getLogBus()
1315
+ ?.warn('getThreadIds: raw enumeration unavailable; falling back to listThreads', { agentId, reason });
1316
+ const { threads } = await memory.listThreads({ filter: { resourceId }, perPage: false });
1250
1317
  return threads.map((t) => t.id);
1251
1318
  }
1252
1319
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,12 +42,12 @@
42
42
  "openai": "^7.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@salesforce/sfdx-agent-sdk": "0.56.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.58.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@eslint/js": "^10.0.1",
49
49
  "@salesforce/harness-conformance": "0.0.1",
50
- "@salesforce/sfdx-agent-sdk": "0.56.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.58.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.10",
53
53
  "@vitest/eslint-plugin": "^1.6.27",