@salesforce/sfdx-agent-harness-mastra 0.63.0 → 0.64.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.64.0] - 2026-09-02
7
+
8
+ ### Features
9
+ - **harness-mastra**: synthesize a terminal error when a Mastra stream closes without a finish @W-24026817@ ([#784](https://github.com/forcedotcom/agentic-dx/pull/784))
10
+
6
11
  ## [0.63.0] - 2026-09-02
7
12
 
8
13
  ### Features
@@ -407,6 +407,40 @@ export declare class MastraApprovalCoordinator {
407
407
  */
408
408
  private driveResumeReturning;
409
409
  private awaitContinuation;
410
+ /**
411
+ * Terminal-event guard (spike agent-offline-failure-modes #07, W-24026817).
412
+ *
413
+ * Called at the pump's silent-close sites — where Mastra's `fullStream`
414
+ * exhausted with neither a `finish`/`error` chunk nor a recognized
415
+ * suspension, so the turn produced no terminal event of its own. A healthy
416
+ * turn never lands here: a normal turn ends on a `finish` chunk, and an
417
+ * approval turn's FINAL resume produces the real continuation `finish` (see
418
+ * ARCHITECTURE.md → "Pump loop", step 5). The realistic trigger is a
419
+ * COORDINATOR-level terminating chunk the adapter intentionally skips —
420
+ * today `tool-call-suspended` (a tool using Mastra's native `suspend()`,
421
+ * which the event adapter maps to `undefined`) — and the guard also closes
422
+ * the class for any future Mastra chunk that ends the stream without a
423
+ * paired finish.
424
+ *
425
+ * Without this, the pump would `initial.end()` the sink silently and the
426
+ * SDK's `DefaultChatSession.wrapEventStream` — which synthesizes its own
427
+ * terminal `finish` ONLY when an error landed — would record the turn as a
428
+ * false-success `chat-stream-completed` with NO terminal `FinishEvent`,
429
+ * breaking the SDK invariant "every chat turn ends with exactly one terminal
430
+ * FinishEvent". Routing through the single `teardown` path pushes the typed
431
+ * `error('incomplete-stream')` + `finish('error')` pair the SDK invariant
432
+ * requires, so the turn fails loudly and recoverably instead. This is the
433
+ * harness-side complement to the SDK-side invariant item #10 pinned.
434
+ *
435
+ * `teardown` short-circuits its sink pushes once the sink is ended
436
+ * (`GenSink.push` no-ops after `end()`), and every terminal path either
437
+ * ended the sink already or set `aborted`/`disposed`/`timedOut` (which the
438
+ * callers check first), so this can never double-signal a turn that already
439
+ * reached a terminal — including 8a's pass-through `empty-stream` error,
440
+ * which arrives as an `error` chunk that ends the sink before the stream
441
+ * closes.
442
+ */
443
+ private guardTerminalOnSilentClose;
410
444
  /**
411
445
  * Push an event onto the sink with MCP-server enrichment applied (when
412
446
  * the toolName resolves to a known MCP catalog entry). Mirrors the
@@ -498,8 +498,8 @@ export class MastraApprovalCoordinator {
498
498
  // returns a stream that closes silently while still
499
499
  // suspended — the pump re-enters here to drive the next id.
500
500
  if (this.pendingSuspendIds.size === 0) {
501
- if (!this.initial.isEnded())
502
- this.initial.end();
501
+ // Terminal-event guard — see guardTerminalOnSilentClose.
502
+ this.guardTerminalOnSilentClose();
503
503
  return;
504
504
  }
505
505
  let nextId = pickIdWithDecision(this.pendingSuspendIds, this.decisions);
@@ -672,9 +672,9 @@ export class MastraApprovalCoordinator {
672
672
  if (sawSuspension || this.pendingSuspendIds.size > 0) {
673
673
  return 'suspended';
674
674
  }
675
- if (!sawTerminal && !this.initial.isEnded()) {
676
- this.pendingMcpToolCallStarts.clear();
677
- this.initial.end();
675
+ if (!sawTerminal) {
676
+ // Terminal-event guard — see guardTerminalOnSilentClose.
677
+ this.guardTerminalOnSilentClose();
678
678
  }
679
679
  return 'terminal';
680
680
  }
@@ -726,6 +726,44 @@ export class MastraApprovalCoordinator {
726
726
  this.continuationResolver = resolve;
727
727
  });
728
728
  }
729
+ /**
730
+ * Terminal-event guard (spike agent-offline-failure-modes #07, W-24026817).
731
+ *
732
+ * Called at the pump's silent-close sites — where Mastra's `fullStream`
733
+ * exhausted with neither a `finish`/`error` chunk nor a recognized
734
+ * suspension, so the turn produced no terminal event of its own. A healthy
735
+ * turn never lands here: a normal turn ends on a `finish` chunk, and an
736
+ * approval turn's FINAL resume produces the real continuation `finish` (see
737
+ * ARCHITECTURE.md → "Pump loop", step 5). The realistic trigger is a
738
+ * COORDINATOR-level terminating chunk the adapter intentionally skips —
739
+ * today `tool-call-suspended` (a tool using Mastra's native `suspend()`,
740
+ * which the event adapter maps to `undefined`) — and the guard also closes
741
+ * the class for any future Mastra chunk that ends the stream without a
742
+ * paired finish.
743
+ *
744
+ * Without this, the pump would `initial.end()` the sink silently and the
745
+ * SDK's `DefaultChatSession.wrapEventStream` — which synthesizes its own
746
+ * terminal `finish` ONLY when an error landed — would record the turn as a
747
+ * false-success `chat-stream-completed` with NO terminal `FinishEvent`,
748
+ * breaking the SDK invariant "every chat turn ends with exactly one terminal
749
+ * FinishEvent". Routing through the single `teardown` path pushes the typed
750
+ * `error('incomplete-stream')` + `finish('error')` pair the SDK invariant
751
+ * requires, so the turn fails loudly and recoverably instead. This is the
752
+ * harness-side complement to the SDK-side invariant item #10 pinned.
753
+ *
754
+ * `teardown` short-circuits its sink pushes once the sink is ended
755
+ * (`GenSink.push` no-ops after `end()`), and every terminal path either
756
+ * ended the sink already or set `aborted`/`disposed`/`timedOut` (which the
757
+ * callers check first), so this can never double-signal a turn that already
758
+ * reached a terminal — including 8a's pass-through `empty-stream` error,
759
+ * which arrives as an `error` chunk that ends the sink before the stream
760
+ * closes.
761
+ */
762
+ guardTerminalOnSilentClose() {
763
+ if (this.initial.isEnded())
764
+ return;
765
+ this.teardown('incomplete-stream', new Error('The model stream closed without a terminal finish or error event.'), 'incomplete-stream');
766
+ }
729
767
  // ───────────────────────── routing helpers ──────────────────────────────
730
768
  /**
731
769
  * Push an event onto the sink with MCP-server enrichment applied (when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.63.0",
3
+ "version": "0.64.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.64.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.65.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.64.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.65.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.10",
53
53
  "@vitest/eslint-plugin": "^1.6.27",