gitlab-ai-provider 6.12.1 → 6.13.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
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
4
4
 
5
+ ## 6.13.0 (2026-09-02)
6
+
7
+ - Merge branch 'feature-add-fable-5-1' into 'main' ([4c4196b](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/4c4196b))
8
+ - feat(models): add Claude Fable 5.1 model mapping ([e31c530](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/e31c530))
9
+
10
+ ## <small>6.12.2 (2026-08-24)</small>
11
+
12
+ - Merge branch 'fix/workflow-lock-reconnect-race' into 'main' ([51a9277](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/51a9277))
13
+ - fix(workflow): wait out the workflow lock before reconnecting after approval ([35ca694](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/35ca694))
14
+
5
15
  ## <small>6.12.1 (2026-07-29)</small>
6
16
 
7
17
  - Merge branch 'ghavenga-cache-breakpoint-placement' into 'main' ([e8d0fe5](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/e8d0fe5))
package/README.md CHANGED
@@ -121,6 +121,7 @@ const customModel = gitlab.agenticChat('duo-chat-opus-4-5', {
121
121
 
122
122
  | Model ID | Provider | Backend Model |
123
123
  | ------------------------ | --------- | ---------------------------- |
124
+ | `duo-chat-fable-5-1` | Anthropic | `claude-fable-5-1` |
124
125
  | `duo-chat-fable-5` | Anthropic | `claude-fable-5` |
125
126
  | `duo-chat-opus-5` | Anthropic | `claude-opus-5` |
126
127
  | `duo-chat-opus-4-8` | Anthropic | `claude-opus-4-8` |
package/dist/index.d.mts CHANGED
@@ -894,6 +894,15 @@ declare class GitLabWorkflowLanguageModel implements LanguageModelV3 {
894
894
  private executeToolAndRespond;
895
895
  private cleanupClient;
896
896
  private approveAndResume;
897
+ /**
898
+ * Close the active client and wait for its close handshake, so a following
899
+ * reconnect to the same workflow does not race the Workhorse lock.
900
+ */
901
+ private closeClientAndWait;
902
+ /**
903
+ * Surface a terminal error on the stream and drop the workflow.
904
+ */
905
+ private failStream;
897
906
  private buildWorkflowMetadata;
898
907
  private getGitInfo;
899
908
  /**
@@ -1566,6 +1575,22 @@ declare class GitLabWorkflowClient {
1566
1575
  * Close the WebSocket connection.
1567
1576
  */
1568
1577
  close(): void;
1578
+ /**
1579
+ * Close the socket and wait for the close handshake to actually land.
1580
+ *
1581
+ * `close()` returns as soon as the close frame is queued. That is too early
1582
+ * when the caller is about to reconnect to the same workflow, because
1583
+ * Workhorse releases the workflow lock only once its own stop handshake with
1584
+ * DWS completes. Awaiting `onclose` (or the timeout) keeps the reconnect from
1585
+ * racing the lock and getting rejected with close code 1013.
1586
+ *
1587
+ * Best effort: never rejects, and resolves after `timeoutMs` if the server
1588
+ * never answers.
1589
+ *
1590
+ * @param timeoutMs - Maximum time to wait for the close event
1591
+ * @default WS_CLOSE_WAIT_TIMEOUT_MS
1592
+ */
1593
+ closeAndWait(timeoutMs?: number): Promise<void>;
1569
1594
  /**
1570
1595
  * Check if the WebSocket is currently connected.
1571
1596
  */
package/dist/index.d.ts CHANGED
@@ -894,6 +894,15 @@ declare class GitLabWorkflowLanguageModel implements LanguageModelV3 {
894
894
  private executeToolAndRespond;
895
895
  private cleanupClient;
896
896
  private approveAndResume;
897
+ /**
898
+ * Close the active client and wait for its close handshake, so a following
899
+ * reconnect to the same workflow does not race the Workhorse lock.
900
+ */
901
+ private closeClientAndWait;
902
+ /**
903
+ * Surface a terminal error on the stream and drop the workflow.
904
+ */
905
+ private failStream;
897
906
  private buildWorkflowMetadata;
898
907
  private getGitInfo;
899
908
  /**
@@ -1566,6 +1575,22 @@ declare class GitLabWorkflowClient {
1566
1575
  * Close the WebSocket connection.
1567
1576
  */
1568
1577
  close(): void;
1578
+ /**
1579
+ * Close the socket and wait for the close handshake to actually land.
1580
+ *
1581
+ * `close()` returns as soon as the close frame is queued. That is too early
1582
+ * when the caller is about to reconnect to the same workflow, because
1583
+ * Workhorse releases the workflow lock only once its own stop handshake with
1584
+ * DWS completes. Awaiting `onclose` (or the timeout) keeps the reconnect from
1585
+ * racing the lock and getting rejected with close code 1013.
1586
+ *
1587
+ * Best effort: never rejects, and resolves after `timeoutMs` if the server
1588
+ * never answers.
1589
+ *
1590
+ * @param timeoutMs - Maximum time to wait for the close event
1591
+ * @default WS_CLOSE_WAIT_TIMEOUT_MS
1592
+ */
1593
+ closeAndWait(timeoutMs?: number): Promise<void>;
1569
1594
  /**
1570
1595
  * Check if the WebSocket is currently connected.
1571
1596
  */
package/dist/index.js CHANGED
@@ -482,6 +482,10 @@ var WorkflowType = /* @__PURE__ */ ((WorkflowType2) => {
482
482
  })(WorkflowType || {});
483
483
  var WS_KEEPALIVE_PING_INTERVAL_MS = 45e3;
484
484
  var WS_HEARTBEAT_INTERVAL_MS = 6e4;
485
+ var WS_CLOSE_WORKFLOW_LOCKED = 1013;
486
+ var WS_CLOSE_WAIT_TIMEOUT_MS = 3e3;
487
+ var WS_WORKFLOW_LOCK_RETRY_DELAY_MS = 1500;
488
+ var WS_WORKFLOW_LOCK_MAX_RETRIES = 8;
485
489
  var DEFAULT_WORKFLOW_DEFINITION = "chat" /* CHAT */;
486
490
  var DEFAULT_CLIENT_CAPABILITIES = ["shell_command"];
487
491
  var CLIENT_VERSION = "8.51.0";
@@ -1464,6 +1468,7 @@ var import_openai = __toESM(require("openai"));
1464
1468
  // src/model-mappings.ts
1465
1469
  var MODEL_MAPPINGS = {
1466
1470
  // Anthropic models
1471
+ "duo-chat-fable-5-1": { provider: "anthropic", model: "claude-fable-5-1" },
1467
1472
  "duo-chat-fable-5": { provider: "anthropic", model: "claude-fable-5" },
1468
1473
  "duo-chat-opus-5": { provider: "anthropic", model: "claude-opus-5" },
1469
1474
  "duo-chat-opus-4-8": { provider: "anthropic", model: "claude-opus-4-8" },
@@ -2526,7 +2531,7 @@ var import_node_async_hooks = require("async_hooks");
2526
2531
  var import_isomorphic_ws = __toESM(require("isomorphic-ws"));
2527
2532
 
2528
2533
  // src/version.ts
2529
- var VERSION = true ? "6.12.0" : "0.0.0-dev";
2534
+ var VERSION = true ? "6.12.2" : "0.0.0-dev";
2530
2535
 
2531
2536
  // src/gitlab-workflow-client.ts
2532
2537
  var WS_CONNECT_TIMEOUT_MS = 3e4;
@@ -2676,6 +2681,48 @@ var GitLabWorkflowClient = class {
2676
2681
  }
2677
2682
  }
2678
2683
  }
2684
+ /**
2685
+ * Close the socket and wait for the close handshake to actually land.
2686
+ *
2687
+ * `close()` returns as soon as the close frame is queued. That is too early
2688
+ * when the caller is about to reconnect to the same workflow, because
2689
+ * Workhorse releases the workflow lock only once its own stop handshake with
2690
+ * DWS completes. Awaiting `onclose` (or the timeout) keeps the reconnect from
2691
+ * racing the lock and getting rejected with close code 1013.
2692
+ *
2693
+ * Best effort: never rejects, and resolves after `timeoutMs` if the server
2694
+ * never answers.
2695
+ *
2696
+ * @param timeoutMs - Maximum time to wait for the close event
2697
+ * @default WS_CLOSE_WAIT_TIMEOUT_MS
2698
+ */
2699
+ closeAndWait(timeoutMs = WS_CLOSE_WAIT_TIMEOUT_MS) {
2700
+ const sock = this.socket;
2701
+ const pending = sock && (sock.readyState === import_isomorphic_ws.default.OPEN || sock.readyState === import_isomorphic_ws.default.CONNECTING);
2702
+ if (this.closed || !pending) {
2703
+ this.close();
2704
+ return Promise.resolve();
2705
+ }
2706
+ return new Promise((resolve3) => {
2707
+ let settled = false;
2708
+ const finish = () => {
2709
+ if (settled) return;
2710
+ settled = true;
2711
+ clearTimeout(timer);
2712
+ resolve3();
2713
+ };
2714
+ const timer = setTimeout(finish, timeoutMs);
2715
+ const previous = sock.onclose;
2716
+ sock.onclose = (event) => {
2717
+ try {
2718
+ previous?.call(sock, event);
2719
+ } finally {
2720
+ finish();
2721
+ }
2722
+ };
2723
+ this.close();
2724
+ });
2725
+ }
2679
2726
  /**
2680
2727
  * Check if the WebSocket is currently connected.
2681
2728
  */
@@ -4635,19 +4682,20 @@ var GitLabWorkflowLanguageModel = class _GitLabWorkflowLanguageModel {
4635
4682
  }
4636
4683
  ss.approvalPending = false;
4637
4684
  ss.deferredClose = null;
4638
- this.cleanupClient(ss, false);
4639
4685
  const approval = decision.approved ? { approval: { tool_name: tools[0]?.name, tool_args_json: tools[0]?.args } } : { rejection: { message: decision.message ?? "User rejected" } };
4640
4686
  const newStartReq = {
4641
4687
  ...startReq,
4642
4688
  approval,
4643
4689
  preapproved_tools: decision.approved ? [...startReq.preapproved_tools ?? [], ...tools.map((t) => t.name)] : startReq.preapproved_tools ?? []
4644
4690
  };
4645
- const newClient = new GitLabWorkflowClient();
4646
- this.activeClients.add(newClient);
4647
- ss.activeClient = newClient;
4648
- const modelRef = await this.resolveModelRef();
4649
- try {
4650
- await newClient.connect(
4691
+ const reconnect = async () => {
4692
+ if (ss.streamClosed) return;
4693
+ await this.closeClientAndWait(ss);
4694
+ const client = new GitLabWorkflowClient();
4695
+ this.activeClients.add(client);
4696
+ ss.activeClient = client;
4697
+ const modelRef = await this.resolveModelRef();
4698
+ await client.connect(
4651
4699
  {
4652
4700
  instanceUrl: this.config.instanceUrl,
4653
4701
  modelRef,
@@ -4658,23 +4706,63 @@ var GitLabWorkflowLanguageModel = class _GitLabWorkflowLanguageModel {
4658
4706
  aiCatalogItemVersionId: wsExtras?.aiCatalogItemVersionId,
4659
4707
  workflowDefinition: wsExtras?.workflowDefinition
4660
4708
  },
4661
- (event) => this.handleWorkflowEvent(
4709
+ (event) => {
4710
+ if (event.type === "closed" && event.code === WS_CLOSE_WORKFLOW_LOCKED) {
4711
+ void retryWhileLocked();
4712
+ return;
4713
+ }
4714
+ this.handleWorkflowEvent(
4715
+ ss,
4716
+ event,
4717
+ controller,
4718
+ client,
4719
+ toolExecutor,
4720
+ nextTextId,
4721
+ availableToolNames,
4722
+ newStartReq,
4723
+ wsExtras
4724
+ );
4725
+ }
4726
+ );
4727
+ client.sendStartRequest(newStartReq);
4728
+ };
4729
+ let lockRetries = 0;
4730
+ const retryWhileLocked = async () => {
4731
+ if (ss.streamClosed) return;
4732
+ if (++lockRetries > WS_WORKFLOW_LOCK_MAX_RETRIES) {
4733
+ this.failStream(
4662
4734
  ss,
4663
- event,
4664
4735
  controller,
4665
- newClient,
4666
- toolExecutor,
4667
- nextTextId,
4668
- availableToolNames,
4669
- newStartReq,
4670
- wsExtras
4671
- )
4672
- );
4673
- newClient.sendStartRequest(newStartReq);
4674
- } catch (err) {
4675
- this.cleanupClient(ss, true);
4676
- if (!ss.streamClosed) controller.error(err);
4677
- }
4736
+ new GitLabError({
4737
+ message: "Failed to acquire lock on workflow after reconnect retries",
4738
+ statusCode: WS_CLOSE_WORKFLOW_LOCKED
4739
+ })
4740
+ );
4741
+ return;
4742
+ }
4743
+ await new Promise((r) => setTimeout(r, WS_WORKFLOW_LOCK_RETRY_DELAY_MS * lockRetries));
4744
+ await reconnect().catch((err) => this.failStream(ss, controller, err));
4745
+ };
4746
+ await reconnect().catch((err) => this.failStream(ss, controller, err));
4747
+ }
4748
+ /**
4749
+ * Close the active client and wait for its close handshake, so a following
4750
+ * reconnect to the same workflow does not race the Workhorse lock.
4751
+ */
4752
+ async closeClientAndWait(ss) {
4753
+ const client = ss.activeClient;
4754
+ if (!client) return;
4755
+ this.activeClients.delete(client);
4756
+ ss.activeClient = null;
4757
+ await client.closeAndWait();
4758
+ }
4759
+ /**
4760
+ * Surface a terminal error on the stream and drop the workflow.
4761
+ */
4762
+ failStream(ss, controller, error) {
4763
+ this.cleanupClient(ss, true);
4764
+ if (ss.streamClosed) return;
4765
+ controller.error(error);
4678
4766
  }
4679
4767
  // ---------------------------------------------------------------------------
4680
4768
  // Workflow metadata