@stigmer/runner 3.1.0 → 3.1.2
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 +49 -8
- package/dist/activities/execute-cursor/attachment-resolver.js +83 -26
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/cas-observations.d.ts +13 -0
- package/dist/activities/execute-cursor/cas-observations.js +39 -5
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +19 -1
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -0
- package/dist/activities/execute-cursor/index.js +102 -22
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.js +10 -1
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +32 -7
- package/dist/activities/execute-cursor/prompt-builder.js +38 -20
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +1 -10
- package/dist/activities/execute-cursor/skill-resolver.js +3 -55
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/stigmer-link.d.ts +35 -0
- package/dist/activities/execute-cursor/stigmer-link.js +73 -0
- package/dist/activities/execute-cursor/stigmer-link.js.map +1 -0
- package/dist/activities/execute-cursor/workspace-provision.d.ts +3 -2
- package/dist/activities/execute-cursor/workspace-provision.js +5 -3
- package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +59 -2
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +17 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +22 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.js +36 -6
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +2 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +32 -0
- package/dist/shared/artifact-storage.js +74 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/implement-plan-prompt.d.ts +42 -0
- package/dist/shared/implement-plan-prompt.js +66 -0
- package/dist/shared/implement-plan-prompt.js.map +1 -0
- package/dist/shared/plan-mode-prompt.d.ts +30 -0
- package/dist/shared/plan-mode-prompt.js +51 -0
- package/dist/shared/plan-mode-prompt.js.map +1 -0
- package/dist/shared/status.d.ts +11 -0
- package/dist/shared/status.js +12 -2
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-row.d.ts +30 -1
- package/dist/shared/tool-row.js +53 -4
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/session-root.d.ts +35 -0
- package/dist/shared/workspace/session-root.js +53 -0
- package/dist/shared/workspace/session-root.js.map +1 -0
- package/dist/shared/workspace/workspace-lock.d.ts +86 -0
- package/dist/shared/workspace/workspace-lock.js +187 -0
- package/dist/shared/workspace/workspace-lock.js.map +1 -0
- package/package.json +7 -5
- package/src/__tests__/config.test.ts +8 -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__/attachment-resolver.test.ts +161 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +125 -2
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +55 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +28 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +63 -4
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +36 -2
- package/src/activities/execute-cursor/attachment-resolver.ts +116 -34
- package/src/activities/execute-cursor/cas-observations.ts +41 -5
- package/src/activities/execute-cursor/hook-script.ts +19 -1
- package/src/activities/execute-cursor/index.ts +114 -26
- package/src/activities/execute-cursor/message-translator.ts +9 -1
- package/src/activities/execute-cursor/prompt-builder.ts +60 -22
- package/src/activities/execute-cursor/skill-resolver.ts +3 -59
- package/src/activities/execute-cursor/stigmer-link.ts +78 -0
- package/src/activities/execute-cursor/workspace-provision.ts +5 -3
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +4 -1
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +88 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +7 -2
- package/src/activities/execute-deep-agent/index.ts +70 -2
- package/src/activities/execute-deep-agent/prompt-builder.ts +39 -0
- package/src/activities/execute-deep-agent/setup.ts +25 -17
- package/src/config.ts +17 -0
- package/src/middleware/__tests__/approval-gate.test.ts +77 -12
- package/src/middleware/approval-gate.ts +39 -7
- package/src/runner-manager.ts +6 -1
- package/src/runner.ts +6 -1
- package/src/shared/__tests__/artifact-storage.test.ts +111 -1
- package/src/shared/__tests__/implement-plan-prompt.test.ts +48 -0
- package/src/shared/__tests__/secret-leak-scan.test.ts +105 -0
- package/src/shared/__tests__/status.test.ts +7 -2
- package/src/shared/__tests__/tool-row.test.ts +136 -1
- package/src/shared/artifact-storage.ts +83 -1
- package/src/shared/implement-plan-prompt.ts +73 -0
- package/src/shared/plan-mode-prompt.ts +51 -0
- package/src/shared/status.ts +11 -1
- package/src/shared/tool-row.ts +58 -5
- package/src/shared/workspace/__tests__/session-root.test.ts +42 -0
- package/src/shared/workspace/__tests__/workspace-lock.test.ts +200 -0
- package/src/shared/workspace/session-root.ts +60 -0
- package/src/shared/workspace/workspace-lock.ts +241 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
2
3
|
import { buildEnhancedSystemPrompt } from "../prompt-builder.js";
|
|
4
|
+
import { PLAN_MODE_DIRECTIVE } from "../../../shared/plan-mode-prompt.js";
|
|
3
5
|
import { SourceType } from "../../../shared/workspace/types.js";
|
|
4
6
|
import type { ProvisionResult } from "../../../shared/workspace/types.js";
|
|
5
7
|
|
|
@@ -205,4 +207,90 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
205
207
|
|
|
206
208
|
expect(prompt).not.toContain("## Workspace");
|
|
207
209
|
});
|
|
210
|
+
|
|
211
|
+
describe("plan mode", () => {
|
|
212
|
+
const base = {
|
|
213
|
+
instructions: "Test",
|
|
214
|
+
provisionResults: [],
|
|
215
|
+
containerRoot: "",
|
|
216
|
+
skillsPromptSection: "",
|
|
217
|
+
workspaceFileRefs: [],
|
|
218
|
+
workspaceRoot: "/workspace",
|
|
219
|
+
injectedFiles: [],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
it("appends the shared plan-mode directive as the final section", () => {
|
|
223
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
224
|
+
...base,
|
|
225
|
+
interactionMode: InteractionMode.PLAN,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(prompt).toContain("## Plan mode");
|
|
229
|
+
expect(prompt.endsWith(PLAN_MODE_DIRECTIVE)).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("omits the directive for Agent mode", () => {
|
|
233
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
234
|
+
...base,
|
|
235
|
+
interactionMode: InteractionMode.AGENT,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(prompt).not.toContain("## Plan mode");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("omits the directive when the mode is unset", () => {
|
|
242
|
+
const prompt = buildEnhancedSystemPrompt(base);
|
|
243
|
+
|
|
244
|
+
expect(prompt).not.toContain("## Plan mode");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("build from plan", () => {
|
|
249
|
+
const base = {
|
|
250
|
+
instructions: "Test",
|
|
251
|
+
provisionResults: [],
|
|
252
|
+
containerRoot: "",
|
|
253
|
+
skillsPromptSection: "",
|
|
254
|
+
workspaceFileRefs: [],
|
|
255
|
+
workspaceRoot: "/workspace",
|
|
256
|
+
injectedFiles: [],
|
|
257
|
+
};
|
|
258
|
+
const planFile = {
|
|
259
|
+
filename: "plan.md",
|
|
260
|
+
path: ".stigmer/inputs/plan.md",
|
|
261
|
+
size: 1024,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
it("appends the implement-plan directive pointing at the injected plan", () => {
|
|
265
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
266
|
+
...base,
|
|
267
|
+
buildFromPlan: true,
|
|
268
|
+
injectedFiles: [planFile],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(prompt).toContain("## Implement the approved plan");
|
|
272
|
+
expect(prompt).toContain("`.stigmer/inputs/plan.md`");
|
|
273
|
+
expect(prompt).toContain("APPROVED");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("falls back to the conversation-plan variant when the plan did not inject", () => {
|
|
277
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
278
|
+
...base,
|
|
279
|
+
buildFromPlan: true,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(prompt).toContain("## Implement the approved plan");
|
|
283
|
+
expect(prompt).toContain("conversation above");
|
|
284
|
+
expect(prompt).not.toContain("plan.md");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("omits the directive for an ordinary execution", () => {
|
|
288
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
289
|
+
...base,
|
|
290
|
+
injectedFiles: [planFile],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(prompt).not.toContain("## Implement the approved plan");
|
|
294
|
+
});
|
|
295
|
+
});
|
|
208
296
|
});
|
|
@@ -51,7 +51,9 @@ import type { SetupResult } from "../setup.js";
|
|
|
51
51
|
vi.mock("@temporalio/activity", () => ({
|
|
52
52
|
Context: {
|
|
53
53
|
current: () => ({
|
|
54
|
-
|
|
54
|
+
// A real AbortSignal: the workspace-lock wait registers abort listeners
|
|
55
|
+
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
56
|
+
cancellationSignal: new AbortController().signal,
|
|
55
57
|
heartbeat: vi.fn(),
|
|
56
58
|
}),
|
|
57
59
|
},
|
|
@@ -187,7 +189,9 @@ function fakeMemoryGateSetup(opts: {
|
|
|
187
189
|
agent: {},
|
|
188
190
|
session: { spec: { workspaceEntries: [] } },
|
|
189
191
|
workspaceBackend: {
|
|
190
|
-
|
|
192
|
+
// Unique per test file: the workspace turn lock keys on this path, and a
|
|
193
|
+
// path shared across files would serialize parallel test workers for real.
|
|
194
|
+
rootDir: "/tmp/stigmer-test-ws-seq-gate",
|
|
191
195
|
exists: vi.fn(async () => false),
|
|
192
196
|
readFile: vi.fn(async () => ""),
|
|
193
197
|
},
|
|
@@ -252,6 +256,7 @@ const memoryConfig: Config = {
|
|
|
252
256
|
checkpointerProxyEndpoint: null,
|
|
253
257
|
primaryModel: "claude-sonnet",
|
|
254
258
|
cursorStreamStallTimeoutMs: 180000,
|
|
259
|
+
workspaceLockTimeoutMs: 900000,
|
|
255
260
|
};
|
|
256
261
|
|
|
257
262
|
function toolCallIds(status: AgentExecutionStatus): string[] {
|
|
@@ -19,7 +19,13 @@ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/a
|
|
|
19
19
|
import { ExecutionPhase, FileCaptureClass, FileChangeSetStatus, InteractionMode, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
20
20
|
import { activityStarted, activityFinished } from "../../idle-watchdog.js";
|
|
21
21
|
import { normalizeActivityInput, type ExecuteActivityInput } from "../../shared/activity-input.js";
|
|
22
|
-
import { persistStatus, slimStatus, utcTimestamp } from "../../shared/status.js";
|
|
22
|
+
import { persistStatus, reportSetupProgress, slimStatus, utcTimestamp } from "../../shared/status.js";
|
|
23
|
+
import {
|
|
24
|
+
acquireWorkspaceLock,
|
|
25
|
+
WorkspaceLockCancelledError,
|
|
26
|
+
WorkspaceLockTimeoutError,
|
|
27
|
+
type ReleaseWorkspaceLock,
|
|
28
|
+
} from "../../shared/workspace/workspace-lock.js";
|
|
23
29
|
import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
|
|
24
30
|
import { publishPlanArtifact } from "../../shared/plan-artifact.js";
|
|
25
31
|
import { classifyTool } from "../../shared/tool-kind.js";
|
|
@@ -48,7 +54,7 @@ import { hasCandidateCaptured } from "../../shared/filereview/events.js";
|
|
|
48
54
|
import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
|
|
49
55
|
import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
|
|
50
56
|
import type { CasCaptureObserver } from "./cas-capture-observer.js";
|
|
51
|
-
import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
|
|
57
|
+
import { collectSubAgentToolCallIds, withholdSecretContentFromMessages } from "../../shared/tool-row.js";
|
|
52
58
|
import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stamp-flowed-rows.js";
|
|
53
59
|
|
|
54
60
|
/** The harness id stamped on the deep-agent's file-review ledger events. */
|
|
@@ -75,6 +81,11 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
75
81
|
const { executionId, threadId, turnSeq } = normalizeActivityInput(arg0, arg1);
|
|
76
82
|
activityStarted();
|
|
77
83
|
let setup: SetupResult | null = null;
|
|
84
|
+
// Exclusive turn lock on the workspace working tree — held across the
|
|
85
|
+
// entire tree-mutating window (decision reconcile, agent writes,
|
|
86
|
+
// candidate capture, write-back) and released in the finally. Mirrors
|
|
87
|
+
// the Cursor harness; see shared/workspace/workspace-lock.ts.
|
|
88
|
+
let releaseWorkspaceLock: ReleaseWorkspaceLock | undefined;
|
|
78
89
|
|
|
79
90
|
try {
|
|
80
91
|
console.log(`[ExecuteDeepAgent] Started for execution ${executionId}`);
|
|
@@ -163,6 +174,45 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
163
174
|
const gitRoot = setup.workspaceBackend.rootDir;
|
|
164
175
|
const changeSetId = `${executionId}:${turnSeq}`;
|
|
165
176
|
|
|
177
|
+
// Serialize this turn against every other execution sharing this
|
|
178
|
+
// working tree — an unserialized concurrent write lands inside this
|
|
179
|
+
// turn's baseline→candidate window and gets misattributed to this
|
|
180
|
+
// turn's review. Acquired before ANY tree mutation below (decision
|
|
181
|
+
// reconcile, agent writes, candidate capture, write-back); waiting
|
|
182
|
+
// surfaces a visible state and heartbeats, and a cancel aborts the
|
|
183
|
+
// wait immediately. Mirrors the Cursor harness wiring exactly.
|
|
184
|
+
try {
|
|
185
|
+
releaseWorkspaceLock = await acquireWorkspaceLock(gitRoot, {
|
|
186
|
+
onWaiting: () => reportSetupProgress(
|
|
187
|
+
client, executionId, "Waiting for workspace — in use by another session",
|
|
188
|
+
),
|
|
189
|
+
heartbeat: () => Context.current().heartbeat(),
|
|
190
|
+
signal: Context.current().cancellationSignal,
|
|
191
|
+
timeoutMs: config.workspaceLockTimeoutMs,
|
|
192
|
+
});
|
|
193
|
+
} catch (lockErr) {
|
|
194
|
+
if (lockErr instanceof WorkspaceLockCancelledError) {
|
|
195
|
+
throw new CancelledFailure("Activity cancelled while waiting for the workspace lock");
|
|
196
|
+
}
|
|
197
|
+
if (lockErr instanceof WorkspaceLockTimeoutError) {
|
|
198
|
+
const failedStatus = create(AgentExecutionStatusSchema, {
|
|
199
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
200
|
+
error: lockErr.message,
|
|
201
|
+
completedAt: utcTimestamp(),
|
|
202
|
+
messages: [
|
|
203
|
+
create(AgentMessageSchema, {
|
|
204
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
205
|
+
content: `Execution failed: ${lockErr.message}`,
|
|
206
|
+
timestamp: utcTimestamp(),
|
|
207
|
+
}),
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
await persistStatus(client, executionId, failedStatus, { offload: statusOffload });
|
|
211
|
+
return slimStatus(failedStatus);
|
|
212
|
+
}
|
|
213
|
+
throw lockErr;
|
|
214
|
+
}
|
|
215
|
+
|
|
166
216
|
// (1) Capture-mode resume — reconcile any DECIDED change set FIRST (this
|
|
167
217
|
// drops the per-execution refs), before the next baseline re-pins them
|
|
168
218
|
// (refs are executionId-keyed). Then (2) short-circuit a pure file-review
|
|
@@ -308,6 +358,20 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
308
358
|
executionId,
|
|
309
359
|
});
|
|
310
360
|
|
|
361
|
+
// Never-persist-secret backstop (DD-26 #2): withhold content from any
|
|
362
|
+
// built-in write row targeting a secret-like path, across the top-level
|
|
363
|
+
// and sub-agent transcripts, before ANY persist below. On the deny-gate a
|
|
364
|
+
// secret write is hard-blocked (a COMPLETED row) or — under
|
|
365
|
+
// auto_approve_all, where no gate is installed — flowed; either way the
|
|
366
|
+
// streamed row still carries `args` from handleToolStarted, and the
|
|
367
|
+
// capture-mode stamping pass (which performs the same scrub) does not run
|
|
368
|
+
// here. Idempotent / a no-op in capture mode. This single post-stream
|
|
369
|
+
// sweep is the one choke point that precedes every downstream persist.
|
|
370
|
+
withholdSecretContentFromMessages(
|
|
371
|
+
initialStatus.messages,
|
|
372
|
+
initialStatus.subAgentExecutions,
|
|
373
|
+
);
|
|
374
|
+
|
|
311
375
|
// Turn boundary (capture mode): capture the candidate change set from the
|
|
312
376
|
// git diff and author CANDIDATE_CAPTURED, then stamp the flowed file-edit
|
|
313
377
|
// rows with the change set id — they stay visible in place as
|
|
@@ -590,6 +654,10 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
590
654
|
|
|
591
655
|
} finally {
|
|
592
656
|
await cleanup(setup);
|
|
657
|
+
// Release the workspace turn lock LAST — the next queued turn must
|
|
658
|
+
// not baseline until every mutation of this one has landed.
|
|
659
|
+
// Idempotent and non-throwing (see workspace-lock.ts).
|
|
660
|
+
await releaseWorkspaceLock?.();
|
|
593
661
|
activityFinished();
|
|
594
662
|
}
|
|
595
663
|
},
|
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { relative } from "node:path";
|
|
10
|
+
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
10
11
|
import type { ProvisionResult, GitMetadata } from "../../shared/workspace/types.js";
|
|
11
12
|
import { SourceType } from "../../shared/workspace/types.js";
|
|
13
|
+
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
14
|
+
import {
|
|
15
|
+
buildImplementPlanDirective,
|
|
16
|
+
findApprovedPlanPath,
|
|
17
|
+
} from "../../shared/implement-plan-prompt.js";
|
|
12
18
|
|
|
13
19
|
const RESPONSE_RULES = `
|
|
14
20
|
|
|
@@ -84,6 +90,22 @@ export interface PromptBuilderInput {
|
|
|
84
90
|
workspaceFileRefs: string[];
|
|
85
91
|
workspaceRoot: string;
|
|
86
92
|
injectedFiles: InjectedFile[];
|
|
93
|
+
/**
|
|
94
|
+
* The execution's interaction mode. PLAN appends the shared plan-mode
|
|
95
|
+
* directive so the model knows the turn's deliverable is a plan document.
|
|
96
|
+
* Tool-level write enforcement is separate (see setup.ts permissions) —
|
|
97
|
+
* without this directive the model is silently read-only but never told
|
|
98
|
+
* to produce a plan.
|
|
99
|
+
*/
|
|
100
|
+
interactionMode?: InteractionMode;
|
|
101
|
+
/**
|
|
102
|
+
* The execution is a Build-from-plan turn (spec.execution_config
|
|
103
|
+
* .build_from_plan): appends the shared implement-plan directive, pointing
|
|
104
|
+
* the model at the injected approved plan document (or, when the plan
|
|
105
|
+
* attachment did not materialize, at the conversation's plan). The user
|
|
106
|
+
* message itself is just a short label ("Build from plan").
|
|
107
|
+
*/
|
|
108
|
+
buildFromPlan?: boolean;
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
export interface InjectedFile {
|
|
@@ -128,6 +150,23 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
128
150
|
prompt += RESPONSE_RULES;
|
|
129
151
|
prompt += SUB_AGENT_RULES;
|
|
130
152
|
|
|
153
|
+
// Last sections on purpose: these per-execution directives redefine the
|
|
154
|
+
// turn's deliverable, so they must be the freshest instruction the model
|
|
155
|
+
// reads. (PLAN and build_from_plan are mutually exclusive in practice —
|
|
156
|
+
// the build turn is always an Agent-mode execution.)
|
|
157
|
+
if (input.interactionMode === InteractionMode.PLAN) {
|
|
158
|
+
prompt += "\n\n## Plan mode\n\n" + PLAN_MODE_DIRECTIVE;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (input.buildFromPlan) {
|
|
162
|
+
const planPath = findApprovedPlanPath(
|
|
163
|
+
input.injectedFiles.map((f) => f.path),
|
|
164
|
+
);
|
|
165
|
+
prompt +=
|
|
166
|
+
"\n\n## Implement the approved plan\n\n" +
|
|
167
|
+
buildImplementPlanDirective(planPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
131
170
|
return prompt;
|
|
132
171
|
}
|
|
133
172
|
|
|
@@ -35,6 +35,7 @@ import { isGitWorkTree, isPathCapturable } from "../../shared/filereview/git-sub
|
|
|
35
35
|
import { deriveCaptureMode } from "../../shared/filereview/capture.js";
|
|
36
36
|
import { resolveWorkspacePath } from "../../shared/file-change.js";
|
|
37
37
|
import { ensurePlatformDir } from "../../shared/workspace/platform-dir.js";
|
|
38
|
+
import { resolveSessionWorkspaceRoot } from "../../shared/workspace/session-root.js";
|
|
38
39
|
import { buildWorkspaceFileTree } from "../../shared/workspace/file-tree.js";
|
|
39
40
|
import { reportSetupProgress } from "../../shared/status.js";
|
|
40
41
|
import { resolveEnvironment, type EnvironmentResult } from "./environment.js";
|
|
@@ -49,7 +50,7 @@ import { getDefaultModel } from "../../shared/model-registry.js";
|
|
|
49
50
|
import { buildChatModel } from "../../shared/model-client.js";
|
|
50
51
|
import {
|
|
51
52
|
loadArtifactStorageConfig,
|
|
52
|
-
|
|
53
|
+
resolveUsableArtifactStorage,
|
|
53
54
|
type ArtifactStorage,
|
|
54
55
|
} from "../../shared/artifact-storage.js";
|
|
55
56
|
import {
|
|
@@ -201,20 +202,14 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
201
202
|
await reportSetupProgress(client, executionId, "Resolving environment…");
|
|
202
203
|
const envResult: EnvironmentResult = await resolveEnvironment(client, executionId);
|
|
203
204
|
|
|
204
|
-
// Step 6:
|
|
205
|
-
// `
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
} catch (storageErr) {
|
|
213
|
-
console.warn(
|
|
214
|
-
`[setup] artifact storage unavailable — capture degrades to the deny-gate ` +
|
|
215
|
-
`and tool-output offload is disabled: execution=${executionId}, error=${storageErr}`,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
205
|
+
// Step 6: Resolve a usable artifact store (shared with the Cursor harness so
|
|
206
|
+
// both degrade identically). Returns `undefined` — never throws — when there
|
|
207
|
+
// is no working substrate: a proxy misconfig OR an unwritable local path
|
|
208
|
+
// (which `createArtifactStorage` cannot detect, since it just holds a path).
|
|
209
|
+
// An absent store flips capture mode off (deny-gate fallback below) and
|
|
210
|
+
// disables offload, instead of flowing writes then crashing at the boundary.
|
|
211
|
+
const artifactStorage: ArtifactStorage | undefined =
|
|
212
|
+
await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
|
|
218
213
|
|
|
219
214
|
// Step 7: Provision workspace
|
|
220
215
|
await reportSetupProgress(client, executionId, "Initializing workspace…");
|
|
@@ -372,6 +367,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
372
367
|
workspaceFileRefs: execution.spec!.workspaceFileRefs || [],
|
|
373
368
|
workspaceRoot: workspaceBackend.rootDir,
|
|
374
369
|
injectedFiles,
|
|
370
|
+
interactionMode: execution.spec!.executionConfig?.interactionMode,
|
|
371
|
+
buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
|
|
375
372
|
});
|
|
376
373
|
|
|
377
374
|
// Step 9: Construct the LLM model. Resolution to the provider API id
|
|
@@ -631,6 +628,11 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
631
628
|
|
|
632
629
|
/**
|
|
633
630
|
* Provision workspace from session workspace entries.
|
|
631
|
+
*
|
|
632
|
+
* A session with no entries gets its own empty per-session directory (see
|
|
633
|
+
* shared/workspace/session-root.ts) — never the shared root, which would
|
|
634
|
+
* leak other sessions' files into it. Mirrors the Cursor harness's
|
|
635
|
+
* provisionCursorWorkspace exactly.
|
|
634
636
|
*/
|
|
635
637
|
async function provisionWorkspace(
|
|
636
638
|
config: Config,
|
|
@@ -639,13 +641,19 @@ async function provisionWorkspace(
|
|
|
639
641
|
sessionId: string,
|
|
640
642
|
): Promise<{ workspaceBackend: WorkspaceBackend; provisionResults: ProvisionResult[] }> {
|
|
641
643
|
const platformDir = await ensurePlatformDir(sessionId);
|
|
642
|
-
const workspaceBackend = new LocalWorkspaceBackend(config.workspaceRootDir, platformDir);
|
|
643
644
|
|
|
644
645
|
const workspaceEntries = session.spec!.workspaceEntries || [];
|
|
645
646
|
if (workspaceEntries.length === 0) {
|
|
646
|
-
|
|
647
|
+
const sessionRoot = await resolveSessionWorkspaceRoot(
|
|
648
|
+
config.workspaceRootDir, workspaceEntries, sessionId,
|
|
649
|
+
);
|
|
650
|
+
return {
|
|
651
|
+
workspaceBackend: new LocalWorkspaceBackend(sessionRoot, platformDir),
|
|
652
|
+
provisionResults: [],
|
|
653
|
+
};
|
|
647
654
|
}
|
|
648
655
|
|
|
656
|
+
const workspaceBackend = new LocalWorkspaceBackend(config.workspaceRootDir, platformDir);
|
|
649
657
|
const provisioner = new WorkspaceProvisioner();
|
|
650
658
|
const provisionResults = await provisioner.provisionAll(
|
|
651
659
|
workspaceEntries.map(entry => ({
|
package/src/config.ts
CHANGED
|
@@ -37,6 +37,12 @@ import { homedir, tmpdir } from "node:os";
|
|
|
37
37
|
*/
|
|
38
38
|
export const DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS = 180_000;
|
|
39
39
|
|
|
40
|
+
// Re-exported so the runner/manager options mappers default the lock-wait
|
|
41
|
+
// bound from the same constant the lock module owns (no drift across the
|
|
42
|
+
// three construction sites — mirrors DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS).
|
|
43
|
+
export { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
|
|
44
|
+
import { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
|
|
45
|
+
|
|
40
46
|
export interface Config {
|
|
41
47
|
readonly taskQueue: string;
|
|
42
48
|
readonly temporalAddress: string;
|
|
@@ -83,6 +89,12 @@ export interface Config {
|
|
|
83
89
|
* heartbeatTimeout (process liveness) and the 30s keep-alive heartbeat.
|
|
84
90
|
*/
|
|
85
91
|
readonly cursorStreamStallTimeoutMs: number;
|
|
92
|
+
/**
|
|
93
|
+
* Max wait (milliseconds) for the per-workspace turn lock before failing
|
|
94
|
+
* the execution with a "workspace is in use by another session" error.
|
|
95
|
+
* See shared/workspace/workspace-lock.ts for the serialization model.
|
|
96
|
+
*/
|
|
97
|
+
readonly workspaceLockTimeoutMs: number;
|
|
86
98
|
/** Shared mutable token reference for dynamic token updates (manager mode). */
|
|
87
99
|
readonly stigmerTokenRef?: { current: string | null };
|
|
88
100
|
}
|
|
@@ -145,6 +157,10 @@ export function loadConfig(): Config {
|
|
|
145
157
|
? parseInt(process.env.CURSOR_STREAM_STALL_TIMEOUT_MS, 10)
|
|
146
158
|
: DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS;
|
|
147
159
|
|
|
160
|
+
const workspaceLockTimeoutMs = process.env.WORKSPACE_LOCK_TIMEOUT_MS
|
|
161
|
+
? parseInt(process.env.WORKSPACE_LOCK_TIMEOUT_MS, 10)
|
|
162
|
+
: DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS;
|
|
163
|
+
|
|
148
164
|
return {
|
|
149
165
|
taskQueue,
|
|
150
166
|
temporalAddress,
|
|
@@ -162,6 +178,7 @@ export function loadConfig(): Config {
|
|
|
162
178
|
checkpointerProxyEndpoint,
|
|
163
179
|
primaryModel,
|
|
164
180
|
cursorStreamStallTimeoutMs,
|
|
181
|
+
workspaceLockTimeoutMs,
|
|
165
182
|
};
|
|
166
183
|
}
|
|
167
184
|
|
|
@@ -398,7 +398,10 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
398
398
|
expect(mockedInterrupt).not.toHaveBeenCalled();
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
-
it("KEEPS GATING a gitignored write (it cannot be captured or reverted)", async () => {
|
|
401
|
+
it("KEEPS GATING a non-secret gitignored write (it cannot be captured or reverted)", async () => {
|
|
402
|
+
// A NON-secret gitignored path: no CAS routing (captureIgnored unset) and not
|
|
403
|
+
// secret-like, so it stays on the interrupt gate. (A secret-like gitignored
|
|
404
|
+
// write is hard-blocked instead — see the DD-26 #2 deny-gate cases below.)
|
|
402
405
|
const mw = createApprovalGateMiddleware(makeConfig({
|
|
403
406
|
fileCaptureMode: true,
|
|
404
407
|
isCapturablePath: async () => false, // gitignored
|
|
@@ -406,7 +409,7 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
406
409
|
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
407
410
|
|
|
408
411
|
await mw.wrapToolCall!(
|
|
409
|
-
makeRequest({ name: "write", args: { path: ".
|
|
412
|
+
makeRequest({ name: "write", args: { path: "dist/bundle.js" } }),
|
|
410
413
|
passthrough,
|
|
411
414
|
);
|
|
412
415
|
|
|
@@ -650,12 +653,12 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
650
653
|
expect(mockedInterrupt).toHaveBeenCalledTimes(1);
|
|
651
654
|
});
|
|
652
655
|
|
|
653
|
-
it("
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
656
|
+
it("HARD-BLOCKS a secret-like write when capture mode is off (DD-26 #2): never gated, never applied", async () => {
|
|
657
|
+
// DD-26 follow-up #2 supersedes the earlier "gated, not hard-blocked" parity:
|
|
658
|
+
// a secret-like write must NEVER surface its content for approval, in ANY
|
|
659
|
+
// mode. On the deny-gate it is hard-blocked exactly like the capture-mode
|
|
660
|
+
// secret block — never interrupted, never applied. The content never reaches
|
|
661
|
+
// a pending approval or the persisted transcript.
|
|
659
662
|
const handler = vi.fn(passthrough);
|
|
660
663
|
const recordBlockedSecret = vi.fn();
|
|
661
664
|
const mw = createApprovalGateMiddleware(makeConfig({
|
|
@@ -665,14 +668,76 @@ describe("ApprovalGateMiddleware", () => {
|
|
|
665
668
|
}));
|
|
666
669
|
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
667
670
|
|
|
671
|
+
const result = await mw.wrapToolCall!(
|
|
672
|
+
makeRequest({ name: "write", args: { path: ".env", content: "API_KEY=xyz" } }),
|
|
673
|
+
handler,
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect(mockedInterrupt).not.toHaveBeenCalled(); // hard-block, not a pause
|
|
677
|
+
expect(handler).not.toHaveBeenCalled(); // never applied
|
|
678
|
+
expect(recordBlockedSecret).toHaveBeenCalledWith(".env");
|
|
679
|
+
expect((result as ToolMessage).content).toContain("blocked for security");
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("hard-blocks a secret-like edit too (id_rsa via path fragment)", async () => {
|
|
683
|
+
const handler = vi.fn(passthrough);
|
|
684
|
+
const mw = createApprovalGateMiddleware(makeConfig({
|
|
685
|
+
fileCaptureMode: false,
|
|
686
|
+
captureIgnored: false,
|
|
687
|
+
}));
|
|
688
|
+
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
689
|
+
|
|
690
|
+
const result = await mw.wrapToolCall!(
|
|
691
|
+
makeRequest({ name: "edit", args: { path: ".ssh/id_rsa", old_string: "a", new_string: "b" } }),
|
|
692
|
+
handler,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
expect(mockedInterrupt).not.toHaveBeenCalled();
|
|
696
|
+
expect(handler).not.toHaveBeenCalled();
|
|
697
|
+
expect((result as ToolMessage).content).toContain("blocked for security");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("hard-blocks a secret write in a git workspace with no storage (captureMode on, captureIgnored off)", async () => {
|
|
701
|
+
// captureMode true + captureIgnored false = a git workspace with no artifact
|
|
702
|
+
// storage. A gitignored secret write skips the captureIgnored arm and would
|
|
703
|
+
// otherwise have reached the deny-gate (a leak); it is now hard-blocked too,
|
|
704
|
+
// and the recorded path lets the turn boundary author a content-less entry.
|
|
705
|
+
const handler = vi.fn(passthrough);
|
|
706
|
+
const recordBlockedSecret = vi.fn();
|
|
707
|
+
const mw = createApprovalGateMiddleware(makeConfig({
|
|
708
|
+
fileCaptureMode: true,
|
|
709
|
+
isCapturablePath: async () => false, // gitignored
|
|
710
|
+
captureIgnored: false, // no artifact storage
|
|
711
|
+
recordBlockedSecret,
|
|
712
|
+
}));
|
|
713
|
+
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
714
|
+
|
|
715
|
+
const result = await mw.wrapToolCall!(
|
|
716
|
+
makeRequest({ name: "write", args: { path: "secrets.yaml", content: "token: t" } }),
|
|
717
|
+
handler,
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
expect(mockedInterrupt).not.toHaveBeenCalled();
|
|
721
|
+
expect(handler).not.toHaveBeenCalled();
|
|
722
|
+
expect(recordBlockedSecret).toHaveBeenCalledWith("secrets.yaml");
|
|
723
|
+
expect((result as ToolMessage).content).toContain("blocked for security");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("still GATES a NON-secret write when capture mode is off (deny-gate unchanged)", async () => {
|
|
727
|
+
const handler = vi.fn(passthrough);
|
|
728
|
+
const mw = createApprovalGateMiddleware(makeConfig({
|
|
729
|
+
fileCaptureMode: false,
|
|
730
|
+
captureIgnored: false,
|
|
731
|
+
}));
|
|
732
|
+
mockedInterrupt.mockReturnValue({ action: "approve" });
|
|
733
|
+
|
|
668
734
|
await mw.wrapToolCall!(
|
|
669
|
-
makeRequest({ name: "write", args: { path: ".
|
|
735
|
+
makeRequest({ name: "write", args: { path: "notes.md", content: "hi" } }),
|
|
670
736
|
handler,
|
|
671
737
|
);
|
|
672
738
|
|
|
673
|
-
expect(mockedInterrupt).toHaveBeenCalledTimes(1); //
|
|
674
|
-
expect(
|
|
675
|
-
expect(handler).toHaveBeenCalledTimes(1); // flows only after the user approves
|
|
739
|
+
expect(mockedInterrupt).toHaveBeenCalledTimes(1); // a non-secret write still gates
|
|
740
|
+
expect(handler).toHaveBeenCalledTimes(1); // flows after approve
|
|
676
741
|
});
|
|
677
742
|
|
|
678
743
|
it("still auto-approves read-only built-ins when capture mode is off", async () => {
|
|
@@ -138,6 +138,24 @@ const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
|
|
|
138
138
|
shell: "Execute command: {{args.command}}",
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The ToolMessage returned when a secret-like write is hard-blocked (DD-E /
|
|
143
|
+
* DD-26 #2): the write is NEVER applied and the graph continues (this replaces
|
|
144
|
+
* the tool call's side effect, so the model moves on rather than waiting). The
|
|
145
|
+
* path is named (a filename is not itself the secret); the CONTENT is not echoed.
|
|
146
|
+
* Shared by the capture-mode secret block and the deny-gate secret block so both
|
|
147
|
+
* speak with one voice.
|
|
148
|
+
*/
|
|
149
|
+
function secretBlockToolMessage(toolName: string, path: string, toolCallId: string): ToolMessage {
|
|
150
|
+
return new ToolMessage({
|
|
151
|
+
content:
|
|
152
|
+
`Tool '${toolName}' was blocked for security: '${path}' matches a ` +
|
|
153
|
+
`secret-like path Stigmer will not capture for review. Nothing was written.`,
|
|
154
|
+
tool_call_id: toolCallId,
|
|
155
|
+
name: toolName,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
export function createApprovalGateMiddleware(
|
|
142
160
|
config: ApprovalGateConfig,
|
|
143
161
|
): StigmerMiddleware {
|
|
@@ -183,13 +201,7 @@ export function createApprovalGateMiddleware(
|
|
|
183
201
|
// and NEVER captured. Record it so the turn boundary authors a
|
|
184
202
|
// DIFF_UNREVIEWABLE entry (blocking approval); nothing is written.
|
|
185
203
|
config.recordBlockedSecret?.(path);
|
|
186
|
-
return
|
|
187
|
-
content:
|
|
188
|
-
`Tool '${toolName}' was blocked for security: '${path}' matches a ` +
|
|
189
|
-
`secret-like path Stigmer will not capture for review. Nothing was written.`,
|
|
190
|
-
tool_call_id: toolCall.id,
|
|
191
|
-
name: toolName,
|
|
192
|
-
});
|
|
204
|
+
return secretBlockToolMessage(toolName, path, toolCall.id);
|
|
193
205
|
}
|
|
194
206
|
if (category === "write") {
|
|
195
207
|
// Non-secret gitignored write/edit: flows (apply-then-review). The
|
|
@@ -203,6 +215,26 @@ export function createApprovalGateMiddleware(
|
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
// Deny-gate secret hard-block (DD-26 #2): a built-in file WRITE to a
|
|
219
|
+
// secret-like path that reaches here has no capture substrate for it — the
|
|
220
|
+
// classic no-storage deny-gate, or a git workspace with no artifact storage
|
|
221
|
+
// whose gitignored write skipped the captureIgnored arm above. It must NOT
|
|
222
|
+
// surface its content for approval, so hard-block it (never applied, graph
|
|
223
|
+
// continues) exactly like the capture-mode secret block — a secret write is
|
|
224
|
+
// never applied or persisted in ANY mode. Placed AFTER the capture block so
|
|
225
|
+
// capture-mode paths stay byte-identical (a capturable write already flowed;
|
|
226
|
+
// a captureIgnored gitignored secret is already blocked). Deletes carry no
|
|
227
|
+
// content and stay on the deny-gate. recordBlockedSecret is a no-op unless a
|
|
228
|
+
// turn boundary reads it (the git-no-storage case, where it authors a
|
|
229
|
+
// content-less DIFF_UNREVIEWABLE).
|
|
230
|
+
if (!serverSlug && category === "write") {
|
|
231
|
+
const path = extractFilePath(toolCall.args);
|
|
232
|
+
if (path !== null && isSecretLikePath(path)) {
|
|
233
|
+
config.recordBlockedSecret?.(path);
|
|
234
|
+
return secretBlockToolMessage(toolName, path, toolCall.id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
206
238
|
const requirement = resolveToolApproval(
|
|
207
239
|
toolName,
|
|
208
240
|
serverSlug,
|
package/src/runner-manager.ts
CHANGED
|
@@ -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 } from "./config.js";
|
|
27
|
+
import { 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
|
+
/** Max wait for the per-workspace turn lock (ms). @default 900000 */
|
|
95
|
+
readonly workspaceLockTimeoutMs?: number;
|
|
96
|
+
|
|
94
97
|
/** Checkpointer type for LangGraph agent state. @default "memory" (or "http" if proxyEndpoint is set) */
|
|
95
98
|
readonly checkpointerType?: "memory" | "http";
|
|
96
99
|
|
|
@@ -576,6 +579,8 @@ export function mapManagerOptionsToConfig(
|
|
|
576
579
|
primaryModel: options.primaryModel ?? "gpt-4.1",
|
|
577
580
|
cursorStreamStallTimeoutMs:
|
|
578
581
|
options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
|
|
582
|
+
workspaceLockTimeoutMs:
|
|
583
|
+
options.workspaceLockTimeoutMs ?? DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS,
|
|
579
584
|
};
|
|
580
585
|
}
|
|
581
586
|
|