@salesforce/sfdx-agent-harness-mastra 0.62.0 → 0.63.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.63.0] - 2026-09-02
7
+
8
+ ### Features
9
+ - **agent-sdk,harness-mastra,harness-claude,harness-openai**: reject concurrent turns with a typed SESSION_BUSY error @W-24044572@ ([#783](https://github.com/forcedotcom/agentic-dx/pull/783))
10
+
6
11
  ## [0.62.0] - 2026-09-02
7
12
 
8
13
  ### Fixes
@@ -261,6 +261,18 @@ export declare class MastraApprovalCoordinator {
261
261
  private timedOut;
262
262
  constructor(opts: MastraApprovalCoordinatorOptions);
263
263
  initialSink(): GenSink<ChatEvent>;
264
+ /**
265
+ * True once this turn has reached a terminal event — a natural `finish`
266
+ * pushed by the pump, or the synthetic `error` + `finish('error')` pair a
267
+ * `teardown` (abort / dispose / timeout / pump-error) emits. Both end the
268
+ * single per-turn sink, so `initial.isEnded()` is the authoritative "this
269
+ * turn is over" signal. A coordinator still pumping — including one PARKED on
270
+ * a `tool-approval-request` or a consumer-tool `submitToolResult` — has not
271
+ * ended its sink and is therefore NOT settled. `MastraHarness.stream()` reads
272
+ * this to reject a concurrent turn with `SESSION_BUSY` (a still-active
273
+ * coordinator) versus safely replace a settled one.
274
+ */
275
+ isSettled(): boolean;
264
276
  /**
265
277
  * Mastra's current run id. The harness's MCP-progress dispatcher uses
266
278
  * this to filter incoming `notifications/progress` to the coordinator
@@ -184,6 +184,20 @@ export class MastraApprovalCoordinator {
184
184
  initialSink() {
185
185
  return this.initial;
186
186
  }
187
+ /**
188
+ * True once this turn has reached a terminal event — a natural `finish`
189
+ * pushed by the pump, or the synthetic `error` + `finish('error')` pair a
190
+ * `teardown` (abort / dispose / timeout / pump-error) emits. Both end the
191
+ * single per-turn sink, so `initial.isEnded()` is the authoritative "this
192
+ * turn is over" signal. A coordinator still pumping — including one PARKED on
193
+ * a `tool-approval-request` or a consumer-tool `submitToolResult` — has not
194
+ * ended its sink and is therefore NOT settled. `MastraHarness.stream()` reads
195
+ * this to reject a concurrent turn with `SESSION_BUSY` (a still-active
196
+ * coordinator) versus safely replace a settled one.
197
+ */
198
+ isSettled() {
199
+ return this.initial.isEnded();
200
+ }
187
201
  /**
188
202
  * Mastra's current run id. The harness's MCP-progress dispatcher uses
189
203
  * this to filter incoming `notifications/progress` to the coordinator
@@ -1618,12 +1618,20 @@ export class MastraHarness {
1618
1618
  // round-trip per tool call.
1619
1619
  const { lookupPolicy, batchApprovals } = this.buildPolicyGate(agentState, options);
1620
1620
  const gatingActive = lookupPolicy !== undefined;
1621
- // Dispose any coordinator the prior turn on this thread allocated.
1622
- // Without this a stale coordinator from the prior turn would still
1623
- // route settle calls and accumulate listeners. Mode-flip safe: the
1624
- // new coordinator is constructed fresh below regardless of the new
1625
- // turn's mode.
1621
+ // Concurrency guard (spike agent-offline-failure-modes #10, W-24044572):
1622
+ // reject a second turn on a thread whose prior turn is still ACTIVE with a
1623
+ // typed `SESSION_BUSY` error, instead of silently disposing the first
1624
+ // (which surfaced to the first consumer as a spurious `DISPOSED` — a
1625
+ // "silent lost turn"). A coordinator is active until its sink ends (natural
1626
+ // finish or teardown), so one still pumping — including one parked on an
1627
+ // approval / consumer-tool result — blocks a new turn. An already-settled
1628
+ // coordinator (finished or errored) is safe to dispose-and-replace, so the
1629
+ // idempotent settling of an already-terminal turn is preserved.
1626
1630
  const priorCoordinator = agentState.coordinators.get(threadId);
1631
+ if (priorCoordinator && !priorCoordinator.isSettled()) {
1632
+ throw new AgentSDKError(`A turn is already in progress for thread "${threadId}" on agent "${agentId}". ` +
1633
+ 'Only one turn may run at a time per thread; await the in-flight turn before starting another.', AgentSDKErrorType.SESSION_BUSY);
1634
+ }
1627
1635
  if (priorCoordinator) {
1628
1636
  priorCoordinator.dispose();
1629
1637
  agentState.coordinators.delete(threadId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.62.0",
3
+ "version": "0.63.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.63.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.64.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.63.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.64.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.10",
53
53
  "@vitest/eslint-plugin": "^1.6.27",