@stigmer/runner 3.0.8-dev.20260613085218 → 3.0.9-dev.20260615145121
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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/hook-script.d.ts +23 -12
- package/dist/activities/execute-cursor/hook-script.js +85 -51
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +220 -79
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +51 -9
- package/dist/activities/execute-cursor/message-translator.js +146 -20
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/persist-decision.d.ts +42 -0
- package/dist/activities/execute-cursor/persist-decision.js +30 -0
- package/dist/activities/execute-cursor/persist-decision.js.map +1 -0
- package/dist/activities/execute-cursor/prompt-builder.d.ts +25 -0
- package/dist/activities/execute-cursor/prompt-builder.js +54 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +8 -2
- package/dist/activities/execute-cursor/workspace-setup.js +62 -30
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +0 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +32 -8
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +4 -5
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js +4 -5
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +9 -1
- package/dist/activities/execute-deep-agent/streaming.js +4 -5
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-tracker.js +4 -5
- package/dist/activities/execute-deep-agent/subagent-tracker.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -5
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/in-flight.d.ts +35 -0
- package/dist/in-flight.js +61 -0
- package/dist/in-flight.js.map +1 -0
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +90 -29
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +9 -20
- package/dist/shared/grpc-retry.js +9 -52
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/stall-watchdog.d.ts +68 -0
- package/dist/shared/stall-watchdog.js +102 -0
- package/dist/shared/stall-watchdog.js.map +1 -0
- package/dist/shared/status-offload.d.ts +84 -0
- package/dist/shared/status-offload.js +292 -0
- package/dist/shared/status-offload.js.map +1 -0
- package/dist/shared/status.d.ts +34 -3
- package/dist/shared/status.js +102 -9
- package/dist/shared/status.js.map +1 -1
- package/dist/{activities/execute-deep-agent → shared}/streaming-scheduler.d.ts +4 -0
- package/dist/{activities/execute-deep-agent → shared}/streaming-scheduler.js +4 -0
- package/dist/shared/streaming-scheduler.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/config.test.ts +8 -0
- package/src/__tests__/in-flight.test.ts +84 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +74 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +90 -15
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +124 -12
- package/src/activities/execute-cursor/__tests__/persist-decision.test.ts +99 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +244 -0
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +53 -4
- package/src/activities/execute-cursor/hook-script.ts +85 -51
- package/src/activities/execute-cursor/index.ts +187 -38
- package/src/activities/execute-cursor/message-translator.ts +146 -20
- package/src/activities/execute-cursor/persist-decision.ts +54 -0
- package/src/activities/execute-cursor/prompt-builder.ts +59 -0
- package/src/activities/execute-cursor/workspace-setup.ts +76 -44
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder-shared.test.ts +66 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +6 -3
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +70 -0
- package/src/activities/execute-deep-agent/index.ts +17 -5
- package/src/activities/execute-deep-agent/status-builder-shared.ts +27 -5
- package/src/activities/execute-deep-agent/status-builder.ts +3 -5
- package/src/activities/execute-deep-agent/streaming-v3.ts +5 -5
- package/src/activities/execute-deep-agent/streaming.ts +14 -5
- package/src/activities/execute-deep-agent/subagent-tracker.ts +4 -5
- package/src/activities/execute-deep-agent/v3-status-builder.ts +5 -5
- package/src/config.ts +27 -0
- package/src/in-flight.ts +71 -0
- package/src/runner-manager.ts +127 -33
- package/src/runner.ts +6 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/grpc-retry-extended.test.ts +6 -144
- package/src/shared/__tests__/grpc-retry.test.ts +5 -134
- package/src/shared/__tests__/stall-watchdog.test.ts +193 -0
- package/src/shared/__tests__/status-offload.test.ts +256 -0
- package/src/shared/__tests__/status.test.ts +199 -0
- package/src/shared/grpc-retry.ts +9 -72
- package/src/shared/stall-watchdog.ts +122 -0
- package/src/shared/status-offload.ts +342 -0
- package/src/shared/status.ts +142 -8
- package/src/{activities/execute-deep-agent → shared}/streaming-scheduler.ts +4 -0
- package/dist/activities/execute-deep-agent/streaming-scheduler.js.map +0 -1
- /package/src/{activities/execute-deep-agent → shared}/__tests__/streaming-scheduler.test.ts +0 -0
|
@@ -43,10 +43,13 @@ import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_p
|
|
|
43
43
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
44
44
|
import { MessageAccumulator, reconcileDeniedToolCalls, cancelInProgressSubAgentProtos } from "./message-translator.js";
|
|
45
45
|
import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
|
|
46
|
-
import {
|
|
46
|
+
import { startStallWatchdog, StallTimeoutError, formatStallFailure, type StallWatchdog } from "../../shared/stall-watchdog.js";
|
|
47
|
+
import { createArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
47
48
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
48
49
|
import { DeltaEnricher } from "./delta-enricher.js";
|
|
49
50
|
import { TodoTracker } from "./todo-tracker.js";
|
|
51
|
+
import { shouldPersistStreamingStatus } from "./persist-decision.js";
|
|
52
|
+
import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
|
|
50
53
|
import { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
51
54
|
import { resolveMcpServers, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
52
55
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
@@ -127,11 +130,43 @@ async function executeCursorInner(
|
|
|
127
130
|
startedAt: utcTimestamp(),
|
|
128
131
|
});
|
|
129
132
|
|
|
133
|
+
// Artifact storage for offloading oversized tool outputs (screenshots, giant
|
|
134
|
+
// dumps) out of the persisted status, and for publishing the plan artifact.
|
|
135
|
+
// Created once here so it is available to EVERY persist below. Best-effort:
|
|
136
|
+
// if it can't be built (e.g. proxy mode without a token), offload is disabled
|
|
137
|
+
// but persistStatus still enforces the aggregate size cap, so persistence can
|
|
138
|
+
// never silently blow past the gRPC limit.
|
|
139
|
+
let artifactStorage: ArtifactStorage | undefined;
|
|
140
|
+
try {
|
|
141
|
+
artifactStorage = createArtifactStorage(loadArtifactStorageConfig(config));
|
|
142
|
+
} catch (storageErr) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`ExecuteCursor artifact storage unavailable — tool-output offload disabled ` +
|
|
145
|
+
`(aggregate size guard still active): execution=${executionId}, error=${storageErr}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const statusOffload = artifactStorage
|
|
149
|
+
? { artifactStorage, executionId }
|
|
150
|
+
: undefined;
|
|
151
|
+
// ALL status persistence in this activity flows through `persist`, so the
|
|
152
|
+
// single size-bounding guard (offload + aggregate elision) is unforgeable and
|
|
153
|
+
// a future call site cannot accidentally skip it.
|
|
154
|
+
const persist = (s: AgentExecutionStatus = status) =>
|
|
155
|
+
persistStatus(client, executionId, s, { offload: statusOffload });
|
|
156
|
+
|
|
130
157
|
let sessionId: string | undefined;
|
|
131
158
|
let session: import("@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb").Session | undefined;
|
|
132
159
|
let pauseDetected = false;
|
|
133
160
|
let workerShutdownDetected = false;
|
|
161
|
+
// Set by the stall watchdog when the SDK stream makes no progress for
|
|
162
|
+
// config.cursorStreamStallTimeoutMs. stallError carries the recognizable
|
|
163
|
+
// message surfaced to the user; both feed the Phase 11a stall branch.
|
|
164
|
+
let stallDetected = false;
|
|
165
|
+
let stallError: StallTimeoutError | undefined;
|
|
134
166
|
let periodicHeartbeat: ReturnType<typeof startHeartbeat> | undefined;
|
|
167
|
+
// Progress-based stall watchdog (see ../../shared/stall-watchdog.ts). Stopped
|
|
168
|
+
// in the finally on every exit path; complements the liveness heartbeat.
|
|
169
|
+
let stallWatchdog: StallWatchdog | undefined;
|
|
135
170
|
// Session HITL directory (runner-owned, outside the workspace) where the hook
|
|
136
171
|
// script, approval-state file, and denial ledger live. Set once the gate is
|
|
137
172
|
// installed; the WAITING_FOR_APPROVAL path reads the denial ledger from here.
|
|
@@ -227,7 +262,7 @@ async function executeCursorInner(
|
|
|
227
262
|
content: "Execution was rejected by the user during tool approval.",
|
|
228
263
|
timestamp: utcTimestamp(),
|
|
229
264
|
}));
|
|
230
|
-
await
|
|
265
|
+
await persist(status);
|
|
231
266
|
return slimStatus(status);
|
|
232
267
|
}
|
|
233
268
|
}
|
|
@@ -496,6 +531,9 @@ async function executeCursorInner(
|
|
|
496
531
|
let firstTurnAttributionLogged = false;
|
|
497
532
|
let streamErrorMessage: string | undefined;
|
|
498
533
|
let alreadyRetriedWithFreshAgent = false;
|
|
534
|
+
// Most recent tool name observed on the stream, used only to enrich the
|
|
535
|
+
// stall message ("last tool: …") so a wedged turn names the likely culprit.
|
|
536
|
+
let lastToolName: string | undefined;
|
|
499
537
|
|
|
500
538
|
// Periodic heartbeat keeps Temporal informed during silent SDK operations
|
|
501
539
|
// (e.g. long tool calls, MCP requests, model thinking). Without this,
|
|
@@ -525,6 +563,10 @@ async function executeCursorInner(
|
|
|
525
563
|
|
|
526
564
|
const run = await resolution.agent.send(effectivePrompt, {
|
|
527
565
|
onDelta: ({ update }) => {
|
|
566
|
+
// Reset the stall timer on the delta channel too: a long model
|
|
567
|
+
// generation emits token deltas but few discrete stream events, so
|
|
568
|
+
// resetting only in the stream loop would false-positive a stall.
|
|
569
|
+
stallWatchdog?.recordActivity();
|
|
528
570
|
if (update.type === "turn-ended" && update.usage) {
|
|
529
571
|
usageAccumulator.addTurn(update.usage);
|
|
530
572
|
|
|
@@ -552,14 +594,50 @@ async function executeCursorInner(
|
|
|
552
594
|
},
|
|
553
595
|
});
|
|
554
596
|
|
|
597
|
+
// Arm the stall watchdog now that the run exists. The periodic heartbeat
|
|
598
|
+
// above proves the process is alive, not that the agent is progressing: if
|
|
599
|
+
// the stream wedges (a tool call or model connection that never returns),
|
|
600
|
+
// no event/delta arrives, Phase 12 below is never reached, and the
|
|
601
|
+
// execution hangs at EXECUTION_IN_PROGRESS forever. On stall we end the run
|
|
602
|
+
// cleanly via the SDK's run.cancel() (guarded by supports("cancel")), which
|
|
603
|
+
// unblocks the for-await; the stallDetected branch in Phase 11a then
|
|
604
|
+
// reports EXECUTION_FAILED with a recognizable, actionable message.
|
|
605
|
+
stallWatchdog = startStallWatchdog(config.cursorStreamStallTimeoutMs, (idleMs) => {
|
|
606
|
+
stallDetected = true;
|
|
607
|
+
stallError = new StallTimeoutError(idleMs, lastToolName ? `last tool: ${lastToolName}` : undefined);
|
|
608
|
+
console.warn(
|
|
609
|
+
`ExecuteCursor stall detected: execution=${executionId}, idleMs=${idleMs}, lastTool=${lastToolName ?? "none"}`,
|
|
610
|
+
);
|
|
611
|
+
if (run.supports?.("cancel")) {
|
|
612
|
+
void run.cancel().catch((cancelErr) => {
|
|
613
|
+
console.warn(
|
|
614
|
+
`ExecuteCursor run.cancel() after stall failed (non-fatal): execution=${executionId}, ` +
|
|
615
|
+
`error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`,
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
555
621
|
const accumulator = new MessageAccumulator(status.messages, { mergedPolicies });
|
|
622
|
+
// Shared cadence with the native harness: discrete state changes force a
|
|
623
|
+
// flush; high-frequency token deltas ride this scheduler's time cadence
|
|
624
|
+
// (env-tunable via STREAMING_* — see loadStreamingConfig).
|
|
625
|
+
const scheduler = new StreamingUpdateScheduler(loadStreamingConfig());
|
|
556
626
|
let eventCount = 0;
|
|
557
627
|
|
|
628
|
+
try {
|
|
558
629
|
for await (const event of run.stream()) {
|
|
559
630
|
if (pauseDetected || Context.current().cancellationSignal.aborted) {
|
|
560
631
|
pauseDetected = true;
|
|
561
632
|
break;
|
|
562
633
|
}
|
|
634
|
+
if (stallDetected) break;
|
|
635
|
+
|
|
636
|
+
// Progress: reset the stall timer on every stream event.
|
|
637
|
+
stallWatchdog.recordActivity();
|
|
638
|
+
if (event.type === "tool_call" && typeof event.name === "string") {
|
|
639
|
+
lastToolName = event.name;
|
|
640
|
+
}
|
|
563
641
|
|
|
564
642
|
eventRecorder?.record(event, eventCount);
|
|
565
643
|
|
|
@@ -585,8 +663,15 @@ async function executeCursorInner(
|
|
|
585
663
|
}
|
|
586
664
|
}
|
|
587
665
|
|
|
588
|
-
const shouldPersist =
|
|
589
|
-
|
|
666
|
+
const shouldPersist = shouldPersistStreamingStatus(
|
|
667
|
+
{
|
|
668
|
+
deltaEnricherDirty: deltaEnricher.isDirty,
|
|
669
|
+
todosDirty: todoTracker.isDirty,
|
|
670
|
+
contentDirty: accumulator.isDirty,
|
|
671
|
+
},
|
|
672
|
+
scheduler,
|
|
673
|
+
eventCount,
|
|
674
|
+
);
|
|
590
675
|
if (usageAccumulator.hasTurns) {
|
|
591
676
|
status.streamingUsage = create(StreamingUsageSummarySchema, usageAccumulator.snapshot());
|
|
592
677
|
}
|
|
@@ -598,10 +683,11 @@ async function executeCursorInner(
|
|
|
598
683
|
// accumulator tracked sub-agents in memory but they only reached the
|
|
599
684
|
// status (and the subscriber stream) after the loop ended.
|
|
600
685
|
status.subAgentExecutions = accumulator.subAgentExecutions;
|
|
601
|
-
const signal = await
|
|
686
|
+
const signal = await persist(status);
|
|
602
687
|
deltaEnricher.markPersisted();
|
|
603
688
|
todoTracker.markPersisted();
|
|
604
|
-
accumulator.
|
|
689
|
+
accumulator.markPersisted();
|
|
690
|
+
scheduler.markUpdateSent(eventCount);
|
|
605
691
|
heartbeat();
|
|
606
692
|
if (signal === ExecutionControlSignal.STOP) {
|
|
607
693
|
platformStopSignaled = true;
|
|
@@ -616,8 +702,17 @@ async function executeCursorInner(
|
|
|
616
702
|
break;
|
|
617
703
|
}
|
|
618
704
|
}
|
|
705
|
+
} catch (streamErr) {
|
|
706
|
+
// run.cancel() from the stall watchdog can make the stream iterator
|
|
707
|
+
// reject; that is the expected teardown, so swallow it and fall through
|
|
708
|
+
// to the stallDetected branch in Phase 11a. Anything else is a genuine
|
|
709
|
+
// stream failure — rethrow it to the outer error handler.
|
|
710
|
+
if (!stallDetected) throw streamErr;
|
|
711
|
+
console.warn(`ExecuteCursor stream ended via stall cancel: execution=${executionId}`);
|
|
712
|
+
}
|
|
619
713
|
|
|
620
714
|
periodicHeartbeat.stop();
|
|
715
|
+
stallWatchdog.stop();
|
|
621
716
|
// Check both the heartbeat flag AND the shutdown signal directly.
|
|
622
717
|
// Race condition: the heartbeat timer may detect Temporal's CancelledFailure
|
|
623
718
|
// (from worker.shutdown()) before the AbortSignal microtask propagates,
|
|
@@ -638,7 +733,7 @@ async function executeCursorInner(
|
|
|
638
733
|
// sub-agent the parent had delegated is no longer executing. Mark it
|
|
639
734
|
// CANCELLED rather than leaving a permanent IN_PROGRESS "zombie" in the
|
|
640
735
|
// final snapshot (parity with the native harness's cancelSubAgents()).
|
|
641
|
-
if (pauseDetected || workerShutdownDetected || Context.current().cancellationSignal.aborted) {
|
|
736
|
+
if (pauseDetected || workerShutdownDetected || stallDetected || Context.current().cancellationSignal.aborted) {
|
|
642
737
|
accumulator.cancelInProgressSubAgents();
|
|
643
738
|
}
|
|
644
739
|
status.subAgentExecutions = accumulator.subAgentExecutions;
|
|
@@ -654,7 +749,7 @@ async function executeCursorInner(
|
|
|
654
749
|
// call statuses before run.wait() / structured output extraction.
|
|
655
750
|
// This is unconditional (not throttled) because finalize is a
|
|
656
751
|
// once-per-execution correctness boundary.
|
|
657
|
-
await
|
|
752
|
+
await persist(status);
|
|
658
753
|
heartbeat();
|
|
659
754
|
|
|
660
755
|
// End OTel turn span with accumulated token usage
|
|
@@ -678,7 +773,29 @@ async function executeCursorInner(
|
|
|
678
773
|
}
|
|
679
774
|
|
|
680
775
|
|
|
681
|
-
// Phase 11a: Handle worker shutdown, pause, or infrastructure cancellation.
|
|
776
|
+
// Phase 11a: Handle stall, worker shutdown, pause, or infrastructure cancellation.
|
|
777
|
+
|
|
778
|
+
// Stall: the watchdog cancelled a turn that made no progress for longer
|
|
779
|
+
// than config.cursorStreamStallTimeoutMs (a wedged tool call or a dead
|
|
780
|
+
// model connection). The keep-alive heartbeat proves liveness, so Temporal
|
|
781
|
+
// never reaps this on its own — this branch is the only clean exit. We
|
|
782
|
+
// RETURN (not throw): re-running the identical prompt via Temporal retry
|
|
783
|
+
// would very likely wedge again. accumulator.finalize() above already
|
|
784
|
+
// cleared isStreaming, so the UI spinners stop.
|
|
785
|
+
if (stallDetected) {
|
|
786
|
+
const err = stallError ?? new StallTimeoutError(config.cursorStreamStallTimeoutMs);
|
|
787
|
+
status.phase = ExecutionPhase.EXECUTION_FAILED;
|
|
788
|
+
status.error = formatStallFailure(err);
|
|
789
|
+
status.completedAt = utcTimestamp();
|
|
790
|
+
status.messages.push(create(AgentMessageSchema, {
|
|
791
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
792
|
+
content: `Execution failed: the agent made no progress for too long and was stopped (${err.message}). You can retry or resume.`,
|
|
793
|
+
timestamp: utcTimestamp(),
|
|
794
|
+
}));
|
|
795
|
+
await persist(status);
|
|
796
|
+
console.warn(`ExecuteCursor stalled: execution=${executionId}, events=${eventCount}, error=${status.error}`);
|
|
797
|
+
return slimStatus(status);
|
|
798
|
+
}
|
|
682
799
|
|
|
683
800
|
// Worker shutdown: the runner-manager aborted the shutdown signal before
|
|
684
801
|
// calling worker.shutdown(). This is NOT a user-initiated pause — it's
|
|
@@ -692,7 +809,7 @@ async function executeCursorInner(
|
|
|
692
809
|
content: "Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
|
|
693
810
|
timestamp: utcTimestamp(),
|
|
694
811
|
}));
|
|
695
|
-
await
|
|
812
|
+
await persist(status); console.log(`ExecuteCursor interrupted (worker shutdown): execution=${executionId}, events=${eventCount}`);
|
|
696
813
|
throw new CancelledFailure("Activity cancelled (worker shutdown, not user pause)");
|
|
697
814
|
}
|
|
698
815
|
|
|
@@ -705,7 +822,7 @@ async function executeCursorInner(
|
|
|
705
822
|
content: "Execution paused by user. Use resume to continue.",
|
|
706
823
|
timestamp: utcTimestamp(),
|
|
707
824
|
}));
|
|
708
|
-
await
|
|
825
|
+
await persist(status); console.log(`ExecuteCursor paused: execution=${executionId}, events=${eventCount}`);
|
|
709
826
|
throw new CancelledFailure("Activity paused by orchestrator");
|
|
710
827
|
}
|
|
711
828
|
|
|
@@ -721,7 +838,7 @@ async function executeCursorInner(
|
|
|
721
838
|
content: "Execution interrupted: the agent was unresponsive for too long. You can retry or resume.",
|
|
722
839
|
timestamp: utcTimestamp(),
|
|
723
840
|
}));
|
|
724
|
-
await
|
|
841
|
+
await persist(status); console.log(`ExecuteCursor interrupted (infrastructure cancel): execution=${executionId}, events=${eventCount}`);
|
|
725
842
|
throw new CancelledFailure("Activity cancelled (heartbeat timeout, not user pause)");
|
|
726
843
|
}
|
|
727
844
|
|
|
@@ -734,7 +851,7 @@ async function executeCursorInner(
|
|
|
734
851
|
content: "Execution stopped by the platform.",
|
|
735
852
|
timestamp: utcTimestamp(),
|
|
736
853
|
}));
|
|
737
|
-
await
|
|
854
|
+
await persist(status); try { resolution.agent.close(); } catch { /* best effort */ }
|
|
738
855
|
console.log(`ExecuteCursor completed (platform stop): execution=${executionId}`);
|
|
739
856
|
return slimStatus(status);
|
|
740
857
|
}
|
|
@@ -751,7 +868,7 @@ async function executeCursorInner(
|
|
|
751
868
|
const deniedToolCalls = reconcileDeniedToolCalls(status.messages, deniedLedger, mergedPolicies);
|
|
752
869
|
if (deniedToolCalls.length > 0) {
|
|
753
870
|
status.phase = ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL;
|
|
754
|
-
await
|
|
871
|
+
await persist(status);
|
|
755
872
|
console.log(`ExecuteCursor returning WAITING_FOR_APPROVAL: ${deniedToolCalls.length} tools pending`);
|
|
756
873
|
return slimStatus(status);
|
|
757
874
|
}
|
|
@@ -858,8 +975,10 @@ async function executeCursorInner(
|
|
|
858
975
|
console.warn("Failed to update session with fresh agentId (non-fatal):", updateErr);
|
|
859
976
|
}
|
|
860
977
|
|
|
978
|
+
let retryWatchdog: StallWatchdog | undefined;
|
|
861
979
|
const retryRun = await freshAgent.send(freshPrompt, {
|
|
862
980
|
onDelta: ({ update }) => {
|
|
981
|
+
retryWatchdog?.recordActivity();
|
|
863
982
|
if (update.type === "turn-ended" && update.usage) {
|
|
864
983
|
usageAccumulator.addTurn(update.usage);
|
|
865
984
|
}
|
|
@@ -867,19 +986,31 @@ async function executeCursorInner(
|
|
|
867
986
|
try { heartbeat(); } catch { /* swallow during retry */ }
|
|
868
987
|
},
|
|
869
988
|
});
|
|
989
|
+
// Mirror the primary stream's stall protection: a wedged retry must
|
|
990
|
+
// not hang the activity. On stall, cancel the run so the loop ends and
|
|
991
|
+
// retryRun.wait() resolves down the existing non-finished failure path.
|
|
992
|
+
retryWatchdog = startStallWatchdog(config.cursorStreamStallTimeoutMs, (idleMs) => {
|
|
993
|
+
console.warn(`ExecuteCursor retry stall detected: execution=${executionId}, idleMs=${idleMs}`);
|
|
994
|
+
if (retryRun.supports?.("cancel")) void retryRun.cancel().catch(() => { /* best effort */ });
|
|
995
|
+
});
|
|
870
996
|
|
|
871
997
|
streamErrorMessage = undefined;
|
|
872
998
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if (
|
|
879
|
-
|
|
999
|
+
try {
|
|
1000
|
+
for await (const retryEvent of retryRun.stream()) {
|
|
1001
|
+
if (Context.current().cancellationSignal.aborted) break;
|
|
1002
|
+
retryWatchdog.recordActivity();
|
|
1003
|
+
accumulator.processEvent(retryEvent);
|
|
1004
|
+
if (retryEvent.type === "status") {
|
|
1005
|
+
const retryStatusEvent = retryEvent as { status?: string; message?: string };
|
|
1006
|
+
if (retryStatusEvent.status === "ERROR" && retryStatusEvent.message) {
|
|
1007
|
+
streamErrorMessage = retryStatusEvent.message;
|
|
1008
|
+
}
|
|
880
1009
|
}
|
|
1010
|
+
heartbeat();
|
|
881
1011
|
}
|
|
882
|
-
|
|
1012
|
+
} finally {
|
|
1013
|
+
retryWatchdog.stop();
|
|
883
1014
|
}
|
|
884
1015
|
|
|
885
1016
|
const retryResult = await retryRun.wait();
|
|
@@ -953,8 +1084,10 @@ async function executeCursorInner(
|
|
|
953
1084
|
console.warn("Failed to update session with fresh agentId (non-fatal):", updateErr);
|
|
954
1085
|
}
|
|
955
1086
|
|
|
1087
|
+
let retryWatchdog: StallWatchdog | undefined;
|
|
956
1088
|
const retryRun = await freshAgent.send(effectivePrompt, {
|
|
957
1089
|
onDelta: ({ update }) => {
|
|
1090
|
+
retryWatchdog?.recordActivity();
|
|
958
1091
|
if (update.type === "turn-ended" && update.usage) {
|
|
959
1092
|
usageAccumulator.addTurn(update.usage);
|
|
960
1093
|
}
|
|
@@ -962,19 +1095,31 @@ async function executeCursorInner(
|
|
|
962
1095
|
try { heartbeat(); } catch { /* swallow during retry */ }
|
|
963
1096
|
},
|
|
964
1097
|
});
|
|
1098
|
+
// Mirror the primary stream's stall protection: a wedged retry must
|
|
1099
|
+
// not hang the activity. On stall, cancel the run so the loop ends and
|
|
1100
|
+
// retryRun.wait() resolves down the existing non-finished failure path.
|
|
1101
|
+
retryWatchdog = startStallWatchdog(config.cursorStreamStallTimeoutMs, (idleMs) => {
|
|
1102
|
+
console.warn(`ExecuteCursor retry stall detected: execution=${executionId}, idleMs=${idleMs}`);
|
|
1103
|
+
if (retryRun.supports?.("cancel")) void retryRun.cancel().catch(() => { /* best effort */ });
|
|
1104
|
+
});
|
|
965
1105
|
|
|
966
1106
|
streamErrorMessage = undefined;
|
|
967
1107
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
if (
|
|
974
|
-
|
|
1108
|
+
try {
|
|
1109
|
+
for await (const retryEvent of retryRun.stream()) {
|
|
1110
|
+
if (Context.current().cancellationSignal.aborted) break;
|
|
1111
|
+
retryWatchdog.recordActivity();
|
|
1112
|
+
accumulator.processEvent(retryEvent);
|
|
1113
|
+
if (retryEvent.type === "status") {
|
|
1114
|
+
const retryStatusEvent = retryEvent as { status?: string; message?: string };
|
|
1115
|
+
if (retryStatusEvent.status === "ERROR" && retryStatusEvent.message) {
|
|
1116
|
+
streamErrorMessage = retryStatusEvent.message;
|
|
1117
|
+
}
|
|
975
1118
|
}
|
|
1119
|
+
heartbeat();
|
|
976
1120
|
}
|
|
977
|
-
|
|
1121
|
+
} finally {
|
|
1122
|
+
retryWatchdog.stop();
|
|
978
1123
|
}
|
|
979
1124
|
|
|
980
1125
|
const retryResult = await retryRun.wait();
|
|
@@ -1059,9 +1204,8 @@ async function executeCursorInner(
|
|
|
1059
1204
|
// Cursor harness has no auto-publish pipeline, so this is the only
|
|
1060
1205
|
// artifact path; build storage from the same config-driven factory the
|
|
1061
1206
|
// native harness uses.
|
|
1062
|
-
if (interactionMode === InteractionMode.PLAN && finalText) {
|
|
1207
|
+
if (interactionMode === InteractionMode.PLAN && finalText && artifactStorage) {
|
|
1063
1208
|
try {
|
|
1064
|
-
const artifactStorage = createArtifactStorage(loadArtifactStorageConfig(config));
|
|
1065
1209
|
await publishPlanArtifact({ status, executionId, planText: finalText, artifactStorage });
|
|
1066
1210
|
} catch (err) {
|
|
1067
1211
|
console.warn(
|
|
@@ -1073,7 +1217,7 @@ async function executeCursorInner(
|
|
|
1073
1217
|
}
|
|
1074
1218
|
|
|
1075
1219
|
// NOW persist — subscriber sees COMPLETED + structured_output atomically
|
|
1076
|
-
await
|
|
1220
|
+
await persist(status);
|
|
1077
1221
|
|
|
1078
1222
|
console.log(
|
|
1079
1223
|
`ExecuteCursor completed: execution=${executionId}, phase=${ExecutionPhase[status.phase]}, ` +
|
|
@@ -1131,7 +1275,7 @@ async function executeCursorInner(
|
|
|
1131
1275
|
// The aborted Cursor run leaves no live sub-agent — mark any in-flight
|
|
1132
1276
|
// delegation CANCELLED so the final snapshot has no zombie sub-agent.
|
|
1133
1277
|
cancelInProgressSubAgentProtos(status.subAgentExecutions);
|
|
1134
|
-
await
|
|
1278
|
+
await persist(status).catch(() => {}); throw err;
|
|
1135
1279
|
}
|
|
1136
1280
|
|
|
1137
1281
|
// If a non-CancelledFailure error occurs while a pause is in progress,
|
|
@@ -1150,7 +1294,7 @@ async function executeCursorInner(
|
|
|
1150
1294
|
timestamp: utcTimestamp(),
|
|
1151
1295
|
}));
|
|
1152
1296
|
cancelInProgressSubAgentProtos(status.subAgentExecutions);
|
|
1153
|
-
await
|
|
1297
|
+
await persist(status).catch(() => {}); throw new CancelledFailure("Activity paused by orchestrator (error during pause)");
|
|
1154
1298
|
}
|
|
1155
1299
|
|
|
1156
1300
|
// Infrastructure cancellation (e.g. heartbeat timeout) with a
|
|
@@ -1169,7 +1313,7 @@ async function executeCursorInner(
|
|
|
1169
1313
|
timestamp: utcTimestamp(),
|
|
1170
1314
|
}));
|
|
1171
1315
|
cancelInProgressSubAgentProtos(status.subAgentExecutions);
|
|
1172
|
-
await
|
|
1316
|
+
await persist(status).catch(() => {}); throw new CancelledFailure("Activity cancelled (infrastructure, not user pause)");
|
|
1173
1317
|
}
|
|
1174
1318
|
|
|
1175
1319
|
// A thrown CursorSdkError carries structured fields (code/status/endpoint/
|
|
@@ -1207,7 +1351,7 @@ async function executeCursorInner(
|
|
|
1207
1351
|
}),
|
|
1208
1352
|
);
|
|
1209
1353
|
try {
|
|
1210
|
-
await
|
|
1354
|
+
await persist(status);
|
|
1211
1355
|
} catch (persistErr) {
|
|
1212
1356
|
console.error("Failed to persist error status (best-effort):", persistErr);
|
|
1213
1357
|
} return slimStatus(status);
|
|
@@ -1234,13 +1378,18 @@ async function executeCursorInner(
|
|
|
1234
1378
|
);
|
|
1235
1379
|
|
|
1236
1380
|
try {
|
|
1237
|
-
await
|
|
1381
|
+
await persist(status);
|
|
1238
1382
|
} catch (persistErr) {
|
|
1239
1383
|
console.error("Failed to persist error status (best-effort):", persistErr);
|
|
1240
1384
|
}
|
|
1241
1385
|
|
|
1242
1386
|
return slimStatus(status);
|
|
1243
1387
|
} finally {
|
|
1388
|
+
// Disarm the stall watchdog on EVERY exit path (idempotent). The happy
|
|
1389
|
+
// path stops it after the stream loop; this covers throws before that
|
|
1390
|
+
// point so no orphaned timer survives the activity.
|
|
1391
|
+
stallWatchdog?.stop();
|
|
1392
|
+
|
|
1244
1393
|
// Tear down the HITL gate on EVERY exit path (success, error, approval
|
|
1245
1394
|
// pause, cancellation) so attaching a real repo leaves the user's
|
|
1246
1395
|
// .cursor/hooks.json and workspace untouched between turns (issue #173).
|