@salesforce/sfdx-agent-harness-mastra 0.90.0 → 0.91.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 +5 -0
- package/dist/mastra-harness.d.ts +10 -10
- package/dist/mastra-harness.js +52 -55
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
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.91.0] - 2026-09-24
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk,harness-mastra,harness-openai,harness-claude**: stable chatId across compaction — compact in place, never rotate the thread id @W-24153851@ ([#840](https://github.com/forcedotcom/agentic-dx/pull/840))
|
|
10
|
+
|
|
6
11
|
## [0.90.0] - 2026-09-23
|
|
7
12
|
|
|
8
13
|
### Fixes
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -464,17 +464,17 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
464
464
|
/**
|
|
465
465
|
* @requirements
|
|
466
466
|
* - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
|
|
467
|
-
* - MUST
|
|
467
|
+
* - MUST compact the thread IN PLACE, keeping the same `threadId` (a SQLite
|
|
468
|
+
* row PK) so `getId()` never changes across compaction (W-24153851).
|
|
468
469
|
* - MUST chunk-summarize transcripts that exceed `MAX_TRANSCRIPT_CHARS`
|
|
469
470
|
* to keep any single model call inside the context window.
|
|
470
|
-
* - MUST surface summarization or
|
|
471
|
-
* with type `COMPACTION_FAILED` and leave the
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* hygiene, not correctness.
|
|
471
|
+
* - MUST surface summarization or commit failures as `AgentSDKError`
|
|
472
|
+
* with type `COMPACTION_FAILED` and leave the transcript intact on failure.
|
|
473
|
+
* - MUST be fail-closed: summarize FIRST (read-only), then
|
|
474
|
+
* append-the-summary-then-delete-the-originals (append-then-delete-by-
|
|
475
|
+
* captured-id, NOT a re-recall). LibSQL has no atomic multi-row swap, so a
|
|
476
|
+
* crash after the append leaves BOTH the summary and the originals
|
|
477
|
+
* (retryable, larger-but-valid) — never an empty thread.
|
|
478
478
|
* - MUST seed the summary as a user/assistant turn pair where the user
|
|
479
479
|
* turn carries the summary content and the assistant turn is a brief
|
|
480
480
|
* acknowledgment. Storing the summary as an assistant turn (even with
|
|
@@ -483,7 +483,7 @@ export declare class MastraHarness implements MastraAgentHarness {
|
|
|
483
483
|
* it as fabricated, refusing to use the facts on follow-up turns.
|
|
484
484
|
* User turns are treated as authoritative input.
|
|
485
485
|
*/
|
|
486
|
-
compactThread(agentId: string, threadId: string): Promise<
|
|
486
|
+
compactThread(agentId: string, threadId: string): Promise<void>;
|
|
487
487
|
/**
|
|
488
488
|
* Best-effort wrapper around `destroyThread`: swallows the error so the
|
|
489
489
|
* caller's outer flow isn't aborted by a teardown blip, but logs the
|
package/dist/mastra-harness.js
CHANGED
|
@@ -1429,17 +1429,17 @@ export class MastraHarness {
|
|
|
1429
1429
|
/**
|
|
1430
1430
|
* @requirements
|
|
1431
1431
|
* - MUST throw an error if the specified `agentId` is not found in `activeAgents`.
|
|
1432
|
-
* - MUST
|
|
1432
|
+
* - MUST compact the thread IN PLACE, keeping the same `threadId` (a SQLite
|
|
1433
|
+
* row PK) so `getId()` never changes across compaction (W-24153851).
|
|
1433
1434
|
* - MUST chunk-summarize transcripts that exceed `MAX_TRANSCRIPT_CHARS`
|
|
1434
1435
|
* to keep any single model call inside the context window.
|
|
1435
|
-
* - MUST surface summarization or
|
|
1436
|
-
* with type `COMPACTION_FAILED` and leave the
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
* hygiene, not correctness.
|
|
1436
|
+
* - MUST surface summarization or commit failures as `AgentSDKError`
|
|
1437
|
+
* with type `COMPACTION_FAILED` and leave the transcript intact on failure.
|
|
1438
|
+
* - MUST be fail-closed: summarize FIRST (read-only), then
|
|
1439
|
+
* append-the-summary-then-delete-the-originals (append-then-delete-by-
|
|
1440
|
+
* captured-id, NOT a re-recall). LibSQL has no atomic multi-row swap, so a
|
|
1441
|
+
* crash after the append leaves BOTH the summary and the originals
|
|
1442
|
+
* (retryable, larger-but-valid) — never an empty thread.
|
|
1443
1443
|
* - MUST seed the summary as a user/assistant turn pair where the user
|
|
1444
1444
|
* turn carries the summary content and the assistant turn is a brief
|
|
1445
1445
|
* acknowledgment. Storing the summary as an assistant turn (even with
|
|
@@ -1449,49 +1449,54 @@ export class MastraHarness {
|
|
|
1449
1449
|
* User turns are treated as authoritative input.
|
|
1450
1450
|
*/
|
|
1451
1451
|
async compactThread(agentId, threadId) {
|
|
1452
|
-
this.getAgentStateOrThrow(agentId);
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
catch (err) {
|
|
1464
|
-
// The new thread was already created; a carry-forward failure
|
|
1465
|
-
// orphans it (never returned, never retired). Delete it before
|
|
1466
|
-
// surfacing COMPACTION_FAILED so a failed compaction leaves no
|
|
1467
|
-
// trace beyond the still-intact source thread.
|
|
1468
|
-
await this.destroyThreadBestEffort(agentId, newThreadId);
|
|
1469
|
-
throw new AgentSDKError(`Failed to compact thread "${threadId}" for agent "${agentId}": ${getErrorMessage(err)}`, AgentSDKErrorType.COMPACTION_FAILED, { cause: err });
|
|
1470
|
-
}
|
|
1471
|
-
// Empty source still gets compacted away — leaving it attached
|
|
1472
|
-
// would be a leak. Mirrors the non-empty path's destroyThread
|
|
1473
|
-
// call below; both paths exit with the source detached.
|
|
1474
|
-
await this.destroyThreadBestEffort(agentId, threadId);
|
|
1475
|
-
return newThreadId;
|
|
1452
|
+
const agentState = this.getAgentStateOrThrow(agentId);
|
|
1453
|
+
// Defense-in-depth against a direct-SDK caller: refuse to rewrite the
|
|
1454
|
+
// transcript out from under an in-flight turn. The SDK's one-turn guard
|
|
1455
|
+
// (auto path) and the service's 409 on an explicit /compact during a turn
|
|
1456
|
+
// already exclude this upstream. Fail fast rather than dispose a live
|
|
1457
|
+
// coordinator (that surfaces a spurious DISPOSED — W-24044572). Mirrors
|
|
1458
|
+
// stream()'s guard.
|
|
1459
|
+
const priorCoordinator = agentState.coordinators.get(threadId);
|
|
1460
|
+
if (priorCoordinator && !priorCoordinator.isSettled()) {
|
|
1461
|
+
throw new AgentSDKError(`A turn is already in progress for thread "${threadId}" on agent "${agentId}". ` +
|
|
1462
|
+
'Compaction cannot run while a turn is active; await the in-flight turn first.', AgentSDKErrorType.SESSION_BUSY);
|
|
1476
1463
|
}
|
|
1464
|
+
const messages = await this.getMessages(agentId, threadId);
|
|
1465
|
+
// Empty source: nothing to compact. In-place keeps the same threadId and
|
|
1466
|
+
// its (physically-disjoint) session-context store, so this is a no-op.
|
|
1467
|
+
if (messages.length === 0)
|
|
1468
|
+
return;
|
|
1477
1469
|
const transcript = messages
|
|
1478
1470
|
.map((m) => {
|
|
1479
1471
|
const text = typeof m.content === 'string' ? m.content : m.content.map((p) => partToTranscriptText(p)).join(' ');
|
|
1480
1472
|
return `${m.role}: ${text}`;
|
|
1481
1473
|
})
|
|
1482
1474
|
.join('\n');
|
|
1483
|
-
// `undefined` until `createThread` succeeds, so the catch can tell a
|
|
1484
|
-
// pre-create failure (nothing to clean up) from a post-create failure
|
|
1485
|
-
// (the new thread is orphaned and must be deleted).
|
|
1486
|
-
let newThreadId;
|
|
1487
1475
|
try {
|
|
1476
|
+
// Capture the ORIGINAL persisted (raw) message ids to delete after
|
|
1477
|
+
// the summary is appended. Raw recall ids — not `getMessages()`'s
|
|
1478
|
+
// post-mapped ids, which split a tool call into two SDK messages —
|
|
1479
|
+
// are what `memory.deleteMessages` keys on. Captured before the
|
|
1480
|
+
// append so the summary pair's freshly-minted ids are never in the
|
|
1481
|
+
// delete set.
|
|
1482
|
+
const memory = await this.getMemoryOrThrow(agentId);
|
|
1483
|
+
const { messages: rawMessages } = await memory.recall({
|
|
1484
|
+
threadId,
|
|
1485
|
+
resourceId: this.agentResourceId(agentId),
|
|
1486
|
+
perPage: false,
|
|
1487
|
+
});
|
|
1488
|
+
const originalIds = rawMessages.map((m) => m.id);
|
|
1489
|
+
// Summarize FIRST (read-only). No mutation has happened yet, so a
|
|
1490
|
+
// summary failure leaves the transcript fully intact.
|
|
1488
1491
|
const summaryText = transcript.length <= MAX_TRANSCRIPT_CHARS
|
|
1489
1492
|
? await this.summarize(agentId, buildSummaryPrompt(transcript))
|
|
1490
1493
|
: await this.summarizeInChunks(agentId, transcript);
|
|
1491
|
-
|
|
1494
|
+
// Append the summary pair to the SAME thread, THEN delete the
|
|
1495
|
+
// originals by captured id. A crash between the two leaves both
|
|
1496
|
+
// (retryable), never an empty thread.
|
|
1492
1497
|
const userTs = this.clock.now();
|
|
1493
1498
|
const assistantTs = this.clock.nextAfter(userTs);
|
|
1494
|
-
await this.addMessages(agentId,
|
|
1499
|
+
await this.addMessages(agentId, threadId, [
|
|
1495
1500
|
{
|
|
1496
1501
|
id: this.idGenerator.getUniqueId(),
|
|
1497
1502
|
role: 'user',
|
|
@@ -1505,26 +1510,18 @@ export class MastraHarness {
|
|
|
1505
1510
|
createdAt: assistantTs,
|
|
1506
1511
|
},
|
|
1507
1512
|
]);
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
await this.carrySessionContextForward(agentId, threadId, newThreadId);
|
|
1513
|
+
if (originalIds.length > 0) {
|
|
1514
|
+
await memory.deleteMessages(originalIds);
|
|
1515
|
+
}
|
|
1512
1516
|
}
|
|
1513
1517
|
catch (err) {
|
|
1514
|
-
// If the failure came after `createThread`, the new thread is
|
|
1515
|
-
// orphaned (never returned to the caller, source not yet retired).
|
|
1516
|
-
// Delete it so a failed compaction leaves no trace beyond the
|
|
1517
|
-
// still-intact source thread.
|
|
1518
|
-
if (newThreadId !== undefined) {
|
|
1519
|
-
await this.destroyThreadBestEffort(agentId, newThreadId);
|
|
1520
|
-
}
|
|
1521
1518
|
throw new AgentSDKError(`Failed to compact thread "${threadId}" for agent "${agentId}": ${getErrorMessage(err)}`, AgentSDKErrorType.COMPACTION_FAILED, { cause: err });
|
|
1522
1519
|
}
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
|
|
1527
|
-
|
|
1520
|
+
// Reset the per-thread consumer-tool retry counter — the rotation-based
|
|
1521
|
+
// compaction used to get this for free via destroyThread; an in-place
|
|
1522
|
+
// compaction keeps the thread, so reset it explicitly. The transcript the
|
|
1523
|
+
// counts described is gone.
|
|
1524
|
+
this.toolArgsRetryCounts.get(agentId)?.delete(threadId);
|
|
1528
1525
|
}
|
|
1529
1526
|
/**
|
|
1530
1527
|
* Best-effort wrapper around `destroyThread`: swallows the error so the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.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.17.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
45
|
+
"@salesforce/sfdx-agent-sdk": "0.92.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.
|
|
50
|
+
"@salesforce/sfdx-agent-sdk": "0.92.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^5.0.1",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|