Skip to content

Commit 022bd3a

Browse files
merge: upstream main tip efc22ed into this carrier branch
Conflicts: src/core/task/Task.ts and src/core/task/__tests__/Task.spec.ts. Both sides had landed the same cancellation-generation code, so every conflict region was comment text. Three regions in Task.ts and the one in Task.spec.ts take the trunk wording verbatim. The field comment keeps this branch rationale for why the counter must stay monotonic, and names the run-state latch and the resume path that prove it instead of the campaign-internal rule id the branch cited. Refs: pull 2359
2 parents ebd6d9e + efc22ed commit 022bd3a

111 files changed

Lines changed: 4113 additions & 591 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/code-qa.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ jobs:
247247
- name: Verify coverage cache inputs
248248
if: matrix.collect-coverage
249249
run: pnpm --dir src run verify:coverage-cache-inputs
250+
# Validate build/static cache inputs (issue #2290 contract) so a
251+
# stale webview or extension bundle is never restored as a hit.
252+
- name: Verify build cache inputs
253+
if: matrix.collect-coverage
254+
run: pnpm --dir src run verify:build-cache-inputs
250255
- name: Save Turbo cache
251256
if: steps.turbo-cache.outputs.cache-hit != 'true'
252257
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0

‎.github/workflows/visual-regression.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ permissions:
2121
jobs:
2222
webview-visual:
2323
runs-on: ubuntu-latest
24-
timeout-minutes: 10
24+
timeout-minutes: 15
2525
container:
2626
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
2727
options: --ipc=host

‎packages/config-eslint/api-provider-hygiene.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,24 @@ export const apiProviderHygieneConfig = {
6363
},
6464
}
6565

66+
// Shared by every spec lane, not only src/api: an abort assertion must not pass on a raw SDK error.
67+
export const abortSubstringSelector = {
68+
selector:
69+
// Strings contain literal punctuation; only regex source can contain anchors, wildcards,
70+
// optional suffix groups, or word boundaries around the generic abort wording.
71+
"CallExpression[callee.property.name=/^toThrow(Error)?$/]:not([callee.object.property.name='not']):matches([arguments.0.value=/^(the )?(request )?(was )?abort(ed)?[.]*$/i], [arguments.0.regex.pattern=/^[$^]?(?:\\.\\*)?(the )?(request )?(was )?abort(ed)?(?:\\(\\?:(?:ed|ion|al)\\)\\??)?(?:\\\\b)?(?:\\.\\*)?[\\\\.$]*$/i])",
72+
message:
73+
"A bare 'abort' substring also matches a raw SDK error. Assert the AbortError name and the full provider message.",
74+
}
75+
6676
// Spec files under src/api. A loop of expects in one test stops at the first failing case and hides the rest.
6777
export const apiProviderTestSelectors = [
6878
{
6979
selector: "ForOfStatement[right.type='ArrayExpression']:has(CallExpression[callee.name='expect'])",
7080
message:
7181
"Use it.each for repeated cases. A loop of expects in one test stops at the first failing case and does not name it.",
7282
},
83+
abortSubstringSelector,
7384
]
7485

7586
// A new @ts-ignore in a provider spec hides a private-member access. Use bracket notation or a typed double.

‎packages/config-eslint/api-provider-hygiene.test.js‎

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import assert from "node:assert/strict"
22
import { describe, it } from "node:test"
3+
import { fileURLToPath, URL } from "node:url"
34

4-
import { Linter } from "eslint"
5+
import { ESLint, Linter } from "eslint"
56
import typescriptEslint from "typescript-eslint"
67

78
import { apiProviderHygieneConfig, apiProviderTestSelectors, apiSpecTsIgnoreConfig } from "./api-provider-hygiene.js"
@@ -173,6 +174,37 @@ describe("api provider hygiene: spec loops", () => {
173174
assert.deepEqual(lintSpec(code), [])
174175
})
175176
}
177+
178+
for (const code of [
179+
'await expect(p).rejects.toThrow("aborted")',
180+
"expect(() => f()).toThrow('abort')",
181+
'await expect(p).rejects.toThrowError("aborted")',
182+
"expect(() => f()).toThrow(/abort/i)",
183+
"await expect(p).rejects.toThrowError(/aborted/i)",
184+
'await expect(p).rejects.toThrow("was aborted")',
185+
'await expect(p).rejects.toThrow("Request was aborted.")',
186+
'await expect(p).rejects.toThrow("The request was aborted")',
187+
"await expect(p).rejects.toThrow(/was aborted/)",
188+
]) {
189+
it(`flags a bare abort substring: ${code}`, () => {
190+
assert.equal(lintSpec(code).length, 1)
191+
})
192+
}
193+
194+
for (const code of [
195+
'await expect(p).rejects.toThrow("The Requesty request was aborted")',
196+
'await expect(p).rejects.toMatchObject({ name: "AbortError" })',
197+
'await expect(p).rejects.toThrow("API Error")',
198+
'expect(() => f()).not.toThrow("aborted")',
199+
'await expect(p).rejects.not.toThrow("aborted")',
200+
"expect(() => f()).not.toThrowError(/abort/i)",
201+
"await expect(p).rejects.not.toThrowError(/aborted/i)",
202+
"await expect(p).rejects.toThrow(/^The Requesty request was aborted$/)",
203+
]) {
204+
it(`allows: ${code}`, () => {
205+
assert.deepEqual(lintSpec(code), [])
206+
})
207+
}
176208
})
177209

178210
describe("api provider hygiene: hand-built AbortError", () => {
@@ -245,3 +277,68 @@ describe("api provider hygiene: @ts-ignore in specs", () => {
245277
assert.deepEqual(lintSpecFile("// @ts-ignore x\nhandler.client", "services/__tests__/example.spec.ts"), [])
246278
})
247279
})
280+
281+
// Resolve each lane through the extension's real config before exercising its syntax rules.
282+
// Isolating syntax rules avoids requiring fixture files in the TypeScript project.
283+
describe("abort assertions in the extension ESLint config", () => {
284+
const srcRoot = fileURLToPath(new URL("../../src/", import.meta.url))
285+
const eslint = new ESLint({ cwd: srcRoot })
286+
287+
for (const filename of [
288+
"core/task/__tests__/example.spec.ts",
289+
"core/config/__tests__/example.spec.ts",
290+
// The workflow tests are exempt from the path-traversal selectors, not from the abort
291+
// rule; listing one proves the lane that re-applies it actually resolves for them.
292+
"services/__tests__/pr-review-state-workflow.test.ts",
293+
]) {
294+
it(`checks rejected and accepted assertions in ${filename}`, async () => {
295+
const resolved = await eslint.calculateConfigForFile(filename)
296+
const syntaxConfig = [
297+
{
298+
files: ["**/*.ts"],
299+
languageOptions: { parser: typescriptEslint.parser },
300+
rules: { "no-restricted-syntax": resolved.rules["no-restricted-syntax"] },
301+
},
302+
]
303+
for (const [code, expectedCount] of [
304+
['await expect(p).rejects.toThrow("aborted")', 1],
305+
["await expect(p).rejects.toThrowError(/abort/i)", 1],
306+
// An escaped dot in the source text is still a bare abort substring: the pattern
307+
// keeps a backslash, so the matcher must allow it before the trailing dot or anchor.
308+
["await expect(p).rejects.toThrow(/^aborted$/)", 1],
309+
["await expect(p).rejects.toThrow(/.*aborted.*/i)", 1],
310+
["await expect(p).rejects.toThrowError(/abort(?:ed)/i)", 1],
311+
["await expect(p).rejects.toThrow(/aborted\\b/i)", 1],
312+
['await expect(p).rejects.toThrow("abort$")', 0],
313+
['await expect(p).rejects.toThrow("^abort")', 0],
314+
["await expect(p).rejects.toThrow(/.*The Requesty request was aborted.*/i)", 0],
315+
["await expect(p).rejects.toThrow(/The Requesty request was aborted\\b/i)", 0],
316+
["await expect(p).rejects.toThrow(/^Request was aborted\\.$/)", 1],
317+
["await expect(p).rejects.toThrowError(/request aborted\\.$/i)", 1],
318+
["await expect(p).rejects.toThrow(/^request aborted\\$/)", 1],
319+
// An inline non-capturing group is regex source, not part of the asserted
320+
// message: an anchored match that stops short of it lets a bare abort through.
321+
["await expect(p).rejects.toThrow(/abort(?:ed)?/i)", 1],
322+
["await expect(p).rejects.toThrowError(/^request aborted(?:ed)?$/i)", 1],
323+
["await expect(p).rejects.toThrow(/cancel(?:ed)?/i)", 0],
324+
[
325+
"await expect(p).rejects.toThrow(/^The Requesty request was aborted(?:ed)? because the peer closed the stream\\.$/)",
326+
0,
327+
],
328+
['await expect(p).rejects.not.toThrow("aborted")', 0],
329+
["await expect(p).rejects.toThrow(/^Request was aborted because the peer closed the stream\\.$/)", 0],
330+
[
331+
'await expect(p).rejects.toMatchObject({ name: "AbortError", message: "The Requesty request was aborted" })',
332+
0,
333+
],
334+
]) {
335+
const messages = linter.verify(code, syntaxConfig, { filename })
336+
assert.equal(messages.length, expectedCount, code)
337+
if (expectedCount) {
338+
assert.equal(messages[0].ruleId, "no-restricted-syntax")
339+
assert.match(messages[0].message, /bare 'abort' substring/)
340+
}
341+
}
342+
})
343+
}
344+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { experimentIds, experimentIdsSchema, experimentsSchema } from "../experiment.js"
2+
3+
describe("dynamicThinkingEffort experiment", () => {
4+
it("is part of the experiment id enum", () => {
5+
expect(experimentIds).toContain("dynamicThinkingEffort")
6+
expect(experimentIdsSchema.safeParse("dynamicThinkingEffort").success).toBe(true)
7+
})
8+
9+
it("parses enabled and disabled states", () => {
10+
expect(experimentsSchema.parse({ dynamicThinkingEffort: true })).toEqual({ dynamicThinkingEffort: true })
11+
expect(experimentsSchema.parse({ dynamicThinkingEffort: false })).toEqual({ dynamicThinkingEffort: false })
12+
expect(experimentsSchema.parse({})).toEqual({})
13+
})
14+
15+
it("rejects non-boolean values", () => {
16+
expect(experimentsSchema.safeParse({ dynamicThinkingEffort: "yes" }).success).toBe(false)
17+
expect(experimentIdsSchema.safeParse("dynamic-thinking-effort").success).toBe(false)
18+
})
19+
})

‎packages/types/src/__tests__/openai-models.test.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,71 @@ import { openAiCodexDefaultModelId, openAiCodexModels } from "../providers/opena
22
import { openAiNativeDefaultModelId, openAiNativeModels } from "../providers/openai.js"
33

44
describe("OpenAI native models", () => {
5+
it("describes GPT-6.1 Terra pricing and optional native reasoning without changing defaults", () => {
6+
expect(openAiNativeDefaultModelId).toBe("gpt-5.6-sol")
7+
expect(openAiCodexDefaultModelId).toBe("gpt-5.6-sol")
8+
const model = openAiNativeModels["gpt-6.1-terra"]
9+
expect(model).toMatchObject({
10+
maxTokens: 128_000,
11+
contextWindow: 1_050_000,
12+
includedTools: ["apply_patch"],
13+
excludedTools: ["apply_diff", "write_to_file"],
14+
supportsImages: true,
15+
supportsPromptCache: true,
16+
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],
17+
reasoningEffort: "medium",
18+
supportsTemperature: false,
19+
supportsVerbosity: true,
20+
inputPrice: 1,
21+
outputPrice: 5,
22+
cacheWritesPrice: 1.25,
23+
cacheWritesPrice12h: 2.5,
24+
cacheReadsPrice: 0.05,
25+
longContextPricing: {
26+
thresholdTokens: 272_000,
27+
inputPriceMultiplier: 2,
28+
outputPriceMultiplier: 1.5,
29+
cacheWritesPriceMultiplier: 2,
30+
cacheReadsPriceMultiplier: 2,
31+
appliesToServiceTiers: ["default", "flex", "priority"],
32+
},
33+
})
34+
expect(model).not.toHaveProperty("requiredReasoningEffort", true)
35+
expect(model.tiers).toEqual([
36+
{
37+
name: "flex",
38+
contextWindow: 1_050_000,
39+
inputPrice: 0.5,
40+
outputPrice: 2.5,
41+
cacheWritesPrice: 0.625,
42+
cacheReadsPrice: 0.025,
43+
},
44+
{
45+
name: "priority",
46+
contextWindow: 1_050_000,
47+
inputPrice: 2,
48+
outputPrice: 10,
49+
cacheWritesPrice: 2.5,
50+
cacheReadsPrice: 0.1,
51+
},
52+
])
53+
expect(openAiCodexModels["gpt-6.1-terra"]).toMatchObject({
54+
maxTokens: 128_000,
55+
contextWindow: 872_000,
56+
includedTools: ["apply_patch"],
57+
excludedTools: ["apply_diff", "write_to_file"],
58+
supportsImages: true,
59+
supportsPromptCache: true,
60+
supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"],
61+
requiredReasoningEffort: true,
62+
reasoningEffort: "low",
63+
supportsTemperature: false,
64+
supportsVerbosity: true,
65+
inputPrice: 0,
66+
outputPrice: 0,
67+
})
68+
})
69+
570
it("describes GPT-6.1 Sol pricing and required reasoning without changing defaults", () => {
671
expect(openAiNativeDefaultModelId).toBe("gpt-5.6-sol")
772
expect(openAiCodexDefaultModelId).toBe("gpt-5.6-sol")

‎packages/types/src/__tests__/provider-identifiers.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describe("provider identifiers", () => {
114114
providerIdentifiers.nanogpt,
115115
providerIdentifiers.ioIntelligence,
116116
providerIdentifiers.kimiCode,
117+
providerIdentifiers.friendli,
117118
])
118119
expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio])
119120
expect(internalProviders).toEqual([providerIdentifiers.vscodeLm, providerIdentifiers.githubCopilot])

‎packages/types/src/experiment.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const experimentIds = [
1212
"runSlashCommand",
1313
"customTools",
1414
"parallelToolExecution",
15+
"dynamicThinkingEffort",
1516
] as const
1617

1718
export const experimentIdsSchema = z.enum(experimentIds)
@@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
2829
runSlashCommand: z.boolean().optional(),
2930
customTools: z.boolean().optional(),
3031
parallelToolExecution: z.boolean().optional(),
32+
dynamicThinkingEffort: z.boolean().optional(),
3133
})
3234

3335
export type Experiments = z.infer<typeof experimentsSchema>

‎packages/types/src/provider-settings.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const dynamicProviders = [
102102
providerIdentifiers.nanogpt,
103103
providerIdentifiers.ioIntelligence,
104104
providerIdentifiers.kimiCode,
105+
providerIdentifiers.friendli,
105106
] as const
106107

107108
export type DynamicProvider = (typeof dynamicProviders)[number]

‎packages/types/src/providers/openai-codex.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,25 @@ export const openAiCodexDefaultModelId: OpenAiCodexModelId = "gpt-5.6-sol"
3131
* Costs are 0 as they are covered by the subscription.
3232
*/
3333
export const openAiCodexModels = {
34+
// https://github.com/openai/codex/blob/main/codex-rs/models-manager/models.json
35+
"gpt-6.1-terra": {
36+
maxTokens: 128000,
37+
// Use Codex's supported maximum rather than its 500K default compaction budget.
38+
contextWindow: 872000,
39+
includedTools: ["apply_patch"],
40+
excludedTools: ["apply_diff", "write_to_file"],
41+
supportsImages: true,
42+
supportsPromptCache: true,
43+
supportsReasoningEffort: ["low", "medium", "high", "xhigh", "max"],
44+
requiredReasoningEffort: true,
45+
reasoningEffort: "low",
46+
inputPrice: 0,
47+
outputPrice: 0,
48+
supportsVerbosity: true,
49+
supportsTemperature: false,
50+
description:
51+
"GPT-6.1 Terra: Fast, efficient visual reasoning, precise computer use, and natural writing via ChatGPT subscription",
52+
},
3453
// https://github.com/openai/codex/blob/main/codex-rs/models-manager/models.json
3554
"gpt-6.1-sol": {
3655
maxTokens: 128000,

0 commit comments

Comments
 (0)