@salesforce/sfdx-agent-harness-mastra 0.66.0 → 0.67.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,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.67.0] - 2026-09-02
|
|
7
|
+
|
|
8
|
+
### Fixes
|
|
9
|
+
- **harness-mastra**: wall-clock timeout on outbound resume RPC @W-24026787@ ([#790](https://github.com/forcedotcom/agentic-dx/pull/790))
|
|
10
|
+
|
|
6
11
|
## [0.66.0] - 2026-09-02
|
|
7
12
|
|
|
8
13
|
### Features
|
|
@@ -35,6 +35,22 @@ export interface MastraApprovalCoordinatorOptions {
|
|
|
35
35
|
};
|
|
36
36
|
requestContext: RequestContext;
|
|
37
37
|
toolApprovalTimeoutMs: number;
|
|
38
|
+
/**
|
|
39
|
+
* Wall-clock bound on the outbound resume RPC — `agent.approveToolCall(...)`
|
|
40
|
+
* / `agent.declineToolCall(...)` — NOT on the resumed turn's work (6c, spike
|
|
41
|
+
* agent-offline-failure-modes, W-24026787). Those calls return promptly with
|
|
42
|
+
* a fresh `fullStream` handle; the model's actual generation (which can be a
|
|
43
|
+
* long deployment or a slow "thinking" turn) streams through that handle,
|
|
44
|
+
* pumped separately with no timeout — so a healthy resume is never capped.
|
|
45
|
+
* This bound only fires when the establishment round-trip itself hangs and
|
|
46
|
+
* never hands back a stream: after a human decision the "waiting for a human"
|
|
47
|
+
* timer is already cancelled and the session is `busy`, so on plain-REST no
|
|
48
|
+
* idle timer or auto-decline sweep is left to recover it. On timeout the
|
|
49
|
+
* coordinator routes through the shared `teardown('timeout', …)` path so the
|
|
50
|
+
* sink still gets a terminal error+finish and the session is released rather
|
|
51
|
+
* than wedged. Defaults to {@link DEFAULT_RESUME_RPC_TIMEOUT_MS}.
|
|
52
|
+
*/
|
|
53
|
+
resumeRpcTimeoutMs?: number;
|
|
38
54
|
/**
|
|
39
55
|
* Per-tool approval-decision callback (Phase 2a). When defined, gating is
|
|
40
56
|
* **active**: the harness passed `requireToolApproval: true` to Mastra so
|
|
@@ -159,6 +175,7 @@ export declare class MastraApprovalCoordinator {
|
|
|
159
175
|
private readonly memory;
|
|
160
176
|
private readonly requestContext;
|
|
161
177
|
private readonly toolApprovalTimeoutMs;
|
|
178
|
+
private readonly resumeRpcTimeoutMs;
|
|
162
179
|
private readonly lookupPolicy?;
|
|
163
180
|
private readonly batchApprovals;
|
|
164
181
|
private readonly consumerToolNames;
|
|
@@ -23,6 +23,10 @@ function pickIdWithDecision(pendingIds, decisions) {
|
|
|
23
23
|
return undefined;
|
|
24
24
|
}
|
|
25
25
|
const POLICY_DENIED_RESULT_TEXT = 'Denied by tool policy.';
|
|
26
|
+
/** Default {@link MastraApprovalCoordinatorOptions.resumeRpcTimeoutMs} — 5 minutes. */
|
|
27
|
+
const DEFAULT_RESUME_RPC_TIMEOUT_MS = 300_000;
|
|
28
|
+
/** Race sentinel: the resume RPC establishment exceeded {@link DEFAULT_RESUME_RPC_TIMEOUT_MS}. */
|
|
29
|
+
const RESUME_TIMEOUT = 'resume-timeout';
|
|
26
30
|
/**
|
|
27
31
|
* Per-`(agentId, threadId)` coordinator that subsumes the pre-#529
|
|
28
32
|
* `MastraStreamBroker` + `activeRuns` pair into one object owning a single
|
|
@@ -65,6 +69,7 @@ export class MastraApprovalCoordinator {
|
|
|
65
69
|
memory;
|
|
66
70
|
requestContext;
|
|
67
71
|
toolApprovalTimeoutMs;
|
|
72
|
+
resumeRpcTimeoutMs;
|
|
68
73
|
lookupPolicy;
|
|
69
74
|
batchApprovals;
|
|
70
75
|
consumerToolNames;
|
|
@@ -170,6 +175,7 @@ export class MastraApprovalCoordinator {
|
|
|
170
175
|
this.memory = opts.memory;
|
|
171
176
|
this.requestContext = opts.requestContext;
|
|
172
177
|
this.toolApprovalTimeoutMs = opts.toolApprovalTimeoutMs;
|
|
178
|
+
this.resumeRpcTimeoutMs = opts.resumeRpcTimeoutMs ?? DEFAULT_RESUME_RPC_TIMEOUT_MS;
|
|
173
179
|
this.lookupPolicy = opts.lookupPolicy;
|
|
174
180
|
this.batchApprovals = opts.batchApprovals ?? false;
|
|
175
181
|
this.consumerToolNames = opts.consumerToolNames;
|
|
@@ -684,42 +690,95 @@ export class MastraApprovalCoordinator {
|
|
|
684
690
|
* pushed) or the new `fullStream` to pump next.
|
|
685
691
|
*/
|
|
686
692
|
async driveResumeReturning(toolCallId, decision) {
|
|
693
|
+
// Kick off the resume RPC. See `resumeRpcTimeoutMs` for why the bound
|
|
694
|
+
// below is on this handshake only, never the streamed work.
|
|
695
|
+
const resume = decision.kind === 'approve'
|
|
696
|
+
? this.agent.approveToolCall({
|
|
697
|
+
runId: this.currentRunId,
|
|
698
|
+
toolCallId,
|
|
699
|
+
memory: this.memory,
|
|
700
|
+
requestContext: this.requestContext,
|
|
701
|
+
})
|
|
702
|
+
: this.agent.declineToolCall({
|
|
703
|
+
runId: this.currentRunId,
|
|
704
|
+
toolCallId,
|
|
705
|
+
memory: this.memory,
|
|
706
|
+
requestContext: this.requestContext,
|
|
707
|
+
...(decision.modelMessage ? { reason: decision.modelMessage } : {}),
|
|
708
|
+
});
|
|
709
|
+
// 6c (W-24026787): bound the handshake with a wall-clock timer. See
|
|
710
|
+
// `resumeRpcTimeoutMs` for why this is the only recovery backstop left
|
|
711
|
+
// at this point on plain-REST. Race the RPC against the timer; whichever
|
|
712
|
+
// settles first wins and the timer is always cleared so the win path
|
|
713
|
+
// never leaves a stray timer to double-signal.
|
|
714
|
+
let timer;
|
|
715
|
+
const timeout = new Promise((resolve) => {
|
|
716
|
+
timer = setTimeout(() => resolve(RESUME_TIMEOUT), this.resumeRpcTimeoutMs);
|
|
717
|
+
});
|
|
718
|
+
let raced;
|
|
687
719
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
|
|
720
|
+
raced = await Promise.race([resume, timeout]);
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
// The resume RPC rejected outright. Route through teardown so
|
|
724
|
+
// siblings in `pendingSuspendIds` settle as auto-resolved rather
|
|
725
|
+
// than silently orphaning.
|
|
726
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
727
|
+
this.teardown('pump-error', error, 'pump-error');
|
|
728
|
+
return 'done';
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
clearTimeout(timer);
|
|
732
|
+
}
|
|
733
|
+
if (raced === RESUME_TIMEOUT) {
|
|
734
|
+
// Release a belatedly-established run on EVERY timeout path — this
|
|
735
|
+
// must sit ABOVE the aborted/disposed/timedOut guard below. This
|
|
736
|
+
// resume timer is a local, not in `approvalTimers`, so `teardown`'s
|
|
737
|
+
// `cancelAllTimers` can't cancel it: a `POST /cancel` during a hung
|
|
738
|
+
// handshake ends the sink but does NOT short-circuit this timer, so
|
|
739
|
+
// it still fires ~resumeRpcTimeoutMs later, hits the guard, and
|
|
740
|
+
// returns — and if the hung RPC then resolves, nothing else would
|
|
741
|
+
// release its run. Attaching here keeps the release on that
|
|
742
|
+
// abort/dispose-then-late-resolve edge (mirrors the abort/dispose
|
|
743
|
+
// post-await guard below). The RPC is never used to drive another
|
|
744
|
+
// stream — the turn is over; a late rejection is a no-op (also
|
|
745
|
+
// already consumed by the race).
|
|
746
|
+
void resume.then((out) => {
|
|
705
747
|
try {
|
|
706
|
-
this.agent.abortRunStream(
|
|
748
|
+
this.agent.abortRunStream(out.runId);
|
|
707
749
|
}
|
|
708
750
|
catch {
|
|
709
751
|
/* best-effort */
|
|
710
752
|
}
|
|
753
|
+
}, () => {
|
|
754
|
+
/* late rejection: already consumed by the race */
|
|
755
|
+
});
|
|
756
|
+
// abort/dispose/another timeout may have already won and ended the
|
|
757
|
+
// sink — if so, nothing more to do (the release above still ran).
|
|
758
|
+
if (this.aborted || this.disposed || this.timedOut)
|
|
711
759
|
return 'done';
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
760
|
+
this.timedOut = true;
|
|
761
|
+
// Route through the SAME teardown path abort/dispose/approval-timeout
|
|
762
|
+
// use, so the single-terminal invariant holds and the session is
|
|
763
|
+
// released. `teardown`'s sink pushes no-op once the sink has ended,
|
|
764
|
+
// so this can't double-signal a turn that already terminated.
|
|
765
|
+
this.teardown('timeout', new Error(`Tool resume RPC timed out after ${this.resumeRpcTimeoutMs}ms for tool call "${toolCallId}".`), RESUME_TIMEOUT);
|
|
766
|
+
return 'done';
|
|
715
767
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
768
|
+
const output = raced;
|
|
769
|
+
// Post-await guard: abort/dispose may have fired during the round-trip.
|
|
770
|
+
// Release the new run if so.
|
|
771
|
+
if (this.aborted || this.disposed) {
|
|
772
|
+
try {
|
|
773
|
+
this.agent.abortRunStream(output.runId);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
/* best-effort */
|
|
777
|
+
}
|
|
721
778
|
return 'done';
|
|
722
779
|
}
|
|
780
|
+
this.currentRunId = output.runId;
|
|
781
|
+
return output.fullStream;
|
|
723
782
|
}
|
|
724
783
|
awaitContinuation() {
|
|
725
784
|
return new Promise((resolve) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.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.
|
|
45
|
+
"@salesforce/sfdx-agent-sdk": "0.68.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.68.0",
|
|
51
51
|
"@types/node": "^22.20.1",
|
|
52
52
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
53
53
|
"@vitest/eslint-plugin": "^1.6.27",
|