@stigmer/runner 3.1.2 → 3.1.4
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/attachment-resolver.d.ts +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +29 -0
- package/dist/activities/execute-cursor/capture-flow.js +59 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
- package/dist/activities/execute-cursor/command-provenance.js +25 -115
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +358 -557
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
- package/dist/activities/execute-cursor/todo-tracker.js +15 -43
- package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
- package/dist/activities/execute-cursor/turn-boundary.js +163 -0
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
- package/dist/activities/execute-cursor/turn-stream.js +249 -0
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
- package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
- package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
- package/dist/activities/execute-deep-agent/index.js +73 -4
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +8 -1
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
- package/dist/activities/execute-deep-agent/streaming.js +5 -1
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/capture.d.ts +18 -1
- package/dist/shared/filereview/capture.js +105 -28
- package/dist/shared/filereview/capture.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +63 -0
- package/dist/shared/filereview/cas-progress.js +128 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +38 -0
- package/dist/shared/filereview/cas-substrate.js +48 -15
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/command-provenance.d.ts +93 -0
- package/dist/shared/filereview/command-provenance.js +132 -0
- package/dist/shared/filereview/command-provenance.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +16 -0
- package/dist/shared/filereview/events.js +37 -0
- package/dist/shared/filereview/events.js.map +1 -1
- package/dist/shared/filereview/git-substrate.d.ts +48 -0
- package/dist/shared/filereview/git-substrate.js +78 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +5 -2
- package/dist/shared/filereview/index.js +5 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/line-counts.d.ts +48 -0
- package/dist/shared/filereview/line-counts.js +72 -0
- package/dist/shared/filereview/line-counts.js.map +1 -0
- package/dist/shared/filereview/progress.d.ts +167 -0
- package/dist/shared/filereview/progress.js +196 -0
- package/dist/shared/filereview/progress.js.map +1 -0
- package/dist/shared/implement-plan-prompt.d.ts +6 -4
- package/dist/shared/implement-plan-prompt.js +38 -6
- package/dist/shared/implement-plan-prompt.js.map +1 -1
- package/dist/shared/plan-artifact.d.ts +59 -11
- package/dist/shared/plan-artifact.js +144 -16
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +15 -4
- package/dist/shared/plan-mode-prompt.js +28 -5
- package/dist/shared/plan-mode-prompt.js.map +1 -1
- package/dist/shared/todos.d.ts +56 -0
- package/dist/shared/todos.js +98 -0
- package/dist/shared/todos.js.map +1 -0
- package/dist/shared/tool-row.d.ts +16 -0
- package/dist/shared/tool-row.js +31 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -0
- package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
- package/src/activities/execute-cursor/capture-flow.ts +78 -0
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +419 -602
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-boundary.ts +274 -0
- package/src/activities/execute-cursor/turn-stream.ts +418 -0
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +11 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
- package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +91 -4
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
- package/src/activities/execute-deep-agent/streaming.ts +12 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/implement-plan-prompt.test.ts +40 -2
- package/src/shared/__tests__/plan-artifact.test.ts +142 -9
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/capture.test.ts +182 -1
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +100 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/events.test.ts +105 -0
- package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
- package/src/shared/filereview/__tests__/progress.test.ts +338 -0
- package/src/shared/filereview/capture.ts +134 -30
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +79 -18
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/events.ts +50 -0
- package/src/shared/filereview/git-substrate.ts +114 -0
- package/src/shared/filereview/index.ts +36 -1
- package/src/shared/filereview/line-counts.ts +83 -0
- package/src/shared/filereview/progress.ts +310 -0
- package/src/shared/implement-plan-prompt.ts +39 -8
- package/src/shared/plan-artifact.ts +152 -16
- package/src/shared/plan-mode-prompt.ts +28 -5
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cursor harness's stream phase — the single per-event consumption loop that
|
|
3
|
+
* turns a live Cursor SDK run into the streamed transcript, mid-run progress, and
|
|
4
|
+
* the flags that decide how the turn ends.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from the activity entry point (index.ts) so that BOTH the primary
|
|
7
|
+
* turn and the two recovery retries (poisoned-handle / transport-timeout) drive
|
|
8
|
+
* the exact same code. The retries previously re-implemented a stripped "bare"
|
|
9
|
+
* loop that dropped live persist, DD-32/DD-33 mid-run progress, sub-agent
|
|
10
|
+
* tracking, the first-denial early stop, and correct pause/stall handling — so a
|
|
11
|
+
* retry froze the UI and mis-handled a mid-retry pause. One loop removes that
|
|
12
|
+
* drift by construction.
|
|
13
|
+
*
|
|
14
|
+
* This mirrors the deep-agent harness's `streamExecution` seam
|
|
15
|
+
* (execute-deep-agent/streaming.ts): injected `heartbeat`/`isCancelled` so it
|
|
16
|
+
* runs without Temporal, a structural `StreamableRun` so it runs without the live
|
|
17
|
+
* SDK, and a result (the terminal reason) instead of internal terminal mapping.
|
|
18
|
+
* The one deliberate difference: this consumer REPORTS why the stream ended and
|
|
19
|
+
* leaves the terminal MAPPING to the activity, because Cursor's mapping is
|
|
20
|
+
* entangled with worker-shutdown-vs-pause disambiguation, the turn boundary,
|
|
21
|
+
* structured output, and recovery retries — all of which must stay in index.ts.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { create } from "@bufbuild/protobuf";
|
|
25
|
+
import { CancelledFailure } from "@temporalio/activity";
|
|
26
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
27
|
+
import { ExecutionControlSignal } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
28
|
+
import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
|
|
29
|
+
import type { SDKMessage, InteractionUpdate } from "@cursor/sdk";
|
|
30
|
+
import {
|
|
31
|
+
startStallWatchdog,
|
|
32
|
+
StallTimeoutError,
|
|
33
|
+
type StallWatchdog,
|
|
34
|
+
} from "../../shared/stall-watchdog.js";
|
|
35
|
+
import { shouldPersistStreamingStatus } from "./persist-decision.js";
|
|
36
|
+
import { readDenialLedger } from "./approval-state.js";
|
|
37
|
+
import {
|
|
38
|
+
captureFileChangeProgress,
|
|
39
|
+
type ProgressCaptureState,
|
|
40
|
+
type ProgressSubstrate,
|
|
41
|
+
} from "../../shared/filereview/progress.js";
|
|
42
|
+
import type { MessageAccumulator } from "./message-translator.js";
|
|
43
|
+
import type { DeltaEnricher } from "./delta-enricher.js";
|
|
44
|
+
import type { TodoTracker } from "./todo-tracker.js";
|
|
45
|
+
import type { StreamingUpdateScheduler } from "../../shared/streaming-scheduler.js";
|
|
46
|
+
import type { UsageAccumulator } from "./usage-accumulator.js";
|
|
47
|
+
import type { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The subset of the Cursor SDK `Run` the stream phase consumes. Kept structural
|
|
51
|
+
* so the loop is unit-testable with a mock run — no live SDK, no Temporal.
|
|
52
|
+
*/
|
|
53
|
+
export interface StreamableRun {
|
|
54
|
+
stream(): AsyncIterable<SDKMessage>;
|
|
55
|
+
supports?(feature: string): boolean;
|
|
56
|
+
cancel(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type CursorTurnEventRecorder = ReturnType<typeof createCursorEventRecorder>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Why the turn's stream ended. The activity maps each reason to a terminal
|
|
63
|
+
* outcome (see resolvePreBoundaryTerminal in index.ts): `completed` and
|
|
64
|
+
* `first-denial` proceed to the turn boundary; the rest are pre-boundary
|
|
65
|
+
* terminals that return or throw.
|
|
66
|
+
*/
|
|
67
|
+
export type TurnStreamReason =
|
|
68
|
+
| "completed"
|
|
69
|
+
| "paused"
|
|
70
|
+
| "stalled"
|
|
71
|
+
| "first-denial"
|
|
72
|
+
| "platform-stop";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Single owner for every flag the turn's stream produces. Before this, these
|
|
76
|
+
* eleven fields lived as scattered `let`s co-written by FOUR concurrent
|
|
77
|
+
* producers — the SDK `onDelta`, the fs denial-watcher, the stall-watchdog, and
|
|
78
|
+
* the loop — the classic "who writes this field?" hazard. Consolidating them into
|
|
79
|
+
* one object (created once per turn, shared by every producer, the epilogue, and
|
|
80
|
+
* the outer catch/finally) makes ownership explicit and lets the primary turn and
|
|
81
|
+
* both recovery retries drive the identical stream code.
|
|
82
|
+
*
|
|
83
|
+
* Field ownership is annotated per field: exactly one primary writer, plus any
|
|
84
|
+
* documented producer/consumer handshakes.
|
|
85
|
+
*/
|
|
86
|
+
export interface TurnStreamState {
|
|
87
|
+
/** Written by: onDelta (heartbeat cancel) + the loop's cancellation check. Read by: the loop, epilogue, outer catch. */
|
|
88
|
+
pauseDetected: boolean;
|
|
89
|
+
/** Written by: the stall-watchdog callback. Read by: the loop + epilogue. */
|
|
90
|
+
stallDetected: boolean;
|
|
91
|
+
stallError: StallTimeoutError | undefined;
|
|
92
|
+
/** Written by: the loop (on the first ledger entry). Read by: the loop, epilogue, settleRetryTurn. */
|
|
93
|
+
firstDenialDetected: boolean;
|
|
94
|
+
/** Written by: the fs denial-watcher (→ true) + the loop (→ false after a read). Read by: the loop. */
|
|
95
|
+
denialLedgerDirty: boolean;
|
|
96
|
+
/** Written by: the loop (the first-denial run.cancel()). Read by: the turn boundary. */
|
|
97
|
+
denialCancelSettled: Promise<void> | undefined;
|
|
98
|
+
/** Written by: the loop (a platform STOP signal from persist). Read by: the loop + epilogue. */
|
|
99
|
+
platformStopSignaled: boolean;
|
|
100
|
+
/** Written by: the loop (a stream ERROR status event) + the retry setup (reset). Read by: error classification. */
|
|
101
|
+
streamErrorMessage: string | undefined;
|
|
102
|
+
/** Written by: the loop (each tool_call). Read by: the stall-watchdog (message enrichment). */
|
|
103
|
+
lastToolName: string | undefined;
|
|
104
|
+
/** Written by: the loop. Read by: the event recorder, the persist cadence, and logs. */
|
|
105
|
+
eventCount: number;
|
|
106
|
+
/** Written + read by: onDelta (log the first turn's context attribution exactly once). */
|
|
107
|
+
firstTurnAttributionLogged: boolean;
|
|
108
|
+
/** The stall watchdog for the CURRENT stream, armed by consumeCursorTurnStream; held so onDelta can reset it and the activity's finally can stop it. */
|
|
109
|
+
stallWatchdog: StallWatchdog | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function newTurnStreamState(): TurnStreamState {
|
|
113
|
+
return {
|
|
114
|
+
pauseDetected: false,
|
|
115
|
+
stallDetected: false,
|
|
116
|
+
stallError: undefined,
|
|
117
|
+
firstDenialDetected: false,
|
|
118
|
+
denialLedgerDirty: false,
|
|
119
|
+
denialCancelSettled: undefined,
|
|
120
|
+
platformStopSignaled: false,
|
|
121
|
+
streamErrorMessage: undefined,
|
|
122
|
+
lastToolName: undefined,
|
|
123
|
+
eventCount: 0,
|
|
124
|
+
firstTurnAttributionLogged: false,
|
|
125
|
+
stallWatchdog: undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The subset of collaborators the shared onDelta needs. Broken out from
|
|
131
|
+
* CursorTurnStreamDeps because the primary send happens BEFORE the accumulator
|
|
132
|
+
* exists — onDelta only touches usage, the enricher, the heartbeat, and state.
|
|
133
|
+
*/
|
|
134
|
+
export interface TurnOnDeltaDeps {
|
|
135
|
+
readonly usageAccumulator: UsageAccumulator;
|
|
136
|
+
readonly deltaEnricher: DeltaEnricher;
|
|
137
|
+
/** Temporal heartbeat. Injected so the callback is testable without Temporal. */
|
|
138
|
+
readonly heartbeat: () => void;
|
|
139
|
+
readonly promptEstimatedTokens: number;
|
|
140
|
+
readonly executionId: string;
|
|
141
|
+
readonly state: TurnStreamState;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CursorTurnStreamDeps extends TurnOnDeltaDeps {
|
|
145
|
+
readonly status: AgentExecutionStatus;
|
|
146
|
+
readonly accumulator: MessageAccumulator;
|
|
147
|
+
readonly todoTracker: TodoTracker;
|
|
148
|
+
readonly eventRecorder: CursorTurnEventRecorder | undefined;
|
|
149
|
+
readonly scheduler: StreamingUpdateScheduler;
|
|
150
|
+
readonly progressSubstrate: ProgressSubstrate | undefined;
|
|
151
|
+
readonly progressState: ProgressCaptureState;
|
|
152
|
+
readonly changeSetId: string;
|
|
153
|
+
/** Session HITL dir holding the denial ledger; undefined → no gate installed → no first-denial stop. */
|
|
154
|
+
readonly hitlDir: string | undefined;
|
|
155
|
+
readonly stallTimeoutMs: number;
|
|
156
|
+
/** The single size-bounded persist chokepoint. Returns the platform control signal. */
|
|
157
|
+
readonly persist: (status: AgentExecutionStatus) => Promise<ExecutionControlSignal>;
|
|
158
|
+
/** Temporal cancellation check. Injected so the loop is testable without Temporal. */
|
|
159
|
+
readonly isCancelled: () => boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the shared onDelta callback. The Cursor SDK's fine-grained delta channel
|
|
164
|
+
* carries token usage, live shell output, and precise tool-call timings; it also
|
|
165
|
+
* fires far more often than discrete stream events, so it is where the stall
|
|
166
|
+
* timer is reset during a long model generation. A heartbeat CancelledFailure
|
|
167
|
+
* here flags the pause so the loop and epilogue treat it as a user pause (the
|
|
168
|
+
* bare retry loops swallowed this — the cause of the mid-retry mislabel).
|
|
169
|
+
*/
|
|
170
|
+
export function makeCursorTurnOnDelta(
|
|
171
|
+
deps: TurnOnDeltaDeps,
|
|
172
|
+
): (event: { update: InteractionUpdate }) => void {
|
|
173
|
+
const { usageAccumulator, deltaEnricher, heartbeat, promptEstimatedTokens, executionId, state } =
|
|
174
|
+
deps;
|
|
175
|
+
return ({ update }) => {
|
|
176
|
+
// Reset the stall timer on the delta channel too: a long model generation
|
|
177
|
+
// emits token deltas but few discrete stream events, so resetting only in the
|
|
178
|
+
// stream loop would false-positive a stall.
|
|
179
|
+
state.stallWatchdog?.recordActivity();
|
|
180
|
+
if (update.type === "turn-ended" && update.usage) {
|
|
181
|
+
usageAccumulator.addTurn(update.usage);
|
|
182
|
+
|
|
183
|
+
if (!state.firstTurnAttributionLogged) {
|
|
184
|
+
state.firstTurnAttributionLogged = true;
|
|
185
|
+
const sdkInputTokens = update.usage.inputTokens ?? 0;
|
|
186
|
+
const cursorOverhead = Math.max(0, sdkInputTokens - promptEstimatedTokens);
|
|
187
|
+
console.log(
|
|
188
|
+
`ExecuteCursor context attribution (first turn): execution=${executionId}, ` +
|
|
189
|
+
`sdkInputTokens=${sdkInputTokens}, stigmerPreamble=${promptEstimatedTokens}, ` +
|
|
190
|
+
`cursorOverhead=${cursorOverhead} (estimated)`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
deltaEnricher.processDelta(update);
|
|
195
|
+
try {
|
|
196
|
+
heartbeat();
|
|
197
|
+
} catch (hbErr) {
|
|
198
|
+
if (hbErr instanceof CancelledFailure) {
|
|
199
|
+
state.pauseDetected = true;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
throw hbErr;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Consume a Cursor SDK run to completion (or to a clean early stop), driving the
|
|
209
|
+
* transcript, todos, sub-agent tracking, live persist, and DD-32/DD-33 mid-run
|
|
210
|
+
* progress, and reporting WHY the stream ended.
|
|
211
|
+
*
|
|
212
|
+
* Arms + stops its own stall watchdog (stored on `state.stallWatchdog` so the
|
|
213
|
+
* shared onDelta can reset it and the activity's finally can stop it as a
|
|
214
|
+
* backstop). Mutates `deps.status` + `deps.state` in place. The caller owns the
|
|
215
|
+
* post-stream finalize and the terminal mapping.
|
|
216
|
+
*/
|
|
217
|
+
export async function consumeCursorTurnStream(
|
|
218
|
+
run: StreamableRun,
|
|
219
|
+
deps: CursorTurnStreamDeps,
|
|
220
|
+
): Promise<TurnStreamReason> {
|
|
221
|
+
const {
|
|
222
|
+
status,
|
|
223
|
+
accumulator,
|
|
224
|
+
todoTracker,
|
|
225
|
+
deltaEnricher,
|
|
226
|
+
eventRecorder,
|
|
227
|
+
scheduler,
|
|
228
|
+
usageAccumulator,
|
|
229
|
+
progressSubstrate,
|
|
230
|
+
progressState,
|
|
231
|
+
changeSetId,
|
|
232
|
+
hitlDir,
|
|
233
|
+
executionId,
|
|
234
|
+
stallTimeoutMs,
|
|
235
|
+
persist,
|
|
236
|
+
heartbeat,
|
|
237
|
+
isCancelled,
|
|
238
|
+
state,
|
|
239
|
+
} = deps;
|
|
240
|
+
|
|
241
|
+
// Arm the stall watchdog now that the run exists. The activity's periodic
|
|
242
|
+
// heartbeat proves the process is alive, not that the agent is progressing: if
|
|
243
|
+
// the stream wedges (a tool call or model connection that never returns), no
|
|
244
|
+
// event/delta arrives and the turn would hang forever. On stall we end the run
|
|
245
|
+
// cleanly via the SDK's run.cancel() (guarded by supports("cancel")), which
|
|
246
|
+
// unblocks the for-await; the caller then reports EXECUTION_FAILED with a
|
|
247
|
+
// recognizable, actionable message.
|
|
248
|
+
state.stallWatchdog = startStallWatchdog(stallTimeoutMs, (idleMs) => {
|
|
249
|
+
state.stallDetected = true;
|
|
250
|
+
state.stallError = new StallTimeoutError(
|
|
251
|
+
idleMs,
|
|
252
|
+
state.lastToolName ? `last tool: ${state.lastToolName}` : undefined,
|
|
253
|
+
);
|
|
254
|
+
console.warn(
|
|
255
|
+
`ExecuteCursor stall detected: execution=${executionId}, idleMs=${idleMs}, lastTool=${state.lastToolName ?? "none"}`,
|
|
256
|
+
);
|
|
257
|
+
if (run.supports?.("cancel")) {
|
|
258
|
+
void run.cancel().catch((cancelErr) => {
|
|
259
|
+
console.warn(
|
|
260
|
+
`ExecuteCursor run.cancel() after stall failed (non-fatal): execution=${executionId}, ` +
|
|
261
|
+
`error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`,
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
for await (const event of run.stream()) {
|
|
269
|
+
if (state.pauseDetected || isCancelled()) {
|
|
270
|
+
state.pauseDetected = true;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (state.stallDetected) break;
|
|
274
|
+
|
|
275
|
+
// Progress: reset the stall timer on every stream event.
|
|
276
|
+
state.stallWatchdog.recordActivity();
|
|
277
|
+
if (event.type === "tool_call" && typeof event.name === "string") {
|
|
278
|
+
state.lastToolName = event.name;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
eventRecorder?.record(event, state.eventCount);
|
|
282
|
+
|
|
283
|
+
accumulator.processEvent(event);
|
|
284
|
+
todoTracker.processEvent(event);
|
|
285
|
+
|
|
286
|
+
if (event.type === "tool_call" && event.name === "task") {
|
|
287
|
+
accumulator.trackSubAgentExecution(event as Extract<SDKMessage, { type: "tool_call" }>);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// First-denial stop (HITL clean pause). In CAPTURE mode this fires only for
|
|
291
|
+
// an IRREVERSIBLE tool the hook still gates (shell, MCP, or a gitignored
|
|
292
|
+
// write/delete) — file edits flow freely and are captured at the turn
|
|
293
|
+
// boundary, so they never enter the ledger. In the deny-gate FALLBACK
|
|
294
|
+
// (non-git workspace) it fires for every gated file edit too. Either way:
|
|
295
|
+
// the preToolUse hook appends to the denial ledger the instant it gates a
|
|
296
|
+
// tool — before Cursor surfaces the failure to the model — and the fs
|
|
297
|
+
// watcher flips denialLedgerDirty the moment that write lands. Confirming
|
|
298
|
+
// the flag with a read on the very next event (of ANY type — thinking
|
|
299
|
+
// deltas arrive within milliseconds) ends the turn before the model's
|
|
300
|
+
// reaction can persist: waiting for the next tool_call event let the full
|
|
301
|
+
// post-denial reaction (thinking, narration, a workaround shell) stream and
|
|
302
|
+
// persist live (production case aex_01kwj07f7g23c3wp9sn8496z5g). The
|
|
303
|
+
// tool_call-event read stays as the backstop for platforms where fs.watch
|
|
304
|
+
// is unreliable; the current event was already accumulated above, so the
|
|
305
|
+
// anchor's own row is always present for the turn-boundary gate overlay.
|
|
306
|
+
if (!state.firstDenialDetected && hitlDir && (state.denialLedgerDirty || event.type === "tool_call")) {
|
|
307
|
+
state.denialLedgerDirty = false;
|
|
308
|
+
const denials = await readDenialLedger(hitlDir);
|
|
309
|
+
if (denials.length > 0) {
|
|
310
|
+
state.firstDenialDetected = true;
|
|
311
|
+
console.log(
|
|
312
|
+
`ExecuteCursor first denial detected (${denials.length} ledger ` +
|
|
313
|
+
`entr${denials.length === 1 ? "y" : "ies"}); stopping turn to pause ` +
|
|
314
|
+
`cleanly for approval: execution=${executionId}`,
|
|
315
|
+
);
|
|
316
|
+
if (run.supports?.("cancel")) {
|
|
317
|
+
// Kept (not fire-and-forget): awaited timeboxed by the turn boundary
|
|
318
|
+
// so the ledger read and tree capture see a stopped agent.
|
|
319
|
+
state.denialCancelSettled = run.cancel().then(
|
|
320
|
+
() => {},
|
|
321
|
+
(cancelErr: unknown) => {
|
|
322
|
+
console.warn(
|
|
323
|
+
`ExecuteCursor run.cancel() after first denial failed (non-fatal): ` +
|
|
324
|
+
`execution=${executionId}, ` +
|
|
325
|
+
`error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`,
|
|
326
|
+
);
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
deltaEnricher.applyEnrichments(status.messages);
|
|
335
|
+
state.eventCount++;
|
|
336
|
+
|
|
337
|
+
if (event.type === "status") {
|
|
338
|
+
console.log(
|
|
339
|
+
`ExecuteCursor stream status: execution=${executionId}, status=${JSON.stringify(event)}`,
|
|
340
|
+
);
|
|
341
|
+
const statusEvent = event as { status?: string; message?: string };
|
|
342
|
+
if (statusEvent.status === "ERROR" && statusEvent.message) {
|
|
343
|
+
state.streamErrorMessage = statusEvent.message;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const shouldPersist = shouldPersistStreamingStatus(
|
|
348
|
+
{
|
|
349
|
+
deltaEnricherDirty: deltaEnricher.isDirty,
|
|
350
|
+
todosDirty: todoTracker.isDirty,
|
|
351
|
+
contentDirty: accumulator.isDirty,
|
|
352
|
+
},
|
|
353
|
+
scheduler,
|
|
354
|
+
state.eventCount,
|
|
355
|
+
);
|
|
356
|
+
if (usageAccumulator.hasTurns) {
|
|
357
|
+
status.streamingUsage = create(StreamingUsageSummarySchema, usageAccumulator.snapshot());
|
|
358
|
+
}
|
|
359
|
+
if (shouldPersist) {
|
|
360
|
+
// Sync sub-agent executions into status before every persist so the live
|
|
361
|
+
// UI reflects delegation (including the IN_PROGRESS state) while the
|
|
362
|
+
// parent is still running — matching the native harness.
|
|
363
|
+
status.subAgentExecutions = accumulator.subAgentExecutions;
|
|
364
|
+
// Mid-run live capture (DD-32 / DD-33): attach the "N files changed so
|
|
365
|
+
// far" snapshot onto status.file_change_progress, throttled internally by
|
|
366
|
+
// the floor. Never authoritative — the turn-boundary candidate remains
|
|
367
|
+
// the reviewed diff.
|
|
368
|
+
if (progressSubstrate) {
|
|
369
|
+
await captureFileChangeProgress({
|
|
370
|
+
status,
|
|
371
|
+
changeSetId,
|
|
372
|
+
substrate: progressSubstrate,
|
|
373
|
+
state: progressState,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const signal = await persist(status);
|
|
377
|
+
deltaEnricher.markPersisted();
|
|
378
|
+
todoTracker.markPersisted();
|
|
379
|
+
accumulator.markPersisted();
|
|
380
|
+
scheduler.markUpdateSent(state.eventCount);
|
|
381
|
+
heartbeat();
|
|
382
|
+
if (signal === ExecutionControlSignal.STOP) {
|
|
383
|
+
state.platformStopSignaled = true;
|
|
384
|
+
console.warn(`ExecuteCursor platform stop signal received: execution=${executionId}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (state.platformStopSignaled) {
|
|
389
|
+
console.log(
|
|
390
|
+
`ExecuteCursor stopping stream due to platform stop signal: execution=${executionId}`,
|
|
391
|
+
);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch (streamErr) {
|
|
396
|
+
// run.cancel() — from the stall watchdog or the first-denial stop — can make
|
|
397
|
+
// the stream iterator reject as it tears down; that is the expected teardown
|
|
398
|
+
// for both, so swallow it and fall through. Anything else is a genuine stream
|
|
399
|
+
// failure — rethrow it to the activity's error handler.
|
|
400
|
+
if (!state.stallDetected && !state.firstDenialDetected) throw streamErr;
|
|
401
|
+
console.warn(
|
|
402
|
+
`ExecuteCursor stream ended via cancel: execution=${executionId}, ` +
|
|
403
|
+
`stall=${state.stallDetected}, firstDenial=${state.firstDenialDetected}`,
|
|
404
|
+
);
|
|
405
|
+
} finally {
|
|
406
|
+
state.stallWatchdog.stop();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Report why the stream ended, in the same precedence the activity's epilogue
|
|
410
|
+
// applies: a stall (an EXECUTION_FAILED terminal) outranks everything; a
|
|
411
|
+
// platform stop and a first denial are distinct proceed/return outcomes; a
|
|
412
|
+
// pause is the fallback for a cancellation with no other cause.
|
|
413
|
+
if (state.stallDetected) return "stalled";
|
|
414
|
+
if (state.platformStopSignaled) return "platform-stop";
|
|
415
|
+
if (state.firstDenialDetected) return "first-denial";
|
|
416
|
+
if (state.pauseDetected || isCancelled()) return "paused";
|
|
417
|
+
return "completed";
|
|
418
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the DEEP-AGENT approved-command provenance adapter (DD-28).
|
|
3
|
+
*
|
|
4
|
+
* These pin the two things that differ from the Cursor harness: identity turn
|
|
5
|
+
* scoping (a call whose id is absent from the pre-stream settled snapshot is
|
|
6
|
+
* this-turn's) and same-row consent (the executed shell row carries its own
|
|
7
|
+
* server-authored approval_action). The two regressions the plan calls out are
|
|
8
|
+
* asserted explicitly: a seeded WAITING_APPROVAL -> COMPLETED approved shell IS
|
|
9
|
+
* counted, and a prior turn's settled call is NOT re-evaluated. The DD-28 rule
|
|
10
|
+
* itself is covered by shared/filereview/__tests__/command-provenance.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import { create, type MessageInitShape } from "@bufbuild/protobuf";
|
|
15
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
16
|
+
import {
|
|
17
|
+
AgentMessageSchema,
|
|
18
|
+
ToolCallSchema,
|
|
19
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
20
|
+
import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
21
|
+
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
22
|
+
import {
|
|
23
|
+
ApprovalAction,
|
|
24
|
+
MessageType,
|
|
25
|
+
ToolCallStatus,
|
|
26
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
27
|
+
|
|
28
|
+
import { deriveTurnCommandProvenance } from "../command-provenance.js";
|
|
29
|
+
|
|
30
|
+
function toolCall(overrides: MessageInitShape<typeof ToolCallSchema>): ToolCall {
|
|
31
|
+
return create(ToolCallSchema, {
|
|
32
|
+
id: "call-1",
|
|
33
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
34
|
+
...overrides,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function aiMessage(toolCalls: ToolCall[]): AgentMessage {
|
|
39
|
+
return create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, toolCalls });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A COMPLETED shell row (the deep-agent shell tool is `execute`). */
|
|
43
|
+
function executedShell(id: string, overrides: MessageInitShape<typeof ToolCallSchema> = {}): ToolCall {
|
|
44
|
+
return toolCall({ id, name: "execute", args: { command: "seq 1 5 > out.txt" }, ...overrides });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function status(messages: AgentMessage[], subAgents: MessageInitShape<typeof SubAgentExecutionSchema>[] = []) {
|
|
48
|
+
return create(AgentExecutionStatusSchema, {
|
|
49
|
+
messages,
|
|
50
|
+
subAgentExecutions: subAgents.map((sa) => create(SubAgentExecutionSchema, sa)),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const NO_SETTLED = new Set<string>();
|
|
55
|
+
const NO_SUBAGENTS = new Set<string>();
|
|
56
|
+
|
|
57
|
+
describe("deep-agent deriveTurnCommandProvenance", () => {
|
|
58
|
+
it("cites the executed shell's OWN id for a per-command approval (same-row consent)", () => {
|
|
59
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
60
|
+
const provenance = deriveTurnCommandProvenance({
|
|
61
|
+
status: status([aiMessage([shell])]),
|
|
62
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
63
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
64
|
+
globalBypass: false,
|
|
65
|
+
});
|
|
66
|
+
expect(provenance).toBeDefined();
|
|
67
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
68
|
+
expect(provenance!.authorizedByAutoApproveAll).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("cites the executed shell's own id for an APPROVE_ALL on the same row", () => {
|
|
72
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE_ALL });
|
|
73
|
+
const provenance = deriveTurnCommandProvenance({
|
|
74
|
+
status: status([aiMessage([shell])]),
|
|
75
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
76
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
77
|
+
globalBypass: false,
|
|
78
|
+
});
|
|
79
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("cites a prior APPROVE_ALL lease row for a command that ran ungated under the lease", () => {
|
|
83
|
+
// Prior turn: a gated command the user approved-all (settled before this turn).
|
|
84
|
+
const leaseRow = executedShell("lease-row", {
|
|
85
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
86
|
+
approvalAction: ApprovalAction.APPROVE_ALL,
|
|
87
|
+
});
|
|
88
|
+
// This turn: a fresh command runs ungated under the lease — no approval_action.
|
|
89
|
+
const leased = executedShell("execute-leased");
|
|
90
|
+
const provenance = deriveTurnCommandProvenance({
|
|
91
|
+
status: status([aiMessage([leaseRow]), aiMessage([leased])]),
|
|
92
|
+
priorSettledToolCallIds: new Set(["lease-row"]), // the lease row is a prior-turn settled call
|
|
93
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
94
|
+
globalBypass: false,
|
|
95
|
+
});
|
|
96
|
+
expect(provenance).toBeDefined();
|
|
97
|
+
expect(provenance!.consentToolCallIds).toEqual(["lease-row"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("flags auto_approve_all for an ungated command under the global bypass", () => {
|
|
101
|
+
const shell = executedShell("execute-1"); // no approval_action, no lease
|
|
102
|
+
const provenance = deriveTurnCommandProvenance({
|
|
103
|
+
status: status([aiMessage([shell])]),
|
|
104
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
105
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
106
|
+
globalBypass: true,
|
|
107
|
+
});
|
|
108
|
+
expect(provenance).toBeDefined();
|
|
109
|
+
expect(provenance!.consentToolCallIds).toEqual([]);
|
|
110
|
+
expect(provenance!.authorizedByAutoApproveAll).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("fails closed on a file-tool call this turn (even under auto_approve_all)", () => {
|
|
114
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
115
|
+
const write = toolCall({ id: "write-1", name: "write_file", args: { path: "a.txt" } });
|
|
116
|
+
const provenance = deriveTurnCommandProvenance({
|
|
117
|
+
status: status([aiMessage([shell, write])]),
|
|
118
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
119
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
120
|
+
globalBypass: true,
|
|
121
|
+
});
|
|
122
|
+
expect(provenance).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("fails closed on an MCP tool this turn", () => {
|
|
126
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
127
|
+
const mcp = toolCall({ id: "mcp-1", name: "apply_resource", mcpServerSlug: "planton" });
|
|
128
|
+
const provenance = deriveTurnCommandProvenance({
|
|
129
|
+
status: status([aiMessage([shell, mcp])]),
|
|
130
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
131
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
132
|
+
globalBypass: false,
|
|
133
|
+
});
|
|
134
|
+
expect(provenance).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("fails closed on an unrecognized tool name this turn", () => {
|
|
138
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
139
|
+
const unknown = toolCall({ id: "u-1", name: "mystery_tool" });
|
|
140
|
+
const provenance = deriveTurnCommandProvenance({
|
|
141
|
+
status: status([aiMessage([shell, unknown])]),
|
|
142
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
143
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
144
|
+
globalBypass: false,
|
|
145
|
+
});
|
|
146
|
+
expect(provenance).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("fails closed on a top-level sub-agent delegation (task row this turn)", () => {
|
|
150
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
151
|
+
const task = toolCall({ id: "task-1", name: "task", args: { prompt: "go" } });
|
|
152
|
+
const provenance = deriveTurnCommandProvenance({
|
|
153
|
+
status: status([aiMessage([shell, task])]),
|
|
154
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
155
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
156
|
+
globalBypass: false,
|
|
157
|
+
});
|
|
158
|
+
expect(provenance).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("fails closed when a sub-agent produced NEW tool-call rows this turn", () => {
|
|
162
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
163
|
+
const subAgentShell = executedShell("sub-execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
164
|
+
const provenance = deriveTurnCommandProvenance({
|
|
165
|
+
status: status([aiMessage([shell])], [
|
|
166
|
+
{ name: "worker", messages: [aiMessage([subAgentShell])] },
|
|
167
|
+
]),
|
|
168
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
169
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS, // the sub-agent row is new this turn
|
|
170
|
+
globalBypass: false,
|
|
171
|
+
});
|
|
172
|
+
expect(provenance).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does NOT fail closed for a sub-agent whose rows all pre-existed this turn", () => {
|
|
176
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
177
|
+
const priorSubShell = executedShell("sub-prior-1", { approvalAction: ApprovalAction.APPROVE });
|
|
178
|
+
const provenance = deriveTurnCommandProvenance({
|
|
179
|
+
status: status([aiMessage([shell])], [
|
|
180
|
+
{ name: "worker", messages: [aiMessage([priorSubShell])] },
|
|
181
|
+
]),
|
|
182
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
183
|
+
priorSubAgentToolCallIds: new Set(["sub-prior-1"]), // seeded from a prior turn
|
|
184
|
+
globalBypass: false,
|
|
185
|
+
});
|
|
186
|
+
expect(provenance).toBeDefined();
|
|
187
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns undefined when no command executed this turn", () => {
|
|
191
|
+
const pending = executedShell("execute-pending", {
|
|
192
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
193
|
+
});
|
|
194
|
+
const provenance = deriveTurnCommandProvenance({
|
|
195
|
+
status: status([aiMessage([pending])]),
|
|
196
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
197
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
198
|
+
globalBypass: false,
|
|
199
|
+
});
|
|
200
|
+
expect(provenance).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── Regression: identity scoping (the reason positional scoping can't port) ──
|
|
204
|
+
|
|
205
|
+
it("REGRESSION counts a seeded WAITING_APPROVAL -> COMPLETED approved shell (its id was not settled pre-stream)", () => {
|
|
206
|
+
// The shell was proposed + approved in a prior segment (WAITING_APPROVAL, so
|
|
207
|
+
// NOT in the pre-stream settled snapshot) and executes in place this segment.
|
|
208
|
+
const executedThisTurn = executedShell("execute-approved", {
|
|
209
|
+
approvalAction: ApprovalAction.APPROVE,
|
|
210
|
+
});
|
|
211
|
+
const provenance = deriveTurnCommandProvenance({
|
|
212
|
+
status: status([aiMessage([executedThisTurn])]),
|
|
213
|
+
priorSettledToolCallIds: NO_SETTLED, // it was WAITING, not settled, before the stream
|
|
214
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
215
|
+
globalBypass: false,
|
|
216
|
+
});
|
|
217
|
+
expect(provenance).toBeDefined();
|
|
218
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-approved"]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("REGRESSION does NOT re-count a prior turn's already-settled shell", () => {
|
|
222
|
+
const priorShell = executedShell("execute-prior", { approvalAction: ApprovalAction.APPROVE });
|
|
223
|
+
const provenance = deriveTurnCommandProvenance({
|
|
224
|
+
status: status([aiMessage([priorShell])]),
|
|
225
|
+
priorSettledToolCallIds: new Set(["execute-prior"]), // settled in a prior turn
|
|
226
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
227
|
+
globalBypass: false,
|
|
228
|
+
});
|
|
229
|
+
// Nothing executed THIS turn -> nothing to attribute the change set to.
|
|
230
|
+
expect(provenance).toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("REGRESSION a prior turn's settled non-shell call never disqualifies this turn", () => {
|
|
234
|
+
// A prior FAILED edit must be excluded by the settled snapshot — otherwise it
|
|
235
|
+
// would trip the !== SHELL fail-closed on a clean shell-only turn.
|
|
236
|
+
const priorEdit = toolCall({
|
|
237
|
+
id: "edit-prior",
|
|
238
|
+
name: "edit_file",
|
|
239
|
+
status: ToolCallStatus.TOOL_CALL_FAILED,
|
|
240
|
+
args: { path: "a.txt" },
|
|
241
|
+
});
|
|
242
|
+
const shellThisTurn = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
243
|
+
const provenance = deriveTurnCommandProvenance({
|
|
244
|
+
status: status([aiMessage([priorEdit]), aiMessage([shellThisTurn])]),
|
|
245
|
+
priorSettledToolCallIds: new Set(["edit-prior"]),
|
|
246
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
247
|
+
globalBypass: false,
|
|
248
|
+
});
|
|
249
|
+
expect(provenance).toBeDefined();
|
|
250
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -292,5 +292,16 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
292
292
|
|
|
293
293
|
expect(prompt).not.toContain("## Implement the approved plan");
|
|
294
294
|
});
|
|
295
|
+
|
|
296
|
+
it("carries the plan-derived progress-tracking instruction (Tier 3)", () => {
|
|
297
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
298
|
+
...base,
|
|
299
|
+
buildFromPlan: true,
|
|
300
|
+
injectedFiles: [planFile],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(prompt).toContain("to-do list");
|
|
304
|
+
expect(prompt).toContain("break the plan into");
|
|
305
|
+
});
|
|
295
306
|
});
|
|
296
307
|
});
|