Skip to content

[Bug]: Kiro compaction calls an extension the running agent did not advertise #35832

Closed

Description

@coygeek

Summary

Kiro compaction remains enabled when the running server advertises no Kiro extensions. T3 sends the compaction request anyway, turning the action into a failed turn on that valid deployment. A fake-peer run reproduced the failure and showed the same action completing when the peer advertised the extension. Optional Kiro requests should follow the running server's capability advertisement.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.
  • Removed credentials, tokens, private paths, and sensitive data.

Area

apps/server

Steps to reproduce

  1. Use a clean checkout of the stated commit with its locked dependencies installed.
  2. Run the script below against that checkout. It calls the production Kiro adapter using the project's isolated test-host service and a local fake ACP peer.
  3. First advertise _kiro/session/compact and observe a completed compaction. Then advertise an empty extension list, reject the unavailable extension with -32601, and observe that T3 still sends it and fails the turn.

The successful and failing cases ran once each in the recorded reproduction. An earlier setup attempt was inconclusive because a CommonJS resolver could not resolve an import-only workspace export; the final script resolves those exports from their package manifests and reached both cases.

Save this as repro-kiro.mjs outside the checkout and run node repro-kiro.mjs /path/to/t3code. It requires Node with TypeScript stripping support and Git, prints their versions and the target revision, creates all fixture files and disposable HOME, TMPDIR, T3CODE_HOME and working directories under a temporary directory, removes them on exit, and leaves the checkout unchanged. It never launches Kiro or contacts a provider. Exit 0 means no unadvertised compaction call is sent, 1 means the defect reproduced after the advertised-method control succeeded, and 2 means setup or the control failed.

Reproduction script: repro-kiro.mjs
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';

const repo = resolve(process.argv[2]);
const scratch = mkdtempSync(join(tmpdir(), 'kiro-extension-repro-'));
let code = 2;
try {
  for (const dir of ['home', 'tmp', 'state', 'cwd']) mkdirSync(join(scratch, dir));
  Object.assign(process.env, { HOME: join(scratch, 'home'), TMPDIR: join(scratch, 'tmp'), T3CODE_HOME: join(scratch, 'state') });
  process.chdir(join(scratch, 'cwd'));
  const require = createRequire(join(repo, 'apps/server/package.json'));
  const load = name => {
    if (!name.startsWith('@t3tools/')) return import(pathToFileURL(require.resolve(name)).href);
    const [, pkg, ...rest] = name.split('/');
    const base = join(repo, 'packages', pkg);
    const manifest = JSON.parse(readFileSync(join(base, 'package.json')));
    return import(pathToFileURL(join(base, manifest.exports['./' + rest.join('/')].import)).href);
  };
  const [Effect, Exit, Layer, Stream, DateTime, Schema, NodeServices, NodeCrypto, TestProviderHost, IdAllocator, McpProviderSessions, ProviderAdapter, Kiro, KiroSettings, Runtime] = await Promise.all([
    'effect/Effect', 'effect/Exit', 'effect/Layer', 'effect/Stream', 'effect/DateTime', 'effect/Schema',
    '@effect/platform-node/NodeServices', '@effect/platform-node/NodeCrypto',
    '@t3tools/provider-testing/TestProviderHost', '@t3tools/provider-core/server/IdAllocator',
    '@t3tools/provider-core/server/McpProviderSessions', '@t3tools/provider-core/server/ProviderAdapter',
    '@t3tools/provider-kiro/testing', '@t3tools/provider-kiro/settings', '@t3tools/provider-acp/server/AcpSessionRuntime',
  ].map(load));
  console.log('Node ' + process.version);
  console.log(execFileSync('git', ['--version'], { encoding: 'utf8' }).trim());
  console.log('Revision ' + execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim());
  console.log('Effect ' + JSON.parse(readFileSync(require.resolve('effect/package.json'))).version);
  const peer = join(scratch, 'peer.mjs');
  writeFileSync(peer, `import { createInterface } from 'node:readline';
const supports=process.argv[2]==='supported';
for await (const line of createInterface({input:process.stdin})) {
 const r=JSON.parse(line); if(r.id===undefined)continue;
 let result={},error;
 if(r.method==='initialize')result={protocolVersion:1,agentCapabilities:{loadSession:true,_meta:{kiro:{extensionMethods:supports?['_kiro/session/compact']:[]}}},authMethods:[]};
 else if(r.method==='session/new')result={sessionId:'fixture-session',configOptions:[]};
 else if(r.method==='_kiro/session/compact') { if(supports)result={success:true}; else error={code:-32601,message:'Compaction extension not advertised'}; }
 else if(r.method==='session/prompt')result={stopReason:'end_turn'};
 else error={code:-32601,message:'Unsupported fixture method'};
 process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:r.id,...(error?{error}:{result})})+'\\n');
}
`);
  const layer = Layer.mergeAll(NodeServices.layer, NodeCrypto.layer, IdAllocator.layer, McpProviderSessions.layer,
    TestProviderHost.layer().pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))));
  const run = async supports => {
    const methods=[];
    const exit = await Effect.runPromiseExit(Effect.gen(function* () {
      const adapter=yield* Kiro.makeKiroAdapterV2({
        instanceId:'kiro-fixture', settings:Schema.decodeSync(KiroSettings.KiroSettings)({enabled:true}), environment:{},
        selfInvocation:{command:process.execPath,args:[]},
        makeRuntime:input=>Runtime.make({...input,
          spawn:{command:process.execPath,args:[peer,supports?'supported':'unsupported'],extendEnv:false,env:{HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,T3CODE_HOME:process.env.T3CODE_HOME}},
          requestLogger:event=>Effect.sync(()=>{if(event.status==='started')methods.push(event.method);}),
        }),
      });
      const modelSelection={instanceId:'kiro-fixture',model:'default'};
      const runtimePolicy=ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({runtimeMode:'approval-required',interactionMode:'default',cwd:join(scratch,'cwd')});
      const session=yield* adapter.openSession({threadId:'fixture-thread',providerSessionId:'fixture-provider-session',modelSelection,runtimePolicy});
      const providerThread=yield* session.ensureThread({threadId:'fixture-thread',modelSelection,runtimePolicy});
      const now=yield* DateTime.now;
      yield* session.compactThread({
        appThread:{createdBy:'user',creationSource:'web',id:'fixture-thread',projectId:'fixture-project',title:'Fixture',providerInstanceId:'kiro-fixture',modelSelection,runtimeMode:'approval-required',interactionMode:'default',branch:null,worktreePath:null,activeProviderThreadId:providerThread.id,lineage:{parentThreadId:null,relationshipToParent:null,rootThreadId:'fixture-thread'},forkedFrom:null,createdAt:now,updatedAt:now,archivedAt:null,settledOverride:null,settledAt:null,lastVisitedAt:null,deletedAt:null},
        threadId:'fixture-thread',runId:'fixture-run',runOrdinal:1,providerTurnOrdinal:1,attemptId:'fixture-attempt',rootNodeId:'fixture-node',providerThread,
        message:{createdBy:'user',creationSource:'web',messageId:'fixture-message',text:'/compact',attachments:[]},modelSelection,runtimePolicy,
      });
      const events=yield* session.events.pipe(Stream.takeUntil(e=>e.type==='turn.terminal'),Stream.runCollect);
      return [...events].find(e=>e.type==='turn.terminal')?.status;
    }).pipe(Effect.scoped,Effect.provide(layer)));
    console.log(JSON.stringify({advertisedCompact:supports,methods,outcome:exit._tag,status:Exit.isSuccess(exit)?exit.value:undefined}));
    return {exit,methods};
  };
  const control=await run(true);
  if(!Exit.isSuccess(control.exit)||control.exit.value!=='completed'||!control.methods.includes('_kiro/session/compact'))throw new Error('Control failed: '+String(control.exit));
  const failing=await run(false);
  if(!failing.methods.includes('_kiro/session/compact'))code=0;
  else if(Exit.isSuccess(failing.exit)&&failing.exit.value==='failed')code=1;
} catch(error) { console.log('Setup error: '+error.message); }
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

Enable and call optional Kiro methods only if the running initialization result advertises them. The official V3 client migration guide, capability negotiation identifies agentCapabilities._meta.kiro.extensionMethods as authoritative for optional requests and explicitly allows different deployments to return different lists. Its command migration table identifies /compact as an advertised _kiro/session/compact extension, and its troubleshooting guidance directs clients to support deployments with smaller surfaces. The Kiro ACP capability guide also requires optional behavior to follow the running advertisement. These Kiro V3 rules apply while negotiating ACP wire version 1; V3 is not ACP wire v2.

Actual behavior

The production makeKiroAdapterV2 adapter's compactThread path sends _kiro/session/compact after initialization with agentCapabilities._meta.kiro.extensionMethods: []. The deterministic peer returns JSON-RPC -32601 for the unadvertised method; T3 produces a turn.terminal event with status: failed. The same peer advertising the method completes the compaction turn. No real Kiro executable or account was used.

Impact

Minor bug or occasional failure

Impact: P2 - A supported Kiro deployment without the optional compaction extension is offered an action that sends an unavailable method and fails its turn; ordinary chat still works.

The observed scope is a deterministic fake Kiro V3-shaped initialization advertising an empty extension list. NOT_ENOUGH_INFO: no live Kiro deployment's smaller extension catalog or practical compaction workaround was measured; a deployment-specific initialization capture and native compaction check would establish those facts. This is a T3 capability-selection defect, not a claim that Kiro must implement the extension.

Version or commit

T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93; Kiro V3 adapter with ACP wire version 1. NOT_ENOUGH_INFO: no installed Kiro CLI version is involved because the reproduction uses a fake peer, not a real Kiro executable.

Environment

Node v26.12.1, Git 2.56.0, Darwin 27.2.0, arm64, repository-pinned Effect 4.1.3.

Interface: API: makeKiroAdapterV2, openSession, ensureThread, and compactThread; the shared adapter also recognizes the /compact user command.

Earliest failure stage: Integration or transport

Evidence

Relevant output from the recorded run; T3's intervening warning was omitted because the captured method list and terminal status establish the failure:

Node v26.12.1
git version 2.56.0
Revision 59e64eef881632a23f89c4138bcf37f23d92bf93
Effect 4.1.3
{"advertisedCompact":true,"methods":["initialize","session/new","_kiro/session/compact"],"outcome":"Success","status":"completed"}
{"advertisedCompact":false,"methods":["initialize","session/new","_kiro/session/compact"],"outcome":"Success","status":"failed"}
RESULT: defect reproduced

The earlier setup-only attempt printed Package subpath './TestProviderHost' is not defined by "exports" and RESULT: inconclusive; the final script corrects its resolver without modifying production code.

packages/provider-kiro/src/server/adapter.ts#L93-L113 unconditionally sends the compaction method, and packages/provider-kiro/src/server/adapter.ts#L560-L574 always installs that handler. packages/provider-acp/src/server/adapter.ts#L2041-L2044 enables compaction based on handler presence. packages/provider-acp/src/server/adapter.ts#L8544-L8555 calls it for the compaction turn without checking extensionMethods. The fake peer supplies a standard -32601 error only for the unavailable method; the successful control has the identical session setup and model selection.

Expected source: the linked upstream contract. Failure source: the production runtime/adapter run with the supplied fake peer. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete for the fake-peer case. Missing fact: live-deployment prevalence was not measured.

Analysis or suspected cause

Source: the Kiro flavor also installs _session/steer and declares active steering support unconditionally at packages/provider-kiro/src/server/adapter.ts#L74-L80 and packages/provider-kiro/src/server/adapter.ts#L124-L143; packages/provider-acp/src/server/adapter.ts#L8982-L9023 sends it when a turn is active without reading the extension list. This is a second source-only path through the same optional-extension negotiation contract; steering was not exercised by the compaction script.

Restoration check

A peer that advertises the compaction extension should continue to receive it and complete the turn. A peer that omits it should not receive the optional request. The script should then print RESULT: expected behavior and exit 0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions