@stigmer/runner 3.2.1 → 3.2.3

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 (145) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-transform.js +12 -2
  3. package/dist/activities/call-transform.js.map +1 -1
  4. package/dist/activities/call-validate.js +20 -2
  5. package/dist/activities/call-validate.js.map +1 -1
  6. package/dist/activities/discover-mcp-server.js +6 -20
  7. package/dist/activities/discover-mcp-server.js.map +1 -1
  8. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
  9. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  10. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  11. package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
  12. package/dist/activities/execute-cursor/approval-state.js +13 -1
  13. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  14. package/dist/activities/execute-cursor/hook-script.js +38 -0
  15. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  16. package/dist/activities/execute-cursor/index.d.ts +12 -0
  17. package/dist/activities/execute-cursor/index.js +72 -15
  18. package/dist/activities/execute-cursor/index.js.map +1 -1
  19. package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
  20. package/dist/activities/execute-cursor/message-translator.js +78 -2
  21. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/prompt-builder.d.ts +19 -0
  23. package/dist/activities/execute-cursor/prompt-builder.js +21 -0
  24. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  25. package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
  26. package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
  27. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  28. package/dist/activities/execute-cursor/turn-boundary.js +15 -2
  29. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
  31. package/dist/activities/execute-deep-agent/hitl.js +48 -1
  32. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/index.js +10 -1
  34. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +19 -0
  36. package/dist/activities/execute-deep-agent/prompt-builder.js +12 -0
  37. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
  39. package/dist/activities/execute-deep-agent/setup.js +16 -1
  40. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
  42. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  43. package/dist/activities/execute-deep-agent/status-builder.js +6 -0
  44. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  45. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
  46. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  47. package/dist/activities/workflow-event-activities.js +11 -0
  48. package/dist/activities/workflow-event-activities.js.map +1 -1
  49. package/dist/config.d.ts +17 -0
  50. package/dist/config.js +14 -0
  51. package/dist/config.js.map +1 -1
  52. package/dist/middleware/approval-gate.d.ts +20 -0
  53. package/dist/middleware/approval-gate.js +16 -1
  54. package/dist/middleware/approval-gate.js.map +1 -1
  55. package/dist/runner-manager.d.ts +2 -0
  56. package/dist/runner-manager.js +2 -1
  57. package/dist/runner-manager.js.map +1 -1
  58. package/dist/runner.d.ts +2 -0
  59. package/dist/runner.js +2 -1
  60. package/dist/runner.js.map +1 -1
  61. package/dist/shared/approval-policy.d.ts +24 -1
  62. package/dist/shared/approval-policy.js +35 -1
  63. package/dist/shared/approval-policy.js.map +1 -1
  64. package/dist/shared/context-bridge.d.ts +30 -0
  65. package/dist/shared/context-bridge.js +45 -0
  66. package/dist/shared/context-bridge.js.map +1 -0
  67. package/dist/shared/sender-identity.d.ts +50 -0
  68. package/dist/shared/sender-identity.js +69 -0
  69. package/dist/shared/sender-identity.js.map +1 -0
  70. package/dist/shared/with-timeout.d.ts +27 -0
  71. package/dist/shared/with-timeout.js +47 -0
  72. package/dist/shared/with-timeout.js.map +1 -0
  73. package/dist/workflow-engine/do-executor.d.ts +7 -0
  74. package/dist/workflow-engine/do-executor.js +59 -1
  75. package/dist/workflow-engine/do-executor.js.map +1 -1
  76. package/dist/workflow-engine/tasks/call-function.js +68 -1
  77. package/dist/workflow-engine/tasks/call-function.js.map +1 -1
  78. package/dist/workflow-engine/tasks/human-input.js +1 -0
  79. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  80. package/dist/workflow-engine/types.d.ts +48 -0
  81. package/dist/workflow-engine/types.js.map +1 -1
  82. package/dist/workflows/engine-core.js +5 -1
  83. package/dist/workflows/engine-core.js.map +1 -1
  84. package/dist/workflows/human-input-orchestrator.d.ts +2 -1
  85. package/dist/workflows/human-input-orchestrator.js +2 -1
  86. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  87. package/package.json +2 -2
  88. package/src/__tests__/config.test.ts +8 -0
  89. package/src/activities/__tests__/call-validate.test.ts +137 -0
  90. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  91. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  92. package/src/activities/__tests__/workflow-event-activities.test.ts +89 -0
  93. package/src/activities/call-transform.ts +20 -2
  94. package/src/activities/call-validate.ts +27 -2
  95. package/src/activities/discover-mcp-server.ts +7 -28
  96. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
  97. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +65 -0
  98. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
  99. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
  100. package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
  101. package/src/activities/execute-cursor/approval-state.ts +32 -1
  102. package/src/activities/execute-cursor/hook-script.ts +40 -0
  103. package/src/activities/execute-cursor/index.ts +86 -17
  104. package/src/activities/execute-cursor/message-translator.ts +82 -1
  105. package/src/activities/execute-cursor/prompt-builder.ts +44 -0
  106. package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
  107. package/src/activities/execute-cursor/turn-boundary.ts +23 -0
  108. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  109. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  110. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  111. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
  112. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  113. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +58 -0
  114. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  115. package/src/activities/execute-deep-agent/hitl.ts +51 -0
  116. package/src/activities/execute-deep-agent/index.ts +11 -1
  117. package/src/activities/execute-deep-agent/prompt-builder.ts +35 -0
  118. package/src/activities/execute-deep-agent/setup.ts +31 -0
  119. package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
  120. package/src/activities/execute-deep-agent/status-builder.ts +7 -0
  121. package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
  122. package/src/activities/workflow-event-activities.ts +11 -0
  123. package/src/config.ts +23 -0
  124. package/src/middleware/__tests__/approval-gate.test.ts +95 -0
  125. package/src/middleware/approval-gate.ts +37 -0
  126. package/src/runner-manager.ts +6 -1
  127. package/src/runner.ts +6 -1
  128. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  129. package/src/shared/__tests__/context-bridge.test.ts +51 -0
  130. package/src/shared/__tests__/sender-identity.test.ts +92 -0
  131. package/src/shared/__tests__/with-timeout.test.ts +60 -0
  132. package/src/shared/approval-policy.ts +41 -2
  133. package/src/shared/context-bridge.ts +51 -0
  134. package/src/shared/sender-identity.ts +85 -0
  135. package/src/shared/with-timeout.ts +53 -0
  136. package/src/workflow-engine/__tests__/do-executor.test.ts +155 -0
  137. package/src/workflow-engine/__tests__/tasks/call-function.test.ts +94 -0
  138. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
  139. package/src/workflow-engine/do-executor.ts +65 -1
  140. package/src/workflow-engine/tasks/call-function.ts +84 -3
  141. package/src/workflow-engine/tasks/human-input.ts +1 -0
  142. package/src/workflow-engine/types.ts +50 -0
  143. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +1 -0
  144. package/src/workflows/engine-core.ts +5 -1
  145. package/src/workflows/human-input-orchestrator.ts +2 -1
@@ -47,6 +47,7 @@ describe("ExecuteDeepAgent activity", () => {
47
47
  checkpointerProxyEndpoint: null,
48
48
  primaryModel: "gpt-4.1",
49
49
  cursorStreamStallTimeoutMs: 180000,
50
+ agentResolveTimeoutMs: 120000,
50
51
  workspaceLockTimeoutMs: 900000,
51
52
  };
52
53
 
@@ -208,6 +208,64 @@ describe("buildEnhancedSystemPrompt", () => {
208
208
  expect(prompt).not.toContain("## Workspace");
209
209
  });
210
210
 
211
+ describe("rollover context bridge (DD-013)", () => {
212
+ const base = {
213
+ instructions: "Test",
214
+ provisionResults: [],
215
+ containerRoot: "",
216
+ skillsPromptSection: "",
217
+ workspaceFileRefs: [],
218
+ workspaceRoot: "",
219
+ injectedFiles: [],
220
+ };
221
+
222
+ it("appends the bridge as standing session context (every-turn injection)", () => {
223
+ const prompt = buildEnhancedSystemPrompt({
224
+ ...base,
225
+ contextBridge: "Subject: Orders\nUser: where is my order?\nAssistant: Shipped.",
226
+ });
227
+
228
+ expect(prompt).toContain("## Previous conversation context");
229
+ expect(prompt).toContain("User: where is my order?");
230
+ expect(prompt).toContain("Do not repeat it back");
231
+ });
232
+
233
+ it("omits the section when the session carries no bridge", () => {
234
+ const prompt = buildEnhancedSystemPrompt(base);
235
+
236
+ expect(prompt).not.toContain("## Previous conversation context");
237
+ });
238
+ });
239
+
240
+ describe("channel sender identity", () => {
241
+ const base = {
242
+ instructions: "Test",
243
+ provisionResults: [],
244
+ containerRoot: "",
245
+ skillsPromptSection: "",
246
+ workspaceFileRefs: [],
247
+ workspaceRoot: "",
248
+ injectedFiles: [],
249
+ };
250
+
251
+ it("appends the sender as standing session context (every-turn injection)", () => {
252
+ const prompt = buildEnhancedSystemPrompt({
253
+ ...base,
254
+ senderIdentity: { value: "15550001111", kind: "whatsapp_phone" },
255
+ });
256
+
257
+ expect(prompt).toContain("## Conversation sender");
258
+ expect(prompt).toContain("WhatsApp phone number");
259
+ expect(prompt).toContain("15550001111");
260
+ });
261
+
262
+ it("omits the section when the session carries no identity (console sessions)", () => {
263
+ const prompt = buildEnhancedSystemPrompt(base);
264
+
265
+ expect(prompt).not.toContain("## Conversation sender");
266
+ });
267
+ });
268
+
211
269
  describe("plan mode", () => {
212
270
  const base = {
213
271
  instructions: "Test",
@@ -256,6 +256,7 @@ const memoryConfig: Config = {
256
256
  checkpointerProxyEndpoint: null,
257
257
  primaryModel: "claude-sonnet",
258
258
  cursorStreamStallTimeoutMs: 180000,
259
+ agentResolveTimeoutMs: 120000,
259
260
  workspaceLockTimeoutMs: 900000,
260
261
  };
261
262
 
@@ -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
+ }
@@ -47,7 +47,7 @@ import { StatusBuilder } from "./status-builder.js";
47
47
  import { InlinePublisher } from "./inline-publisher.js";
48
48
  import { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
49
49
  import { processPostStream } from "./post-stream.js";
50
- import { resolveResumeInput, reconcileNonExecutingDecisions, type GraphStateSnapshot } from "./hitl.js";
50
+ import { resolveResumeInput, reconcileNonExecutingDecisions, reconcileUnattendedSkips, type GraphStateSnapshot } from "./hitl.js";
51
51
  import { captureApprovalArtifacts } from "./approval-file-change.js";
52
52
  import {
53
53
  applyCaptureDecisions,
@@ -144,6 +144,7 @@ export function createDeepAgentActivities(config: Config) {
144
144
  toolServerMap: setup.toolServerMap,
145
145
  leasedCategories: setup.leasedCategories,
146
146
  globalBypass: setup.globalBypass,
147
+ unattended: setup.unattended,
147
148
  });
148
149
 
149
150
  const resume = resolveResumeInput(
@@ -408,6 +409,7 @@ export function createDeepAgentActivities(config: Config) {
408
409
  toolServerMap: setup.toolServerMap,
409
410
  leasedCategories: setup.leasedCategories,
410
411
  globalBypass: setup.globalBypass,
412
+ unattended: setup.unattended,
411
413
  },
412
414
  streamVersion: setup.streamVersion,
413
415
  // Mid-run live capture (DD-32 / DD-33): attach file_change_progress
@@ -465,6 +467,14 @@ export function createDeepAgentActivities(config: Config) {
465
467
  // completion persists below so the terminal status is what is persisted.
466
468
  reconcileNonExecutingDecisions(initialStatus);
467
469
 
470
+ // Terminalize every tool call the gate auto-skipped under UNATTENDED
471
+ // approval mode (DD-014): the skip has no human decision behind it, so
472
+ // reconcileNonExecutingDecisions cannot see it — this sibling folds the
473
+ // gate's registry into terminal SKIPPED rows with UNATTENDED_SKIP
474
+ // provenance, whatever transient status the stream left behind. No-op
475
+ // for interactive executions (empty registry).
476
+ reconcileUnattendedSkips(initialStatus, setup.unattendedSkips);
477
+
468
478
  // Turn boundary (capture mode): capture the candidate change set from the
469
479
  // git diff and author CANDIDATE_CAPTURED, then stamp the flowed file-edit
470
480
  // rows with the change set id — they stay visible in place as
@@ -10,6 +10,11 @@ import { relative } from "node:path";
10
10
  import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
11
11
  import type { ProvisionResult, GitMetadata } from "../../shared/workspace/types.js";
12
12
  import { SourceType } from "../../shared/workspace/types.js";
13
+ import { formatContextBridgeText } from "../../shared/context-bridge.js";
14
+ import {
15
+ formatSenderIdentityText,
16
+ type SenderIdentity,
17
+ } from "../../shared/sender-identity.js";
13
18
  import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
14
19
  import {
15
20
  buildImplementPlanDirective,
@@ -106,6 +111,24 @@ export interface PromptBuilderInput {
106
111
  * message itself is just a short label ("Build from plan").
107
112
  */
108
113
  buildFromPlan?: boolean;
114
+ /**
115
+ * Rollover context bridge (cloud DD-013): a digest of the previous
116
+ * session's conversation, read from `SessionSpec.metadata`. Injected on
117
+ * EVERY turn by design: the native system prompt is rebuilt per
118
+ * invocation (never checkpointed with the message history), so a
119
+ * first-turn-only injection would vanish from turn 2 onward. The bridge
120
+ * is standing session context, like skills.
121
+ */
122
+ contextBridge?: string;
123
+ /**
124
+ * Channel sender identity (attribution, not authorization): the
125
+ * provider-verified identifier of the person on the channel, read from
126
+ * `SessionSpec.metadata`. Injected on EVERY turn like the bridge — the
127
+ * native system prompt is rebuilt per invocation, and the sender is
128
+ * constant for the session's lifetime (channel sessions are keyed
129
+ * per-sender).
130
+ */
131
+ senderIdentity?: SenderIdentity;
109
132
  }
110
133
 
111
134
  export interface InjectedFile {
@@ -147,6 +170,18 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
147
170
  prompt += buildInjectedFilesSection(input.injectedFiles);
148
171
  }
149
172
 
173
+ if (input.senderIdentity) {
174
+ prompt +=
175
+ "\n\n## Conversation sender\n\n" +
176
+ formatSenderIdentityText(input.senderIdentity);
177
+ }
178
+
179
+ if (input.contextBridge) {
180
+ prompt +=
181
+ "\n\n## Previous conversation context\n\n" +
182
+ formatContextBridgeText(input.contextBridge);
183
+ }
184
+
150
185
  prompt += RESPONSE_RULES;
151
186
  prompt += SUB_AGENT_RULES;
152
187
 
@@ -23,6 +23,8 @@ import type { DynamicStructuredTool } from "@langchain/core/tools";
23
23
  import type { Config } from "../../config.js";
24
24
  import type { StigmerClient } from "../../client/stigmer-client.js";
25
25
  import { createCheckpointer } from "../../shared/checkpointer/factory.js";
26
+ import { readContextBridge } from "../../shared/context-bridge.js";
27
+ import { readSenderIdentity } from "../../shared/sender-identity.js";
26
28
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
27
29
  import { resolveMcpServers } from "../../shared/mcp-resolver.js";
28
30
  import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
@@ -57,6 +59,7 @@ import {
57
59
  import {
58
60
  mergeApprovalPolicies,
59
61
  deriveActiveLeases,
62
+ isUnattendedApprovalMode,
60
63
  type MergedToolPolicy,
61
64
  } from "../../shared/approval-policy.js";
62
65
  import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
@@ -123,6 +126,20 @@ export interface SetupResult {
123
126
  * the gate (see ActiveLeases / deriveActiveLeases).
124
127
  */
125
128
  readonly globalBypass: boolean;
129
+ /**
130
+ * Unattended approval mode (ExecutionConfig.approval_mode = UNATTENDED,
131
+ * stamped by approver-less surfaces — channels, guest shares). The gate
132
+ * resolves gated tools as automatic skips instead of interrupting; the
133
+ * builders never seed WAITING_APPROVAL. See isUnattendedApprovalMode.
134
+ */
135
+ readonly unattended: boolean;
136
+ /**
137
+ * Tool-call ids the gate auto-skipped under {@link unattended} this turn.
138
+ * Written ONLY by the approval gate (parent + inherited sub-agent gates
139
+ * share this instance); read by reconcileUnattendedSkips after the stream
140
+ * to stamp terminal SKIPPED rows. Empty set when not unattended.
141
+ */
142
+ readonly unattendedSkips: Set<string>;
126
143
  readonly hasStructuredOutput: boolean;
127
144
  readonly streamVersion: "v2" | "v3";
128
145
  /**
@@ -386,6 +403,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
386
403
  injectedFiles,
387
404
  interactionMode: execution.spec!.executionConfig?.interactionMode,
388
405
  buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
406
+ contextBridge: readContextBridge(session.spec!.metadata),
407
+ senderIdentity: readSenderIdentity(session.spec!.metadata),
389
408
  });
390
409
 
391
410
  // Step 9: Construct the LLM model. Resolution to the provider API id
@@ -436,6 +455,14 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
436
455
  leases,
437
456
  );
438
457
 
458
+ // Unattended approval mode (DD-014): approver-less surfaces (channels,
459
+ // guest shares) stamp APPROVAL_MODE_UNATTENDED, and the gate resolves
460
+ // gated tools as automatic skips instead of interrupting. The registry is
461
+ // the gate→reconciler channel for the skipped tool-call ids; one instance
462
+ // is shared with sub-agent gates via the inherited config.
463
+ const unattended = isUnattendedApprovalMode(execution);
464
+ const unattendedSkips = new Set<string>();
465
+
439
466
  // The approval gate config is the single source of truth for HITL gating,
440
467
  // built once and inherited verbatim by sub-agents (so a mutating tool inside
441
468
  // a sub-agent is gated identically to one in the parent). Null under the
@@ -472,6 +499,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
472
499
  isCapturablePath,
473
500
  captureIgnored: captureMode && !!artifactStorage,
474
501
  recordBlockedSecret: (rawPath: string) => casObserver.recordBlockedSecret(rawPath),
502
+ unattended,
503
+ unattendedSkips,
475
504
  }
476
505
  : null;
477
506
 
@@ -644,6 +673,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
644
673
  toolServerMap,
645
674
  leasedCategories: leases.categories,
646
675
  globalBypass,
676
+ unattended,
677
+ unattendedSkips,
647
678
  hasStructuredOutput: !!outputSchema,
648
679
  streamVersion,
649
680
  casObserver,
@@ -157,6 +157,14 @@ export interface ApprovalProvenanceInputs {
157
157
  readonly leasedCategories?: ReadonlySet<ToolApprovalCategory>;
158
158
  /** Pre-armed spec.auto_approve_all — the one whole-run global bypass. */
159
159
  readonly globalBypass: boolean;
160
+ /**
161
+ * Unattended approval mode (ExecutionConfig.approval_mode = UNATTENDED):
162
+ * the gate auto-skips gated calls instead of interrupting, so the builders
163
+ * must NOT seed WAITING_APPROVAL rows or flip the phase to
164
+ * WAITING_FOR_APPROVAL — the row streams as RUNNING and the post-stream
165
+ * `reconcileUnattendedSkips` stamps the terminal SKIPPED + provenance.
166
+ */
167
+ readonly unattended?: boolean;
160
168
  }
161
169
 
162
170
  /** Shared empty set so a provider without leases allocates nothing per call. */
@@ -319,6 +319,13 @@ export class StatusBuilder {
319
319
  return { requiresApproval: false, message: "", serverSlug };
320
320
  }
321
321
 
322
+ // Unattended mode: the gate auto-skips instead of interrupting, so no row
323
+ // is ever WAITING_APPROVAL and the phase never flips — the streamed row
324
+ // runs to its skip result and reconcileUnattendedSkips terminalizes it.
325
+ if (this.approvalProvider.unattended) {
326
+ return { requiresApproval: false, message: "", serverSlug };
327
+ }
328
+
322
329
  if (serverSlug) {
323
330
  const key = `${serverSlug}/${toolName}`;
324
331
  const policy = this.approvalProvider.policies.get(key);
@@ -419,6 +419,13 @@ export class V3StatusBuilder implements ExecutionStatusWriter {
419
419
  return { requiresApproval: false, message: "", serverSlug };
420
420
  }
421
421
 
422
+ // Unattended mode: the gate auto-skips instead of interrupting, so no row
423
+ // is ever WAITING_APPROVAL and the phase never flips — the streamed row
424
+ // runs to its skip result and reconcileUnattendedSkips terminalizes it.
425
+ if (this.approvalProvider.unattended) {
426
+ return { requiresApproval: false, message: "", serverSlug };
427
+ }
428
+
422
429
  if (serverSlug) {
423
430
  const key = `${serverSlug}/${toolName}`;
424
431
  const policy = this.approvalProvider.policies.get(key);
@@ -34,6 +34,7 @@ import {
34
34
  ArtifactCreatedPayloadSchema,
35
35
  HumanInputOutcomeInfoSchema,
36
36
  } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/event_pb";
37
+ import { ApiResourceAuditActorSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/status_pb";
37
38
  import { WorkflowExecutionStatusSchema, WorkflowTaskSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
38
39
  import {
39
40
  WorkflowExecutionUpdateStatusInputSchema,
@@ -221,6 +222,7 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
221
222
  case: "taskStarted",
222
223
  value: create(TaskStartedPayloadSchema, {
223
224
  taskKind: TASK_KIND_MAP[desc.taskKind] ?? 0,
225
+ inputSummary: toJsonObject(desc.inputSummary),
224
226
  attemptNumber: desc.attemptNumber,
225
227
  }),
226
228
  };
@@ -233,6 +235,7 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
233
235
  value: create(TaskCompletedPayloadSchema, {
234
236
  taskKind: TASK_KIND_MAP[desc.taskKind] ?? 0,
235
237
  durationMs: BigInt(desc.durationMs),
238
+ outputSummary: toJsonObject(desc.outputSummary),
236
239
  costMicros: BigInt(desc.costMicros),
237
240
  tokensUsed: BigInt(desc.tokensUsed),
238
241
  }),
@@ -307,6 +310,14 @@ export function toProtoEvent(desc: WorkflowEventDescriptor): WorkflowExecutionEv
307
310
  case: "approvalResolved",
308
311
  value: create(ApprovalResolvedPayloadSchema, {
309
312
  resolvedBy: desc.resolvedBy,
313
+ resolvedByActor: desc.resolvedByActor
314
+ ? create(ApiResourceAuditActorSchema, {
315
+ id: desc.resolvedByActor.id,
316
+ displayName: desc.resolvedByActor.display_name ?? "",
317
+ email: desc.resolvedByActor.email ?? "",
318
+ avatar: desc.resolvedByActor.avatar ?? "",
319
+ })
320
+ : undefined,
310
321
  comment: desc.comment,
311
322
  waitDurationMs: BigInt(desc.waitDurationMs),
312
323
  }),
package/src/config.ts CHANGED
@@ -37,6 +37,17 @@ import { homedir, tmpdir } from "node:os";
37
37
  */
38
38
  export const DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS = 180_000;
39
39
 
40
+ /**
41
+ * Default bound for Cursor Agent.create/Agent.resume (ms). These SDK calls
42
+ * have no timeout of their own; a degraded transport (dead proxy connection,
43
+ * stale HTTP/2 session) hangs them forever — which surfaces as an opaque
44
+ * Temporal heartbeat timeout minutes later instead of an actionable error.
45
+ * 120s comfortably covers a slow create (token exchange + workspace
46
+ * registration) while failing fast enough to diagnose. Single source of truth
47
+ * for env-loaded and options-mapped config (mirrors the stall timeout above).
48
+ */
49
+ export const DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS = 120_000;
50
+
40
51
  // Re-exported so the runner/manager options mappers default the lock-wait
41
52
  // bound from the same constant the lock module owns (no drift across the
42
53
  // three construction sites — mirrors DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS).
@@ -89,6 +100,13 @@ export interface Config {
89
100
  * heartbeatTimeout (process liveness) and the 30s keep-alive heartbeat.
90
101
  */
91
102
  readonly cursorStreamStallTimeoutMs: number;
103
+ /**
104
+ * Bound (milliseconds) for Cursor Agent.create/Agent.resume. On expiry the
105
+ * execution fails fast with an explicit transport diagnosis instead of
106
+ * hanging into Temporal's heartbeat timeout. See
107
+ * {@link DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS} for why this exists.
108
+ */
109
+ readonly agentResolveTimeoutMs: number;
92
110
  /**
93
111
  * Max wait (milliseconds) for the per-workspace turn lock before failing
94
112
  * the execution with a "workspace is in use by another session" error.
@@ -174,6 +192,10 @@ export function loadConfig(): Config {
174
192
  ? parseInt(process.env.WORKSPACE_LOCK_TIMEOUT_MS, 10)
175
193
  : DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS;
176
194
 
195
+ const agentResolveTimeoutMs = process.env.CURSOR_AGENT_RESOLVE_TIMEOUT_MS
196
+ ? parseInt(process.env.CURSOR_AGENT_RESOLVE_TIMEOUT_MS, 10)
197
+ : DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS;
198
+
177
199
  return {
178
200
  taskQueue,
179
201
  temporalAddress,
@@ -191,6 +213,7 @@ export function loadConfig(): Config {
191
213
  checkpointerProxyEndpoint,
192
214
  primaryModel,
193
215
  cursorStreamStallTimeoutMs,
216
+ agentResolveTimeoutMs,
194
217
  workspaceLockTimeoutMs,
195
218
  };
196
219
  }
@@ -784,4 +784,99 @@ describe("ApprovalGateMiddleware", () => {
784
784
  expect(result).toBeInstanceOf(ToolMessage);
785
785
  expect((result as ToolMessage).content).toContain("unknown action");
786
786
  });
787
+
788
+ describe("unattended approval mode (DD-014)", () => {
789
+ const gatedMcpPolicies = new Map<string, MergedToolPolicy>([
790
+ ["srv/gated_tool", {
791
+ toolName: "gated_tool",
792
+ mcpServerSlug: "srv",
793
+ requiresApproval: true,
794
+ approvalMessage: "Run gated_tool",
795
+ source: "classifier_default",
796
+ }],
797
+ ]);
798
+
799
+ it("skips a gated MCP tool without interrupting and records it in the registry", async () => {
800
+ const unattendedSkips = new Set<string>();
801
+ const mw = createApprovalGateMiddleware(makeConfig({
802
+ policies: gatedMcpPolicies,
803
+ toolServerMap: new Map([["gated_tool", "srv"]]),
804
+ unattended: true,
805
+ unattendedSkips,
806
+ }));
807
+
808
+ const handler = vi.fn(passthrough);
809
+ const result = await mw.wrapToolCall!(
810
+ makeRequest({ name: "gated_tool", args: { target: "prod" } }),
811
+ handler,
812
+ );
813
+
814
+ // Gateway invariant: the side effect never ran, and no interrupt was
815
+ // raised — the turn continues instead of parking WAITING_FOR_APPROVAL.
816
+ expect(handler).not.toHaveBeenCalled();
817
+ expect(mockedInterrupt).not.toHaveBeenCalled();
818
+ expect((result as ToolMessage).content).toContain("skipped automatically");
819
+ expect(unattendedSkips.has("call_abc123")).toBe(true);
820
+ });
821
+
822
+ it("skips a gated built-in (shell) the same way", async () => {
823
+ const unattendedSkips = new Set<string>();
824
+ const mw = createApprovalGateMiddleware(makeConfig({
825
+ unattended: true,
826
+ unattendedSkips,
827
+ }));
828
+
829
+ const handler = vi.fn(passthrough);
830
+ const result = await mw.wrapToolCall!(
831
+ makeRequest({ name: "shell", args: { command: "rm -rf /" } }),
832
+ handler,
833
+ );
834
+
835
+ expect(handler).not.toHaveBeenCalled();
836
+ expect(mockedInterrupt).not.toHaveBeenCalled();
837
+ expect((result as ToolMessage).content).toContain("skipped automatically");
838
+ expect(unattendedSkips.has("call_abc123")).toBe(true);
839
+ });
840
+
841
+ it("still runs tools the policy chain auto-approved (un-gating stays the operator lever)", async () => {
842
+ // The operator's tool_approval_overrides path: an un-gated tool is
843
+ // absent from the policy map, so unattended mode never touches it.
844
+ const unattendedSkips = new Set<string>();
845
+ const mw = createApprovalGateMiddleware(makeConfig({
846
+ toolServerMap: new Map([["book_appointment", "clinic"]]),
847
+ unattended: true,
848
+ unattendedSkips,
849
+ }));
850
+
851
+ const result = await mw.wrapToolCall!(
852
+ makeRequest({ name: "book_appointment", args: { slot: "mon-10" } }),
853
+ passthrough,
854
+ );
855
+
856
+ expect((result as ToolMessage).content).toBe("tool result");
857
+ expect(unattendedSkips.size).toBe(0);
858
+ expect(mockedInterrupt).not.toHaveBeenCalled();
859
+ });
860
+
861
+ it("never emits an execution receipt for an unattended skip (no side effect)", async () => {
862
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
863
+ try {
864
+ const mw = createApprovalGateMiddleware(makeConfig({
865
+ policies: gatedMcpPolicies,
866
+ toolServerMap: new Map([["gated_tool", "srv"]]),
867
+ unattended: true,
868
+ unattendedSkips: new Set<string>(),
869
+ }));
870
+
871
+ await mw.wrapToolCall!(makeRequest({ name: "gated_tool" }), passthrough);
872
+
873
+ const receipts = logSpy.mock.calls
874
+ .map((c) => String(c[0]))
875
+ .filter((line) => line.includes("[hitl-gateway] receipt"));
876
+ expect(receipts).toHaveLength(0);
877
+ } finally {
878
+ logSpy.mockRestore();
879
+ }
880
+ });
881
+ });
787
882
  });
@@ -51,6 +51,7 @@ import {
51
51
  type PolicySource,
52
52
  POLICY_ENGINE_VERSION,
53
53
  resolveApprovalMessage,
54
+ unattendedSkipMessage,
54
55
  } from "../shared/approval-policy.js";
55
56
  import { toolApprovalCategory, type ToolApprovalCategory } from "../shared/tool-kind.js";
56
57
  import { extractFilePath } from "../shared/file-tools.js";
@@ -121,6 +122,26 @@ export interface ApprovalGateConfig {
121
122
  * secret; the CONTENT never leaves the workspace). Absent ⇒ nothing recorded.
122
123
  */
123
124
  readonly recordBlockedSecret?: (rawPath: string) => void;
125
+ /**
126
+ * Unattended approval mode (ExecutionConfig.approval_mode = UNATTENDED):
127
+ * the creating surface — a messaging channel, a guest share — has no
128
+ * approver, so a gated tool is resolved as an automatic SKIP (the model is
129
+ * told to adapt) instead of `interrupt()`. The execution never enters
130
+ * WAITING_FOR_APPROVAL. What is gated is unchanged — only the resolution
131
+ * differs; an operator un-gates a specific tool for the agent via
132
+ * tool_approval_overrides, not by weakening this mode.
133
+ */
134
+ readonly unattended?: boolean;
135
+ /**
136
+ * Registry of tool-call ids this gate auto-skipped under {@link unattended}
137
+ * — the gate is the single WRITER; `reconcileUnattendedSkips` (hitl.ts) is
138
+ * the reader that folds each id into a terminal TOOL_CALL_SKIPPED row with
139
+ * UNATTENDED_SKIP provenance after the stream. In-process, per-execution
140
+ * state keyed by the framework's own tool-call id (direct identity, no
141
+ * matching); inherited verbatim by sub-agent gates so their skips land in
142
+ * the same registry.
143
+ */
144
+ readonly unattendedSkips?: Set<string>;
124
145
  }
125
146
 
126
147
  interface ApprovalDecision {
@@ -249,6 +270,22 @@ export function createApprovalGateMiddleware(
249
270
  return await handler(request);
250
271
  }
251
272
 
273
+ // Unattended surfaces (channels, guest shares) have no approver, so a
274
+ // gate that would interrupt() here resolves as an automatic SKIP: the
275
+ // tool does NOT run (the gateway invariant holds — no side effect
276
+ // without a backing authorization), the model is told to adapt in plain
277
+ // language, and the turn continues to normal completion instead of
278
+ // parking in WAITING_FOR_APPROVAL forever. The registry entry lets the
279
+ // post-stream reconciler stamp the terminal SKIPPED row + provenance.
280
+ if (config.unattended) {
281
+ config.unattendedSkips?.add(toolCall.id);
282
+ return new ToolMessage({
283
+ content: unattendedSkipMessage(toolName),
284
+ tool_call_id: toolCall.id,
285
+ name: toolName,
286
+ });
287
+ }
288
+
252
289
  const approvalRequest = {
253
290
  tool_call_id: toolCall.id,
254
291
  tool_name: toolName,
@@ -24,7 +24,7 @@ import {
24
24
  } from "@temporalio/worker";
25
25
  import type { PayloadCodec } from "@temporalio/common";
26
26
  import type { Config } from "./config.js";
27
- import { DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
27
+ import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
28
28
  import type { WorkerActivities } from "./worker.js";
29
29
  import { resolveWorkflowSource, OTEL_WORKFLOW_INTERCEPTOR_MODULE } from "./workflow-source.js";
30
30
  import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
@@ -91,6 +91,9 @@ export interface RunnerManagerOptions {
91
91
  /** No-progress bound for the Cursor harness stream (ms). @default 180000 */
92
92
  readonly cursorStreamStallTimeoutMs?: number;
93
93
 
94
+ /** Bound for Cursor Agent.create/resume (ms). @default 120000 */
95
+ readonly agentResolveTimeoutMs?: number;
96
+
94
97
  /** Max wait for the per-workspace turn lock (ms). @default 900000 */
95
98
  readonly workspaceLockTimeoutMs?: number;
96
99
 
@@ -598,6 +601,8 @@ export function mapManagerOptionsToConfig(
598
601
  primaryModel: options.primaryModel ?? "gpt-4.1",
599
602
  cursorStreamStallTimeoutMs:
600
603
  options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
604
+ agentResolveTimeoutMs:
605
+ options.agentResolveTimeoutMs ?? DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS,
601
606
  workspaceLockTimeoutMs:
602
607
  options.workspaceLockTimeoutMs ?? DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
603
608
  };