opencode-gitlab-duo-agentic 0.2.12 → 0.2.14

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.
Files changed (2) hide show
  1. package/dist/index.js +78 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -547,24 +547,26 @@ var WORKFLOW_STATUS = {
547
547
  function isCheckpointAction(action) {
548
548
  return "newCheckpoint" in action && action.newCheckpoint != null;
549
549
  }
550
- var TURN_COMPLETE_STATUSES = /* @__PURE__ */ new Set([
551
- WORKFLOW_STATUS.INPUT_REQUIRED,
550
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
552
551
  WORKFLOW_STATUS.FINISHED,
553
552
  WORKFLOW_STATUS.FAILED,
554
- WORKFLOW_STATUS.STOPPED,
555
- WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED,
556
- WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED
553
+ WORKFLOW_STATUS.STOPPED
557
554
  ]);
558
- function isTurnComplete(status) {
559
- return TURN_COMPLETE_STATUSES.has(status);
555
+ function isTerminal(status) {
556
+ return TERMINAL_STATUSES.has(status);
560
557
  }
561
- var AWAITING_RESPONSE_STATUSES = /* @__PURE__ */ new Set([
562
- WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED,
558
+ var TURN_BOUNDARY_STATUSES = /* @__PURE__ */ new Set([
563
559
  WORKFLOW_STATUS.INPUT_REQUIRED,
564
560
  WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED
565
561
  ]);
566
- function isAwaitingResponse(status) {
567
- return AWAITING_RESPONSE_STATUSES.has(status);
562
+ function isTurnBoundary(status) {
563
+ return TURN_BOUNDARY_STATUSES.has(status);
564
+ }
565
+ function isToolApproval(status) {
566
+ return status === WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED;
567
+ }
568
+ function isTurnComplete(status) {
569
+ return isTerminal(status) || isTurnBoundary(status);
568
570
  }
569
571
 
570
572
  // src/workflow/websocket-client.ts
@@ -784,17 +786,6 @@ function mapActionToToolRequest(action) {
784
786
  return null;
785
787
  }
786
788
 
787
- // src/utils/debug-log.ts
788
- import { appendFileSync } from "fs";
789
- var LOG = "/tmp/duo-workflow-debug.log";
790
- function dlog(msg) {
791
- try {
792
- appendFileSync(LOG, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
793
- `);
794
- } catch {
795
- }
796
- }
797
-
798
789
  // src/workflow/session.ts
799
790
  var WorkflowSession = class {
800
791
  #client;
@@ -809,6 +800,7 @@ var WorkflowSession = class {
809
800
  #socket;
810
801
  #queue;
811
802
  #startRequestSent = false;
803
+ #pendingApproval = false;
812
804
  constructor(client, modelId, cwd) {
813
805
  this.#client = client;
814
806
  this.#tokenService = new WorkflowTokenService(client);
@@ -832,6 +824,7 @@ var WorkflowSession = class {
832
824
  this.#checkpoint = createCheckpointState();
833
825
  this.#tokenService.clear();
834
826
  this.#closeConnection();
827
+ this.#pendingApproval = false;
835
828
  this.#startRequestSent = false;
836
829
  }
837
830
  // ---------------------------------------------------------------------------
@@ -842,13 +835,29 @@ var WorkflowSession = class {
842
835
  if (!this.#workflowId) {
843
836
  this.#workflowId = await this.#createWorkflow(goal);
844
837
  }
838
+ const queue = new AsyncQueue();
839
+ this.#queue = queue;
840
+ await this.#connectSocket(queue);
841
+ }
842
+ /**
843
+ * Open a WebSocket and wire its callbacks to the given queue.
844
+ * Replaces any existing socket but does NOT create a new queue.
845
+ */
846
+ async #connectSocket(queue) {
845
847
  await this.#tokenService.get(this.#rootNamespaceId);
846
- this.#queue = new AsyncQueue();
847
- const queue = this.#queue;
848
848
  const socket = new WorkflowWebSocketClient({
849
849
  action: (action) => this.#handleAction(action, queue),
850
850
  error: (error) => queue.push({ type: "error", message: error.message }),
851
- close: (_code, _reason) => queue.close()
851
+ close: (_code, _reason) => {
852
+ this.#socket = void 0;
853
+ if (this.#pendingApproval) {
854
+ this.#pendingApproval = false;
855
+ this.#reconnectWithApproval(queue);
856
+ } else {
857
+ this.#queue = void 0;
858
+ queue.close();
859
+ }
860
+ }
852
861
  });
853
862
  const url = buildWebSocketUrl(this.#client.instanceUrl, this.#modelId);
854
863
  await socket.connect(url, {
@@ -891,7 +900,6 @@ var WorkflowSession = class {
891
900
  * Send a tool result back to DWS on the existing connection.
892
901
  */
893
902
  sendToolResult(requestId, output, error) {
894
- dlog(`sendToolResult: reqId=${requestId} output=${output.length}b socket=${!!this.#socket}`);
895
903
  if (!this.#socket) throw new Error("Not connected");
896
904
  this.#socket.send({
897
905
  actionResponse: {
@@ -924,10 +932,15 @@ var WorkflowSession = class {
924
932
  #handleAction(action, queue) {
925
933
  if (isCheckpointAction(action)) {
926
934
  const ckpt = action.newCheckpoint.checkpoint;
935
+ const status = action.newCheckpoint.status;
927
936
  const deltas = extractAgentTextDeltas(ckpt, this.#checkpoint);
928
937
  for (const delta of deltas) {
929
938
  queue.push({ type: "text-delta", value: delta });
930
939
  }
940
+ if (isToolApproval(status)) {
941
+ this.#pendingApproval = true;
942
+ return;
943
+ }
931
944
  const toolRequests = extractToolRequests(ckpt, this.#checkpoint);
932
945
  for (const req of toolRequests) {
933
946
  queue.push({
@@ -937,20 +950,15 @@ var WorkflowSession = class {
937
950
  args: req.args
938
951
  });
939
952
  }
940
- const status = action.newCheckpoint.status;
941
- if (isTurnComplete(status) && !isAwaitingResponse(status)) {
942
- dlog(`action: checkpoint status=${status} CLOSE`);
953
+ if (isTurnComplete(status)) {
943
954
  queue.close();
944
955
  this.#closeConnection();
945
- } else {
946
- dlog(`action: checkpoint status=${status} KEEP-ALIVE`);
947
956
  }
948
957
  return;
949
958
  }
950
959
  const toolAction = action;
951
960
  const mapped = mapActionToToolRequest(toolAction);
952
961
  if (mapped) {
953
- dlog(`action: standalone ${mapped.toolName} reqId=${mapped.requestId}`);
954
962
  queue.push({
955
963
  type: "tool-request",
956
964
  requestId: mapped.requestId,
@@ -962,7 +970,45 @@ var WorkflowSession = class {
962
970
  // ---------------------------------------------------------------------------
963
971
  // Private: connection management
964
972
  // ---------------------------------------------------------------------------
973
+ /**
974
+ * Auto-approve at DWS protocol level and reconnect.
975
+ *
976
+ * DWS closed the stream after TOOL_CALL_APPROVAL_REQUIRED. We open a new
977
+ * WebSocket, send startRequest with approval, and wire it to the SAME queue
978
+ * so Phase 3 in the model continues consuming events seamlessly.
979
+ *
980
+ * The actual tool execution still goes through OpenCode's permission system
981
+ * when the standalone action arrives on the new stream.
982
+ */
983
+ #reconnectWithApproval(queue) {
984
+ this.#connectSocket(queue).then(() => {
985
+ if (!this.#socket || !this.#workflowId) {
986
+ queue.close();
987
+ return;
988
+ }
989
+ const mcpTools = this.#toolsConfig?.mcpTools ?? [];
990
+ this.#socket.send({
991
+ startRequest: {
992
+ workflowID: this.#workflowId,
993
+ clientVersion: WORKFLOW_CLIENT_VERSION,
994
+ workflowDefinition: WORKFLOW_DEFINITION,
995
+ goal: "",
996
+ workflowMetadata: JSON.stringify({ extended_logging: false }),
997
+ clientCapabilities: ["shell_command"],
998
+ mcpTools,
999
+ additional_context: [],
1000
+ preapproved_tools: mcpTools.map((t) => t.name),
1001
+ approval: { approval: {} }
1002
+ }
1003
+ });
1004
+ this.#startRequestSent = true;
1005
+ }).catch(() => {
1006
+ this.#queue = void 0;
1007
+ queue.close();
1008
+ });
1009
+ }
965
1010
  #closeConnection() {
1011
+ this.#pendingApproval = false;
966
1012
  this.#socket?.close();
967
1013
  this.#socket = void 0;
968
1014
  this.#queue = void 0;
@@ -1420,7 +1466,6 @@ var DuoWorkflowModel = class {
1420
1466
  const toolResults = extractToolResults(options.prompt);
1421
1467
  const session = this.#resolveSession(sessionID);
1422
1468
  const textId = randomUUID3();
1423
- dlog(`doStream: goal=${goal?.length ?? 0}ch toolResults=${toolResults.length} hasStarted=${session.hasStarted} pending=${this.#pendingToolRequests.size} sent=${this.#sentToolCallIds.size}`);
1424
1469
  if (sessionID !== this.#stateSessionId) {
1425
1470
  this.#pendingToolRequests.clear();
1426
1471
  this.#multiCallGroups.clear();
@@ -1451,10 +1496,6 @@ var DuoWorkflowModel = class {
1451
1496
  const freshResults = toolResults.filter(
1452
1497
  (r) => !model.#sentToolCallIds.has(r.toolCallId)
1453
1498
  );
1454
- dlog(`phase1: ${toolResults.length} total, ${freshResults.length} fresh, sentIds=${[...model.#sentToolCallIds].join(",")}`);
1455
- for (const r of toolResults) {
1456
- dlog(` tr: id=${r.toolCallId} inSent=${model.#sentToolCallIds.has(r.toolCallId)} inPending=${model.#pendingToolRequests.has(r.toolCallId)} out=${r.output.length}b`);
1457
- }
1458
1499
  let sentToolResults = false;
1459
1500
  for (const result of freshResults) {
1460
1501
  const hashIdx = result.toolCallId.indexOf("#");
@@ -1479,11 +1520,9 @@ var DuoWorkflowModel = class {
1479
1520
  }
1480
1521
  const pending = model.#pendingToolRequests.get(result.toolCallId);
1481
1522
  if (!pending) {
1482
- dlog(`phase1: SKIP ${result.toolCallId} (not pending)`);
1483
1523
  model.#sentToolCallIds.add(result.toolCallId);
1484
1524
  continue;
1485
1525
  }
1486
- dlog(`phase1: SEND ${result.toolCallId} output=${result.output.length}b`);
1487
1526
  session.sendToolResult(result.toolCallId, result.output, result.error);
1488
1527
  sentToolResults = true;
1489
1528
  model.#sentToolCallIds.add(result.toolCallId);
@@ -1549,11 +1588,8 @@ var DuoWorkflowModel = class {
1549
1588
  try {
1550
1589
  mapped = mapDuoToolRequest(event.toolName, event.args);
1551
1590
  } catch {
1552
- dlog(`phase3: MAPPING FAILED ${event.toolName}`);
1553
1591
  continue;
1554
1592
  }
1555
- const mName = Array.isArray(mapped) ? mapped.map((m) => m.toolName).join(",") : mapped.toolName;
1556
- dlog(`phase3: tool-request ${event.toolName} \u2192 ${mName} reqId=${event.requestId}`);
1557
1593
  if (hasText) {
1558
1594
  controller.enqueue({ type: "text-end", id: textId });
1559
1595
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",