[Bug]: ACP keeps stale config choices when an update precedes a config-write response
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- Nửa ngày
- Mức phù hợp với người mới
- 50/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- backend
Hướng nghiên cứu
The bug is in packages/provider-acp/src/server/AcpSessionRuntime.ts: the write path around lines 2072-2089 skips adopting the response's configOptions when the consumed-notification count changed during the write, even when the notification arrived before the response. Read the config-option update handling near lines 1694-1708 and the response-to-cache path near lines 1880-1897, then run sessionConfig.test.ts. Done when the attached repro-config.mjs exits 0 and the cache holds the response's high effort and high-only choices.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
An ACP agent can successfully change a configuration option while T3 keeps the previous dependent choices and values. When the agent sends a configuration notification before completing the write, T3 ignores the later response even when that response contains newer state. A controlled stdio peer reproduced the mismatch through the production runtime; the same write without the notification updated the cached configuration correctly.
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Use the stated checkout with its frozen-lockfile dependencies installed and a Node runtime supporting native TypeScript imports.
- Run the script below against the checkout. Its control writes profile=large and receives the changed dependent effort configuration without an intervening notification.
- The failing variant starts the same write, waits for the runtime to consume an intermediate config_option_update, and only then releases the later full response. Compare the response with runtime.getConfigOptions.
The final script reproduced the defect once. Synchronization uses the runtime's configuration stream and a fixture-only JSON-RPC request, with no sleeps or elapsed-time assertions.
Save this as repro-config.mjs and run node repro-config.mjs /path/to/checkout. Prerequisites are Node with native TypeScript support and the checkout's installed dependencies. The script prints Node, Git, Effect and checkout versions, creates its peer and all state under mktemp-equivalent mkdtempSync, redirects HOME, TMPDIR, T3CODE_HOME and the cache there, removes that directory on exit, and leaves the checkout unchanged. Exit 0 means expected behavior, exit 1 means the defect reproduced, and exit 2 means setup or the control was inconclusive.
Reproduction script: repro-config.mjs
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { execFileSync } from 'node:child_process';
const repo = resolve(process.argv[2] ?? '.');
const scratch = mkdtempSync(join(tmpdir(), 'acp-config-repro-'));
let code = 2;
try {
for (const dir of ['home','tmp','state','cache','workspace']) mkdirSync(join(scratch,dir));
Object.assign(process.env, {HOME:join(scratch,'home'),TMPDIR:join(scratch,'tmp'),T3CODE_HOME:join(scratch,'state'),XDG_CACHE_HOME:join(scratch,'cache')});
console.log('Node '+process.version);
console.log('Git '+execFileSync('git',['--version'],{encoding:'utf8'}).trim());
console.log('Commit '+execFileSync('git',['-C',repo,'rev-parse','HEAD'],{encoding:'utf8'}).trim());
const require = createRequire(join(repo,'packages/provider-acp/package.json'));
const load = async name => import(pathToFileURL(require.resolve(name)).href);
const Effect = await load('effect/Effect');
const Layer = await load('effect/Layer');
const Stream = await load('effect/Stream');
const Fiber = await load('effect/Fiber');
const NodeServices = await load('@effect/platform-node/NodeServices');
const NodeCrypto = await load('@effect/platform-node/NodeCrypto');
const Runtime = await import(pathToFileURL(join(repo,'packages/provider-acp/src/server/AcpSessionRuntime.ts')).href);
const pkg = JSON.parse(readFileSync(require.resolve('effect/package.json'),'utf8'));
console.log('Effect '+pkg.version);
const peer=join(scratch,'peer.mjs');
writeFileSync(peer, `import readline from 'node:readline';
const variant=process.argv[2]; let pending;
const option=(id,current,values)=>({id,name:id,type:'select',currentValue:current,options:values.map(value=>({value,name:value}))});
const old=[option('profile','small',['small','large']),option('effort','low',['low'])];
const interim=[option('profile','large',['small','large']),option('effort','low',['low'])];
const final=[option('profile','large',['small','large']),option('effort','high',['high'])];
const send=x=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',...x})+'\\n');
for await (const line of readline.createInterface({input:process.stdin})) {
const m=JSON.parse(line); if (!('id' in m)) continue;
if(m.method==='initialize') send({id:m.id,result:{protocolVersion:1,agentCapabilities:{},agentInfo:{name:'fixture',version:'1.0.0'},authMethods:[]}});
else if(m.method==='session/new') send({id:m.id,result:{sessionId:'fixture-session',configOptions:old}});
else if(m.method==='session/set_config_option') {
if(variant==='control') send({id:m.id,result:{configOptions:final}});
else {pending=m.id;send({method:'session/update',params:{sessionId:'fixture-session',update:{sessionUpdate:'config_option_update',configOptions:interim}}});}
} else if(m.method==='_fixture/release') {send({id:pending,result:{configOptions:final}});send({id:m.id,result:{}});}
else send({id:m.id,error:{code:-32601,message:'Unknown fixture method'}});
}
`);
const run = variant => Effect.gen(function*(){
const runtime = yield* Runtime.AcpSessionRuntime;
yield* runtime.start();
let response;
if(variant==='control') response=yield* runtime.setConfigOption('profile','large');
else {
const write=yield* runtime.setConfigOption('profile','large').pipe(Effect.forkScoped);
yield* runtime.configOptionChanges.pipe(Stream.filter(options=>options.some(o=>o.id==='profile'&&o.currentValue==='large')),Stream.runHead);
yield* runtime.request('_fixture/release',{});
response=yield* Fiber.join(write);
}
const cached=yield* runtime.getConfigOptions;
const responseEffort=response.configOptions.find(o=>o.id==='effort');
const cachedEffort=cached.find(o=>o.id==='effort');
console.log(variant+': response effort='+responseEffort?.currentValue+' cached effort='+cachedEffort?.currentValue+' cached choices='+JSON.stringify(cachedEffort?.options));
return responseEffort?.currentValue==='high'&&cachedEffort?.currentValue==='high';
}).pipe(Effect.provide(Runtime.layer({spawn:{command:process.execPath,args:[peer,variant],cwd:join(scratch,'workspace'),env:{...process.env}},cwd:join(scratch,'workspace'),clientInfo:{name:'fixture-client',version:'1.0.0'}}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer,NodeCrypto.layer)))),Effect.scoped);
if (!(await Effect.runPromise(run('control')))) throw new Error('Control failed');
code=(await Effect.runPromise(run('notification-before-response')))?0:1;
} catch(error) { console.error(String(error)); code=2; }
finally {rmSync(scratch,{recursive:true,force:true});}
console.log(code===0?'RESULT: expected behavior':code===1?'RESULT: defect reproduced':'RESULT: inconclusive');
process.exitCode=code;
Expected behavior
After the successful configuration write completes, the runtime's configuration list should include the dependent values and choices returned by that response. A notification that was already consumed before the agent produces its response should not cause that newer response to be discarded.
The pinned schema-v1.21.0 SetSessionConfigOptionResponse defines configOptions as the full set of options and current values. The pinned schema-v2.0.0-alpha.3 response has the same contract. The fetched configuration documentation explains that this complete state carries dependent changes. T3 also promises that running agents' option changes update the picker in docs/user/providers-acp.md#L85-L95.
Actual behavior
A fake ACP v1 peer initially advertises profile=small and effort=low with only the low effort choice. On a request to set profile=large, it sends a complete config_option_update with profile=large and effort=low, then later completes the request with a complete configuration containing effort=high and only the high effort choice. The returned response contains high, but runtime.getConfigOptions still contains low and its obsolete choices. This was reproduced through AcpSessionRuntime.layer and its actual stdio transport, with no real agent, credentials, provider prompt, or live T3 state.
Impact
Minor bug or occasional failure
Impact: P2 - A successful setting change can leave stale dependent values and available choices in the shared ACP runtime. This is a bounded configuration-state failure while the session can continue; the control shows the ordinary response path works.
The reproduction directly establishes the runtime cache mismatch for ACP v1 and two ordinary select options. The affected update logic is shared, but ACP v2 and a complete app UI flow were not run. A later complete config_option_update is a possible recovery path supported by the source, but it was not exercised as a workaround in this run. Whether a specific real agent emits this ordering is NOT_ENOUGH_INFO: an agent/version-specific protocol capture would establish that reach.
Version or commit
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93.
Environment
Node v26.12.1, Effect 4.1.3, Git 2.56.0, Darwin 27.2.0 arm64. The synthetic peer negotiates ACP protocol 1. Repository dependency and schema pins were preserved.
Logs or stack traces
The script printed the following relevant lines and exited 1. The output contains only synthetic identifiers and values.
Node v26.12.1
Git git version 2.56.0
Commit 59e64eef881632a23f89c4138bcf37f23d92bf93
Effect 4.1.3
control: response effort=high cached effort=high cached choices=[{"value":"high","name":"high"}]
notification-before-response: response effort=high cached effort=low cached choices=[{"value":"low","name":"low"}]
RESULT: defect reproduced
Source: packages/provider-acp/src/server/AcpSessionRuntime.ts#L1694-L1708 counts consumed configuration notifications. packages/provider-acp/src/server/AcpSessionRuntime.ts#L2072-L2089 skips adopting the response whenever that count changed during a write, including a notification received before the response. packages/provider-acp/src/server/AcpSessionRuntime.ts#L1880-L1897 is the omitted response-to-cache/event path.
The existing focused suite also passed: sessionConfig.test.ts, provider-acp-registry driver.test.ts and effect-acp client.test.ts, 3 files and 50 tests with the pinned Vite+ runner, one worker. Those checks cover nearby behavior and do not establish restoration of this defect.
Evidence
Expected source: the two pinned ACP schemas and the configuration documentation linked above. Failure source: a controlled run of the production AcpSessionRuntime using its stdio transport. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete for the runtime failure. Missing fact: affected real-agent/version ordering and the complete UI manifestation were not established.
Restoration check
Run the reproduction script unchanged. Both variants must retain the returned high effort and its high-only choices in runtime.getConfigOptions, and the script must print RESULT: expected behavior and exit 0. Preserve handling for notifications that really follow a response.
References
- https://github.com/pingdotgg/t3code/issues/21739 — concerns the final session/load response being abandoned after replay idle; this reproduction starts a new session normally and loses the response to a later configuration write.
- https://github.com/pingdotgg/t3code/issues/35693 — concerns a separate probe discovering models that an older session never advertised; this reproduction provides the new values and choices in the same session's successful write response.
- https://github.com/pingdotgg/t3code/pull/35199 — addresses applying the model and implicit effort when a new Copilot session starts; this reproduction directly calls the shared runtime setter after session startup and checks its cache against its own returned response.
- Ngôn ngữ chính
- TypeScript
- Star
- 24.8k
- Fork
- 6.4k
- Merge trung bình
- 59 phút
- Pull request đã merge (30 ngày)
- 418
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của pingdotgg/t3code
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
Maintainer thường phản hồi trong vòng 1 ngày
-
via-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug]: MCP App inline frames never go below 80px, so compact apps get blank space under themĐang mở
Độ khó 2/5 Nửa ngày Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Question card options show LaTeX as raw source, though the question text renders itCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug]: Settings inheritance popover shows raw slugs for branch naming, default permissions and response streamingCó thể đã có người làm @juliusmarminge đã nhận 1 ngày trước. Đang mởvouch:denounced
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của pingdotgg/t3code
Issue tương tự
-
bug platform:mobile root-cause:found
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
openchamber/openchamber#6926 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
push-based/user-flow#356 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
Cygnux-Labs/Tracekit#827 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
www build: a refused import shows only "ENOENT scandir <outDir>", never the build-audience errorĐang mởkind:defect
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 91/100
Maintainer thường phản hồi trong vòng 1 ngày
-
type:bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
nightgauge/nightgauge#3266 ·
Maintainer thường phản hồi trong vòng 1 ngày