@stigmer/runner 3.11.0 → 3.11.1-dev.20260812192248
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/README.md +2 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +6 -4
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +14 -2
- package/dist/activities/emit-event.js +52 -17
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +19 -0
- package/dist/activities/execute-cursor/error-classifier.js +34 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +46 -10
- package/dist/activities/execute-cursor/index.js +126 -36
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
- package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
- package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +33 -0
- package/dist/activities/execute-cursor/prompt-builder.js +37 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
- package/dist/activities/execute-cursor/turn-recovery.js +193 -0
- package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
- package/dist/activities/execute-deep-agent/setup.js +16 -7
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
- package/dist/activities/execute-deep-agent/shell-env.js +7 -9
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +7 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +17 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +14 -2
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -5
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/run-command.d.ts +5 -2
- package/dist/activities/run-command.js +22 -10
- package/dist/activities/run-command.js.map +1 -1
- package/dist/activities/run-env.d.ts +40 -0
- package/dist/activities/run-env.js +66 -0
- package/dist/activities/run-env.js.map +1 -0
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -26
- package/dist/client/stigmer-client.js +88 -24
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +41 -9
- package/dist/encryption/config.js +63 -23
- package/dist/encryption/config.js.map +1 -1
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +57 -0
- package/dist/middleware/path-normalization.js +109 -0
- package/dist/middleware/path-normalization.js.map +1 -0
- package/dist/middleware/types.d.ts +8 -0
- package/dist/payload-codecs.d.ts +2 -1
- package/dist/payload-codecs.js +7 -3
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +17 -3
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +7 -3
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +23 -1
- package/dist/shared/artifact-storage.js +51 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +4 -20
- package/dist/shared/checkpointer/http-saver.js +15 -57
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-catchup.d.ts +9 -2
- package/dist/shared/conversation-catchup.js +39 -6
- package/dist/shared/conversation-catchup.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +5 -1
- package/dist/shared/grpc-retry.js +5 -1
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/http-retry.d.ts +51 -9
- package/dist/shared/http-retry.js +60 -9
- package/dist/shared/http-retry.js.map +1 -1
- package/dist/shared/mcp-manager.d.ts +8 -4
- package/dist/shared/mcp-manager.js +30 -4
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
- package/dist/shared/mcp-schema-sanitizer.js +197 -0
- package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
- package/dist/shared/model-client.d.ts +6 -0
- package/dist/shared/model-client.js +14 -3
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-pricing-data.d.ts +2 -0
- package/dist/shared/model-pricing-data.js +13 -3
- package/dist/shared/model-pricing-data.js.map +1 -1
- package/dist/shared/model-registry.js +3 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +7 -0
- package/dist/shared/plan-mode-permissions.js +7 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/registry-endpoint.d.ts +20 -0
- package/dist/shared/registry-endpoint.js +25 -0
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +24 -0
- package/dist/shared/runner-credential-keys.js +47 -0
- package/dist/shared/runner-credential-keys.js.map +1 -0
- package/dist/workflow-engine/resolve.d.ts +19 -1
- package/dist/workflow-engine/resolve.js +37 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +2 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +3 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/encryption-codec.test.ts +53 -0
- package/src/__tests__/preflight.test.ts +15 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +39 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/discover-mcp-server.ts +16 -2
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +111 -2
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/error-classifier.ts +40 -3
- package/src/activities/execute-cursor/index.ts +154 -38
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +51 -0
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
- package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +22 -17
- package/src/activities/execute-deep-agent/setup.ts +16 -8
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +7 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +26 -3
- package/src/activities/hydrate-workflow-execution.ts +8 -5
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/bootstrap.ts +5 -0
- package/src/client/__tests__/stigmer-client.test.ts +104 -5
- package/src/client/stigmer-client.ts +128 -29
- package/src/encryption/config.ts +87 -23
- package/src/middleware/__tests__/path-normalization.test.ts +140 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/path-normalization.ts +125 -0
- package/src/middleware/types.ts +11 -0
- package/src/payload-codecs.ts +8 -2
- package/src/runner-manager.ts +21 -3
- package/src/runner.ts +7 -3
- package/src/shared/__tests__/artifact-storage.test.ts +145 -2
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/http-retry.test.ts +5 -4
- package/src/shared/__tests__/mcp-manager.test.ts +53 -13
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +46 -0
- package/src/shared/__tests__/model-pricing.test.ts +62 -2
- package/src/shared/__tests__/model-registry.test.ts +22 -0
- package/src/shared/artifact-storage.ts +75 -10
- package/src/shared/checkpointer/http-saver.ts +15 -60
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +98 -9
- package/src/shared/mcp-manager.ts +35 -4
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/model-client.ts +21 -3
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +7 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/connect-mcp-server.ts +2 -0
- package/src/workflows/types.ts +8 -0
- package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
- package/dist/activities/execute-cursor/mcp-config.js +0 -39
- package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getPricingTable,
|
|
4
|
+
_resetPricingCache,
|
|
5
|
+
DEFAULT_PRICING,
|
|
6
|
+
} from "../model-pricing-data.js";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Verifies the cursor-runner display estimate resolves Cursor speed variants
|
|
@@ -110,3 +115,59 @@ describe("getCursorModelPricing — speed variant resolution", () => {
|
|
|
110
115
|
expect(p.inputPricePerMillion).toBe(5.0);
|
|
111
116
|
});
|
|
112
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("getPricingTable failure caching (#468)", () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
_resetPricingCache();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
_resetPricingCache();
|
|
126
|
+
vi.useRealTimers();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("retries after the short failure TTL instead of pinning DEFAULT_PRICING for an hour", async () => {
|
|
130
|
+
// The model-registry.ts failure-cache policy, applied here: wrong default
|
|
131
|
+
// rates for cost tracking must not persist a full success TTL.
|
|
132
|
+
vi.useFakeTimers();
|
|
133
|
+
const registryResponse = () =>
|
|
134
|
+
new Response(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
models: [
|
|
137
|
+
{
|
|
138
|
+
id: "composer-2.5",
|
|
139
|
+
displayName: "Composer 2.5",
|
|
140
|
+
provider: "cursor",
|
|
141
|
+
harness: "cursor",
|
|
142
|
+
costTier: "economy",
|
|
143
|
+
pricing: {
|
|
144
|
+
inputPricePerMillion: 0.5,
|
|
145
|
+
outputPricePerMillion: 2.5,
|
|
146
|
+
cacheWritePricePerMillion: 0,
|
|
147
|
+
cacheReadPricePerMillion: 0.2,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
}),
|
|
152
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
153
|
+
);
|
|
154
|
+
const fetchSpy = vi
|
|
155
|
+
.fn<typeof fetch>()
|
|
156
|
+
.mockRejectedValueOnce(new Error("network error"))
|
|
157
|
+
.mockResolvedValueOnce(registryResponse());
|
|
158
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
159
|
+
|
|
160
|
+
// First call fails and degrades to the default table.
|
|
161
|
+
expect((await getPricingTable())[0]).toBe(DEFAULT_PRICING);
|
|
162
|
+
|
|
163
|
+
// Within the failure TTL the fallback stays cached (no refetch).
|
|
164
|
+
vi.advanceTimersByTime(30_000);
|
|
165
|
+
expect((await getPricingTable())[0]).toBe(DEFAULT_PRICING);
|
|
166
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
167
|
+
|
|
168
|
+
// Past the failure TTL the registry is refetched and real rates recover.
|
|
169
|
+
vi.advanceTimersByTime(31_000);
|
|
170
|
+
expect((await getPricingTable())[0]?.model).toBe("composer-2.5");
|
|
171
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the turn-recovery digest (issue #366).
|
|
3
|
+
*
|
|
4
|
+
* The digest is the replacement agent's only account of the work its lost
|
|
5
|
+
* predecessor did, so these pins cover the three doctrine properties it
|
|
6
|
+
* inherits from the DD-013 bridge composer — bounded lines, drop-oldest
|
|
7
|
+
* budget enforcement with disclosure, never-throw — plus the rendering
|
|
8
|
+
* contract per message/tool-call kind.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import { create } from "@bufbuild/protobuf";
|
|
13
|
+
import {
|
|
14
|
+
AgentMessageSchema,
|
|
15
|
+
ToolCallSchema,
|
|
16
|
+
type AgentMessage,
|
|
17
|
+
type ToolCall,
|
|
18
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
19
|
+
import { MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
20
|
+
|
|
21
|
+
import { composeTurnRecoveryDigest, formatTurnRecoveryText } from "../turn-recovery.js";
|
|
22
|
+
|
|
23
|
+
function aiMessage(content: string, toolCalls: ToolCall[] = []): AgentMessage {
|
|
24
|
+
return create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, content, toolCalls });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ToolCallFields = Partial<
|
|
28
|
+
Pick<ToolCall, "name" | "argsPreview" | "approvalMessage" | "status" | "error">
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
function toolCall(overrides: ToolCallFields): ToolCall {
|
|
32
|
+
return create(ToolCallSchema, {
|
|
33
|
+
id: "tc-1",
|
|
34
|
+
name: "Shell",
|
|
35
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
36
|
+
...overrides,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("composeTurnRecoveryDigest", () => {
|
|
41
|
+
it("renders assistant text and tool calls oldest-first", () => {
|
|
42
|
+
const digest = composeTurnRecoveryDigest([
|
|
43
|
+
aiMessage("Let me check the config first.", [
|
|
44
|
+
toolCall({ name: "Read", argsPreview: '{"path":"config.yaml"}' }),
|
|
45
|
+
]),
|
|
46
|
+
aiMessage("The port is wrong — fixing it."),
|
|
47
|
+
]);
|
|
48
|
+
expect(digest).toBe(
|
|
49
|
+
[
|
|
50
|
+
"Assistant: Let me check the config first.",
|
|
51
|
+
'Tool: Read({"path":"config.yaml"}) — completed',
|
|
52
|
+
"Assistant: The port is wrong — fixing it.",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("prefers the resolved approval message for a tool line — the same description the user approved against", () => {
|
|
58
|
+
const digest = composeTurnRecoveryDigest([
|
|
59
|
+
aiMessage("", [
|
|
60
|
+
toolCall({
|
|
61
|
+
approvalMessage: "Write file: gated.txt",
|
|
62
|
+
argsPreview: '{"path":"gated.txt","content":"..."}',
|
|
63
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
64
|
+
}),
|
|
65
|
+
]),
|
|
66
|
+
]);
|
|
67
|
+
expect(digest).toBe("Tool: Write file: gated.txt — paused for user approval");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("maps each terminal status honestly and treats in-flight calls as interrupted", () => {
|
|
71
|
+
const digest = composeTurnRecoveryDigest([
|
|
72
|
+
aiMessage("", [
|
|
73
|
+
toolCall({ name: "A", status: ToolCallStatus.TOOL_CALL_COMPLETED }),
|
|
74
|
+
toolCall({ name: "B", status: ToolCallStatus.TOOL_CALL_FAILED, error: "exit 1" }),
|
|
75
|
+
toolCall({ name: "C", status: ToolCallStatus.TOOL_CALL_FAILED }),
|
|
76
|
+
toolCall({ name: "D", status: ToolCallStatus.TOOL_CALL_SKIPPED }),
|
|
77
|
+
toolCall({ name: "E", status: ToolCallStatus.TOOL_CALL_RUNNING }),
|
|
78
|
+
toolCall({ name: "F", status: ToolCallStatus.TOOL_CALL_PENDING }),
|
|
79
|
+
]),
|
|
80
|
+
]);
|
|
81
|
+
expect(digest).toBe(
|
|
82
|
+
[
|
|
83
|
+
"Tool: A — completed",
|
|
84
|
+
"Tool: B — failed: exit 1",
|
|
85
|
+
"Tool: C — failed",
|
|
86
|
+
"Tool: D — skipped",
|
|
87
|
+
"Tool: E — interrupted before it finished",
|
|
88
|
+
"Tool: F — interrupted before it finished",
|
|
89
|
+
].join("\n"),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("keeps system notices but skips human messages (already in <user_request>), thinking, and blanks", () => {
|
|
94
|
+
const digest = composeTurnRecoveryDigest([
|
|
95
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_HUMAN, content: "Fix the build" }),
|
|
96
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_SYSTEM, content: "Budget warning: 80% used" }),
|
|
97
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_THINKING, content: "hmm, maybe the lockfile" }),
|
|
98
|
+
aiMessage(" "),
|
|
99
|
+
]);
|
|
100
|
+
expect(digest).toBe("System: Budget warning: 80% used");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns undefined when nothing renders", () => {
|
|
104
|
+
expect(composeTurnRecoveryDigest([])).toBeUndefined();
|
|
105
|
+
expect(
|
|
106
|
+
composeTurnRecoveryDigest([
|
|
107
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_HUMAN, content: "only the request" }),
|
|
108
|
+
]),
|
|
109
|
+
).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("truncates a long assistant line at the per-line budget with an ellipsis", () => {
|
|
113
|
+
const digest = composeTurnRecoveryDigest([aiMessage("x".repeat(1000))]);
|
|
114
|
+
expect(digest).toBe(`Assistant: ${"x".repeat(400)}\u2026`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("drops the OLDEST lines when over the whole-digest budget and discloses the omission", () => {
|
|
118
|
+
// 30 lines of ~311 chars each (~9.6k total) against the 4000-char budget:
|
|
119
|
+
// the newest lines must survive, the oldest go, and the notice leads.
|
|
120
|
+
const messages = Array.from({ length: 30 }, (_, i) =>
|
|
121
|
+
aiMessage(`step ${String(i).padStart(2, "0")} ${"y".repeat(300)}`),
|
|
122
|
+
);
|
|
123
|
+
const digest = composeTurnRecoveryDigest(messages);
|
|
124
|
+
expect(digest).toBeDefined();
|
|
125
|
+
expect(digest!.length).toBeLessThanOrEqual(4000);
|
|
126
|
+
const lines = digest!.split("\n");
|
|
127
|
+
expect(lines[0]).toBe("[\u2026 earlier activity in this turn omitted for length]");
|
|
128
|
+
// Recency wins: the last line is the newest entry, the first entries are gone.
|
|
129
|
+
expect(lines[lines.length - 1]).toContain("step 29");
|
|
130
|
+
expect(digest).not.toContain("step 00");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("never throws — a malformed message degrades to no digest, not a failed recovery", () => {
|
|
134
|
+
// Force the internal iteration to blow up: content getter that throws.
|
|
135
|
+
const poison = new Proxy(aiMessage("ok"), {
|
|
136
|
+
get(target, prop, receiver) {
|
|
137
|
+
if (prop === "content") throw new Error("corrupt row");
|
|
138
|
+
return Reflect.get(target, prop, receiver);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
expect(composeTurnRecoveryDigest([poison as AgentMessage])).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("formatTurnRecoveryText", () => {
|
|
146
|
+
it("frames a digest with the work-already-done preamble", () => {
|
|
147
|
+
const text = formatTurnRecoveryText("Assistant: did things");
|
|
148
|
+
expect(text).toContain("session holding that conversation was lost");
|
|
149
|
+
expect(text).toContain("do not start the task over");
|
|
150
|
+
expect(text.endsWith("Assistant: did things")).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("still discloses the state loss when there is no transcript — without it the appended decisions would read as reactions to proposals this agent never made", () => {
|
|
154
|
+
for (const empty of [undefined, "", " "]) {
|
|
155
|
+
const text = formatTurnRecoveryText(empty);
|
|
156
|
+
expect(text).toContain("session holding that conversation was lost");
|
|
157
|
+
expect(text).toContain("no transcript of your progress is available");
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -131,6 +131,29 @@ function extractFromCandidate(v: unknown): RunErrorSources | undefined {
|
|
|
131
131
|
return text.length > 0 ? { sdkError: undefined, sdkResultFields: text } : undefined;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Stable lead sentence of the detail-free fallback error (all five detail
|
|
136
|
+
* channels empty — empirically the shape of Cursor-side capacity rejections,
|
|
137
|
+
* oss#492). This is what end users read in embedded surfaces, so it must be
|
|
138
|
+
* actionable product copy, not a diagnostic; the diagnostic context follows
|
|
139
|
+
* in a parenthetical. Mirrors the COST_LIMIT_ERROR_PREFIX convention: a
|
|
140
|
+
* consumer that needs to recognize this failure can match on the prefix.
|
|
141
|
+
* Do not reword without checking consumers, and NEVER include the phrase
|
|
142
|
+
* "retry or resume" — sdk-react's isInterruptedError (MessageThread.tsx)
|
|
143
|
+
* reframes any error containing it as a neutral resumable notice instead of
|
|
144
|
+
* a failure alert.
|
|
145
|
+
*/
|
|
146
|
+
export const DETAIL_FREE_FALLBACK_USER_PREFIX =
|
|
147
|
+
"The model may be temporarily overloaded — please retry, or switch to a different model.";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Stable lead sentence of the transport-timeout fallback error (0 messages,
|
|
151
|
+
* ~30s duration — the SDK's default timeout with no stream established).
|
|
152
|
+
* Same user-facing rules as DETAIL_FREE_FALLBACK_USER_PREFIX above.
|
|
153
|
+
*/
|
|
154
|
+
export const TRANSPORT_TIMEOUT_USER_PREFIX =
|
|
155
|
+
"The connection to the model could not be established — please retry.";
|
|
156
|
+
|
|
134
157
|
const AUTH_PATTERNS = [
|
|
135
158
|
"unauthenticated", "unauthorized", "401", "forbidden",
|
|
136
159
|
"permission_denied", "invalid api key", "not logged in",
|
|
@@ -317,7 +340,10 @@ function classifyFromSources(opts: SynthesizeErrorOpts): ClassifiedError {
|
|
|
317
340
|
const { model, mode, agentId } = opts.fallbackContext;
|
|
318
341
|
return {
|
|
319
342
|
category: "network",
|
|
320
|
-
message:
|
|
343
|
+
message:
|
|
344
|
+
`${TRANSPORT_TIMEOUT_USER_PREFIX} ` +
|
|
345
|
+
`(Transport timeout: ${opts.durationMs}ms, 0 messages received. ` +
|
|
346
|
+
`Model=${model}, mode=${mode}, agentId=${agentId})`,
|
|
321
347
|
retryable: true,
|
|
322
348
|
source: "fallback",
|
|
323
349
|
};
|
|
@@ -332,11 +358,22 @@ function classifyFromSources(opts: SynthesizeErrorOpts): ClassifiedError {
|
|
|
332
358
|
};
|
|
333
359
|
}
|
|
334
360
|
|
|
361
|
+
// The honest last resort: every detail channel was empty. Observed in prod
|
|
362
|
+
// only during provider capacity incidents (oss#492: Composer 2.5 degradation
|
|
363
|
+
// — the SDK rejection that carries ERROR_RESOURCE_EXHAUSTED in Cursor's IDE
|
|
364
|
+
// arrives here detail-free), so the copy leads with the capacity hypothesis
|
|
365
|
+
// hedged ("may be"), and retryable is true: the observed cause is transient
|
|
366
|
+
// by nature, and nothing gates a recovery loop on unknown+retryable. The
|
|
367
|
+
// parenthetical keeps the exact Model=/mode=/agentId= tokens for log-grep
|
|
368
|
+
// continuity and the env integration test's matcher.
|
|
335
369
|
const { model, mode, agentId } = opts.fallbackContext;
|
|
336
370
|
return {
|
|
337
371
|
category: "unknown",
|
|
338
|
-
message:
|
|
339
|
-
|
|
372
|
+
message:
|
|
373
|
+
`${DETAIL_FREE_FALLBACK_USER_PREFIX} ` +
|
|
374
|
+
`(No error detail from the Cursor SDK. ` +
|
|
375
|
+
`Model=${model}, mode=${mode}, agentId=${agentId})`,
|
|
376
|
+
retryable: true,
|
|
340
377
|
source: "fallback",
|
|
341
378
|
};
|
|
342
379
|
}
|
|
@@ -41,7 +41,7 @@ import type { Config } from "../../config.js";
|
|
|
41
41
|
import { StigmerClient } from "../../client/stigmer-client.js";
|
|
42
42
|
import { describeExecutionError } from "../../shared/model-error.js";
|
|
43
43
|
import { resolveAgentWithTransportRecovery } from "./session-lifecycle.js";
|
|
44
|
-
import type { AgentResolution, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
|
|
44
|
+
import type { AgentResolution, AgentResolutionReason, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
|
|
45
45
|
import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
46
46
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
47
47
|
import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
|
|
@@ -92,7 +92,8 @@ import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
|
|
|
92
92
|
import { resolveSkills } from "./skill-resolver.js";
|
|
93
93
|
import { removeStigmerSymlink } from "../../shared/workspace/stigmer-link.js";
|
|
94
94
|
import { resolveAttachments } from "./attachment-resolver.js";
|
|
95
|
-
import { buildEnhancedPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInputFiles, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
95
|
+
import { buildEnhancedPrompt, buildHitlRecoveryPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInputFiles, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
|
|
96
|
+
import { composeTurnRecoveryDigest } from "./turn-recovery.js";
|
|
96
97
|
import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
|
|
97
98
|
import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
|
|
98
99
|
import {
|
|
@@ -1161,28 +1162,43 @@ async function executeCursorInner(
|
|
|
1161
1162
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1162
1163
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1163
1164
|
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1165
|
+
// The turn's recorded transcript, seeded from the persisted execution
|
|
1166
|
+
// on a reinvocation (Phase 3). Consumed only by the HITL-recovery
|
|
1167
|
+
// shape — reached from HERE when the stored handle failed to resume
|
|
1168
|
+
// at resolution time (issue #366 crossing 2).
|
|
1169
|
+
turnRecoveryDigest: isReinvocation
|
|
1170
|
+
? composeTurnRecoveryDigest(status.messages)
|
|
1171
|
+
: undefined,
|
|
1164
1172
|
});
|
|
1165
1173
|
|
|
1166
|
-
// Phase 10a: Inject structured output instruction for Cursor
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1174
|
+
// Phase 10a: Inject the structured output instruction for the Cursor
|
|
1175
|
+
// harness. A per-turn directive, so like buildFromPlan it must ride every
|
|
1176
|
+
// prompt this turn sends — the primary AND the poisoned-handle recovery
|
|
1177
|
+
// rebuild (the transport retry re-sends effectivePrompt and inherits it).
|
|
1178
|
+
const withStructuredOutputDirective = (basePrompt: string): string =>
|
|
1179
|
+
appendStructuredOutputDirective(basePrompt, structuredOutputSchema);
|
|
1180
|
+
const effectivePrompt = withStructuredOutputDirective(prompt);
|
|
1172
1181
|
|
|
1173
1182
|
// Phase 10a1: The turn's vision payload. The invariant is "images
|
|
1174
|
-
// accompany the user's turn message
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1179
|
-
//
|
|
1180
|
-
//
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1183
|
+
// accompany the user's turn message, wherever the conversation does not
|
|
1184
|
+
// already hold them" (primarySendCarriesImages): the ONLY send that
|
|
1185
|
+
// skips them is a HITL re-invocation of a successfully RESUMED agent,
|
|
1186
|
+
// whose native conversation carries the images from the original send.
|
|
1187
|
+
// Every send that starts an empty conversation re-delivers them — the
|
|
1188
|
+
// ordinary first/fresh-agent primary send, the HITL primary send after a
|
|
1189
|
+
// resolution-time resume failure, and both mid-send recovery retries
|
|
1190
|
+
// (which always run on a fresh agent, so their sites pass turnImages
|
|
1191
|
+
// unconditionally). Attachments re-resolve on every invocation
|
|
1192
|
+
// (Phase 5b), so the bytes are in hand even on a re-invocation.
|
|
1193
|
+
const turnImages = toCursorImages(visionImages);
|
|
1194
|
+
const primarySendImages = primarySendCarriesImages(approvalDecisions, resolution.reason)
|
|
1195
|
+
? turnImages
|
|
1196
|
+
: [];
|
|
1197
|
+
const toSendMessage = (
|
|
1198
|
+
sendPrompt: string,
|
|
1199
|
+
images: { data: string; mimeType: string }[],
|
|
1200
|
+
): string | SDKUserMessage =>
|
|
1201
|
+
images.length > 0 ? { text: sendPrompt, images } : sendPrompt;
|
|
1186
1202
|
|
|
1187
1203
|
// Phase 10a2: Log Stigmer preamble size for context trimming diagnostics
|
|
1188
1204
|
const promptChars = effectivePrompt.length;
|
|
@@ -1298,7 +1314,7 @@ async function executeCursorInner(
|
|
|
1298
1314
|
// The stall watchdog is armed inside consumeCursorTurnStream (it needs the
|
|
1299
1315
|
// run to cancel), stored on turnState.stallWatchdog so this shared onDelta can
|
|
1300
1316
|
// reset it and the activity's finally can stop it as a backstop.
|
|
1301
|
-
const run = await resolution.agent.send(toSendMessage(effectivePrompt), {
|
|
1317
|
+
const run = await resolution.agent.send(toSendMessage(effectivePrompt, primarySendImages), {
|
|
1302
1318
|
onDelta: (event) => {
|
|
1303
1319
|
if (!turnFirstEventEmitted) {
|
|
1304
1320
|
turnFirstEventEmitted = true;
|
|
@@ -1669,10 +1685,11 @@ async function executeCursorInner(
|
|
|
1669
1685
|
// poisoned-handle path leaked the fresh agent — it closed the stale one.)
|
|
1670
1686
|
resolution = { ...resolution, agent: freshAgent, agentId: freshAgent.agentId, isNew: true };
|
|
1671
1687
|
turnState.streamErrorMessage = undefined;
|
|
1672
|
-
// The retry carries the turn's
|
|
1673
|
-
//
|
|
1674
|
-
// silently lose the user's photo on
|
|
1675
|
-
|
|
1688
|
+
// The retry always carries the turn's full image payload — never the
|
|
1689
|
+
// primary send's HITL-trimmed set: the fresh agent's conversation is
|
|
1690
|
+
// empty, so skipping them here would silently lose the user's photo on
|
|
1691
|
+
// a recovered turn (issue #366's vision corollary).
|
|
1692
|
+
const retryRun = await freshAgent.send(toSendMessage(retryPrompt, turnImages), {
|
|
1676
1693
|
onDelta: makeCursorTurnOnDelta(onDeltaDeps),
|
|
1677
1694
|
});
|
|
1678
1695
|
await consumeCursorTurnStream(retryRun, streamDeps);
|
|
@@ -1812,6 +1829,10 @@ async function executeCursorInner(
|
|
|
1812
1829
|
attachments: attachmentEntries,
|
|
1813
1830
|
vision: visionPromptInfo,
|
|
1814
1831
|
pendingApprovals: adjudicatedApprovals,
|
|
1832
|
+
// Without the applied set, the HITL-recovery prompt would tell
|
|
1833
|
+
// the fresh agent to carry out writes the runner already
|
|
1834
|
+
// exact-applied (the primary call at Phase 10 passes it too).
|
|
1835
|
+
appliedToolCallIds,
|
|
1815
1836
|
interactionMode,
|
|
1816
1837
|
// buildFromPlan was silently dropped here until T03 Sitting 3 —
|
|
1817
1838
|
// a build turn that hit handle recovery lost its directive. The
|
|
@@ -1822,6 +1843,10 @@ async function executeCursorInner(
|
|
|
1822
1843
|
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
1823
1844
|
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
1824
1845
|
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
1846
|
+
// Composed fresh (not reused from Phase 10): the failed primary
|
|
1847
|
+
// stream may have appended partial work onto status.messages,
|
|
1848
|
+
// and the replacement agent should know about that too.
|
|
1849
|
+
turnRecoveryDigest: composeTurnRecoveryDigest(status.messages),
|
|
1825
1850
|
});
|
|
1826
1851
|
|
|
1827
1852
|
console.log(
|
|
@@ -1837,7 +1862,14 @@ async function executeCursorInner(
|
|
|
1837
1862
|
console.warn("Failed to update session with fresh agentId (non-fatal):", updateErr);
|
|
1838
1863
|
}
|
|
1839
1864
|
|
|
1840
|
-
|
|
1865
|
+
// Same per-turn directive rule as buildFromPlan above: a
|
|
1866
|
+
// structured-output turn keeps its output contract on the rebuilt
|
|
1867
|
+
// prompt (the transport retry re-sends effectivePrompt and
|
|
1868
|
+
// inherits it without help).
|
|
1869
|
+
const outcome = await runRecoveryStream(
|
|
1870
|
+
freshAgent,
|
|
1871
|
+
withStructuredOutputDirective(freshPrompt),
|
|
1872
|
+
);
|
|
1841
1873
|
if (!outcome.proceeded) {
|
|
1842
1874
|
if (outcome.terminal.kind === "return") return slimStatus(status);
|
|
1843
1875
|
throw new CancelledFailure(outcome.terminal.message);
|
|
@@ -2400,6 +2432,15 @@ export interface BuildPromptInput {
|
|
|
2400
2432
|
* usually blank.
|
|
2401
2433
|
*/
|
|
2402
2434
|
conversationCatchup?: string;
|
|
2435
|
+
/**
|
|
2436
|
+
* The turn's recorded transcript rendered as digest lines
|
|
2437
|
+
* (turn-recovery.ts), composed from `status.messages` at the call site.
|
|
2438
|
+
* Consumed ONLY by the HITL-recovery shape — a fresh agent that replaced
|
|
2439
|
+
* a lost one mid-HITL needs the story of the work it no longer remembers
|
|
2440
|
+
* (issue #366); every other shape either has native context or no prior
|
|
2441
|
+
* work to tell.
|
|
2442
|
+
*/
|
|
2443
|
+
turnRecoveryDigest?: string;
|
|
2403
2444
|
}
|
|
2404
2445
|
|
|
2405
2446
|
/**
|
|
@@ -2411,20 +2452,28 @@ export interface BuildPromptInput {
|
|
|
2411
2452
|
* volume, or cloud server-side state) — there is no separate continuation
|
|
2412
2453
|
* store. The prompt therefore depends only on how the agent was resolved:
|
|
2413
2454
|
*
|
|
2414
|
-
* 1. HITL reinvocation -> buildReinvocationPrompt (approval decisions
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
* 2.
|
|
2418
|
-
*
|
|
2455
|
+
* 1. HITL reinvocation, -> buildReinvocationPrompt (approval decisions
|
|
2456
|
+
* resumed agent only; the resumed agent's native context
|
|
2457
|
+
* carries the prior conversation)
|
|
2458
|
+
* 2. HITL reinvocation, -> buildHitlRecoveryPrompt (full context +
|
|
2459
|
+
* fresh agent after the turn's recorded transcript + decisions;
|
|
2460
|
+
* resume failure the replacement agent's conversation is
|
|
2461
|
+
* empty, and both fresh-agent crossings —
|
|
2462
|
+
* resolution-time resume failure and mid-send
|
|
2463
|
+
* poisoned-handle recovery — land here by
|
|
2464
|
+
* keying on the reason, issue #366)
|
|
2465
|
+
* 3. resumed_successfully -> raw userMessage (native context carries it)
|
|
2466
|
+
* 4. first execution / fresh -> buildEnhancedPrompt (full instructions +
|
|
2419
2467
|
* agent after resume failure skills; no prior conversation to inherit)
|
|
2420
2468
|
*/
|
|
2421
2469
|
/**
|
|
2422
2470
|
* Whether this activity invocation is a HITL re-invocation — the turn resumes
|
|
2423
2471
|
* an agent purely to convey approval decisions, carrying NO user message.
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
2426
|
-
* vision payload
|
|
2427
|
-
*
|
|
2472
|
+
* Discriminates the two surfaces that depend on the agent already holding
|
|
2473
|
+
* this turn's content natively — the prompt shape (below) and the primary
|
|
2474
|
+
* send's vision payload — but never alone: both pair it with
|
|
2475
|
+
* `resolution.reason`, because a FRESH agent mid-HITL holds nothing and
|
|
2476
|
+
* needs the full re-delivery (issue #366).
|
|
2428
2477
|
*/
|
|
2429
2478
|
export function isHitlReinvocation(
|
|
2430
2479
|
approvalDecisions: Map<string, ApprovalAction> | undefined,
|
|
@@ -2432,6 +2481,39 @@ export function isHitlReinvocation(
|
|
|
2432
2481
|
return approvalDecisions !== undefined && approvalDecisions.size > 0;
|
|
2433
2482
|
}
|
|
2434
2483
|
|
|
2484
|
+
/**
|
|
2485
|
+
* Whether the PRIMARY send delivers the turn's vision payload. The invariant
|
|
2486
|
+
* is "images accompany the user's turn message, wherever the conversation
|
|
2487
|
+
* does not already hold them" — so the only send that skips them is a HITL
|
|
2488
|
+
* re-invocation of a successfully RESUMED agent, whose native conversation
|
|
2489
|
+
* carries the images from the original send. A fresh agent mid-HITL
|
|
2490
|
+
* (resolution-time resume failure — issue #366's vision corollary) holds
|
|
2491
|
+
* nothing and needs the re-delivery. The mid-send recovery retries always
|
|
2492
|
+
* run on a fresh agent, so their send sites carry the payload
|
|
2493
|
+
* unconditionally rather than consulting this.
|
|
2494
|
+
*/
|
|
2495
|
+
export function primarySendCarriesImages(
|
|
2496
|
+
approvalDecisions: Map<string, ApprovalAction> | undefined,
|
|
2497
|
+
reason: AgentResolutionReason,
|
|
2498
|
+
): boolean {
|
|
2499
|
+
return !(isHitlReinvocation(approvalDecisions) && reason === "resumed_successfully");
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Append the structured-output contract to a prompt when the execution
|
|
2504
|
+
* requests one. A per-turn directive (the buildFromPlan rule): it must ride
|
|
2505
|
+
* every prompt this turn sends — the primary send AND the poisoned-handle
|
|
2506
|
+
* recovery rebuild, which previously lost it (issue #366 ride-along).
|
|
2507
|
+
*/
|
|
2508
|
+
export function appendStructuredOutputDirective(
|
|
2509
|
+
basePrompt: string,
|
|
2510
|
+
schema: Record<string, unknown> | undefined,
|
|
2511
|
+
): string {
|
|
2512
|
+
if (!schema) return basePrompt;
|
|
2513
|
+
const schemaStr = JSON.stringify(schema, null, 2);
|
|
2514
|
+
return basePrompt + `\n\n---\nCRITICAL OUTPUT REQUIREMENT:\nYour final response MUST be a single valid JSON object (no markdown, no commentary, no code fences) that matches this schema:\n${schemaStr}\n\nRespond with ONLY the JSON object. Nothing else.`;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2435
2517
|
export function buildPrompt(input: BuildPromptInput): string {
|
|
2436
2518
|
const {
|
|
2437
2519
|
resolution,
|
|
@@ -2448,10 +2530,44 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2448
2530
|
conversationCatchup,
|
|
2449
2531
|
} = input;
|
|
2450
2532
|
|
|
2451
|
-
// HITL reinvocation: the
|
|
2452
|
-
//
|
|
2453
|
-
//
|
|
2533
|
+
// HITL reinvocation: the decisions-only prompt is correct ONLY while the
|
|
2534
|
+
// agent's native context still carries the prior conversation — which only
|
|
2535
|
+
// resumed_successfully guarantees. Any other reason means a fresh agent
|
|
2536
|
+
// mid-HITL (in practice created_after_resume_failure: the stored handle
|
|
2537
|
+
// failed to resume, or a poisoned handle was replaced mid-send), which
|
|
2538
|
+
// gets the full recovery shape instead — enhanced context + the turn's
|
|
2539
|
+
// recorded transcript + the same decisions — because the bare decisions on
|
|
2540
|
+
// an empty conversation strand the agent with instructions and no story,
|
|
2541
|
+
// and the session inherits that amnesia permanently (issue #366).
|
|
2454
2542
|
if (isHitlReinvocation(approvalDecisions)) {
|
|
2543
|
+
if (resolution.reason !== "resumed_successfully") {
|
|
2544
|
+
return buildHitlRecoveryPrompt(
|
|
2545
|
+
{
|
|
2546
|
+
instructions,
|
|
2547
|
+
userMessage,
|
|
2548
|
+
skills,
|
|
2549
|
+
datastoreUsages: input.datastoreUsages ?? [],
|
|
2550
|
+
channelMessaging: input.channelMessaging ?? [],
|
|
2551
|
+
subAgents,
|
|
2552
|
+
workspaceDirs,
|
|
2553
|
+
workspaceFileRefs,
|
|
2554
|
+
attachments,
|
|
2555
|
+
vision: input.vision,
|
|
2556
|
+
interactionMode,
|
|
2557
|
+
buildFromPlan,
|
|
2558
|
+
contextBridge: input.contextBridge,
|
|
2559
|
+
senderIdentity: input.senderIdentity,
|
|
2560
|
+
sessionContext: input.sessionContext,
|
|
2561
|
+
conversationCatchup,
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
turnDigest: input.turnRecoveryDigest,
|
|
2565
|
+
pendingApprovals: input.pendingApprovals,
|
|
2566
|
+
approvalDecisions,
|
|
2567
|
+
appliedToolCallIds: input.appliedToolCallIds,
|
|
2568
|
+
},
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2455
2571
|
return buildReinvocationPrompt(
|
|
2456
2572
|
input.pendingApprovals,
|
|
2457
2573
|
approvalDecisions,
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
* endpoint is unreachable.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
resolveModelRegistryUrl,
|
|
13
|
+
buildRegistryHeaders,
|
|
14
|
+
REGISTRY_RETRY_POLICY,
|
|
15
|
+
} from "../../shared/registry-endpoint.js";
|
|
16
|
+
import { fetchWithRetry } from "../../shared/http-retry.js";
|
|
12
17
|
|
|
13
18
|
/** Per-million rates for a speed/mode variant (e.g. "fast") of a base model. */
|
|
14
19
|
export interface CursorVariantPricing {
|
|
@@ -53,6 +58,10 @@ interface RegistryEntry {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
const CACHE_TTL_MS = 3_600_000; // 1 hour
|
|
61
|
+
// Failed fetches are cached much shorter than successes (the model-registry.ts
|
|
62
|
+
// policy): a transient failure must not pin DEFAULT_PRICING — wrong rates for
|
|
63
|
+
// cost tracking — for a full hour.
|
|
64
|
+
const FAILURE_CACHE_TTL_MS = 60_000;
|
|
56
65
|
|
|
57
66
|
const DEFAULT_PRICING: CursorModelPricing = {
|
|
58
67
|
model: "unknown",
|
|
@@ -104,7 +113,11 @@ function parseVariants(
|
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
async function fetchFromApi(): Promise<readonly CursorModelPricing[]> {
|
|
107
|
-
const res = await
|
|
116
|
+
const res = await fetchWithRetry(
|
|
117
|
+
resolveModelRegistryUrl(),
|
|
118
|
+
{ headers: buildRegistryHeaders() },
|
|
119
|
+
REGISTRY_RETRY_POLICY,
|
|
120
|
+
);
|
|
108
121
|
if (!res.ok) throw new Error(`Model registry fetch failed: ${res.status}`);
|
|
109
122
|
const data: unknown = await res.json();
|
|
110
123
|
const table = parsePricingTable(data);
|
|
@@ -138,7 +151,7 @@ export async function getPricingTable(): Promise<readonly CursorModelPricing[]>
|
|
|
138
151
|
`Failed to fetch model registry from API, using default pricing: ${err}`,
|
|
139
152
|
);
|
|
140
153
|
const fallback = [DEFAULT_PRICING];
|
|
141
|
-
cache = { data: fallback, expiresAt: Date.now() +
|
|
154
|
+
cache = { data: fallback, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS };
|
|
142
155
|
return fallback;
|
|
143
156
|
})
|
|
144
157
|
.finally(() => {
|
|
@@ -148,4 +161,10 @@ export async function getPricingTable(): Promise<readonly CursorModelPricing[]>
|
|
|
148
161
|
return inflightFetch;
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
/** Exposed for testing — resets the in-memory cache. */
|
|
165
|
+
export function _resetPricingCache(): void {
|
|
166
|
+
cache = null;
|
|
167
|
+
inflightFetch = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
export { DEFAULT_PRICING };
|