@stigmer/runner 3.2.2 → 3.3.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.
Files changed (117) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
  3. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  4. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  5. package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
  6. package/dist/activities/execute-cursor/approval-state.js +13 -1
  7. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  8. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +9 -1
  9. package/dist/activities/execute-cursor/blueprint-resolver.js +1 -0
  10. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  11. package/dist/activities/execute-cursor/hook-script.js +38 -0
  12. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  13. package/dist/activities/execute-cursor/index.d.ts +2 -0
  14. package/dist/activities/execute-cursor/index.js +56 -13
  15. package/dist/activities/execute-cursor/index.js.map +1 -1
  16. package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
  17. package/dist/activities/execute-cursor/message-translator.js +78 -2
  18. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  19. package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -1
  20. package/dist/activities/execute-cursor/prompt-builder.js +4 -0
  21. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
  23. package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
  24. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  25. package/dist/activities/execute-cursor/turn-boundary.js +15 -2
  26. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
  28. package/dist/activities/execute-deep-agent/hitl.js +48 -1
  29. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/index.js +10 -1
  31. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  32. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -0
  33. package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
  34. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
  36. package/dist/activities/execute-deep-agent/setup.js +36 -3
  37. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
  39. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  40. package/dist/activities/execute-deep-agent/status-builder.js +6 -0
  41. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
  43. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  44. package/dist/activities/workflow-event-activities.js +9 -0
  45. package/dist/activities/workflow-event-activities.js.map +1 -1
  46. package/dist/config.d.ts +8 -0
  47. package/dist/config.js +2 -0
  48. package/dist/config.js.map +1 -1
  49. package/dist/middleware/approval-gate.d.ts +20 -0
  50. package/dist/middleware/approval-gate.js +16 -1
  51. package/dist/middleware/approval-gate.js.map +1 -1
  52. package/dist/runner-manager.js +3 -0
  53. package/dist/runner-manager.js.map +1 -1
  54. package/dist/runner.js +3 -0
  55. package/dist/runner.js.map +1 -1
  56. package/dist/shared/approval-policy.d.ts +24 -1
  57. package/dist/shared/approval-policy.js +35 -1
  58. package/dist/shared/approval-policy.js.map +1 -1
  59. package/dist/shared/datastore-attachment.d.ts +79 -0
  60. package/dist/shared/datastore-attachment.js +129 -0
  61. package/dist/shared/datastore-attachment.js.map +1 -0
  62. package/dist/shared/with-timeout.d.ts +10 -0
  63. package/dist/shared/with-timeout.js +14 -1
  64. package/dist/shared/with-timeout.js.map +1 -1
  65. package/dist/workflow-engine/tasks/human-input.js +1 -0
  66. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  67. package/dist/workflow-engine/types.d.ts +22 -0
  68. package/dist/workflow-engine/types.js.map +1 -1
  69. package/dist/workflows/human-input-orchestrator.d.ts +2 -1
  70. package/dist/workflows/human-input-orchestrator.js +2 -1
  71. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  74. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  75. package/src/activities/__tests__/workflow-event-activities.test.ts +29 -0
  76. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
  77. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +25 -0
  78. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
  79. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
  80. package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
  81. package/src/activities/execute-cursor/approval-state.ts +32 -1
  82. package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
  83. package/src/activities/execute-cursor/hook-script.ts +40 -0
  84. package/src/activities/execute-cursor/index.ts +64 -15
  85. package/src/activities/execute-cursor/message-translator.ts +82 -1
  86. package/src/activities/execute-cursor/prompt-builder.ts +12 -1
  87. package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
  88. package/src/activities/execute-cursor/turn-boundary.ts +23 -0
  89. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  90. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  91. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  92. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
  93. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  94. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  95. package/src/activities/execute-deep-agent/hitl.ts +51 -0
  96. package/src/activities/execute-deep-agent/index.ts +11 -1
  97. package/src/activities/execute-deep-agent/prompt-builder.ts +9 -0
  98. package/src/activities/execute-deep-agent/setup.ts +57 -2
  99. package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
  100. package/src/activities/execute-deep-agent/status-builder.ts +7 -0
  101. package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
  102. package/src/activities/workflow-event-activities.ts +9 -0
  103. package/src/config.ts +11 -0
  104. package/src/middleware/__tests__/approval-gate.test.ts +95 -0
  105. package/src/middleware/approval-gate.ts +37 -0
  106. package/src/runner-manager.ts +3 -0
  107. package/src/runner.ts +3 -0
  108. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  109. package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
  110. package/src/shared/__tests__/with-timeout.test.ts +16 -1
  111. package/src/shared/approval-policy.ts +41 -2
  112. package/src/shared/datastore-attachment.ts +166 -0
  113. package/src/shared/with-timeout.ts +15 -1
  114. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
  115. package/src/workflow-engine/tasks/human-input.ts +1 -0
  116. package/src/workflow-engine/types.ts +23 -0
  117. package/src/workflows/human-input-orchestrator.ts +2 -1
@@ -39,7 +39,7 @@ import type { Run, ConversationTurn } from "@cursor/sdk";
39
39
 
40
40
  import type { Config } from "../../config.js";
41
41
  import { StigmerClient } from "../../client/stigmer-client.js";
42
- import { resolveAgent } from "./session-lifecycle.js";
42
+ import { resolveAgentWithTransportRecovery } from "./session-lifecycle.js";
43
43
  import type { AgentResolution, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
44
44
  import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
45
45
  import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
@@ -55,9 +55,13 @@ import { DeltaEnricher } from "./delta-enricher.js";
55
55
  import { TodoTracker } from "./todo-tracker.js";
56
56
  import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
57
57
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
58
- import { resolveMcpServers, validateMcpServerEnv } from "./mcp-resolver.js";
58
+ import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
59
+ import {
60
+ injectDatastoreAttachment,
61
+ synthesizeDatastoreAttachment,
62
+ } from "../../shared/datastore-attachment.js";
59
63
  import { mergeApprovalPolicies } from "./approval-policy.js";
60
- import { deriveActiveLeases } from "../../shared/approval-policy.js";
64
+ import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
61
65
  import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
62
66
  import { resolveExecutionEnv } from "./env-resolver.js";
63
67
  import { resolveBlueprint } from "./blueprint-resolver.js";
@@ -117,7 +121,6 @@ import type { ClassifiedError } from "./error-classifier.js";
117
121
  import { createAgent, createCloudAgent } from "./session-lifecycle.js";
118
122
  import { setMaxListeners } from "node:events";
119
123
  import { startHeartbeat } from "../../shared/heartbeat.js";
120
- import { withTimeout } from "../../shared/with-timeout.js";
121
124
  import { getShutdownSignalForQueue } from "../../runner-manager.js";
122
125
 
123
126
  /**
@@ -584,6 +587,33 @@ async function executeCursorInner(
584
587
  client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
585
588
  heartbeat, secretKeys,
586
589
  );
590
+
591
+ // Phase 4a2: Synthesize the datastore records attachment (T05).
592
+ // Deliberately AFTER resolve + backfill: the attachment has no
593
+ // McpServerUsage and reports discovered capabilities, so the
594
+ // backfill's destructiveHint tightener can never force-gate
595
+ // delete_record (which on channels would be silently skipped).
596
+ // Empty approval maps keep it approval-free by construction.
597
+ if (blueprint.datastoreUsages.length > 0) {
598
+ const scopedCredential =
599
+ (await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
600
+ ?? config.stigmerTokenRef?.current
601
+ ?? config.stigmerToken;
602
+ const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
603
+ bridgeEndpoint: config.mcpBridgeEndpoint,
604
+ credential: scopedCredential,
605
+ backendEndpoint: config.stigmerBackendEndpoint,
606
+ });
607
+ if (attachment) {
608
+ const resolvedServers = injectDatastoreAttachment(
609
+ mcpResolution.resolvedServers, attachment,
610
+ );
611
+ mcpResolution = {
612
+ resolvedServers,
613
+ cursorConfig: toCursorMcpConfig(resolvedServers),
614
+ };
615
+ }
616
+ }
587
617
  const mcpConfig = mcpResolution.cursorConfig;
588
618
 
589
619
  // Phase 4b: Merge approval policies from all layers.
@@ -722,6 +752,11 @@ async function executeCursorInner(
722
752
  // absent a git tree keeps gating gitignored writes and a non-git workspace
723
753
  // falls back to the deny-gate entirely (no regression).
724
754
  const captureIgnored = captureMode && !!artifactStorage;
755
+ // Unattended approval mode (DD-014): approver-less surfaces (channels,
756
+ // guest shares) stamp APPROVAL_MODE_UNATTENDED; the hook then records
757
+ // approval denials with the non-pausing "unattended" kind, so the
758
+ // first-denial stop never fires and the turn boundary settles the denied
759
+ // calls as SKIPPED instead of pausing a turn nobody can approve.
725
760
  const approvalState = buildApprovalState(
726
761
  mergedPolicies,
727
762
  globalBypass,
@@ -730,6 +765,7 @@ async function executeCursorInner(
730
765
  captureMode,
731
766
  captureIgnored,
732
767
  gitWorkspace,
768
+ isUnattendedApprovalMode(execution),
733
769
  );
734
770
  const hitlGate = await installHitlGate({
735
771
  workspaceRoot: primaryWorkspaceDir,
@@ -842,21 +878,29 @@ async function executeCursorInner(
842
878
 
843
879
  // Agent.create/Agent.resume have no timeout of their own — a degraded
844
880
  // transport (dead proxy connection, stale HTTP/2 session) hangs them
845
- // forever, which the periodic heartbeat would happily keep alive. The
846
- // bound converts that hang into an immediate, named transport failure.
847
- // The message carries "timed out" so the error classifier's network
848
- // patterns mark it retryable.
881
+ // forever, which the periodic heartbeat would happily keep alive. Each
882
+ // attempt is bounded; on expiry the wrapper resets the proxy transport
883
+ // and retries once, so a stale-session hang recovers without failing the
884
+ // execution. A second expiry propagates a plain Error to the generic
885
+ // catch below, which persists EXECUTION_FAILED (no Temporal retry —
886
+ // the activity returns rather than throws, and maximumAttempts is 1).
849
887
  heartbeatPhase = "resolving_agent";
850
888
  const resolveTimeoutSeconds = Math.round(config.agentResolveTimeoutMs / 1000);
851
- let resolution: AgentResolution = await withTimeout(
852
- config.agentResolveTimeoutMs,
853
- () =>
889
+ let resolution: AgentResolution = await resolveAgentWithTransportRecovery({
890
+ harnessStateId: threadId,
891
+ createOptions,
892
+ mode: agentMode,
893
+ timeoutMs: config.agentResolveTimeoutMs,
894
+ buildTimeoutMessage: (finalAttempt) =>
854
895
  `Cursor agent ${threadId ? "resume" : "create"} timed out after ${resolveTimeoutSeconds}s ` +
855
896
  `(${config.proxyEndpoint ? `via proxy ${config.proxyEndpoint}` : "direct Cursor API connection"}). ` +
856
- `The transport connection is likely dead. Retry the message; if this persists, ` +
857
- `check proxy and network health.`,
858
- () => resolveAgent(threadId, createOptions, agentMode),
859
- );
897
+ `The transport connection is likely dead. ` +
898
+ (finalAttempt
899
+ ? `An automatic retry on a fresh transport connection also timed out. ` +
900
+ `Retry the message later; if this persists, check proxy and network health.`
901
+ : `Resetting the transport and retrying automatically.`),
902
+ resetTransport: closeProxySessions,
903
+ });
860
904
 
861
905
  console.log(
862
906
  `ExecuteCursor agent resolved: execution=${executionId}, ` +
@@ -907,6 +951,7 @@ async function executeCursorInner(
907
951
  instructions: blueprint.instructions,
908
952
  userMessage: spec.message,
909
953
  skills: skillMetadata,
954
+ datastoreUsages: blueprint.datastoreUsages,
910
955
  subAgents: blueprint.subAgents,
911
956
  workspaceDirs: blueprint.workspaceDirs,
912
957
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
@@ -1487,6 +1532,7 @@ async function executeCursorInner(
1487
1532
  instructions: blueprint.instructions,
1488
1533
  userMessage: spec.message,
1489
1534
  skills: skillMetadata,
1535
+ datastoreUsages: blueprint.datastoreUsages,
1490
1536
  subAgents: blueprint.subAgents,
1491
1537
  workspaceDirs: blueprint.workspaceDirs,
1492
1538
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
@@ -2043,6 +2089,8 @@ export interface BuildPromptInput {
2043
2089
  instructions: string;
2044
2090
  userMessage: string;
2045
2091
  skills: import("./prompt-builder.js").SkillMetadata[];
2092
+ /** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
2093
+ datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
2046
2094
  subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
2047
2095
  workspaceDirs: string[];
2048
2096
  workspaceFileRefs: string[];
@@ -2142,6 +2190,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2142
2190
  instructions,
2143
2191
  userMessage,
2144
2192
  skills,
2193
+ datastoreUsages: input.datastoreUsages ?? [],
2145
2194
  subAgents,
2146
2195
  workspaceDirs,
2147
2196
  workspaceFileRefs,
@@ -36,7 +36,7 @@ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/a
36
36
  import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
37
37
  import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
38
38
  import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
39
- import { MessageType, ToolCallStatus, SubAgentStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
39
+ import { ApprovalPolicySource, MessageType, ToolCallStatus, SubAgentStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
40
40
  import type { SDKMessage } from "@cursor/sdk";
41
41
  import type { MergedToolPolicy } from "./approval-policy.js";
42
42
  import { lookupMcpToolPolicy, resolveApprovalMessage, builtInRequiresApproval, getBuiltInApprovalMessage, SALIENT_ARG_FIELDS } from "./approval-policy.js";
@@ -44,6 +44,7 @@ import {
44
44
  POLICY_ENGINE_VERSION,
45
45
  resolveApprovalProvenance,
46
46
  toProtoPolicySource,
47
+ unattendedSkipMessage,
47
48
  } from "../../shared/approval-policy.js";
48
49
  import {
49
50
  approvalDenials,
@@ -1916,6 +1917,86 @@ export function detectUnattributedHookBlocks(
1916
1917
  return blocks;
1917
1918
  }
1918
1919
 
1920
+ /**
1921
+ * Stamp the tool calls the hook denied under UNATTENDED approval mode
1922
+ * (DD-014) as terminal TOOL_CALL_SKIPPED rows with UNATTENDED_SKIP
1923
+ * provenance — the Cursor twin of the native harness's
1924
+ * `reconcileUnattendedSkips`, so both harnesses persist the same honest
1925
+ * shape for a platform-resolved skip.
1926
+ *
1927
+ * An unattended denial never pauses the run (its ledger kind is excluded
1928
+ * from {@link approvalDenials}), so the stream leaves the denied call as a
1929
+ * FAILED row carrying Cursor's generic hook-block error — dishonest ("the
1930
+ * tool broke") and alarming in a transcript an org admin reviews. This pass
1931
+ * correlates the unattended ledger entries to their streamed calls with the
1932
+ * SAME two identities the rest of the file uses — exact token, then
1933
+ * (category, workspace-normalized path) for the abs-vs-rel drift — and
1934
+ * settles each match to SKIPPED with a result explaining the skip.
1935
+ *
1936
+ * Scope: hook-blocked FAILED rows and still-non-terminal (PENDING/RUNNING)
1937
+ * rows only — a COMPLETED row is a real execution and is never rewritten
1938
+ * (an unattended deny cannot produce one). `approval_action`/`approved_by`
1939
+ * stay untouched: server-owned, human-decision-only fields (DD-014 D-e).
1940
+ * Unmatched ledger entries need no synthesis — there is no pause to
1941
+ * surface; the model already saw the deny and adapted in-turn.
1942
+ *
1943
+ * Returns how many tool calls were stamped.
1944
+ */
1945
+ export function stampUnattendedSkippedToolCalls(
1946
+ messages: readonly AgentMessage[],
1947
+ subAgentExecutions: readonly SubAgentExecution[],
1948
+ unattendedLedger: readonly DeniedLedgerEntry[],
1949
+ workspaceRoot?: string,
1950
+ ): number {
1951
+ if (unattendedLedger.length === 0) return 0;
1952
+
1953
+ const ledgerTokens = new Set(unattendedLedger.map((e) => e.token));
1954
+ const ledgerNormalizedSalients = new Set<string>();
1955
+ if (workspaceRoot) {
1956
+ for (const entry of unattendedLedger) {
1957
+ const decoded = decodeIdentityToken(entry.token);
1958
+ if (!decoded) continue;
1959
+ const normalized = normalizedFileSalient(decoded.key, decoded.salient, workspaceRoot);
1960
+ if (normalized) ledgerNormalizedSalients.add(normalized);
1961
+ }
1962
+ }
1963
+
1964
+ const matchesLedger = (tc: ToolCall): boolean => {
1965
+ if (ledgerTokens.has(toolCallIdentityToken(tc))) return true;
1966
+ if (!workspaceRoot) return false;
1967
+ const id = toolIdentity(tc.name, tc.mcpServerSlug, toolCallArgs(tc));
1968
+ const normalized = normalizedFileSalient(id.key, id.salient, workspaceRoot);
1969
+ return !!normalized && ledgerNormalizedSalients.has(normalized);
1970
+ };
1971
+
1972
+ let stamped = 0;
1973
+ const apply = (msgs: readonly AgentMessage[]): void => {
1974
+ for (const msg of msgs) {
1975
+ for (const tc of msg.toolCalls) {
1976
+ const deniedShape =
1977
+ (tc.status === ToolCallStatus.TOOL_CALL_FAILED && isHookBlockError(tc.error)) ||
1978
+ tc.status === ToolCallStatus.TOOL_CALL_PENDING ||
1979
+ tc.status === ToolCallStatus.TOOL_CALL_RUNNING;
1980
+ if (!deniedShape || !matchesLedger(tc)) continue;
1981
+ tc.status = ToolCallStatus.TOOL_CALL_SKIPPED;
1982
+ tc.approvalPolicySource = ApprovalPolicySource.UNATTENDED_SKIP;
1983
+ tc.policyEngineVersion = POLICY_ENGINE_VERSION;
1984
+ tc.error = "";
1985
+ tc.result = unattendedSkipMessage(tc.name);
1986
+ tc.isStreaming = false;
1987
+ if (!tc.completedAt) tc.completedAt = utcTimestamp();
1988
+ stamped++;
1989
+ }
1990
+ }
1991
+ };
1992
+
1993
+ apply(messages);
1994
+ for (const subAgent of subAgentExecutions) {
1995
+ apply(subAgent.messages);
1996
+ }
1997
+ return stamped;
1998
+ }
1999
+
1919
2000
  /**
1920
2001
  * Compute a streamed tool call's identity token in the same canonical space the
1921
2002
  * preToolUse hook records denials in (see {@link toolIdentity} / primaryToken).
@@ -17,10 +17,11 @@
17
17
  */
18
18
 
19
19
  import { resolve } from "node:path";
20
- import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
20
+ import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
21
21
  import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
22
22
  import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
23
23
  import { formatContextBridgeText } from "../../shared/context-bridge.js";
24
+ import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
24
25
  import {
25
26
  formatSenderIdentityText,
26
27
  type SenderIdentity,
@@ -53,6 +54,12 @@ export interface EnhancedPromptOptions {
53
54
  instructions: string;
54
55
  userMessage: string;
55
56
  skills: SkillMetadata[];
57
+ /**
58
+ * Datastores attached via `datastore_usages` — rendered as the
59
+ * `<available_datastores>` section (DD-005 SD-5, skills precedent)
60
+ * pointing the model at the synthesized record tools.
61
+ */
62
+ datastoreUsages?: DatastoreUsage[];
56
63
  subAgents: SubAgent[];
57
64
  workspaceDirs: string[];
58
65
  workspaceFileRefs: string[];
@@ -113,6 +120,10 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
113
120
  sections.push(formatSkillsSection(options.skills));
114
121
  }
115
122
 
123
+ if (options.datastoreUsages !== undefined && options.datastoreUsages.length > 0) {
124
+ sections.push(formatDatastoresSection(options.datastoreUsages));
125
+ }
126
+
116
127
  if (options.subAgents.length > 0) {
117
128
  sections.push(formatSubAgentsSection(options.subAgents));
118
129
  }
@@ -41,6 +41,7 @@ import { join } from "node:path";
41
41
 
42
42
  import { Agent } from "@cursor/sdk";
43
43
  import type { SDKAgent, CursorAgentPlatformOptions, AgentDefinition } from "@cursor/sdk";
44
+ import { withTimeout, TimeoutError } from "../../shared/with-timeout.js";
44
45
  import type { CursorMcpServerConfig } from "./mcp-resolver.js";
45
46
 
46
47
  // ---------------------------------------------------------------------------
@@ -437,6 +438,81 @@ export async function resolveAgent(
437
438
  };
438
439
  }
439
440
 
441
+ // ---------------------------------------------------------------------------
442
+ // Transport-recovery wrapper
443
+ // ---------------------------------------------------------------------------
444
+
445
+ export interface ResolveWithTransportRecoveryOptions {
446
+ harnessStateId: string;
447
+ createOptions: CreateAgentOptions | CreateCloudAgentOptions;
448
+ mode: "local" | "cloud";
449
+ /** Bound applied to each attempt independently. */
450
+ timeoutMs: number;
451
+ /**
452
+ * Builds the timeout rejection message. `finalAttempt` is true when no
453
+ * further automatic retry will follow, so the message can be honest about
454
+ * whether "retry" means the system or the user.
455
+ */
456
+ buildTimeoutMessage: (finalAttempt: boolean) => string;
457
+ /**
458
+ * Closes the transport the hung attempt is riding on so the next attempt
459
+ * dials fresh. Injected (rather than imported from the http2 interceptor)
460
+ * to keep this module transport-agnostic and the recovery unit-testable.
461
+ */
462
+ resetTransport: () => void;
463
+ }
464
+
465
+ /**
466
+ * Resolve a Cursor Agent with one automatic recovery from a transport hang.
467
+ *
468
+ * Agent.create/Agent.resume have no timeout of their own — a degraded
469
+ * transport (dead proxy connection, stale HTTP/2 session) hangs them forever.
470
+ * Each attempt is bounded by `timeoutMs`; when the first attempt expires,
471
+ * the transport is reset and the full resolveAgent() is retried once. Only
472
+ * a TimeoutError triggers recovery: deterministic failures (auth, validation)
473
+ * propagate immediately — resetting the transport cannot fix them.
474
+ *
475
+ * The retry re-invokes resolveAgent(), not createAgent() directly: at
476
+ * resolve time only the transport is suspect, not the agent handle, so a
477
+ * timed-out resume is retried resume-first and conversation context is
478
+ * preserved. (This deliberately diverges from the stream-phase recovery in
479
+ * index.ts, which jumps to a fresh create because there the handle itself
480
+ * has already failed a run.)
481
+ *
482
+ * Orphan semantics: withTimeout bounds the wait, not the work, so the first
483
+ * attempt's promise survives its expiry. resetTransport() closes the HTTP/2
484
+ * session that attempt is riding on, so the orphan rejects promptly and
485
+ * withTimeout's attached catch absorbs the late rejection (settled promise —
486
+ * no unhandledRejection). In the residual case where the orphan completes
487
+ * before the reset, it leaves an agent that never receives a prompt: no LLM
488
+ * cost, no state impact, cleaned up with the sandbox.
489
+ */
490
+ export async function resolveAgentWithTransportRecovery(
491
+ opts: ResolveWithTransportRecoveryOptions,
492
+ ): Promise<AgentResolution> {
493
+ const attempt = (finalAttempt: boolean) =>
494
+ withTimeout(
495
+ opts.timeoutMs,
496
+ () => opts.buildTimeoutMessage(finalAttempt),
497
+ () => resolveAgent(opts.harnessStateId, opts.createOptions, opts.mode),
498
+ );
499
+
500
+ try {
501
+ return await attempt(false);
502
+ } catch (err) {
503
+ if (!(err instanceof TimeoutError)) throw err;
504
+
505
+ console.warn(
506
+ `resolveAgentWithTransportRecovery: agent resolution timed out after ` +
507
+ `${opts.timeoutMs}ms (sessionId=${opts.createOptions.sessionId}, mode=${opts.mode}, ` +
508
+ `resume=${!!opts.harnessStateId}) — resetting transport and retrying once`,
509
+ );
510
+ opts.resetTransport();
511
+
512
+ return attempt(true);
513
+ }
514
+ }
515
+
440
516
  /**
441
517
  * Dispose a Cursor Agent when a session is deleted.
442
518
  * Best-effort: logs and swallows errors.
@@ -42,6 +42,7 @@ import {
42
42
  denialKindOf,
43
43
  primaryToken,
44
44
  readDenialLedger,
45
+ unattendedDenials,
45
46
  type ApprovalGrant,
46
47
  } from "./approval-state.js";
47
48
  import { deriveTurnCommandProvenance } from "./command-provenance.js";
@@ -50,6 +51,7 @@ import {
50
51
  clearProvisionalPostDenialNarration,
51
52
  detectUnattributedHookBlocks,
52
53
  reconcileDeniedToolCalls,
54
+ stampUnattendedSkippedToolCalls,
53
55
  type UnattributedHookBlock,
54
56
  } from "./message-translator.js";
55
57
 
@@ -302,6 +304,27 @@ export async function runTurnBoundary(opts: TurnBoundaryOptions): Promise<TurnBo
302
304
  }
303
305
  }
304
306
 
307
+ // Unattended approval mode (DD-014): denials the hook resolved with the
308
+ // non-pausing "unattended" kind never became gates above; settle their
309
+ // streamed rows (FAILED-with-hook-error or interrupted non-terminal) to
310
+ // honest TOOL_CALL_SKIPPED + UNATTENDED_SKIP provenance — the same shape
311
+ // the native harness's reconcileUnattendedSkips persists. Runs before the
312
+ // #205 attribution pass, which then never sees them as hook-block FAILED
313
+ // rows (they are in the full ledger regardless, so attribution was safe
314
+ // either way).
315
+ const stampedUnattended = stampUnattendedSkippedToolCalls(
316
+ status.messages,
317
+ status.subAgentExecutions,
318
+ unattendedDenials(deniedLedger),
319
+ primaryWorkspaceDir,
320
+ );
321
+ if (stampedUnattended > 0) {
322
+ console.log(
323
+ `ExecuteCursor turn boundary: ${stampedUnattended} unattended-mode tool call(s) ` +
324
+ `settled as SKIPPED (no approver on this surface; execution=${executionId})`,
325
+ );
326
+ }
327
+
305
328
  // Issue #205 invariant: a blocked tool must never silently complete. Match
306
329
  // this turn's hook-blocked FAILED rows against the FULL ledger (all kinds) —
307
330
  // anything left over was denied by a hook that is not ours (or by our hook
@@ -239,6 +239,7 @@ const httpConfig: Config = {
239
239
  temporalAddress: "localhost:7233",
240
240
  temporalNamespace: "default",
241
241
  stigmerBackendEndpoint: "http://localhost:7234",
242
+ mcpBridgeEndpoint: null,
242
243
  stigmerToken: null,
243
244
  cursorApiKey: "",
244
245
  workspaceRootDir: "/tmp/test",
@@ -284,6 +284,7 @@ const baseConfig: Config = {
284
284
  temporalAddress: "localhost:7233",
285
285
  temporalNamespace: "default",
286
286
  stigmerBackendEndpoint: "http://localhost:7234",
287
+ mcpBridgeEndpoint: null,
287
288
  stigmerToken: null,
288
289
  cursorApiKey: "",
289
290
  workspaceRootDir: "/tmp/test",
@@ -238,6 +238,7 @@ const baseConfig: Config = {
238
238
  temporalAddress: "localhost:7233",
239
239
  temporalNamespace: "default",
240
240
  stigmerBackendEndpoint: "http://localhost:7234",
241
+ mcpBridgeEndpoint: null,
241
242
  stigmerToken: null,
242
243
  cursorApiKey: "",
243
244
  workspaceRootDir: "/tmp/test",
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { Command } from "@langchain/langgraph";
3
- import { resolveResumeInput, reconcileNonExecutingDecisions } from "../hitl.js";
4
- import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
3
+ import { resolveResumeInput, reconcileNonExecutingDecisions, reconcileUnattendedSkips } from "../hitl.js";
4
+ import { ApprovalAction, ApprovalPolicySource, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
5
  import { create } from "@bufbuild/protobuf";
6
6
  import {
7
7
  AgentMessageSchema,
@@ -238,3 +238,104 @@ describe("reconcileNonExecutingDecisions", () => {
238
238
  expect(status.messages[0].toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
239
239
  });
240
240
  });
241
+
242
+ describe("reconcileUnattendedSkips (DD-014)", () => {
243
+ function statusWithCalls(
244
+ toolCalls: Array<{ id: string; status: ToolCallStatus; result?: string }>,
245
+ ) {
246
+ return create(AgentExecutionStatusSchema, {
247
+ messages: [
248
+ create(AgentMessageSchema, {
249
+ toolCalls: toolCalls.map(tc =>
250
+ create(ToolCallSchema, {
251
+ id: tc.id,
252
+ name: "gated_tool",
253
+ status: tc.status,
254
+ result: tc.result ?? "",
255
+ }),
256
+ ),
257
+ }),
258
+ ],
259
+ });
260
+ }
261
+
262
+ it("terminalizes a registry-recorded call to SKIPPED with UNATTENDED_SKIP provenance", () => {
263
+ // The stream saw the gate's skip ToolMessage as a normal tool result and
264
+ // marked the call COMPLETED — the reconciler owns the honest terminal shape.
265
+ const status = statusWithCalls([
266
+ { id: "call-1", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "was skipped" },
267
+ ]);
268
+
269
+ reconcileUnattendedSkips(status, new Set(["call-1"]));
270
+
271
+ const tc = status.messages[0].toolCalls[0];
272
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
273
+ expect(tc.approvalPolicySource).toBe(ApprovalPolicySource.UNATTENDED_SKIP);
274
+ expect(tc.policyEngineVersion).not.toBe("");
275
+ // Server-owned human-decision fields stay untouched (DD-014 D-e).
276
+ expect(tc.approvalAction).toBe(ApprovalAction.UNSPECIFIED);
277
+ expect(tc.approvedBy).toBe("");
278
+ });
279
+
280
+ it("backfills the skip result on a row whose tool events never fired", () => {
281
+ const status = statusWithCalls([
282
+ { id: "call-1", status: ToolCallStatus.TOOL_CALL_RUNNING },
283
+ ]);
284
+
285
+ reconcileUnattendedSkips(status, new Set(["call-1"]));
286
+
287
+ const tc = status.messages[0].toolCalls[0];
288
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
289
+ expect(tc.result).toContain("skipped automatically");
290
+ });
291
+
292
+ it("touches only registry entries and no-ops on an empty/absent registry", () => {
293
+ const status = statusWithCalls([
294
+ { id: "call-1", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "real result" },
295
+ ]);
296
+
297
+ reconcileUnattendedSkips(status, new Set(["other-call"]));
298
+ reconcileUnattendedSkips(status, new Set());
299
+ reconcileUnattendedSkips(status, undefined);
300
+
301
+ const tc = status.messages[0].toolCalls[0];
302
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
303
+ expect(tc.result).toBe("real result");
304
+ expect(tc.approvalPolicySource).toBe(ApprovalPolicySource.UNSPECIFIED);
305
+ });
306
+
307
+ it("covers sub-agent transcripts (sub-agent gates share the parent registry)", () => {
308
+ const status = create(AgentExecutionStatusSchema, {
309
+ messages: [],
310
+ subAgentExecutions: [{
311
+ messages: [
312
+ create(AgentMessageSchema, {
313
+ toolCalls: [create(ToolCallSchema, {
314
+ id: "sub-call-1",
315
+ name: "gated_tool",
316
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
317
+ })],
318
+ }),
319
+ ],
320
+ }],
321
+ });
322
+
323
+ reconcileUnattendedSkips(status, new Set(["sub-call-1"]));
324
+
325
+ const tc = status.subAgentExecutions[0].messages[0].toolCalls[0];
326
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
327
+ expect(tc.approvalPolicySource).toBe(ApprovalPolicySource.UNATTENDED_SKIP);
328
+ });
329
+
330
+ it("is idempotent", () => {
331
+ const status = statusWithCalls([
332
+ { id: "call-1", status: ToolCallStatus.TOOL_CALL_COMPLETED },
333
+ ]);
334
+
335
+ reconcileUnattendedSkips(status, new Set(["call-1"]));
336
+ const after = JSON.stringify(status.messages[0].toolCalls[0]);
337
+ reconcileUnattendedSkips(status, new Set(["call-1"]));
338
+
339
+ expect(JSON.stringify(status.messages[0].toolCalls[0])).toBe(after);
340
+ });
341
+ });
@@ -35,6 +35,7 @@ describe("ExecuteDeepAgent activity", () => {
35
35
  temporalAddress: "localhost:7233",
36
36
  temporalNamespace: "default",
37
37
  stigmerBackendEndpoint: "http://localhost:7234",
38
+ mcpBridgeEndpoint: null,
38
39
  stigmerToken: null,
39
40
  cursorApiKey: "",
40
41
  workspaceRootDir: "/tmp/test",
@@ -244,6 +244,7 @@ const memoryConfig: Config = {
244
244
  temporalAddress: "localhost:7233",
245
245
  temporalNamespace: "default",
246
246
  stigmerBackendEndpoint: "http://localhost:7234",
247
+ mcpBridgeEndpoint: null,
247
248
  stigmerToken: null,
248
249
  cursorApiKey: "",
249
250
  workspaceRootDir: "/tmp/test",
@@ -17,9 +17,11 @@ import { Command } from "@langchain/langgraph";
17
17
  import type { AgentExecution, AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
18
18
  import {
19
19
  ApprovalAction,
20
+ ApprovalPolicySource,
20
21
  ToolCallStatus,
21
22
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
22
23
  import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
24
+ import { POLICY_ENGINE_VERSION, unattendedSkipMessage } from "../../shared/approval-policy.js";
23
25
 
24
26
  // APPROVE_ALL resumes the interrupted tool exactly like APPROVE. Its
25
27
  // "auto-approve the rest of the run" effect is realized in setup.ts (the
@@ -233,3 +235,52 @@ export function reconcileNonExecutingDecisions(status: AgentExecutionStatus): vo
233
235
  apply(subAgent.messages);
234
236
  }
235
237
  }
238
+
239
+ /**
240
+ * Terminalize every tool call the approval gate auto-skipped under UNATTENDED
241
+ * approval mode — the sibling of {@link reconcileNonExecutingDecisions} for
242
+ * skips that have no human decision behind them.
243
+ *
244
+ * The gate (the single writer of the registry) records each auto-skipped
245
+ * tool-call id at the moment it returns the skip ToolMessage; this reconciler
246
+ * (the single writer of the terminal row) folds each id into:
247
+ * - `status = TOOL_CALL_SKIPPED` — the tool did not run, whatever transient
248
+ * status the stream left behind (COMPLETED from a tool_finished that carried
249
+ * the skip message, or RUNNING when no tool events fired);
250
+ * - `approval_policy_source = UNATTENDED_SKIP` — the resolution layer,
251
+ * overriding the gating-layer source stamped at tool-start (the
252
+ * AUTO_APPROVE_ALL precedent: layer-4 resolutions own the resolved call);
253
+ * - a result backfilled from {@link unattendedSkipMessage} when the stream
254
+ * delivered none, so the transcript row is never blank.
255
+ *
256
+ * `approval_action` / `approved_by` are deliberately NOT touched — those are
257
+ * server-owned fields recording HUMAN decisions only (DD-014 D-e). No
258
+ * approval-request event exists for these calls, so the pending-approvals
259
+ * projection stays empty by construction. Idempotent: re-running re-resolves
260
+ * identically. Covers sub-agent transcripts because sub-agent gates inherit
261
+ * the parent's registry instance.
262
+ */
263
+ export function reconcileUnattendedSkips(
264
+ status: AgentExecutionStatus,
265
+ unattendedSkips: ReadonlySet<string> | undefined,
266
+ ): void {
267
+ if (!unattendedSkips || unattendedSkips.size === 0) return;
268
+
269
+ const apply = (messages: readonly AgentMessage[]): void => {
270
+ for (const msg of messages) {
271
+ for (const tc of msg.toolCalls) {
272
+ if (!unattendedSkips.has(tc.id)) continue;
273
+ tc.status = ToolCallStatus.TOOL_CALL_SKIPPED;
274
+ tc.approvalPolicySource = ApprovalPolicySource.UNATTENDED_SKIP;
275
+ tc.policyEngineVersion = POLICY_ENGINE_VERSION;
276
+ tc.isStreaming = false;
277
+ if (!tc.result) tc.result = unattendedSkipMessage(tc.name);
278
+ }
279
+ }
280
+ };
281
+
282
+ apply(status.messages);
283
+ for (const subAgent of status.subAgentExecutions) {
284
+ apply(subAgent.messages);
285
+ }
286
+ }