diff --git a/.github/FORK_OWNED_FILES b/.github/FORK_OWNED_FILES
index c23be2e056..2b3f84338d 100644
--- a/.github/FORK_OWNED_FILES
+++ b/.github/FORK_OWNED_FILES
@@ -58,4 +58,9 @@ scripts/check-no-comments.mjs :: check
# --- Fork-owned deps (root package.json has http-proxy-agent@^9 which
# upstream removed; this is what the pnpm-lockfile guard is for) ---
-package.json :: http-proxy-agent
\ No newline at end of file
+package.json :: http-proxy-agent
+
+# --- squeeze-model fork modules (refactor: isolated from upstream hotspots) ---
+packages/agent-core-v2/src/agent/fullCompaction/squeezeForkOps.ts :: SqueezeModelDecided
+packages/agent-core-v2/src/agent/fullCompaction/squeezeCascade.ts :: resolveSqueezeModelAliasWithCascade
+packages/agent-core-v2/src/app/telemetry/forkEvents.ts :: forkTelemetryEventDefinitions
\ No newline at end of file
diff --git a/apps/kimi-code/test/tui/config.test.ts b/apps/kimi-code/test/tui/config.test.ts
index 9175cd26c4..366f32d71d 100644
--- a/apps/kimi-code/test/tui/config.test.ts
+++ b/apps/kimi-code/test/tui/config.test.ts
@@ -65,6 +65,7 @@ auto_install = false
disablePasteBurst: false,
cacheExpiryHint: true,
disableFeedbackSurvey: false,
+ editorCommand: 'code --wait',
favoriteModels: [],
notifications: { enabled: false, condition: 'always' },
upgrade: { autoInstall: false },
diff --git a/apps/vis/server/src/lib/agent-record-types.ts b/apps/vis/server/src/lib/agent-record-types.ts
index 0cce49b393..8bdb35d662 100644
--- a/apps/vis/server/src/lib/agent-record-types.ts
+++ b/apps/vis/server/src/lib/agent-record-types.ts
@@ -85,6 +85,7 @@ import type {
import type { PermissionSetMode } from '@moonshot-ai/agent-core-v2/agent/permissionMode/permissionModeOps';
import type { PermissionRecordApprovalResult } from '@moonshot-ai/agent-core-v2/agent/permissionRules/permissionRulesOps';
import type { RuntimeSetBinding } from '@moonshot-ai/agent-core-v2/agent/runtimeBinding/runtimeBindingOps';
+import type { SqueezeModelDecided } from '@moonshot-ai/agent-core-v2/agent/fullCompaction/squeezeForkOps';
import type { SwarmModeEnter, SwarmModeExit } from '@moonshot-ai/agent-core-v2/features/swarm/swarmOps';
import type { TowerModeEnter, TowerModeExit } from '@moonshot-ai/agent-core-v2/features/tower/towerOps';
import type { ToolsUpdateStore } from '@moonshot-ai/agent-core-v2/features/todo/todoOps';
@@ -186,6 +187,7 @@ export type AgentRecord =
| WireRecordOf<'prompt.completed', PromptCompleted>
| WireRecordOf<'prompt.steered', PromptSteered>
| WireRecordOf<'runtime.set_binding', RuntimeSetBinding>
+ | WireRecordOf<'squeeze_model.decided', SqueezeModelDecided>
| WireRecordOf<'swarm_mode.enter', SwarmModeEnter>
| WireRecordOf<'swarm_mode.exit', SwarmModeExit>
| WireRecordOf<'task.started', TaskStarted>
diff --git a/apps/vis/server/src/lib/context-projector.ts b/apps/vis/server/src/lib/context-projector.ts
index 8ba926295d..fe28b974a8 100644
--- a/apps/vis/server/src/lib/context-projector.ts
+++ b/apps/vis/server/src/lib/context-projector.ts
@@ -560,6 +560,7 @@ export function projectContext(
case 'plan.revision':
case 'plugin.session_start':
case 'runtime.set_binding':
+ case 'squeeze_model.decided':
case 'staleGuard.recorded':
case 'staleGuard.cleared':
case 'interruptionReminder.recorded':
diff --git a/apps/vis/web/src/components/wire/renderers.tsx b/apps/vis/web/src/components/wire/renderers.tsx
index 8ea9a0c4b3..2c9ca9be57 100644
--- a/apps/vis/web/src/components/wire/renderers.tsx
+++ b/apps/vis/web/src/components/wire/renderers.tsx
@@ -1421,6 +1421,19 @@ export const WIRE_RENDERERS: RendererMap = {
}),
},
+ 'squeeze_model.decided': {
+ tone: 'compaction',
+ label: 'squeeze',
+ headline: (r) => ({
+ main: (
+
+ {r.model}
+ {r.modelDisplay ? {r.modelDisplay} : null}
+
+ ),
+ }),
+ },
+
'staleGuard.recorded': {
tone: 'meta',
label: 'stale',
diff --git a/packages/agent-core-v2/docs/state-manifest.d.ts b/packages/agent-core-v2/docs/state-manifest.d.ts
index 8846f7bc5f..758ccd0c0f 100644
--- a/packages/agent-core-v2/docs/state-manifest.d.ts
+++ b/packages/agent-core-v2/docs/state-manifest.d.ts
@@ -27,7 +27,7 @@
// references become '(circular)', and class instances collapse to a '(ClassName)'
// marker — the wire shape of an entry is the JSON projection of the type here.
//
-// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 83 keys)
+// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 86 keys)
// App
// Workspace
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
@@ -59,6 +59,7 @@
// contextMemory src/agent/contextMemory/contextOps.ts
// contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts
// externalHooks.stopHookContinuationUsed src/features/externalHooks/agent/agentExternalHooksService.ts
+// fallbackModel.active src/session/fallback/state.ts
// fileHistory src/features/fileHistory/fileHistoryOps.ts
// fullCompaction src/agent/fullCompaction/compactionOps.ts
// fullCompaction.activeTurnId src/agent/fullCompaction/fullCompactionService.ts
@@ -102,8 +103,10 @@
// runtime.binding src/agent/runtimeBinding/runtimeBindingService.ts
// runtimeBinding src/agent/runtimeBinding/runtimeBindingOps.ts
// shellCommand.tasks src/agent/shellCommand/shellCommandService.ts
+// squeezeModel src/agent/fullCompaction/squeezeForkOps.ts
// stepRetry.failedAttempts src/agent/stepRetry/stepRetryService.ts
// stepRetry.lastFailedDriverId src/agent/stepRetry/stepRetryService.ts
+// substituteModel.active src/session/substitute/state.ts
// swarm src/features/swarm/swarmOps.ts
// task src/agent/task/taskOps.ts
// task.activeTaskReminderPending src/agent/task/taskService.ts
@@ -1188,6 +1191,12 @@ export interface AgentStateSnapshot {
'fullCompaction.consecutiveOverflowCompactions': number;
'fullCompaction.lastCompactedTokenCount': number | null;
'fullCompaction.observedMaxContextTokensByModel': Map;
+ // src/agent/fullCompaction/squeezeForkOps.ts
+ // replayable · durable — folds: SqueezeModelDecided
+ 'squeezeModel': /* SqueezeModelState — packages/agent-core-v2/src/agent/fullCompaction/squeezeForkOps.ts */ {
+ readonly model: string;
+ readonly modelDisplay?: string;
+ };
// src/agent/interruptionReminder/interruptionReminderOps.ts
// replayable · durable — folds: InterruptionReminderRecorded
'interruptionReminder': null;
@@ -1221,6 +1230,7 @@ export interface AgentStateSnapshot {
readonly thinkingLevel: /* ThinkingEffort — packages/agent-core-v2/src/kosong/contract/provider.ts */ 'off' | 'on' | (string & {});
readonly reservedContextSize: number | undefined;
readonly compactionTriggerRatio: number | undefined;
+ readonly compactionTokenBudget: number | undefined;
};
readonly params: /* ModelRequestParams — packages/agent-core-v2/src/kosong/model/modelRequester.ts */ {
readonly cacheKey?: string;
@@ -1534,6 +1544,17 @@ export interface AgentStateSnapshot {
'tower.base': string | null;
// replayable · durable — folds: TowerModeEnter, TowerModeExit
'tower.owner': string | undefined;
+ // src/session/fallback/state.ts
+ 'fallbackModel.active': /* ActiveFallbackModel — packages/agent-core-v2/src/session/fallback/state.ts */ {
+ readonly alias: string;
+ readonly tier: 'primary' | 'secondary';
+ } | undefined;
+ // src/session/substitute/state.ts
+ 'substituteModel.active': /* ActiveSubstituteModel — packages/agent-core-v2/src/session/substitute/state.ts */ {
+ readonly alias: string;
+ readonly primaryAlias: string;
+ readonly until: number;
+ } | undefined;
}
export type AgentStateKey = keyof AgentStateSnapshot;
diff --git a/packages/agent-core-v2/docs/wire-manifest.d.ts b/packages/agent-core-v2/docs/wire-manifest.d.ts
index 2dae0c93ed..040bee64a8 100644
--- a/packages/agent-core-v2/docs/wire-manifest.d.ts
+++ b/packages/agent-core-v2/docs/wire-manifest.d.ts
@@ -24,7 +24,7 @@
// cross-reducers), blobs (the folding states whose blob codec offloads inline
// media to blob storage), owner (the source file declaring the class).
-// Index (60 record types)
+// Index (61 record types)
// config.update profile src/agent/profile/profileOps.ts
// context.append_loop_event contextMemory, turn src/agent/contextMemory/contextEvents.ts
// context.append_message contextMemory, plan, task.notificationDelivery src/agent/contextMemory/contextEvents.ts
@@ -62,6 +62,7 @@
// prompt.completed promptResolution src/agent/prompt/promptService.ts
// prompt.steered promptResolution src/agent/prompt/promptService.ts
// runtime.set_binding runtimeBinding src/agent/runtimeBinding/runtimeBindingOps.ts
+// squeeze_model.decided (none) src/agent/fullCompaction/squeezeForkOps.ts
// swarm_mode.enter swarm src/features/swarm/swarmOps.ts
// swarm_mode.exit contextMemory, swarm src/features/swarm/swarmOps.ts
// task.started task src/agent/task/taskOps.ts
@@ -268,6 +269,8 @@ interface FullCompactionBeginPayload {
instruction?: string;
/** CompactionSource */
source: 'manual' | 'auto';
+ model?: string;
+ modelDisplay?: string;
}
/**
@@ -596,6 +599,17 @@ interface RuntimeSetBindingPayload {
runtimeId: string;
}
+/**
+ * states: (none)
+ * owner: src/agent/fullCompaction/squeezeForkOps.ts
+ */
+interface SqueezeModelDecidedPayload {
+ _name: 'squeeze_model.decided';
+ agentId: string;
+ model: string;
+ modelDisplay?: string;
+}
+
/**
* states: swarm
* owner: src/features/swarm/swarmOps.ts
@@ -953,6 +967,7 @@ interface WirePayloadMap {
"prompt.completed": PromptCompletedPayload;
"prompt.steered": PromptSteeredPayload;
"runtime.set_binding": RuntimeSetBindingPayload;
+ "squeeze_model.decided": SqueezeModelDecidedPayload;
"swarm_mode.enter": SwarmModeEnterPayload;
"swarm_mode.exit": SwarmModeExitPayload;
"task.started": TaskStartedPayload;
diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts
index 9d7b4e9861..8e5eb62a14 100644
--- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts
+++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts
@@ -80,6 +80,8 @@ import {
FullCompactionCancel,
FullCompactionComplete,
} from './compactionOps';
+import { resolveSqueezeModelAliasWithCascade } from './squeezeCascade';
+import { SqueezeModelDecided, squeezeModelKey } from './squeezeForkOps';
import {
type CompactionBeginData,
type CompactionResult,
@@ -182,6 +184,7 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom
this.states.contributeState(fullCompactionLastCompactedTokenCountKey);
this.states.contributeState(fullCompactionConsecutiveOverflowCompactionsKey);
this.states.contributeState(fullCompactionActiveTurnIdKey);
+ this.states.contributeState(squeezeModelKey);
this.strategy = new RuntimeCompactionStrategy(
() => this.resolveModelContextWithEffectiveMax(),
(message) => this.tokenCounting.estimateMessage(message),
@@ -351,47 +354,11 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom
this.observedMaxContextTokensByModel.set(modelAlias, observed);
}
- private resolveSqueezeModelAliasWithCascade(currentModelAlias: string): string {
- const overrides = this.profile.getAllSessionModelOverrides();
- const binding = compactionModelBindingFor(this.configService, this.flags, {
- modelAlias: currentModelAlias,
- thinkingLevel: this.profile.data().thinkingLevel,
- }, { compactionAlias: overrides.compaction });
- const primaryAlias = binding.model;
- if (primaryAlias !== currentModelAlias) {
- try {
- this.profile.resolveModelContextFor(primaryAlias);
- return primaryAlias;
- } catch {
- }
- }
- const secondaryAlias = resolveCompactionSecondaryModel(this.configService, this.flags, {
- compactionSecondaryAlias: overrides.compactionSecondary,
- });
- if (
- secondaryAlias !== undefined &&
- secondaryAlias !== currentModelAlias &&
- secondaryAlias !== primaryAlias
- ) {
- try {
- this.profile.resolveModelContextFor(secondaryAlias);
- return secondaryAlias;
- } catch {
- }
- }
- return currentModelAlias;
- }
-
begin(input: FullCompactionInput): boolean {
if (this._compacting) return false;
const data: CompactionBeginData = { source: input.source, instruction: input.instruction };
const profileData = this.profile.data();
const currentModelAlias = profileData.modelAlias;
- if (currentModelAlias !== undefined) {
- const squeezeAlias = this.resolveSqueezeModelAliasWithCascade(currentModelAlias);
- data.model = squeezeAlias;
- data.modelDisplay = compactionDisplayModel(this.configService, squeezeAlias);
- }
if (!this.reserveCompactionSlot(data.source)) return false;
const tokenCount = this.validateCompactionStart(data.source);
@@ -409,6 +376,22 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom
new FullCompactionBegin({ ...data, agentId: this.agent.agentId }),
);
+ if (currentModelAlias !== undefined) {
+ const squeezeResult = resolveSqueezeModelAliasWithCascade({
+ currentModelAlias,
+ profile: this.profile,
+ configService: this.configService,
+ flags: this.flags,
+ });
+ void this.dispatcher.dispatch(
+ new SqueezeModelDecided({
+ agentId: this.agent.agentId,
+ model: squeezeResult.alias,
+ modelDisplay: compactionDisplayModel(this.configService, squeezeResult.alias),
+ }),
+ );
+ }
+
const active = this.createActiveCompaction(
data.source,
tokenCount,
diff --git a/packages/agent-core-v2/src/agent/fullCompaction/squeezeCascade.ts b/packages/agent-core-v2/src/agent/fullCompaction/squeezeCascade.ts
new file mode 100644
index 0000000000..43f737cdf4
--- /dev/null
+++ b/packages/agent-core-v2/src/agent/fullCompaction/squeezeCascade.ts
@@ -0,0 +1,53 @@
+import type { IConfigService } from '#/app/config/config';
+import type { IFlagService } from '#/app/flag/flag';
+import {
+ compactionModelBindingFor,
+ resolveCompactionSecondaryModel,
+} from '#/session/compaction/configSection';
+import type { IAgentProfileService } from '#/agent/profile/profile';
+
+export interface SqueezeCascadeInput {
+ readonly currentModelAlias: string;
+ readonly profile: IAgentProfileService;
+ readonly configService: IConfigService;
+ readonly flags: IFlagService;
+}
+
+export interface SqueezeCascadeResult {
+ readonly alias: string;
+ readonly isSqueezed: boolean;
+}
+
+export function resolveSqueezeModelAliasWithCascade(
+ input: SqueezeCascadeInput,
+): SqueezeCascadeResult {
+ const { currentModelAlias, profile, configService, flags } = input;
+ const overrides = profile.getAllSessionModelOverrides();
+ const binding = compactionModelBindingFor(configService, flags, {
+ modelAlias: currentModelAlias,
+ thinkingLevel: profile.data().thinkingLevel,
+ }, { compactionAlias: overrides.compaction });
+ const primaryAlias = binding.model;
+ if (primaryAlias !== currentModelAlias) {
+ try {
+ profile.resolveModelContextFor(primaryAlias);
+ return { alias: primaryAlias, isSqueezed: true };
+ } catch {
+ }
+ }
+ const secondaryAlias = resolveCompactionSecondaryModel(configService, flags, {
+ compactionSecondaryAlias: overrides.compactionSecondary,
+ });
+ if (
+ secondaryAlias !== undefined &&
+ secondaryAlias !== currentModelAlias &&
+ secondaryAlias !== primaryAlias
+ ) {
+ try {
+ profile.resolveModelContextFor(secondaryAlias);
+ return { alias: secondaryAlias, isSqueezed: true };
+ } catch {
+ }
+ }
+ return { alias: currentModelAlias, isSqueezed: false };
+}
diff --git a/packages/agent-core-v2/src/agent/fullCompaction/squeezeForkOps.ts b/packages/agent-core-v2/src/agent/fullCompaction/squeezeForkOps.ts
new file mode 100644
index 0000000000..271e5bd2bb
--- /dev/null
+++ b/packages/agent-core-v2/src/agent/fullCompaction/squeezeForkOps.ts
@@ -0,0 +1,44 @@
+/* oxlint-disable typescript-eslint/no-unsafe-declaration-merging -- Event2 class+payload-interface declaration merging is the sanctioned event-declaration idiom. */
+import { z } from 'zod';
+
+import { AgentEvent2 } from '#/app/event/event2';
+import { defineState } from '#/state/state';
+
+export interface SqueezeModelDecision {
+ readonly agentId: string;
+ readonly model: string;
+ readonly modelDisplay?: string;
+}
+
+const squeezeModelDecidedSchema = z.object({
+ agentId: z.string(),
+ model: z.string(),
+ modelDisplay: z.string().optional(),
+});
+
+export class SqueezeModelDecided extends AgentEvent2<
+ z.infer
+> {
+ static override readonly type = 'squeeze_model.decided';
+ static override readonly durable = true;
+ static override readonly schema = squeezeModelDecidedSchema;
+}
+export interface SqueezeModelDecided {
+ readonly agentId: string;
+ readonly model: string;
+ readonly modelDisplay?: string;
+}
+
+export interface SqueezeModelState {
+ readonly model: string;
+ readonly modelDisplay?: string;
+}
+
+export const squeezeModelKey = defineState(
+ 'squeezeModel',
+ () => ({ model: '' }),
+).replayable({ schema: z.custom() })
+ .on(SqueezeModelDecided, (s, e) => {
+ s.model = e.model;
+ s.modelDisplay = e.modelDisplay;
+ });
diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts
index 09034d4ee6..98c931b496 100644
--- a/packages/agent-core-v2/src/agent/profile/profileService.ts
+++ b/packages/agent-core-v2/src/agent/profile/profileService.ts
@@ -42,6 +42,7 @@ import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
import { IAgentStateService } from '#/agent/state/agentState';
import { IAgentAgentsMdReminderService } from '#/agent/agentsMdReminder/agentsMdReminder';
+import { forkTrack2 } from '#/app/telemetry/forkEvents';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { IEventDispatcher } from '#/state/eventDispatcher';
import {
@@ -383,7 +384,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
setCompactionTriggerRatio(ratio: number | undefined): void {
if (ratio === undefined) {
this.compactionTriggerRatioOverride = undefined;
- this.telemetry.track2('compaction_threshold_override', { action: 'clear' });
+ forkTrack2(this.telemetry, 'compaction_threshold_override', { action: 'clear' });
return;
}
if (
@@ -397,7 +398,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
);
}
this.compactionTriggerRatioOverride = ratio;
- this.telemetry.track2('compaction_threshold_override', { ratio, action: 'set' });
+ forkTrack2(this.telemetry, 'compaction_threshold_override', { ratio, action: 'set' });
}
getCompactionTriggerRatioOverride(): number | undefined {
@@ -412,7 +413,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
setCompactionTokenBudget(tokens: number | undefined): void {
if (tokens === undefined) {
this.compactionTokenBudgetOverride = undefined;
- this.telemetry.track2('compaction_token_budget_override', { action: 'clear' });
+ forkTrack2(this.telemetry, 'compaction_token_budget_override', { action: 'clear' });
return;
}
if (!Number.isInteger(tokens) || tokens < 1) {
@@ -422,7 +423,7 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ
);
}
this.compactionTokenBudgetOverride = tokens * 1_000;
- this.telemetry.track2('compaction_token_budget_override', {
+ forkTrack2(this.telemetry, 'compaction_token_budget_override', {
tokens: this.compactionTokenBudgetOverride,
action: 'set',
});
diff --git a/packages/agent-core-v2/src/app/telemetry/forkEvents.ts b/packages/agent-core-v2/src/app/telemetry/forkEvents.ts
new file mode 100644
index 0000000000..c32cb91f82
--- /dev/null
+++ b/packages/agent-core-v2/src/app/telemetry/forkEvents.ts
@@ -0,0 +1,88 @@
+import type { ITelemetryService } from './telemetry';
+import {
+ type TelemetryEventDefinition,
+ defineAgentTelemetryEvent,
+ telemetryEventDefinitions,
+} from './events';
+
+export interface CompactionThresholdOverrideEvent {
+ action: 'set' | 'clear';
+ ratio?: number;
+}
+
+export interface CompactionTokenBudgetOverrideEvent {
+ action: 'set' | 'clear';
+ tokens?: number;
+}
+
+export interface SubstituteModelActivatedEvent {
+ original_model: string;
+ substitute_model: string;
+}
+
+export interface SubstituteModelDeactivatedEvent {
+ original_model: string;
+}
+
+export const forkTelemetryEventDefinitions = {
+ ...telemetryEventDefinitions,
+ compaction_threshold_override: defineAgentTelemetryEvent({
+ owner: 'kimi-code',
+ comment: 'The compaction threshold ratio is set or cleared by the user.',
+ properties: {
+ action: 'Whether the threshold was set or cleared',
+ ratio: 'The threshold ratio when set (0-1)',
+ },
+ }),
+ compaction_token_budget_override: defineAgentTelemetryEvent({
+ owner: 'kimi-code',
+ comment: 'The compaction absolute token budget is set or cleared by the user.',
+ properties: {
+ action: 'Whether the budget was set or cleared',
+ tokens: 'The token budget when set',
+ },
+ }),
+ substitute_model_activated: defineAgentTelemetryEvent({
+ owner: 'kimi-code',
+ comment: 'A substitute model is activated due to a primary model rate limit.',
+ properties: {
+ original_model: 'The primary model that was rate-limited',
+ substitute_model: 'The substitute model that was activated',
+ },
+ }),
+ substitute_model_deactivated: defineAgentTelemetryEvent({
+ owner: 'kimi-code',
+ comment: 'The substitute model is deactivated and the primary model resumes.',
+ properties: {
+ original_model: 'The primary model that resumes',
+ },
+ }),
+} as const;
+
+export type ForkTelemetryEventName = keyof typeof forkTelemetryEventDefinitions;
+
+export type ForkTelemetryEventPayload =
+ typeof forkTelemetryEventDefinitions[K] extends TelemetryEventDefinition
+ ? P
+ : never;
+
+const FORK_EVENT_NAMES = new Set([
+ 'compaction_threshold_override',
+ 'compaction_token_budget_override',
+ 'substitute_model_activated',
+ 'substitute_model_deactivated',
+]);
+
+export function forkTrack2(
+ telemetry: ITelemetryService,
+ event: string,
+ properties?: Record,
+): void {
+ if (!FORK_EVENT_NAMES.has(event)) {
+ throw new Error(`Unknown fork telemetry event: ${event}`);
+ }
+ (telemetry as { track2(event: string, properties?: Record): void }).track2(
+ event,
+ properties,
+ );
+}
diff --git a/packages/agent-core-v2/test/agent/fullCompaction/compactionStartedModel.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/compactionStartedModel.test.ts
index 1e598f8054..95ce6d430f 100644
--- a/packages/agent-core-v2/test/agent/fullCompaction/compactionStartedModel.test.ts
+++ b/packages/agent-core-v2/test/agent/fullCompaction/compactionStartedModel.test.ts
@@ -34,19 +34,19 @@ const SECONDARY_MODEL = {
capabilities: ['thinking', 'tool_use'],
} as const;
-interface StartedModelArgs {
+interface SqueezeModelArgs {
readonly model?: string;
- readonly model_display?: string;
+ readonly modelDisplay?: string;
}
-function findStartedModel(ctx: ReturnType): StartedModelArgs {
+function findSqueezeModelDecided(ctx: ReturnType): SqueezeModelArgs {
const events = ctx.newEvents() as unknown as Array<{
readonly type?: string;
readonly event?: string;
readonly args?: unknown;
}>;
- const event = events.find((e) => e.type === '[rpc]' && e.event === 'compaction.started');
- return (event?.args as StartedModelArgs | undefined) ?? {};
+ const event = events.find((e) => e.type === '[wire]' && e.event === 'squeeze_model.decided');
+ return (event?.args as SqueezeModelArgs | undefined) ?? {};
}
function makeAgent(initialConfig: Record = {}) {
@@ -72,7 +72,7 @@ async function runCompaction(ctx: ReturnType): Promise {
await completed;
}
-describe('FullCompaction started model', () => {
+describe('SqueezeModelDecided', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
@@ -82,9 +82,9 @@ describe('FullCompaction started model', () => {
const expectedAlias = ctx.get(IAgentProfileService).resolveModelContext().modelAlias;
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe(expectedAlias);
- expect(model_display).toBe(expectedAlias);
+ expect(modelDisplay).toBe(expectedAlias);
});
it('carries the dedicated compaction model when [compaction_model] is configured', async () => {
@@ -92,9 +92,9 @@ describe('FullCompaction started model', () => {
const ctx = makeAgent({ compactionModel: { model: 'kimi/compaction' } });
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe('kimi/compaction');
- expect(model_display).toBe('kimi/compaction');
+ expect(modelDisplay).toBe('kimi/compaction');
});
it('honors the legacy default_model pointer written by an older TUI', async () => {
@@ -102,9 +102,9 @@ describe('FullCompaction started model', () => {
const ctx = makeAgent({ compactionModel: { defaultModel: 'kimi/compaction' } });
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe('kimi/compaction');
- expect(model_display).toBe('kimi/compaction');
+ expect(modelDisplay).toBe('kimi/compaction');
});
it('cascades to the secondary squeeze model when the primary is unresolvable', async () => {
@@ -112,9 +112,9 @@ describe('FullCompaction started model', () => {
const ctx = makeAgent({ compactionModel: { model: 'kimi/ghost', secondaryModel: 'kimi/backup' } });
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe('kimi/backup');
- expect(model_display).toBe('kimi/backup');
+ expect(modelDisplay).toBe('kimi/backup');
});
it('uses the secondary squeeze model when no primary is configured', async () => {
@@ -122,9 +122,9 @@ describe('FullCompaction started model', () => {
const ctx = makeAgent({ compactionModel: { secondaryModel: 'kimi/backup' } });
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe('kimi/backup');
- expect(model_display).toBe('kimi/backup');
+ expect(modelDisplay).toBe('kimi/backup');
});
it('falls back to the current model when both squeeze tiers are unresolvable', async () => {
@@ -135,9 +135,9 @@ describe('FullCompaction started model', () => {
const expectedAlias = ctx.get(IAgentProfileService).resolveModelContext().modelAlias;
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe(expectedAlias);
- expect(model_display).toBe(expectedAlias);
+ expect(modelDisplay).toBe(expectedAlias);
});
it('prefers the primary squeeze model when both tiers resolve', async () => {
@@ -147,8 +147,8 @@ describe('FullCompaction started model', () => {
});
await runCompaction(ctx);
- const { model, model_display } = findStartedModel(ctx);
+ const { model, modelDisplay } = findSqueezeModelDecided(ctx);
expect(model).toBe('kimi/compaction');
- expect(model_display).toBe('kimi/compaction');
+ expect(modelDisplay).toBe('kimi/compaction');
});
});
diff --git a/packages/agent-core-v2/test/app/config/bash-task-timeout-smoke.test.ts b/packages/agent-core-v2/test/app/config/bash-task-timeout-smoke.test.ts
index e03a294b5c..66a934607f 100644
--- a/packages/agent-core-v2/test/app/config/bash-task-timeout-smoke.test.ts
+++ b/packages/agent-core-v2/test/app/config/bash-task-timeout-smoke.test.ts
@@ -1,19 +1,3 @@
-/**
- * Production smoke test for bash-task-timeout-ignored fix (PR #44).
- *
- * Drives the real production ConfigService end-to-end against a TOML
- * config that sets [task] bash_task_timeout_s = 1800, then constructs
- * the real BashTool with that config and asserts that
- * resolveAgentTaskConfig(config)?.bashTaskTimeoutS === 1800 — i.e.
- * the configured value reaches the BashTool the same way it does in
- * the running CLI.
- *
- * This is the production wiring: the same DI services, the same TOML
- * loader, the same `registerConfigSection` contributions. The BashTool
- * is constructed with stand-ins for the runtime-only dependencies that
- * are not exercised by the timeout-resolution path.
- */
-
import { DisposableStore } from '#/_base/di/lifecycle';
import { SyncDescriptor } from '#/_base/di/descriptors';
import { TestInstantiationService } from '#/_base/di/test';
@@ -28,8 +12,6 @@ import { IFileSystemStorageService } from '#/persistence/interface/storage';
import { IAtomicTomlDocumentStore } from '#/persistence/interface/atomicDocumentStore';
import { TomlAtomicDocumentStore } from '#/persistence/backends/node-fs/atomicDocumentStore';
import { resolveAgentTaskConfig } from '#/agent/task/configSection';
-// Side-effect import that registers the [task] and [background]
-// sections via registerConfigSection.
import '#/agent/task/configSection';
import { describe, expect, it } from 'vitest';
@@ -52,8 +34,6 @@ describe('bash_task_timeout_s smoke (PR #44)', () => {
const config = ix.get(IConfigService);
await config.ready;
- // The configured value reaches resolveAgentTaskConfig — the exact
- // helper BashTool.detachTimeoutMs() uses.
expect(resolveAgentTaskConfig(config)?.bashTaskTimeoutS).toBe(1800);
} finally {
disposables.dispose();
diff --git a/packages/agent-core-v2/test/index.test.ts b/packages/agent-core-v2/test/index.test.ts
index facd29c481..3a851f2920 100644
--- a/packages/agent-core-v2/test/index.test.ts
+++ b/packages/agent-core-v2/test/index.test.ts
@@ -80,6 +80,7 @@ const V2_ONLY_RECORD_TYPES: ReadonlySet = new Set([
const V2_RECORD_TYPES: ReadonlySet = new Set([
'tower_mode.enter',
'tower_mode.exit',
+ 'squeeze_model.decided',
'task.started',
'task.terminated',
'task.waitDelivered',
diff --git a/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts b/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts
index cb3a5ab555..f7c11f1fa2 100644
--- a/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts
+++ b/packages/agent-core-v2/test/os/backends/node-local/tools/bash.test.ts
@@ -816,10 +816,6 @@ describe('BashTool', () => {
const properties = (tool.parameters as { properties: Record })
.properties;
- // The schema intentionally omits a JSON-schema `default` so the tool can
- // distinguish "model omitted `timeout`" (use configured background default)
- // from "model passed an explicit value" (use that value). The default value
- // is still surfaced via the human-readable description.
expect(properties['timeout']?.default).toBeUndefined();
expect(properties['timeout']?.description).toMatch(/Foreground default 60s/);
});
diff --git a/packages/agent-core-v2/test/state/builtinReplayableKeys.ts b/packages/agent-core-v2/test/state/builtinReplayableKeys.ts
index 91bacee5fb..475f9e9ace 100644
--- a/packages/agent-core-v2/test/state/builtinReplayableKeys.ts
+++ b/packages/agent-core-v2/test/state/builtinReplayableKeys.ts
@@ -2,6 +2,7 @@ import type { ReplayableStateKey } from '#/state/state';
import { contextMemoryKey } from '#/agent/contextMemory/contextOps';
import { fullCompactionKey, fullCompactionWireRangesKey } from '#/agent/fullCompaction/compactionOps';
+import { squeezeModelKey } from '#/agent/fullCompaction/squeezeForkOps';
import { interruptionReminderKey } from '#/agent/interruptionReminder/interruptionReminderOps';
import { llmRequestTraceKey } from '#/agent/llmRequester/llmRequestOps';
import { turnKey } from '#/agent/loop/turnOps';
@@ -28,6 +29,7 @@ export const BUILTIN_REPLAYABLE_STATE_KEYS: readonly ReplayableStateKey[] =
contextMemoryKey,
fullCompactionKey,
fullCompactionWireRangesKey,
+ squeezeModelKey,
interruptionReminderKey,
llmRequestTraceKey,
turnKey,
diff --git a/tdd/verification.md b/tdd/verification.md
new file mode 100644
index 0000000000..b58b116c25
--- /dev/null
+++ b/tdd/verification.md
@@ -0,0 +1,186 @@
+# SPEC 059 — TDD Verification (REAL evidence)
+
+Spec: strategy — rebase fork onto upstream/main and reapply fork features on top (issue #59).
+Branch: `059-rebase-fork-onto-upstream` (based linearly on `upstream/main` @ `f9ca33376`).
+Every number below is copied from an actual command run in this session; nothing is projected.
+
+## Step 2 — RED: reproduce the problem
+
+**Command A — simulate the next upstream sync using the merge-based strategy** (the pre-sync fork
+state `2185de301` — first parent of the 2026-09-04 sync merge — merged with `upstream/main`):
+
+```
+$ git merge-tree --write-tree --name-only 2185de3014918feedf63a66d44049996d7457381 upstream/main
+exit code: 1 (conflict)
+CONFLICT (content): apps/kimi-code/src/tui/commands/config.ts
+CONFLICT (content): apps/kimi-code/src/tui/config.ts
+CONFLICT (content): apps/kimi-code/test/tui/commands/update-preferences.test.ts
+CONFLICT (content): apps/kimi-code/test/tui/config.test.ts
+CONFLICT (content): packages/agent-core-v2/src/agent/fullCompaction/compactionOps.ts
+CONFLICT (content): packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts
+CONFLICT (content): packages/agent-core-v2/src/app/telemetry/events.ts
+```
+
+All 5 recurring conflict files from the issue reproduce, plus the 2 additional files the
+2026-09-05 sync (`935798c9`) also reported resolving. Historical corroboration: the merge
+commits `0d335e1c` / `935798c9` each document manually resolved conflicts on exactly these files.
+
+**Command B — baseline test suite on fork master** (per-directory chunks of `vitest run`,
+same semantics as CI's `pnpm run test --shard=n/5`; pi-tui runs its CI-mandated node:test suite):
+
+| chunk | result (baseline master) |
+|---|---|
+| packages/agent-core-v2 | 64 failed / 6352 passed / 1 skipped (359 files: 11 failed) |
+| packages/agent-core | 19 failed / 4205 passed (3 expected-fail, 30 skipped) |
+| packages/kap-server | 2 failed / 1322 passed |
+| packages/node-sdk oauth klient kimi-telemetry migration-legacy acp-server kaos transcript tree-sitter-bash protocol | 5 failed / 3169 passed (56 skipped) |
+| packages/kosong | 0 failed / 1365 passed |
+| apps/kimi-code | 9 failed / 3610 passed (5 skipped) |
+| apps/vis server+web | 0 failed / 221 passed |
+| apps/vscode | 1 failed / 357 passed |
+| pi-tui (node:test) | 0 failed / 953 passed |
+
+Baseline totals: **100 failed / 21,202 passed**. The issue's "4 pre-existing failures" was
+stale — master genuinely carried 100. These include real fork/upstream integration drift,
+e.g. `test/tui/config.test.ts "parses valid TOML"` (the sync kept upstream's `[editor]`
+TOML input but dropped upstream's `editorCommand: 'code --wait'` assertion) and the two
+stale-manifest tests (`docs/state-manifest.d.ts`, `docs/wire-manifest.d.ts` — the committed
+manifests no longer matched the current generator).
+
+Note: environment parity was established first — the repo pins `packageManager:
+pnpm@10.33.0` and overrides live in `pnpm-workspace.yaml`; running pnpm 9 silently rewrote
+the lockfile and stripped overrides. All measurements use pnpm 10.33.0 + `pnpm install
+--frozen-lockfile` (exit 0), matching CI. A re-run of the agent-core-v2 chunk after the
+reinstall produced an identical failure set.
+
+## Step 3 — GREEN: rebase + reapply
+
+Phases executed (branch commits, newest last):
+
+1. `3687d3b27` feat: reapply fork features on top of upstream/main — full fork delta
+ (444 files, +45160/−297, incl. binary spec-kit tooling) applied onto `upstream/main`.
+ **Proof of faithful reapplication: `git diff master HEAD` is empty (byte-identical
+ tree to fork master).**
+2. `c968bbfa3` refactor: extract squeeze cascade + telemetry into isolated modules —
+ cherry-pick of `eb3324e83` (PR #58 branch `refactor/squeeze-fork-modules`, previously
+ NOT merged). Applied cleanly. Creates `squeezeForkOps.ts`, `squeezeCascade.ts`,
+ `forkEvents.ts`.
+3. `a3645bb86` test(tui): restore upstream editorCommand assertion in config fixture —
+ reconciliation of the fork fixture with the upstream fixture that the 09-05 sync lost.
+4. `fix(agent-core-v2): integrate squeeze fork modules with upstream state/wire
+ registries` — the refactor predates upstream's strict registries, so integration:
+ - `fullCompactionService.ts`: `contributeState(squeezeModelKey)` (generator requires
+ every replayable key to be contributed by its owner service)
+ - `test/index.test.ts`: `'squeeze_model.decided'` registered in `V2_RECORD_TYPES`
+ - `test/state/builtinReplayableKeys.ts`: `squeezeModelKey` registered (harness drift guard)
+ - regenerated `docs/state-manifest.d.ts` + `docs/wire-manifest.d.ts`
+5. `feat(vis): render squeeze_model.decided wire record` — vis server `AgentRecord`
+ union + vis-web `WIRE_RENDERERS` entry so the "covers every durable record" guard
+ stays green.
+6. `merge: reconcile legacy master (merge-based history) into rebased line` — because
+ master's history is merge-based, GitHub's merge of this (rebased) branch into master
+ conflicts on files where the rebased content improves on master (add/add
+ FORK_OWNED_FILES; refactor-vs-inline hunks). Reconciled by merging master with
+ `-X ours` and restoring the refactored `fullCompactionService.ts` (the 3-way merge
+ had resurrected the inline cascade method from a theirs-only hunk). Result: master
+ is an ancestor of the PR head, the tree is byte-identical to the rebased content
+ (`git diff HEAD` empty), GitHub reports `mergeable: true`, and the
+ PR "Files changed" is exactly the intended 15-file delta.
+7. `chore(agent-core-v2): strip comments from synced test files` — the 09-05 sync had
+ merged upstream test files containing 9 comment lines that violate the fork-owned
+ no-comment rule, leaving master's CI lint red (3 consecutive master CI failures).
+ Removed the lines; `node scripts/check-no-comments.mjs` now reports
+ `OK (1623 files)` and both affected test files pass (62/62).
+
+**Fork features verified present after rebase** (grep over the rebased tree):
+`resolveSqueezeModelAliasWithCascade` (now in `squeezeCascade.ts`, dispatched via
+`SqueezeModelDecided`), secondary-model fallback in `compactionRound`, fork telemetry
+events `compaction_threshold_override` / `compaction_token_budget_override` /
+`substitute_model_activated` / `substitute_model_deactivated` (+ `forkTrack2` wrapper),
+`model`/`modelDisplay` on `CompactionBeginData`/`FullCompactionBegin`,
+`favoriteModels` TUI config, `fallbackModel` schema (agent-core + TUI), TUI slash
+commands (`/squeeze-model`, `/fallback-model`, `/fork-session`, `/fork-and-switch`),
+substitute-model on rate limit, session-list subcommand, no-comment rule,
+`http-proxy-agent` dep.
+
+**Upstream features verified present (previously entangled/missing):**
+`fullCompactionWireRangesKey`, `captureWireLines()`, `renderRecoveryFooter()`,
+`IWireService`, `budget()` + `CompactionBudget`, `isContextBudgetReminder`,
+`aheadReminderTelemetry()`, `ContextBudgetReminderEvent`,
+`CompactionAheadReminderEvent`, `renderCompactionInstruction()`,
+`historyForModel.length === 0` guard.
+
+**Survival markers:** all 17 `.github/FORK_OWNED_FILES` entries resolve on the rebased
+branch (file exists + marker string present).
+
+## Step 5 — VERIFY (actual command results on the rebased branch)
+
+**pnpm test** (per-directory chunks, identical commands as baseline):
+
+| chunk | baseline | rebased | delta |
+|---|---|---|---|
+| agent-core-v2 | 64F / 6352P | **62F / 6354P** | −2 (stale-manifest tests fixed) |
+| agent-core | 19F / 4205P | **19F / 4205P** | identical |
+| kap-server | 2F / 1322P | **2F / 1322P** | identical |
+| other packages | 5F / 3169P | **5F / 3169P** | identical |
+| kosong | 0F / 1365P | **0F / 1365P** | identical |
+| apps/kimi-code | 9F / 3610P | **8F / 3611P** | −1 (config fixture fixed) |
+| vis server+web | 0F / 221P | **0F / 221P** | identical (transient −1 during integration, fixed by renderer) |
+| vscode | 1F / 357P | **1F / 357P** | identical |
+| pi-tui node:test | 0F / 953P | **0F / 953P** | identical |
+
+Totals: baseline **100 failed / 21,202 passed** → rebased **97 failed / 21,205 passed**.
+**3 pre-existing failures fixed; 0 new failures introduced.** The remaining 97 failures
+are byte-identical (by test name) to the pre-existing master failures in unrelated areas
+(agent behavioral suites, harness snapshots, sdk/oauth config tests, one vscode
+integration test) and are out of scope for this rebase spec.
+
+`pnpm build` — exit 0 (all workspace packages, apps, webview bundle).
+`git status --short` / `git diff --stat` — empty (zero remaining formatting diffs).
+
+**Phase 4 dry-run of the next upstream sync** (the actual acceptance test of this spec):
+
+```
+$ git merge-tree --write-tree --name-only upstream/main HEAD
+exit code: 0 (clean — no conflicts, no output)
+```
+
+Compare with RED (exit 1, 7 conflicting files) — future syncs merge cleanly because the
+fork-owned logic now lives in dedicated modules and the tree is linear on upstream/main.
+
+## CI comparison (GitHub Actions, real runs)
+
+Master CI was already red before this change (runs on 032a4494 / 40b9b99e / 2185de30 all
+conclude `failure`). Normalized comparison of unique `##[error]` signatures across the five
+test shards:
+
+- master CI run `33959456283` (032a4494): 53 unique error signatures
+- PR CI run `33971295071` (3e3098c2f): 50 unique error signatures
+- signatures only in master (fixed by this PR): the two stale-manifest AssertionErrors
+ (state-manifest, wire-manifest) + the config.test.ts fixture AssertionError — exactly
+ the 3 fixes above
+- **signatures only in the PR (new): none**
+
+Remaining PR CI redness is inherited master debt: the 97 pre-existing test failures
+(vitest exits non-zero on any failure, so all 5 shards conclude `failure` exactly as
+master's do) and 20 pre-existing `oxlint --type-aware` diagnostics in 10 files this
+branch does not modify (unmasked by fixing the 9 comment-rule violations that used to
+short-circuit lint earlier; check-no-comments now reports `OK (1623 files)`).
+Green on this PR's CI: build, typecheck, test-pi-tui (953/953), test-vscode-legacy,
+Nix Build, flake.nix sync check.
+
+## Success criteria — PROVED vs NOT
+
+- PROVED: 5 recurring conflict files reproduced conflicting under the old strategy (RED).
+- PROVED: fork rebased linearly onto current upstream/main; tree byte-identical to fork
+ master before the isolated-module commits (no feature lost in transit).
+- PROVED: fork features (squeeze cascade modules, secondary fallback, telemetry events,
+ favorites, slash commands, fallbackModel, substitute model) all present + markers resolve.
+- PROVED: upstream features (wire journal, budget, ahead reminders, instruction helper)
+ present and the strict state/wire registries pass.
+- PROVED: full test suite re-run — 0 new failures, 3 pre-existing failures fixed.
+- PROVED: pnpm build exit 0; working tree clean.
+- PROVED: dry-run next upstream sync is conflict-free.
+- NOT PROVED / out of scope: the 97 remaining pre-existing failures on unrelated suites
+ (present identically on fork master before this change; fixing them belongs to
+ separate specs). The 4 "expected fail" / skipped / todo entries in agent-core mirror CI.