@ai-agent-forge/agent-forge 0.85.6 → 0.85.8
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/CHANGELOG.md +20 -0
- package/dist/bundle/.build-manifest.json +12 -12
- package/dist/bundle/chunks/{chunk-7YVQMWO5.js → chunk-BTBDSCF4.js} +5 -5
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/plugin-command.d.ts.map +1 -1
- package/dist/cli/plugin-command.js +58 -0
- package/dist/cli/plugin-command.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +6 -0
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +5 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/plugins/ecosystem/plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/plugin-bootstrap.js +2 -1
- package/dist/plugins/ecosystem/plugin-bootstrap.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +27 -3
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/compaction.md +2 -0
- package/package.json +1 -1
|
@@ -67,6 +67,14 @@ export async function handlePluginEcosystemCommand(args, options = {}) {
|
|
|
67
67
|
if (args[0] !== "plugin")
|
|
68
68
|
return false;
|
|
69
69
|
const write = options.write ?? ((line) => console.log(line));
|
|
70
|
+
// Batch update convenience (0.85.7 user seat finding: a factory range bump
|
|
71
|
+
// left seven plugins outdated and the per-plugin hint meant seven manual
|
|
72
|
+
// commands). The ecosystem CLI protocol is unchanged — this outer layer
|
|
73
|
+
// composes `state list` + one `update` per active plugin.
|
|
74
|
+
if (args[1] === "update" && args[2] === "--all") {
|
|
75
|
+
await runBatchUpdate(args.slice(3), options, write);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
70
78
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
71
79
|
const cwd = options.cwd ?? process.cwd();
|
|
72
80
|
const market = options.registry === undefined && options.installer === undefined
|
|
@@ -82,4 +90,54 @@ export async function handlePluginEcosystemCommand(args, options = {}) {
|
|
|
82
90
|
await market?.close();
|
|
83
91
|
}
|
|
84
92
|
}
|
|
93
|
+
async function runBatchUpdate(restArgs, options, write) {
|
|
94
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
95
|
+
const cwd = options.cwd ?? process.cwd();
|
|
96
|
+
const scopeIndex = restArgs.indexOf("--scope");
|
|
97
|
+
const scope = scopeIndex !== -1 && restArgs[scopeIndex + 1] === "project" ? "project" : "global";
|
|
98
|
+
const market = options.registry === undefined && options.installer === undefined
|
|
99
|
+
? await createDefaultPluginMarketWiringV1({ agentDir, cwd })
|
|
100
|
+
: undefined;
|
|
101
|
+
try {
|
|
102
|
+
const cli = buildMarketCli({ agentDir, cwd, market, overrides: options });
|
|
103
|
+
const listResponse = await cli.execute({ argv: ["state", "list", "--scope", scope] });
|
|
104
|
+
if (listResponse.status !== "succeeded") {
|
|
105
|
+
printResponse(listResponse, write);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const entries = (listResponse.data?.entries ?? []).filter((entry) => entry.lifecycle === "active" && entry.scope === scope);
|
|
109
|
+
if (entries.length === 0) {
|
|
110
|
+
write("No active plugins to update.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const updated = [];
|
|
114
|
+
const current = [];
|
|
115
|
+
const failed = [];
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const response = await cli.execute({ argv: ["update", entry.pluginId, "--scope", scope] });
|
|
118
|
+
if (response.status === "succeeded") {
|
|
119
|
+
updated.push(entry.pluginId);
|
|
120
|
+
write(response.text);
|
|
121
|
+
}
|
|
122
|
+
else if (response.error?.code === "already_installed") {
|
|
123
|
+
current.push(entry.pluginId);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
failed.push(entry.pluginId);
|
|
127
|
+
write(response.text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const summary = [
|
|
131
|
+
`updated ${updated.length}${updated.length > 0 ? ` (${updated.join(", ")})` : ""}`,
|
|
132
|
+
`already up to date ${current.length}${current.length > 0 ? ` (${current.join(", ")})` : ""}`,
|
|
133
|
+
`failed ${failed.length}${failed.length > 0 ? ` (${failed.join(", ")})` : ""}`,
|
|
134
|
+
].join("; ");
|
|
135
|
+
write(`plugin update --all [${scope}]: ${summary}`);
|
|
136
|
+
if (failed.length > 0)
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await market?.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
85
143
|
//# sourceMappingURL=plugin-command.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-command.js","sourceRoot":"","sources":["../../src/cli/plugin-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.ts\";\nimport { defaultPluginHostFactsV1, projectPluginScopeRefV1 } from \"../plugins/ecosystem/default-active-bindings.ts\";\nimport {\n\tcreateDefaultPluginMarketWiringV1,\n\ttype DefaultPluginMarketWiringV1,\n} from \"../plugins/ecosystem/default-plugin-market.ts\";\nimport {\n\tcreatePluginEcosystemCliV1,\n\tcreatePluginSelectionStoreV1,\n\tcreatePluginStateStoreV1,\n\ttype PluginDistributionV1,\n\ttype PluginEcosystemCliResponseV1,\n\ttype PluginEcosystemCliV1,\n\ttype PluginEcosystemRegistryV1,\n\ttype PluginInstallerV1,\n\ttype PluginSelectionStoreV1,\n\ttype PluginStateStoreV1,\n} from \"../plugins/ecosystem/index.ts\";\nimport { listBuiltinPluginManifestsV1 } from \"../profile/plugin-profiles.ts\";\nimport type { PluginManagerControllerV1 } from \"./plugin-manager-controller.ts\";\nimport { createPluginManagerControllerV1 } from \"./plugin-manager-controller.ts\";\n\nexport interface PluginCommandOptionsV1 {\n\treadonly agentDir?: string;\n\t/** Working directory used for the project-local selection store. */\n\treadonly cwd?: string;\n\treadonly host?: {\n\t\treadonly os: string;\n\t\treadonly arch: string;\n\t\treadonly runtime: readonly { readonly name: string; readonly version: string }[];\n\t\treadonly apiVersion: string;\n\t\treadonly hostFeatures: readonly string[];\n\t};\n\t/** Optional host-owned boundaries used by install/update/remove commands. */\n\treadonly registry?: PluginEcosystemRegistryV1;\n\treadonly installer?: PluginInstallerV1;\n\t/** Optional host-owned package download boundary used by install/update. */\n\treadonly distribution?: PluginDistributionV1;\n\t/** Override persistence in tests or an embedding host; defaults remain agentDir files. */\n\treadonly stateStore?: PluginStateStoreV1;\n\t/** Override the project-local persistence boundary in tests or an embedding host. */\n\treadonly projectStateStore?: PluginStateStoreV1;\n\treadonly selectionStore?: PluginSelectionStoreV1;\n\treadonly projectSelectionStore?: PluginSelectionStoreV1;\n\treadonly write?: (line: string) => void;\n}\n\nfunction printResponse(response: PluginEcosystemCliResponseV1, write: (line: string) => void): void {\n\twrite(response.text);\n\tif (response.status !== \"succeeded\") process.exitCode = 1;\n}\n\ninterface MarketCliOptionsV1 {\n\treadonly agentDir: string;\n\treadonly cwd: string;\n\treadonly market?: DefaultPluginMarketWiringV1;\n\treadonly overrides?: PluginCommandOptionsV1;\n}\n\nfunction buildMarketCli(options: MarketCliOptionsV1): PluginEcosystemCliV1 {\n\tconst { market, overrides } = options;\n\treturn createPluginEcosystemCliV1({\n\t\thost: overrides?.host ?? defaultPluginHostFactsV1(),\n\t\tmanifests: listBuiltinPluginManifestsV1(),\n\t\tregistry: overrides?.registry ?? market?.cliRegistry,\n\t\t// Multi-source acquisition (`plugin install <ref>`) rides along with the\n\t\t// default market wiring; a fully host-injected stack replaces it wholesale.\n\t\t...(market?.acquisition === undefined ? {} : { acquisition: market.acquisition }),\n\t\tinstaller: overrides?.installer ?? market?.installer,\n\t\t...(overrides?.distribution !== undefined || market?.distribution !== undefined\n\t\t\t? { distribution: overrides?.distribution ?? market!.distribution }\n\t\t\t: {}),\n\t\tstateStore:\n\t\t\toverrides?.stateStore ??\n\t\t\tmarket?.stateStore ??\n\t\t\tcreatePluginStateStoreV1({\n\t\t\t\tfilePath: join(options.agentDir, \"plugin-state.json\"),\n\t\t\t}),\n\t\tprojectStateStore:\n\t\t\toverrides?.projectStateStore ??\n\t\t\tmarket?.projectStateStore ??\n\t\t\tcreatePluginStateStoreV1({\n\t\t\t\tfilePath: join(options.cwd, CONFIG_DIR_NAME, \"plugin-state.json\"),\n\t\t\t}),\n\t\tprojectScopeRef: projectPluginScopeRefV1(options.cwd),\n\t\tselectionStore:\n\t\t\toverrides?.selectionStore ??\n\t\t\tcreatePluginSelectionStoreV1({ filePath: join(options.agentDir, \".plugin-selection.json\") }),\n\t\tprojectSelectionStore:\n\t\t\toverrides?.projectSelectionStore ??\n\t\t\tcreatePluginSelectionStoreV1({ filePath: join(options.cwd, CONFIG_DIR_NAME, \".plugin-selection.json\") }),\n\t});\n}\n\n/**\n * Default TUI manager controller over the default marketplace wiring. The\n * controller reads the global or project state store per scope; callers must\n * invoke `close` after the config screen closes.\n */\nexport async function createDefaultPluginManagerControllerV1(options: {\n\treadonly agentDir?: string;\n\treadonly cwd?: string;\n\treadonly scope: \"global\" | \"project\";\n}): Promise<{ readonly controller: PluginManagerControllerV1; readonly close: () => Promise<void> }> {\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst market = await createDefaultPluginMarketWiringV1({ agentDir, cwd });\n\tconst cli = buildMarketCli({ agentDir, cwd, market });\n\tconst controller = createPluginManagerControllerV1({\n\t\tregistry: market.registry,\n\t\tstateStore: options.scope === \"project\" ? market.projectStateStore : market.stateStore,\n\t\tcli,\n\t});\n\treturn { controller, close: market.close };\n}\n\n/**\n * Handle the independent ecosystem command namespace before legacy package\n * commands. Install/update/remove default to the disk install store plus an\n * optional configured marketplace origin; host injection of registry and\n * installer still wins entirely.\n */\nexport async function handlePluginEcosystemCommand(\n\targs: readonly string[],\n\toptions: PluginCommandOptionsV1 = {},\n): Promise<boolean> {\n\tif (args[0] !== \"plugin\") return false;\n\tconst write = options.write ?? ((line: string) => console.log(line));\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst market =\n\t\toptions.registry === undefined && options.installer === undefined\n\t\t\t? await createDefaultPluginMarketWiringV1({ agentDir, cwd })\n\t\t\t: undefined;\n\ttry {\n\t\tconst cli = buildMarketCli({ agentDir, cwd, market, overrides: options });\n\t\tconst response = await cli.execute({ argv: args.slice(1) });\n\t\tprintResponse(response, write);\n\t\treturn true;\n\t} finally {\n\t\tawait market?.close();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"plugin-command.js","sourceRoot":"","sources":["../../src/cli/plugin-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.ts\";\nimport { defaultPluginHostFactsV1, projectPluginScopeRefV1 } from \"../plugins/ecosystem/default-active-bindings.ts\";\nimport {\n\tcreateDefaultPluginMarketWiringV1,\n\ttype DefaultPluginMarketWiringV1,\n} from \"../plugins/ecosystem/default-plugin-market.ts\";\nimport {\n\tcreatePluginEcosystemCliV1,\n\tcreatePluginSelectionStoreV1,\n\tcreatePluginStateStoreV1,\n\ttype PluginDistributionV1,\n\ttype PluginEcosystemCliResponseV1,\n\ttype PluginEcosystemCliV1,\n\ttype PluginEcosystemRegistryV1,\n\ttype PluginInstallerV1,\n\ttype PluginSelectionStoreV1,\n\ttype PluginStateStoreV1,\n} from \"../plugins/ecosystem/index.ts\";\nimport { listBuiltinPluginManifestsV1 } from \"../profile/plugin-profiles.ts\";\nimport type { PluginManagerControllerV1 } from \"./plugin-manager-controller.ts\";\nimport { createPluginManagerControllerV1 } from \"./plugin-manager-controller.ts\";\n\nexport interface PluginCommandOptionsV1 {\n\treadonly agentDir?: string;\n\t/** Working directory used for the project-local selection store. */\n\treadonly cwd?: string;\n\treadonly host?: {\n\t\treadonly os: string;\n\t\treadonly arch: string;\n\t\treadonly runtime: readonly { readonly name: string; readonly version: string }[];\n\t\treadonly apiVersion: string;\n\t\treadonly hostFeatures: readonly string[];\n\t};\n\t/** Optional host-owned boundaries used by install/update/remove commands. */\n\treadonly registry?: PluginEcosystemRegistryV1;\n\treadonly installer?: PluginInstallerV1;\n\t/** Optional host-owned package download boundary used by install/update. */\n\treadonly distribution?: PluginDistributionV1;\n\t/** Override persistence in tests or an embedding host; defaults remain agentDir files. */\n\treadonly stateStore?: PluginStateStoreV1;\n\t/** Override the project-local persistence boundary in tests or an embedding host. */\n\treadonly projectStateStore?: PluginStateStoreV1;\n\treadonly selectionStore?: PluginSelectionStoreV1;\n\treadonly projectSelectionStore?: PluginSelectionStoreV1;\n\treadonly write?: (line: string) => void;\n}\n\nfunction printResponse(response: PluginEcosystemCliResponseV1, write: (line: string) => void): void {\n\twrite(response.text);\n\tif (response.status !== \"succeeded\") process.exitCode = 1;\n}\n\ninterface MarketCliOptionsV1 {\n\treadonly agentDir: string;\n\treadonly cwd: string;\n\treadonly market?: DefaultPluginMarketWiringV1;\n\treadonly overrides?: PluginCommandOptionsV1;\n}\n\nfunction buildMarketCli(options: MarketCliOptionsV1): PluginEcosystemCliV1 {\n\tconst { market, overrides } = options;\n\treturn createPluginEcosystemCliV1({\n\t\thost: overrides?.host ?? defaultPluginHostFactsV1(),\n\t\tmanifests: listBuiltinPluginManifestsV1(),\n\t\tregistry: overrides?.registry ?? market?.cliRegistry,\n\t\t// Multi-source acquisition (`plugin install <ref>`) rides along with the\n\t\t// default market wiring; a fully host-injected stack replaces it wholesale.\n\t\t...(market?.acquisition === undefined ? {} : { acquisition: market.acquisition }),\n\t\tinstaller: overrides?.installer ?? market?.installer,\n\t\t...(overrides?.distribution !== undefined || market?.distribution !== undefined\n\t\t\t? { distribution: overrides?.distribution ?? market!.distribution }\n\t\t\t: {}),\n\t\tstateStore:\n\t\t\toverrides?.stateStore ??\n\t\t\tmarket?.stateStore ??\n\t\t\tcreatePluginStateStoreV1({\n\t\t\t\tfilePath: join(options.agentDir, \"plugin-state.json\"),\n\t\t\t}),\n\t\tprojectStateStore:\n\t\t\toverrides?.projectStateStore ??\n\t\t\tmarket?.projectStateStore ??\n\t\t\tcreatePluginStateStoreV1({\n\t\t\t\tfilePath: join(options.cwd, CONFIG_DIR_NAME, \"plugin-state.json\"),\n\t\t\t}),\n\t\tprojectScopeRef: projectPluginScopeRefV1(options.cwd),\n\t\tselectionStore:\n\t\t\toverrides?.selectionStore ??\n\t\t\tcreatePluginSelectionStoreV1({ filePath: join(options.agentDir, \".plugin-selection.json\") }),\n\t\tprojectSelectionStore:\n\t\t\toverrides?.projectSelectionStore ??\n\t\t\tcreatePluginSelectionStoreV1({ filePath: join(options.cwd, CONFIG_DIR_NAME, \".plugin-selection.json\") }),\n\t});\n}\n\n/**\n * Default TUI manager controller over the default marketplace wiring. The\n * controller reads the global or project state store per scope; callers must\n * invoke `close` after the config screen closes.\n */\nexport async function createDefaultPluginManagerControllerV1(options: {\n\treadonly agentDir?: string;\n\treadonly cwd?: string;\n\treadonly scope: \"global\" | \"project\";\n}): Promise<{ readonly controller: PluginManagerControllerV1; readonly close: () => Promise<void> }> {\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst market = await createDefaultPluginMarketWiringV1({ agentDir, cwd });\n\tconst cli = buildMarketCli({ agentDir, cwd, market });\n\tconst controller = createPluginManagerControllerV1({\n\t\tregistry: market.registry,\n\t\tstateStore: options.scope === \"project\" ? market.projectStateStore : market.stateStore,\n\t\tcli,\n\t});\n\treturn { controller, close: market.close };\n}\n\n/**\n * Handle the independent ecosystem command namespace before legacy package\n * commands. Install/update/remove default to the disk install store plus an\n * optional configured marketplace origin; host injection of registry and\n * installer still wins entirely.\n */\nexport async function handlePluginEcosystemCommand(\n\targs: readonly string[],\n\toptions: PluginCommandOptionsV1 = {},\n): Promise<boolean> {\n\tif (args[0] !== \"plugin\") return false;\n\tconst write = options.write ?? ((line: string) => console.log(line));\n\t// Batch update convenience (0.85.7 user seat finding: a factory range bump\n\t// left seven plugins outdated and the per-plugin hint meant seven manual\n\t// commands). The ecosystem CLI protocol is unchanged — this outer layer\n\t// composes `state list` + one `update` per active plugin.\n\tif (args[1] === \"update\" && args[2] === \"--all\") {\n\t\tawait runBatchUpdate(args.slice(3), options, write);\n\t\treturn true;\n\t}\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst market =\n\t\toptions.registry === undefined && options.installer === undefined\n\t\t\t? await createDefaultPluginMarketWiringV1({ agentDir, cwd })\n\t\t\t: undefined;\n\ttry {\n\t\tconst cli = buildMarketCli({ agentDir, cwd, market, overrides: options });\n\t\tconst response = await cli.execute({ argv: args.slice(1) });\n\t\tprintResponse(response, write);\n\t\treturn true;\n\t} finally {\n\t\tawait market?.close();\n\t}\n}\n\ninterface BatchUpdateEntry {\n\treadonly pluginId: string;\n\treadonly scope: string;\n\treadonly lifecycle: string;\n}\n\nasync function runBatchUpdate(\n\trestArgs: readonly string[],\n\toptions: PluginCommandOptionsV1,\n\twrite: (line: string) => void,\n): Promise<void> {\n\tconst agentDir = options.agentDir ?? getAgentDir();\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst scopeIndex = restArgs.indexOf(\"--scope\");\n\tconst scope = scopeIndex !== -1 && restArgs[scopeIndex + 1] === \"project\" ? \"project\" : \"global\";\n\tconst market =\n\t\toptions.registry === undefined && options.installer === undefined\n\t\t\t? await createDefaultPluginMarketWiringV1({ agentDir, cwd })\n\t\t\t: undefined;\n\ttry {\n\t\tconst cli = buildMarketCli({ agentDir, cwd, market, overrides: options });\n\t\tconst listResponse = await cli.execute({ argv: [\"state\", \"list\", \"--scope\", scope] });\n\t\tif (listResponse.status !== \"succeeded\") {\n\t\t\tprintResponse(listResponse, write);\n\t\t\treturn;\n\t\t}\n\t\tconst entries = ((listResponse.data?.entries ?? []) as readonly BatchUpdateEntry[]).filter(\n\t\t\t(entry) => entry.lifecycle === \"active\" && entry.scope === scope,\n\t\t);\n\t\tif (entries.length === 0) {\n\t\t\twrite(\"No active plugins to update.\");\n\t\t\treturn;\n\t\t}\n\t\tconst updated: string[] = [];\n\t\tconst current: string[] = [];\n\t\tconst failed: string[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tconst response = await cli.execute({ argv: [\"update\", entry.pluginId, \"--scope\", scope] });\n\t\t\tif (response.status === \"succeeded\") {\n\t\t\t\tupdated.push(entry.pluginId);\n\t\t\t\twrite(response.text);\n\t\t\t} else if (response.error?.code === \"already_installed\") {\n\t\t\t\tcurrent.push(entry.pluginId);\n\t\t\t} else {\n\t\t\t\tfailed.push(entry.pluginId);\n\t\t\t\twrite(response.text);\n\t\t\t}\n\t\t}\n\t\tconst summary = [\n\t\t\t`updated ${updated.length}${updated.length > 0 ? ` (${updated.join(\", \")})` : \"\"}`,\n\t\t\t`already up to date ${current.length}${current.length > 0 ? ` (${current.join(\", \")})` : \"\"}`,\n\t\t\t`failed ${failed.length}${failed.length > 0 ? ` (${failed.join(\", \")})` : \"\"}`,\n\t\t].join(\"; \");\n\t\twrite(`plugin update --all [${scope}]: ${summary}`);\n\t\tif (failed.length > 0) process.exitCode = 1;\n\t} finally {\n\t\tawait market?.close();\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compaction engine and branch summarization for AgentSession.\n *\n * Extracted from `agent-session.ts`. Every function here operates on the\n * internal session facade ({@link AgentSessionInternals}); `AgentSession`\n * keeps same-name private delegates so call sites, dynamic dispatch, and test\n * spies keep working unchanged. Method-to-method calls go through the session\n * parameter (not direct function calls) to preserve that dispatch.\n */\n\nimport type { AgentContext, AgentMessage } from \"@agent-forge/agent-core\";\nimport { contentText, retryAssistantCall, uuidv7 } from \"@agent-forge/ai\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tAuthResult,\n\tContext,\n\tModel,\n\tProviderHeaders,\n\tRetryCallbacks,\n\tRetryPolicy,\n\tSimpleStreamOptions,\n\tUsage,\n} from \"@agent-forge/ai/compat\";\nimport { completeSimple, streamSimple } from \"@agent-forge/ai/compat\";\nimport type { SummaryAttemptRecordV1 } from \"../host/summary-retry.ts\";\nimport {\n\ttype AgentSessionInternals,\n\tdetachedSessionSnapshot,\n\tSessionCancelledError,\n} from \"./agent-session-internal.ts\";\nimport { dispatchCompactionDecisionLifecycle, dispatchCompactionFailedLifecycle } from \"./agent-session-lifecycle.ts\";\nimport { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport {\n\ttype CompactionPreparation,\n\ttype CompactionResult,\n\tcalculateContextTokens,\n\tcollectEntriesForBranchSummary,\n\testimateContextTokens,\n\testimateTokens,\n} from \"./compaction/index.ts\";\nimport type { ExecutionFactsV1 } from \"./extensions/index.ts\";\nimport {\n\tDEFAULT_COMPACTION_POLICY_NAME,\n\tSESSION_COMPACTION_COMMIT_EVENT_ID,\n\tSESSION_COMPACTION_COMMIT_EVENT_VERSION,\n\ttype SessionCompactionCommitEventDataV1,\n} from \"./extensions/index.ts\";\nimport type { SummaryModelFactsV1, SummaryModelInvocationV1 } from \"./extensions/public-api.ts\";\nimport { SUMMARY_MODEL_INVOCATION_ERROR_CODES } from \"./extensions/public-api.ts\";\nimport type { BranchSummaryEntry, CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\nimport { getLatestCompactionEntry } from \"./session-manager.ts\";\nimport type { CompactionSettings } from \"./settings-manager.ts\";\n\nfunction withoutDeletedHeaders(headers: ProviderHeaders | undefined): Record<string, string> | undefined {\n\treturn headers\n\t\t? Object.fromEntries(Object.entries(headers).filter((entry): entry is [string, string] => entry[1] !== null))\n\t\t: undefined;\n}\n\nfunction estimateMessagesTokens(messages: AgentMessage[]): number {\n\tlet tokens = 0;\n\tfor (const message of messages) {\n\t\ttokens += estimateTokens(message);\n\t}\n\treturn tokens;\n}\n\nexport async function getRequiredRequestAuth(\n\tsession: AgentSessionInternals,\n\tmodel: Model<Api>,\n): Promise<{\n\tmodel: Model<Api>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}> {\n\tlet result: AuthResult | undefined;\n\ttry {\n\t\tresult = await session._modelRuntime.getAuth(model);\n\t} catch (error) {\n\t\tconst cause = error instanceof Error ? error.cause : undefined;\n\t\tif (cause instanceof Error && cause.message === \"authHeader requires a resolved API key\") {\n\t\t\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n\t\t}\n\t\tthrow error;\n\t}\n\tif (result && (result.auth.apiKey || result.auth.headers)) {\n\t\tconst requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;\n\t\treturn {\n\t\t\tmodel: requestModel,\n\t\t\tapiKey: result.auth.apiKey,\n\t\t\theaders: withoutDeletedHeaders(result.auth.headers),\n\t\t\tenv: result.env,\n\t\t};\n\t}\n\n\tconst isOAuth = session._modelRuntime.isUsingOAuth(model.provider);\n\tif (isOAuth) {\n\t\tthrow new Error(\n\t\t\t`Authentication failed for \"${model.provider}\". ` +\n\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t`Run '/login ${model.provider}' to re-authenticate.`,\n\t\t);\n\t}\n\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n}\n\nexport async function getSummarizationRequestAuth(\n\tsession: AgentSessionInternals,\n\tmodel: Model<Api>,\n): Promise<{\n\tmodel: Model<Api>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}> {\n\tif (session.agent.streamFunction === streamSimple) {\n\t\treturn getRequiredRequestAuth(session, model);\n\t}\n\n\ttry {\n\t\tconst result = await session._modelRuntime.getAuth(model);\n\t\tif (!result) return { model };\n\t\tconst requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;\n\t\treturn {\n\t\t\tmodel: requestModel,\n\t\t\tapiKey: result.auth.apiKey,\n\t\t\theaders: withoutDeletedHeaders(result.auth.headers),\n\t\t\tenv: result.env,\n\t\t};\n\t} catch {\n\t\treturn { model };\n\t}\n}\n\n/** Provider-agnostic facts extracted from the auth-resolved summary model. */\nfunction summaryModelFacts(model: Model<Api>): SummaryModelFactsV1 {\n\treturn {\n\t\tid: model.id,\n\t\tcontextWindow: model.contextWindow,\n\t\tmaxTokens: model.maxTokens,\n\t\treasoning: model.reasoning,\n\t};\n}\n\n/**\n * Build the host-owned summary invocation boundary (V1) for one summary run.\n *\n * The closure captures the auth resolved once per run (model with its\n * auth-level baseUrl override, API key, headers, provider env), the retry\n * budget, and the retry callbacks that mirror retries into session events.\n * Transport selection lazily observes `session.agent.streamFunction` at each\n * invoke so request behavior (timeouts, retries, attribution headers) stays\n * identical to agent turns; without one the host falls back to\n * `completeSimple`. Every completion is cache-free (`cacheRetention: \"none\"`)\n * and routed through `routingSessionId` when the caller supplies one; callers\n * without a session (branch summaries) get a fresh routing id per call.\n */\nexport function createSummaryModelInvocationV1(\n\tsession: AgentSessionInternals,\n\tauth: {\n\t\tmodel: Model<Api>;\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t\tenv?: Record<string, string>;\n\t},\n\tretry: RetryPolicy,\n\tcallbacks?: RetryCallbacks,\n\toptions: { routingSessionId?: string } = {},\n): SummaryModelInvocationV1 {\n\treturn {\n\t\tinvoke: async (input, signal) => {\n\t\t\t// Host-owned output cap: never exceed the summary model's limit.\n\t\t\t// Models without a positive cap stay unclamped.\n\t\t\tconst maxTokens =\n\t\t\t\tinput.maxTokens === undefined || auth.model.maxTokens <= 0\n\t\t\t\t\t? input.maxTokens\n\t\t\t\t\t: Math.min(input.maxTokens, auth.model.maxTokens);\n\t\t\tconst requestOptions: SimpleStreamOptions = {\n\t\t\t\t...(maxTokens === undefined ? {} : { maxTokens }),\n\t\t\t\tsignal,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\theaders: auth.headers,\n\t\t\t\tenv: auth.env,\n\t\t\t\t// One-off summaries must never write the prompt cache.\n\t\t\t\tcacheRetention: \"none\",\n\t\t\t\tsessionId: options.routingSessionId ?? uuidv7(),\n\t\t\t};\n\t\t\tif (auth.model.reasoning && input.thinkingLevel && input.thinkingLevel !== \"off\") {\n\t\t\t\trequestOptions.reasoning = input.thinkingLevel;\n\t\t\t}\n\t\t\tconst context: Context = {\n\t\t\t\tsystemPrompt: input.systemPrompt,\n\t\t\t\tmessages: [{ role: \"user\", content: [{ type: \"text\", text: input.prompt }], timestamp: Date.now() }],\n\t\t\t};\n\t\t\tconst produce = async (): Promise<AssistantMessage> => {\n\t\t\t\tconst streamFn = session.agent.streamFunction;\n\t\t\t\treturn streamFn\n\t\t\t\t\t? (await streamFn(auth.model, context, requestOptions)).result()\n\t\t\t\t\t: completeSimple(auth.model, context, requestOptions);\n\t\t\t};\n\t\t\tlet response: AssistantMessage;\n\t\t\ttry {\n\t\t\t\tresponse = await retryAssistantCall(produce, retry, requestOptions.signal, callbacks);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof Error && error.name === \"AbortError\") {\n\t\t\t\t\treturn { version: 1, status: \"aborted\" };\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.transportError,\n\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (response.stopReason === \"aborted\") {\n\t\t\t\treturn { version: 1, status: \"aborted\" };\n\t\t\t}\n\t\t\tif (response.stopReason === \"error\") {\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.providerError,\n\t\t\t\t\t\tmessage: response.errorMessage || \"Unknown error\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (response.stopReason === \"length\") {\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.lengthCap,\n\t\t\t\t\t\tmessage: \"generation hit the token cap and the summary is incomplete\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (response.content.some((block) => block.type === \"toolCall\")) {\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.toolCall,\n\t\t\t\t\t\tmessage: \"attempted to call a tool\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tversion: 1,\n\t\t\t\tstatus: \"completed\",\n\t\t\t\ttext: contentText(response.content),\n\t\t\t\tusage: response.usage,\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport async function compactBeforeNextAssistantResponse(\n\tsession: AgentSessionInternals,\n\tcontext: AgentContext,\n\tsignal?: AbortSignal,\n): Promise<AgentContext> {\n\tconst model = session.model;\n\tconst settings = session.settingsManager.getCompactionSettings();\n\n\tif (!model || model.contextWindow <= 0) {\n\t\treturn context;\n\t}\n\tconst contextTokens = estimateContextTokens(context.messages).tokens;\n\tconst decision = await decideCompaction(session, {\n\t\treason: \"threshold\",\n\t\tcontextOverflow: false,\n\t\trecoverableLength: false,\n\t\tcontextUsage: {\n\t\t\tcontextTokens,\n\t\t\tcontextWindow: model.contextWindow,\n\t\t\treserveTokens: settings.reserveTokens,\n\t\t},\n\t\tcanContinue: false,\n\t\tsignal: signal ?? session._policyDecisionAbortController?.signal,\n\t});\n\tawait dispatchCompactionDecisionLifecycle(session, {\n\t\treason: \"threshold\",\n\t\tcompact: decision.compact,\n\t\t// The pre-check never retries the turn: the runAutoCompaction call below\n\t\t// passes a fixed false, so the policy's retry flag is not the fact here.\n\t\twillRetry: false,\n\t\tcontextTokens,\n\t\tcontextWindow: model.contextWindow,\n\t\treserveTokens: settings.reserveTokens,\n\t\t...(decision.failureMessage === undefined ? {} : { failureMessage: decision.failureMessage }),\n\t});\n\tif (!decision.compact) return context;\n\n\tawait session._runAutoCompaction(\"threshold\", false);\n\treturn {\n\t\t...context,\n\t\tmessages: session.agent.state.messages.slice(),\n\t};\n}\n\nexport function executionFacts(\n\tsession: AgentSessionInternals,\n\tphase: ExecutionFactsV1[\"phase\"],\n\toperationId?: string,\n\trevision?: ExecutionFactsV1[\"inputRevision\"],\n): ExecutionFactsV1 {\n\tconst inputRevision = detachedSessionSnapshot(revision ?? session.sessionManager.getRevision());\n\treturn Object.freeze({\n\t\tversion: 1 as const,\n\t\toperationId:\n\t\t\toperationId ??\n\t\t\t`${phase}:${inputRevision.sessionId}:${inputRevision.sequence}:${inputRevision.leafId ?? \"root\"}`,\n\t\tsessionId: session.sessionId,\n\t\tinputRevision,\n\t\tphase,\n\t});\n}\n\nexport async function runCompactionSummary(\n\tsession: AgentSessionInternals,\n\tpreparation: CompactionPreparation,\n\tauth: {\n\t\tmodel: Model<Api>;\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t\tenv?: Record<string, string>;\n\t},\n\tcustomInstructions: string | undefined,\n\tsignal: AbortSignal,\n\treason: \"manual\" | \"threshold\" | \"overflow\",\n\texecution: ExecutionFactsV1,\n): Promise<CompactionResult> {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\tconst result = await runtime.summarizeCompaction(session._compactionPolicyName, {\n\t\tpreparation,\n\t\tmodel: summaryModelFacts(auth.model),\n\t\tinvocation: createSummaryModelInvocationV1(\n\t\t\tsession,\n\t\t\tauth,\n\t\t\tsummarizationRetryPolicy(session, execution.operationId, execution.inputRevision, signal),\n\t\t\tsummarizationRetryCallbacks(session, { source: \"compaction\", reason }),\n\t\t\t{ routingSessionId: session.sessionId },\n\t\t),\n\t\tcustomInstructions,\n\t\tsignal,\n\t\treason,\n\t\tthinkingLevel: session.thinkingLevel,\n\t\tsessionId: session.sessionId,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"compaction\" },\n\t\texecution,\n\t});\n\t// 包内收窄:invocation 结果按 provider 原样透传 usage;压缩 usage 合并需要内部 Usage 形状。\n\treturn { ...result, usage: result.usage as Usage | undefined };\n}\n\nexport async function decideCompaction(\n\tsession: AgentSessionInternals,\n\tinput: {\n\t\treason: \"overflow\" | \"threshold\";\n\t\tcontextOverflow: boolean;\n\t\trecoverableLength: boolean;\n\t\tcontextUsage?: { contextTokens: number; contextWindow: number; reserveTokens: number };\n\t\tcanContinue: boolean;\n\t\t/** 便宜缓解的事实输入(threshold):分支快照与近期保护预算,仅策略消费。 */\n\t\tbranchEntries?: readonly SessionEntry[];\n\t\tkeepRecentTokens?: number;\n\t\texecution?: ExecutionFactsV1;\n\t\tsignal?: AbortSignal;\n\t},\n) {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\t// 内核机制兜底:默认策略名且无人注册(默认插件被禁用或失败隔离)时,机制层\n\t// 不做压缩决策(无执行者),回合继续;缺失经 warn 日志与手动 /compact 的显式\n\t// 错误暴露。默认插件不得成为隐藏前提(宪法 §2/§5)。显式配置的其他策略名缺\n\t// 失仍然显式失败——配置错误必须可见(宪法 §7)。\n\tif (\n\t\tsession._compactionPolicyName === DEFAULT_COMPACTION_POLICY_NAME &&\n\t\t!runtime.hasCompactionPolicy(session._compactionPolicyName)\n\t) {\n\t\tsession._hostLogger?.warn(\"compaction\", \"no compaction policy registered; skipping automatic compaction\", {\n\t\t\tfields: { policyName: session._compactionPolicyName, reason: input.reason },\n\t\t});\n\t\treturn { compact: false, retry: false };\n\t}\n\treturn runtime.decideCompaction(session._compactionPolicyName, {\n\t\tenabled: session.settingsManager.getCompactionSettings().enabled,\n\t\trecoveryAttempted: session._overflowRecoveryAttempted,\n\t\t...input,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"compaction\" },\n\t\texecution: input.execution ?? executionFacts(session, \"compaction\"),\n\t\tsignal: input.signal,\n\t});\n}\n\nexport async function prepareCompaction(\n\tsession: AgentSessionInternals,\n\tpathEntries: SessionEntry[],\n\tsettings: CompactionSettings,\n\texecution?: ExecutionFactsV1,\n\tsignal?: AbortSignal,\n): Promise<CompactionPreparation | undefined> {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\tconst preparation = await runtime.prepareCompaction(session._compactionPolicyName, {\n\t\tentries: pathEntries,\n\t\tsettings,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"compaction\" },\n\t\texecution: execution ?? executionFacts(session, \"compaction\"),\n\t\tsignal,\n\t});\n\tif (preparation === undefined) return undefined;\n\tconst value = preparation as Record<string, unknown>;\n\tif (\n\t\t!preparation ||\n\t\ttypeof preparation !== \"object\" ||\n\t\ttypeof value.firstKeptEntryId !== \"string\" ||\n\t\t!Array.isArray(value.messagesToSummarize) ||\n\t\t!Array.isArray(value.turnPrefixMessages) ||\n\t\ttypeof value.isSplitTurn !== \"boolean\" ||\n\t\t!Number.isFinite(value.tokensBefore) ||\n\t\ttypeof value.fileOps !== \"object\" ||\n\t\ttypeof value.settings !== \"object\"\n\t) {\n\t\tthrow new Error(`Compaction policy returned an invalid preparation: ${session._compactionPolicyName}`);\n\t}\n\treturn preparation as CompactionPreparation;\n}\n\n/** Capture the branch facts used by one optimistic compaction commit. */\nexport function captureCompactionCommitBoundary(\n\tsession: AgentSessionInternals,\n\treason: \"manual\" | \"threshold\" | \"overflow\",\n): {\n\tpathEntries: SessionEntry[];\n\texpectedRevision: ReturnType<SessionManager[\"getRevision\"]>;\n\toperationId: string;\n} {\n\tconst pathEntries = session.sessionManager.getBranch();\n\tconst expectedRevision = session.sessionManager.getRevision();\n\treturn {\n\t\tpathEntries,\n\t\texpectedRevision,\n\t\toperationId: `compaction:${reason}:${expectedRevision.sessionId}:${expectedRevision.sequence}:${expectedRevision.leafId ?? \"root\"}`,\n\t};\n}\n\n/** Queue compaction work so canonical commit and runtime projection stay ordered. */\nexport function enqueueCompaction<T>(session: AgentSessionInternals, operation: () => Promise<T>): Promise<T> {\n\tconst previous = session._compactionQueue;\n\tlet release!: () => void;\n\tsession._compactionQueue = new Promise<void>((resolve) => {\n\t\trelease = resolve;\n\t});\n\tconst queued = previous\n\t\t.then(() => {\n\t\t\tif (session._reloadInProgress) {\n\t\t\t\tthrow new Error(\"Session reload in progress; compaction was not started\");\n\t\t\t}\n\t\t\treturn operation();\n\t\t})\n\t\t.finally(release);\n\tsession._activeCompactionPromise = queued;\n\tvoid queued.then(\n\t\t() => {\n\t\t\tif (session._activeCompactionPromise === queued) session._activeCompactionPromise = undefined;\n\t\t},\n\t\t() => {\n\t\t\tif (session._activeCompactionPromise === queued) session._activeCompactionPromise = undefined;\n\t\t},\n\t);\n\treturn queued;\n}\n\n/**\n * Persist an optimistic compaction result before projecting its context.\n *\n * A revision conflict is a failed compaction, not a signal to recompute or\n * append against newer session history. A replay is a successful prior\n * commit and still requires the caller to apply the runtime projection.\n */\nexport function commitCompaction(\n\tsession: AgentSessionInternals,\n\tinput: {\n\t\texpectedRevision: ReturnType<SessionManager[\"getRevision\"]>;\n\t\toperationId: string;\n\t\tsummary: string;\n\t\tfirstKeptEntryId: string;\n\t\ttokensBefore: number;\n\t\tdetails: unknown;\n\t\tusage: Usage | undefined;\n\t},\n): {\n\tentry: CompactionEntry;\n\tstatus: \"committed\" | \"replayed\";\n\trevision: ReturnType<SessionManager[\"getRevision\"]>;\n} {\n\tconst commit = session.sessionManager.commitCompaction({\n\t\tversion: 1,\n\t\toperationId: input.operationId,\n\t\texpectedRevision: input.expectedRevision,\n\t\tsummary: input.summary,\n\t\tfirstKeptEntryId: input.firstKeptEntryId,\n\t\ttokensBefore: input.tokensBefore,\n\t\tdetails: input.details,\n\t\tfromHook: false,\n\t\tusage: input.usage,\n\t});\n\tif (commit.status === \"revision_conflict\") {\n\t\tthrow new Error(\n\t\t\t`Compaction commit revision conflict: expected ${input.expectedRevision.sequence}/${input.expectedRevision.leafId ?? \"root\"}, current ${commit.currentRevision.sequence}/${commit.currentRevision.leafId ?? \"root\"}`,\n\t\t);\n\t}\n\n\tconst entry = session.sessionManager.getEntry(commit.entryId);\n\tif (entry?.type !== \"compaction\" || entry.operationId !== input.operationId) {\n\t\tthrow new Error(`Compaction commit entry is unavailable: ${commit.entryId}`);\n\t}\n\treturn { entry, status: commit.status, revision: commit.revision };\n}\n\n/** Deliver committed compaction facts without making delivery part of the commit. */\nexport async function dispatchCompactionCommitLifecycle(\n\tsession: AgentSessionInternals,\n\treason: \"manual\" | \"threshold\" | \"overflow\",\n\tcommit: {\n\t\tentry: CompactionEntry;\n\t\tstatus: \"committed\" | \"replayed\";\n\t\trevision: ReturnType<SessionManager[\"getRevision\"]>;\n\t},\n): Promise<void> {\n\ttry {\n\t\tconst runtime = session._capabilityRuntime;\n\t\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\t\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\t\tconst entry = commit.entry;\n\t\tconst data: SessionCompactionCommitEventDataV1 = {\n\t\t\tversion: 1,\n\t\t\tstatus: commit.status,\n\t\t\tentryId: entry.id,\n\t\t\toperationId: entry.operationId ?? \"\",\n\t\t\trevision: commit.revision,\n\t\t\treason,\n\t\t\tsummary: entry.summary,\n\t\t\tfirstKeptEntryId: entry.firstKeptEntryId,\n\t\t\ttokensBefore: entry.tokensBefore,\n\t\t\tfromHook: entry.fromHook === true,\n\t\t\t...(entry.usage === undefined ? {} : { usage: entry.usage }),\n\t\t\t...(entry.details === undefined ? {} : { details: entry.details }),\n\t\t};\n\t\tconst delivery = await runtime.dispatchLifecycle({\n\t\t\ttype: SESSION_COMPACTION_COMMIT_EVENT_ID,\n\t\t\tversion: SESSION_COMPACTION_COMMIT_EVENT_VERSION,\n\t\t\tphase: \"committed\",\n\t\t\tdata,\n\t\t\toperationId: entry.operationId,\n\t\t\tsource: \"agent-session\",\n\t\t});\n\t\tfor (const failure of delivery.report.failures) {\n\t\t\tsession._observeLifecycleDeliveryError(\n\t\t\t\tfailure.error,\n\t\t\t\tfailure.pluginId,\n\t\t\t\t`${SESSION_COMPACTION_COMMIT_EVENT_ID}@${SESSION_COMPACTION_COMMIT_EVENT_VERSION}/${failure.kind}`,\n\t\t\t);\n\t\t}\n\t} catch (error) {\n\t\tconst report = (\n\t\t\terror as { report?: { failures?: readonly { error: unknown; pluginId: string; kind: string }[] } }\n\t\t).report;\n\t\t// v8 ignore next -- phase 常量合法且未传 signal,dispatchLifecycle 仅在 transform/execute 被禁止注册时拒绝,report 形态拒绝不可达\n\t\tif (report?.failures) {\n\t\t\tfor (const failure of report.failures) {\n\t\t\t\tsession._observeLifecycleDeliveryError(\n\t\t\t\t\tfailure.error,\n\t\t\t\t\tfailure.pluginId,\n\t\t\t\t\t`${SESSION_COMPACTION_COMMIT_EVENT_ID}@${SESSION_COMPACTION_COMMIT_EVENT_VERSION}/${failure.kind}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} else {\n\t\t\tsession._observeLifecycleDeliveryError(\n\t\t\t\terror,\n\t\t\t\t\"<capability-lifecycle>\",\n\t\t\t\t`${SESSION_COMPACTION_COMMIT_EVENT_ID}@${SESSION_COMPACTION_COMMIT_EVENT_VERSION}`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport async function decideOverflow(\n\tsession: AgentSessionInternals,\n\tmessage: AssistantMessage,\n\texecution?: ExecutionFactsV1,\n\tsignal?: AbortSignal,\n) {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\treturn runtime.decideOverflow(session._overflowPolicyName, {\n\t\tmessage,\n\t\tcontextWindow: session.model?.contextWindow,\n\t\tdesiredMaxOutput: session.model?.maxTokens ?? 0,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"overflow\" },\n\t\texecution: execution ?? executionFacts(session, \"overflow\"),\n\t\tsignal,\n\t});\n}\n\n/**\n * Clear the manual compaction controller and wake any `waitForIdle()` caller.\n * Mirrors the upstream `_clearManualCompactionState` helper: every manual\n * compaction exit path must drop the controller before listeners observe\n * `compaction_end`, so `isIdle` is already true when they run.\n */\nfunction clearManualCompactionState(session: AgentSessionInternals): void {\n\tsession._compactionAbortController = undefined;\n\tsession._resolveIdleWaitIfIdle();\n}\n\n/** Structured cancellation outcome for manual compaction (mirrors navigateTree's shape). */\nexport interface CompactionCancelledResult {\n\treadonly cancelled: true;\n\treadonly aborted: true;\n}\n\nexport async function compactInternal(\n\tsession: AgentSessionInternals,\n\tcustomInstructions?: string,\n): Promise<CompactionResult | CompactionCancelledResult> {\n\tawait session._abortInternal();\n\t// Reload can begin while abort() is waiting for the active agent turn. Do\n\t// not create a fresh controller or begin summary work after that barrier.\n\t// Nothing started, so there is nothing to notify: resolve cancelled.\n\tif (session._reloadInProgress) {\n\t\treturn { cancelled: true, aborted: true };\n\t}\n\tsession._compactionAbortController = new AbortController();\n\tsession._emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!session.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\tconst auth = await getSummarizationRequestAuth(session, session.model);\n\n\t\tconst { pathEntries, expectedRevision, operationId } = captureCompactionCommitBoundary(session, \"manual\");\n\t\tconst settings = session.settingsManager.getCompactionSettings();\n\n\t\tconst execution = executionFacts(session, \"compaction\", operationId);\n\t\tconst preparation = await prepareCompaction(\n\t\t\tsession,\n\t\t\tpathEntries,\n\t\t\tsettings,\n\t\t\texecution,\n\t\t\tsession._compactionAbortController.signal,\n\t\t);\n\t\tif (!preparation) {\n\t\t\t// Check why we can't compact\n\t\t\tconst lastEntry = pathEntries[pathEntries.length - 1];\n\t\t\tif (lastEntry?.type === \"compaction\") {\n\t\t\t\tthrow new Error(\"Already compacted\");\n\t\t\t}\n\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t}\n\n\t\t// Shared default summary generator, also used by automatic compaction.\n\t\tconst { summary, firstKeptEntryId, tokensBefore, usage, details } = await runCompactionSummary(\n\t\t\tsession,\n\t\t\tpreparation,\n\t\t\tauth,\n\t\t\tcustomInstructions,\n\t\t\tsession._compactionAbortController.signal,\n\t\t\t\"manual\",\n\t\t\texecution,\n\t\t);\n\n\t\tif (session._compactionAbortController.signal.aborted || session._reloadInProgress) {\n\t\t\tthrow new SessionCancelledError(\"signal\");\n\t\t}\n\n\t\tconst compactionCommit = commitCompaction(session, {\n\t\t\texpectedRevision,\n\t\t\toperationId,\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId,\n\t\t\ttokensBefore,\n\t\t\tdetails,\n\t\t\tusage,\n\t\t});\n\t\tawait dispatchCompactionCommitLifecycle(session, \"manual\", compactionCommit);\n\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\tawait session._applyContextTransition(sessionContext.messages, \"compaction\");\n\t\t// Context strategies may replace the runtime projection without rewriting\n\t\t// canonical session history. Report the immediately post-compaction runtime\n\t\t// projection, rather than the pre-projection session reconstruction.\n\t\tconst estimatedTokensAfter = estimateMessagesTokens(session.agent.state.messages);\n\n\t\tconst compactionResult: CompactionResult = {\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId,\n\t\t\ttokensBefore,\n\t\t\testimatedTokensAfter,\n\t\t\tusage,\n\t\t\tdetails,\n\t\t};\n\t\t// compaction_end listeners may submit queued prompts, so expose idle state before notifying them.\n\t\tclearManualCompactionState(session);\n\t\tsession._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: compactionResult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn compactionResult;\n\t} catch (error) {\n\t\tconst cancelled =\n\t\t\terror instanceof SessionCancelledError || (error instanceof Error && error.name === \"AbortError\");\n\t\tif (cancelled) {\n\t\t\t// Cancellation is a normal outcome: notify listeners and resolve with\n\t\t\t// the structured cancelled result instead of rejecting (matches\n\t\t\t// navigateTree).\n\t\t\tclearManualCompactionState(session);\n\t\t\tsession._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: true,\n\t\t\t\twillRetry: false,\n\t\t\t});\n\t\t\treturn { cancelled: true, aborted: true };\n\t\t}\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst errorMessage = `Compaction failed: ${message}`;\n\t\tclearManualCompactionState(session);\n\t\tsession._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t\terrorMessage,\n\t\t});\n\t\t// 压缩失败打点(会话轨迹第 2 期,设计 §5.2):lifecycle 事件之外的一行\n\t\t// 进程日志,缺 hostLogger 时为 no-op;取消路径不打点。\n\t\tsession._hostLogger?.error(\"compaction\", \"compaction failed\", { fields: { reason: \"manual\" }, cause: error });\n\t\tawait dispatchCompactionFailedLifecycle(session, { reason: \"manual\", errorMessage });\n\t\tthrow error;\n\t} finally {\n\t\tclearManualCompactionState(session);\n\t}\n}\n\n/**\n * Dispatch automatic compaction after `agent_end` or before prompt submission.\n * Manual compaction does not call this method; it enters through `compact()`.\n *\n * Automatic cases:\n * 1. Overflow with retry: a context-overflow error or recoverable length stop;\n * remove the failed assistant message, compact, and retry the turn once.\n * 2. Overflow without retry: a successful response exceeded the configured\n * context window; compact but preserve the completed response.\n * 3. Threshold without retry: valid or estimated context usage crossed the\n * configured threshold; compact without retrying the completed response.\n *\n * Each case calls `_runAutoCompaction()`. After preparation and the\n * `session_before_compact` hook, that method calls the lower-level `compact()`\n * function imported from `./compaction/index.ts`, unless the hook cancels or\n * supplies a custom result.\n *\n * @param assistantMessage The assistant message to check\n * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages\n */\nexport async function checkCompaction(\n\tsession: AgentSessionInternals,\n\tassistantMessage: AssistantMessage,\n\tskipAbortedCheck = true,\n\tsignal?: AbortSignal,\n): Promise<boolean> {\n\tif (signal?.aborted) return false;\n\tconst settings = session.settingsManager.getCompactionSettings();\n\n\t// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false\n\tif (skipAbortedCheck && assistantMessage.stopReason === \"aborted\") return false;\n\n\tconst contextWindow = session.model?.contextWindow ?? 0;\n\n\t// Skip overflow check if the message came from a different model.\n\t// This handles the case where user switched from a smaller-context model (e.g. opus)\n\t// to a larger-context model (e.g. codex) - the overflow error from the old model\n\t// shouldn't trigger compaction for the new model.\n\tconst sameModel =\n\t\tsession.model !== undefined &&\n\t\tassistantMessage.provider === session.model.provider &&\n\t\tassistantMessage.model === session.model.id;\n\n\t// Skip compaction checks if this assistant message is older than the latest\n\t// compaction boundary. This prevents a stale pre-compaction usage/error\n\t// from retriggering compaction on the first prompt after compaction.\n\tconst compactionEntry = getLatestCompactionEntry(session.sessionManager.getBranch());\n\tconst assistantIsFromBeforeCompaction =\n\t\tcompactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();\n\tif (assistantIsFromBeforeCompaction) {\n\t\treturn false;\n\t}\n\n\t// Automatic cases 1 and 2: context overflow.\n\t// A length stop is recoverable when output ended below the model's original desired limit,\n\t// independent of the configured context size or any context-clamped provider request limit.\n\tconst overflow = sameModel\n\t\t? await decideOverflow(session, assistantMessage, undefined, signal)\n\t\t: { contextOverflow: false, recoverableLength: false };\n\tconst { contextOverflow, recoverableLength } = overflow;\n\tif (contextOverflow || recoverableLength) {\n\t\tconst decision = await decideCompaction(session, {\n\t\t\treason: \"overflow\",\n\t\t\tcontextOverflow,\n\t\t\trecoverableLength,\n\t\t\tcanContinue: assistantMessage.stopReason !== \"stop\",\n\t\t\tsignal,\n\t\t});\n\t\t// The overflow policy request carries no contextUsage, so the threshold\n\t\t// triplet is omitted here rather than fabricated.\n\t\tawait dispatchCompactionDecisionLifecycle(session, {\n\t\t\treason: \"overflow\",\n\t\t\tcompact: decision.compact,\n\t\t\twillRetry: decision.retry === true,\n\t\t\t...(decision.failureMessage === undefined ? {} : { failureMessage: decision.failureMessage }),\n\t\t});\n\t\tif (!decision.compact) {\n\t\t\tif (decision.failureMessage) {\n\t\t\t\tconst errorMessage = decision.failureMessage;\n\t\t\t\tsession._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"overflow\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage,\n\t\t\t\t});\n\t\t\t\t// The policy reported a genuine failure (not a cancellation), so the\n\t\t\t\t// failed event carries the same fact. 进程日志打点与该事件同一事实,\n\t\t\t\t// 缺 hostLogger 时为 no-op。\n\t\t\t\tsession._hostLogger?.error(\"compaction\", \"compaction failed\", {\n\t\t\t\t\tfields: { reason: \"overflow\" },\n\t\t\t\t\tcause: errorMessage,\n\t\t\t\t});\n\t\t\t\tawait dispatchCompactionFailedLifecycle(session, { reason: \"overflow\", errorMessage });\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\tconst willRetry = decision.retry === true;\n\t\tif (willRetry) {\n\t\t\t// A retry excludes the failed assistant response from the next model context.\n\t\t\t// The actual context projection is performed inside the compaction queue so\n\t\t\t// it cannot race a manual compaction's summary, commit, or projection.\n\t\t\tsession._overflowRecoveryAttempted = true;\n\t\t}\n\t\treturn await session._runAutoCompaction(\"overflow\", willRetry);\n\t}\n\n\t// Case 3: threshold compaction without retry.\n\t// For error messages or all-zero usage messages, estimate from the last valid response.\n\t// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage\n\t// responses can still compact and do not reset context accounting.\n\tlet contextTokens: number;\n\tconst directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;\n\tif (assistantMessage.stopReason === \"error\" || directContextTokens === 0) {\n\t\tconst messages = session.agent.state.messages;\n\t\tconst estimate = estimateContextTokens(messages);\n\t\t// Without provider usage, estimate.tokens is the pure message-size estimate.\n\t\t// Only usage-backed estimates need the stale pre-compaction check.\n\t\tif (estimate.lastUsageIndex !== null) {\n\t\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t\t// trigger compaction right after one just finished.\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tcompactionEntry &&\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tcontextTokens = estimate.tokens;\n\t} else {\n\t\tcontextTokens = directContextTokens;\n\t}\n\n\tconst decision = await decideCompaction(session, {\n\t\treason: \"threshold\",\n\t\tcontextOverflow: false,\n\t\trecoverableLength: false,\n\t\tcontextUsage: {\n\t\t\tcontextTokens,\n\t\t\tcontextWindow,\n\t\t\treserveTokens: settings.reserveTokens,\n\t\t},\n\t\tcanContinue: false,\n\t\t// 便宜缓解的事实输入:策略可以据此先做零成本的本地上下文缓解(如\n\t\t// bootstrap 策略的 microcompact),把 LLM 摘要留作兜底。分支快照仅为\n\t\t// 策略读取,内核不解释其内容。\n\t\tbranchEntries: session.sessionManager.getBranch(),\n\t\tkeepRecentTokens: settings.keepRecentTokens,\n\t\tsignal,\n\t});\n\tawait dispatchCompactionDecisionLifecycle(session, {\n\t\treason: \"threshold\",\n\t\tcompact: decision.compact,\n\t\twillRetry: decision.retry === true,\n\t\tcontextTokens,\n\t\tcontextWindow,\n\t\treserveTokens: settings.reserveTokens,\n\t\t...(decision.failureMessage === undefined ? {} : { failureMessage: decision.failureMessage }),\n\t});\n\t// 策略便宜缓解(通用通道):策略通过 decision.relief 返回一个自定义条目,\n\t// 宿主原样追加并重投影上下文——内核不解释条目语义,投影由策略经公共\n\t// registerEntryProjection 通道自行注册(宪法 §4/§5:策略在插件,机制在内核)。\n\tif (decision.relief !== undefined && !decision.compact) {\n\t\tsession.sessionManager.appendCustomEntry(decision.relief.customType, decision.relief.data);\n\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\tawait session._applyContextTransition(sessionContext.messages, \"compaction\");\n\t\tsession._hostLogger?.info(\"compaction\", \"policy relief applied\", {\n\t\t\tfields: { customType: decision.relief.customType },\n\t\t});\n\t\treturn session.agent.hasQueuedMessages();\n\t}\n\treturn decision.compact ? session._runAutoCompaction(\"threshold\", decision.retry === true) : false;\n}\n\n/**\n * Execute threshold or overflow compaction. Manual compaction uses\n * `AgentSession.compact()` instead. Both paths use the selected compaction\n * policy after preparation and extension interception, then commit against the\n * branch revision captured before asynchronous work.\n *\n * @param reason Automatic trigger selected by `_checkCompaction()`\n * @param willRetry Whether to continue the interrupted turn after overflow compaction\n * @returns Whether the post-run loop should call `agent.continue()`\n */\nexport async function runAutoCompaction(\n\tsession: AgentSessionInternals,\n\treason: \"overflow\" | \"threshold\",\n\twillRetry: boolean,\n): Promise<boolean> {\n\tif (session._reloadInProgress) return false;\n\treturn enqueueCompaction(session, () => runAutoCompactionInternal(session, reason, willRetry));\n}\n\nexport async function runAutoCompactionInternal(\n\tsession: AgentSessionInternals,\n\treason: \"overflow\" | \"threshold\",\n\twillRetry: boolean,\n): Promise<boolean> {\n\t// Install the controller before asynchronous auth/preparation so callers can\n\t// cancel every stage, including the overflow-recovery projection.\n\tsession._autoCompactionAbortController = new AbortController();\n\tconst autoCompactionSignal = session._autoCompactionAbortController.signal;\n\t// One outer finally guards every exit path: the early overflow-recovery\n\t// return used to skip the summary-phase cleanup, leaking the aborted\n\t// controller (isCompacting stayed true) and hanging reload's waitForIdle().\n\ttry {\n\t\ttry {\n\t\t\tif (willRetry) {\n\t\t\t\t// Keep overflow recovery's runtime projection under the same queue as\n\t\t\t\t// summary generation, canonical commit, and post-commit projection. This\n\t\t\t\t// prevents a manual compaction that was already queued from observing or\n\t\t\t\t// being interleaved with a partially recovered context.\n\t\t\t\tconst messages = session.agent.state.messages;\n\t\t\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\t\tawait session._applyContextTransition(messages.slice(0, -1), \"overflow_recovery\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (autoCompactionSignal.aborted || session._reloadInProgress) return false;\n\t\t} catch (error) {\n\t\t\tsession._autoCompactionAbortController = undefined;\n\t\t\tthrow error;\n\t\t}\n\n\t\tconst settings = session.settingsManager.getCompactionSettings();\n\t\tlet started = false;\n\n\t\ttry {\n\t\t\tif (!session.model) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst auth = await getSummarizationRequestAuth(session, session.model);\n\t\t\tconst { pathEntries, expectedRevision, operationId } = captureCompactionCommitBoundary(session, reason);\n\n\t\t\tconst execution = executionFacts(session, \"compaction\", operationId);\n\t\t\tconst preparation = await prepareCompaction(session, pathEntries, settings, execution, autoCompactionSignal);\n\t\t\tif (!preparation) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tsession._emit({ type: \"compaction_start\", reason });\n\t\t\tstarted = true;\n\n\t\t\t// Shared default summary generator, also used by manual compaction.\n\t\t\tconst { summary, firstKeptEntryId, tokensBefore, usage, details } = await runCompactionSummary(\n\t\t\t\tsession,\n\t\t\t\tpreparation,\n\t\t\t\tauth,\n\t\t\t\tundefined,\n\t\t\t\tautoCompactionSignal,\n\t\t\t\treason,\n\t\t\t\texecution,\n\t\t\t);\n\n\t\t\tif (autoCompactionSignal.aborted || session._reloadInProgress) {\n\t\t\t\tsession._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: true,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst compactionCommit = commitCompaction(session, {\n\t\t\t\texpectedRevision,\n\t\t\t\toperationId,\n\t\t\t\tsummary,\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\tdetails,\n\t\t\t\tusage,\n\t\t\t});\n\t\t\tawait dispatchCompactionCommitLifecycle(session, reason, compactionCommit);\n\t\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\t\tawait session._applyContextTransition(sessionContext.messages, \"compaction\");\n\t\t\t// See the manual-compaction path: this must describe the strategy-projected\n\t\t\t// runtime context, not canonical history retained by SessionManager.\n\t\t\tconst estimatedTokensAfter = estimateMessagesTokens(session.agent.state.messages);\n\n\t\t\tconst result: CompactionResult = {\n\t\t\t\tsummary,\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\testimatedTokensAfter,\n\t\t\t\tusage,\n\t\t\t\tdetails,\n\t\t\t};\n\t\t\tsession._emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry });\n\n\t\t\tif (willRetry) {\n\t\t\t\tconst messages = session.agent.state.messages;\n\t\t\t\tconst lastMsg = messages[messages.length - 1];\n\t\t\t\t// The overflow response was persisted on message_end before _checkCompaction() removed it\n\t\t\t\t// from agent state. Rebuilding state from the new compaction can restore that kept entry,\n\t\t\t\t// leaving an assistant as the final message. agent.continue() rejects that state, so remove\n\t\t\t\t// the retriable error or truncated-length response again before continuing the interrupted turn.\n\t\t\t\tif (lastMsg?.role === \"assistant\" && (lastMsg.stopReason === \"error\" || lastMsg.stopReason === \"length\")) {\n\t\t\t\t\tawait session._applyContextTransition(messages.slice(0, -1), \"overflow_recovery\");\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Auto-compaction can complete while follow-up/steering/custom messages are waiting.\n\t\t\t// Continue once so queued messages are delivered.\n\t\t\treturn session.agent.hasQueuedMessages();\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\t\tif (started) {\n\t\t\t\tconst formattedErrorMessage =\n\t\t\t\t\treason === \"overflow\"\n\t\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`;\n\t\t\t\tsession._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage: formattedErrorMessage,\n\t\t\t\t});\n\t\t\t\t// 压缩失败打点:与下方 failed 事件同一事实;started 前的取消路径不打点。\n\t\t\t\tsession._hostLogger?.error(\"compaction\", \"compaction failed\", { fields: { reason }, cause: error });\n\t\t\t\tawait dispatchCompactionFailedLifecycle(session, { reason, errorMessage: formattedErrorMessage });\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t} finally {\n\t\tsession._autoCompactionAbortController = undefined;\n\t\tsession._resolveIdleWaitIfIdle();\n\t}\n}\n\n/**\n * Retry policy + callbacks shared by compaction and branch-summary summarization calls.\n * Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient\n * stream drop no longer fails the whole operation. `source` carries the context\n * the TUI needs to render the retry and recreate the underlying indicator.\n */\nexport function summarizationRetryCallbacks(\n\tsession: AgentSessionInternals,\n\tsource: { source: \"branchSummary\" } | { source: \"compaction\"; reason: \"manual\" | \"threshold\" | \"overflow\" },\n): RetryCallbacks {\n\t// Structured trace mirroring SummaryAttemptRecordV1: one record per failed\n\t// attempt (with its backoff) plus the terminal attempt (success or final\n\t// failure) reported by onRetryFinished.\n\tconst attempts: SummaryAttemptRecordV1[] = [];\n\treturn {\n\t\tonRetryScheduled: (attempt, _maxAttempts, delayMs, errorMessage) => {\n\t\t\tattempts.push({ attempt, at: Date.now(), ok: false, failure: errorMessage, waitedMs: delayMs });\n\t\t\tsession._emit({\n\t\t\t\ttype: \"summarization_retry_scheduled\",\n\t\t\t\tattempt,\n\t\t\t\tmaxAttempts: _maxAttempts,\n\t\t\t\tdelayMs,\n\t\t\t\terrorMessage,\n\t\t\t});\n\t\t},\n\t\tonRetryAttemptStart: () => {\n\t\t\tsession._emit({\n\t\t\t\ttype: \"summarization_retry_attempt_start\",\n\t\t\t\t...source,\n\t\t\t});\n\t\t},\n\t\tonRetryFinished: (success, attempt, finalError) => {\n\t\t\t// onRetryFinished reports the number of the last FAILED attempt; the\n\t\t\t// terminal attempt it describes is the next one.\n\t\t\tattempts.push(\n\t\t\t\tsuccess\n\t\t\t\t\t? { attempt: attempt + 1, at: Date.now(), ok: true, waitedMs: 0 }\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tattempt: attempt + 1,\n\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\twaitedMs: 0,\n\t\t\t\t\t\t\t...(finalError === undefined ? {} : { failure: finalError }),\n\t\t\t\t\t\t},\n\t\t\t);\n\t\t\tsession._emit({ type: \"summarization_retry_finished\", source, attempts });\n\t\t},\n\t};\n}\n\n/** Adapt the selected capability retry policy to the shared summary executor. */\nexport function summarizationRetryPolicy(\n\tsession: AgentSessionInternals,\n\toperationId?: string,\n\tinputRevision?: ExecutionFactsV1[\"inputRevision\"],\n\tsignal?: AbortSignal,\n): RetryPolicy {\n\tconst settings = session.settingsManager.getRetrySettings();\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\tconst execution = executionFacts(session, \"retry\", operationId ? `${operationId}:retry` : undefined, inputRevision);\n\treturn {\n\t\t...settings,\n\t\tclassify: (message) =>\n\t\t\truntime.classifyRetry(session._retryPolicyName, {\n\t\t\t\tmessage,\n\t\t\t\tsignal,\n\t\t\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"retry\" },\n\t\t\t\texecution,\n\t\t\t}),\n\t\tdecide: ({ message, retryable, attempt, maxAttempts, baseDelayMs }) =>\n\t\t\truntime.decideRetry(session._retryPolicyName, {\n\t\t\t\tretryable,\n\t\t\t\tenabled: settings.enabled,\n\t\t\t\tattempt,\n\t\t\t\tmaxAttempts,\n\t\t\t\tbaseDelayMs,\n\t\t\t\terrorMessage: message.errorMessage,\n\t\t\t\tsignal,\n\t\t\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"retry\" },\n\t\t\t\texecution,\n\t\t\t}),\n\t};\n}\n\n/**\n * Navigate to a different node in the session tree.\n * Unlike fork() which creates a new session file, this stays in the same file.\n *\n * @param session Internal session facade\n * @param targetId The entry ID to navigate to\n * @param options.summarize Whether user wants to summarize abandoned branch\n * @param options.customInstructions Custom instructions for summarizer\n * @param options.replaceInstructions If true, customInstructions replaces the default prompt\n * @param options.label Label to attach to the branch summary entry\n * @returns Result with editorText (if user message) and cancelled status\n */\nexport async function navigateTree(\n\tsession: AgentSessionInternals,\n\ttargetId: string,\n\toptions: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string } = {},\n): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry }> {\n\tif (session.isStreaming) {\n\t\tthrow new Error(\"Wait for the current response to finish before navigating the session tree.\");\n\t}\n\t// Compaction mutates the active branch; navigating mid-compaction would race the summary commit.\n\tif (session._compactionAbortController !== undefined || session._autoCompactionAbortController !== undefined) {\n\t\tthrow new Error(\n\t\t\t\"Wait for the current compaction or tree navigation to finish before navigating the session tree.\",\n\t\t);\n\t}\n\n\tconst oldLeafId = session.sessionManager.getLeafId();\n\n\t// No-op if already at target\n\tif (targetId === oldLeafId) {\n\t\treturn { cancelled: false };\n\t}\n\n\t// Model required for summarization\n\tif (options.summarize && !session.model) {\n\t\tthrow new Error(\"No model available for summarization\");\n\t}\n\n\tconst targetEntry = session.sessionManager.getEntry(targetId);\n\tif (!targetEntry) {\n\t\tthrow new Error(`Entry ${targetId} not found`);\n\t}\n\n\t// Collect entries to summarize (from old leaf to common ancestor)\n\tconst { entries: entriesToSummarize } = collectEntriesForBranchSummary(session.sessionManager, oldLeafId, targetId);\n\n\tconst customInstructions = options.customInstructions;\n\tconst replaceInstructions = options.replaceInstructions;\n\tconst label = options.label;\n\n\tif (session._reloadInProgress) {\n\t\tthrow new Error(\"Session reload in progress; tree navigation is unavailable\");\n\t}\n\tif (session._branchSummaryAbortController) {\n\t\tthrow new Error(\"Branch navigation is already in progress\");\n\t}\n\n\t// Set up abort controller for summarization\n\tsession._branchSummaryAbortController = new AbortController();\n\tconst branchSummaryPromise = new Promise<void>((resolve) => {\n\t\tsession._resolveBranchSummaryPromise = resolve;\n\t});\n\tsession._branchSummaryPromise = branchSummaryPromise;\n\n\ttry {\n\t\t// Run default summarizer if needed\n\t\tlet summaryText: string | undefined;\n\t\tlet summaryDetails: unknown;\n\t\tlet summaryUsage: Usage | undefined;\n\t\tif (options.summarize && entriesToSummarize.length > 0) {\n\t\t\tconst model = session.model!;\n\t\t\tconst auth = await getSummarizationRequestAuth(session, model);\n\t\t\tconst branchSummarySettings = session.settingsManager.getBranchSummarySettings();\n\t\t\tconst runtime = session._capabilityRuntime;\n\t\t\tif (!runtime || !runtime.hasBranchSummaryPolicy(session._branchSummaryPolicyName)) {\n\t\t\t\tthrow new Error(`Branch summary policy is not registered: ${session._branchSummaryPolicyName}`);\n\t\t\t}\n\t\t\tconst execution = executionFacts(\n\t\t\t\tsession,\n\t\t\t\t\"branch_summary\",\n\t\t\t\t`branch_summary:${session.sessionId}:${oldLeafId ?? \"root\"}:${targetId}`,\n\t\t\t);\n\t\t\tconst result = await runtime.summarizeBranch(session._branchSummaryPolicyName, {\n\t\t\t\tentries: entriesToSummarize,\n\t\t\t\tmodel: summaryModelFacts(auth.model),\n\t\t\t\tinvocation: createSummaryModelInvocationV1(\n\t\t\t\t\tsession,\n\t\t\t\t\tauth,\n\t\t\t\t\tsummarizationRetryPolicy(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\texecution.operationId,\n\t\t\t\t\t\texecution.inputRevision,\n\t\t\t\t\t\tsession._branchSummaryAbortController.signal,\n\t\t\t\t\t),\n\t\t\t\t\tsummarizationRetryCallbacks(session, { source: \"branchSummary\" }),\n\t\t\t\t),\n\t\t\t\tsignal: session._branchSummaryAbortController.signal,\n\t\t\t\tcustomInstructions,\n\t\t\t\treplaceInstructions,\n\t\t\t\treserveTokens: branchSummarySettings.reserveTokens,\n\t\t\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"branch_summary\" },\n\t\t\t\texecution,\n\t\t\t});\n\t\t\tif (result.aborted) {\n\t\t\t\treturn { cancelled: true, aborted: true };\n\t\t\t}\n\t\t\tif (result.error) {\n\t\t\t\tthrow new Error(result.error);\n\t\t\t}\n\t\t\tsummaryText = result.summary;\n\t\t\tsummaryUsage = result.usage as Usage | undefined;\n\t\t\tsummaryDetails = {\n\t\t\t\treadFiles: result.readFiles || [],\n\t\t\t\tmodifiedFiles: result.modifiedFiles || [],\n\t\t\t};\n\t\t}\n\t\tif (session._branchSummaryAbortController.signal.aborted || session._reloadInProgress) {\n\t\t\treturn { cancelled: true, aborted: true };\n\t\t}\n\n\t\t// Determine the new leaf position based on target type\n\t\tlet newLeafId: string | null;\n\t\tlet editorText: string | undefined;\n\n\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t// User message: leaf = parent (null if root), text goes to editor\n\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\teditorText = contentText(targetEntry.message.content, \"\");\n\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t// Custom message: leaf = parent (null if root), text goes to editor\n\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\teditorText = contentText(targetEntry.content, \"\");\n\t\t} else {\n\t\t\t// Non-user message: leaf = selected node\n\t\t\tnewLeafId = targetId;\n\t\t}\n\n\t\t// Switch leaf (with or without summary)\n\t\t// Summary is attached at the navigation target position (newLeafId), not the old branch\n\t\tlet summaryEntry: BranchSummaryEntry | undefined;\n\t\tif (summaryText) {\n\t\t\t// Create summary at target position (can be null for root)\n\t\t\tconst summaryId = session.sessionManager.branchWithSummary(\n\t\t\t\tnewLeafId,\n\t\t\t\tsummaryText,\n\t\t\t\tsummaryDetails,\n\t\t\t\tfalse,\n\t\t\t\tsummaryUsage,\n\t\t\t);\n\t\t\tsummaryEntry = session.sessionManager.getEntry(summaryId) as BranchSummaryEntry;\n\n\t\t\t// Attach label to the summary entry\n\t\t\tif (label) {\n\t\t\t\tsession.sessionManager.appendLabelChange(summaryId, label);\n\t\t\t}\n\t\t} else if (newLeafId === null) {\n\t\t\t// No summary, navigating to root - reset leaf\n\t\t\tsession.sessionManager.resetLeaf();\n\t\t} else {\n\t\t\t// No summary, navigating to non-root\n\t\t\tsession.sessionManager.branch(newLeafId);\n\t\t}\n\n\t\t// Attach label to target entry when not summarizing (no summary entry to label)\n\t\tif (label && !summaryText) {\n\t\t\tsession.sessionManager.appendLabelChange(targetId, label);\n\t\t}\n\n\t\t// Update agent state\n\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\tawait session._applyContextTransition(sessionContext.messages, \"tree\");\n\n\t\treturn { editorText, cancelled: false, summaryEntry };\n\t} finally {\n\t\tsession._branchSummaryAbortController = undefined;\n\t\tsession._resolveIdleWaitIfIdle();\n\t\tsession._resolveBranchSummaryPromise?.();\n\t\tsession._resolveBranchSummaryPromise = undefined;\n\t\tif (session._branchSummaryPromise === branchSummaryPromise) session._branchSummaryPromise = undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compaction engine and branch summarization for AgentSession.\n *\n * Extracted from `agent-session.ts`. Every function here operates on the\n * internal session facade ({@link AgentSessionInternals}); `AgentSession`\n * keeps same-name private delegates so call sites, dynamic dispatch, and test\n * spies keep working unchanged. Method-to-method calls go through the session\n * parameter (not direct function calls) to preserve that dispatch.\n */\n\nimport type { AgentContext, AgentMessage } from \"@agent-forge/agent-core\";\nimport { contentText, retryAssistantCall, uuidv7 } from \"@agent-forge/ai\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tAuthResult,\n\tContext,\n\tModel,\n\tProviderHeaders,\n\tRetryCallbacks,\n\tRetryPolicy,\n\tSimpleStreamOptions,\n\tUsage,\n} from \"@agent-forge/ai/compat\";\nimport { completeSimple, streamSimple } from \"@agent-forge/ai/compat\";\nimport type { SummaryAttemptRecordV1 } from \"../host/summary-retry.ts\";\nimport {\n\ttype AgentSessionInternals,\n\tdetachedSessionSnapshot,\n\tSessionCancelledError,\n} from \"./agent-session-internal.ts\";\nimport { dispatchCompactionDecisionLifecycle, dispatchCompactionFailedLifecycle } from \"./agent-session-lifecycle.ts\";\nimport { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport {\n\ttype CompactionPreparation,\n\ttype CompactionResult,\n\tcalculateContextTokens,\n\tcollectEntriesForBranchSummary,\n\testimateContextTokens,\n\testimateTokens,\n} from \"./compaction/index.ts\";\nimport type { ExecutionFactsV1 } from \"./extensions/index.ts\";\nimport {\n\tDEFAULT_COMPACTION_POLICY_NAME,\n\tSESSION_COMPACTION_COMMIT_EVENT_ID,\n\tSESSION_COMPACTION_COMMIT_EVENT_VERSION,\n\ttype SessionCompactionCommitEventDataV1,\n} from \"./extensions/index.ts\";\nimport type { SummaryModelFactsV1, SummaryModelInvocationV1 } from \"./extensions/public-api.ts\";\nimport { SUMMARY_MODEL_INVOCATION_ERROR_CODES } from \"./extensions/public-api.ts\";\nimport type { BranchSummaryEntry, CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.ts\";\nimport { getLatestCompactionEntry } from \"./session-manager.ts\";\nimport type { CompactionSettings } from \"./settings-manager.ts\";\n\nfunction withoutDeletedHeaders(headers: ProviderHeaders | undefined): Record<string, string> | undefined {\n\treturn headers\n\t\t? Object.fromEntries(Object.entries(headers).filter((entry): entry is [string, string] => entry[1] !== null))\n\t\t: undefined;\n}\n\nfunction estimateMessagesTokens(messages: AgentMessage[]): number {\n\tlet tokens = 0;\n\tfor (const message of messages) {\n\t\ttokens += estimateTokens(message);\n\t}\n\treturn tokens;\n}\n\nexport async function getRequiredRequestAuth(\n\tsession: AgentSessionInternals,\n\tmodel: Model<Api>,\n): Promise<{\n\tmodel: Model<Api>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}> {\n\tlet result: AuthResult | undefined;\n\ttry {\n\t\tresult = await session._modelRuntime.getAuth(model);\n\t} catch (error) {\n\t\tconst cause = error instanceof Error ? error.cause : undefined;\n\t\tif (cause instanceof Error && cause.message === \"authHeader requires a resolved API key\") {\n\t\t\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n\t\t}\n\t\tthrow error;\n\t}\n\tif (result && (result.auth.apiKey || result.auth.headers)) {\n\t\tconst requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;\n\t\treturn {\n\t\t\tmodel: requestModel,\n\t\t\tapiKey: result.auth.apiKey,\n\t\t\theaders: withoutDeletedHeaders(result.auth.headers),\n\t\t\tenv: result.env,\n\t\t};\n\t}\n\n\tconst isOAuth = session._modelRuntime.isUsingOAuth(model.provider);\n\tif (isOAuth) {\n\t\tthrow new Error(\n\t\t\t`Authentication failed for \"${model.provider}\". ` +\n\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t`Run '/login ${model.provider}' to re-authenticate.`,\n\t\t);\n\t}\n\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n}\n\nexport async function getSummarizationRequestAuth(\n\tsession: AgentSessionInternals,\n\tmodel: Model<Api>,\n): Promise<{\n\tmodel: Model<Api>;\n\tapiKey?: string;\n\theaders?: Record<string, string>;\n\tenv?: Record<string, string>;\n}> {\n\tif (session.agent.streamFunction === streamSimple) {\n\t\treturn getRequiredRequestAuth(session, model);\n\t}\n\n\ttry {\n\t\tconst result = await session._modelRuntime.getAuth(model);\n\t\tif (!result) return { model };\n\t\tconst requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;\n\t\treturn {\n\t\t\tmodel: requestModel,\n\t\t\tapiKey: result.auth.apiKey,\n\t\t\theaders: withoutDeletedHeaders(result.auth.headers),\n\t\t\tenv: result.env,\n\t\t};\n\t} catch {\n\t\treturn { model };\n\t}\n}\n\n/** Provider-agnostic facts extracted from the auth-resolved summary model. */\nfunction summaryModelFacts(model: Model<Api>): SummaryModelFactsV1 {\n\treturn {\n\t\tid: model.id,\n\t\tcontextWindow: model.contextWindow,\n\t\tmaxTokens: model.maxTokens,\n\t\treasoning: model.reasoning,\n\t};\n}\n\n/**\n * Build the host-owned summary invocation boundary (V1) for one summary run.\n *\n * The closure captures the auth resolved once per run (model with its\n * auth-level baseUrl override, API key, headers, provider env), the retry\n * budget, and the retry callbacks that mirror retries into session events.\n * Transport selection lazily observes `session.agent.streamFunction` at each\n * invoke so request behavior (timeouts, retries, attribution headers) stays\n * identical to agent turns; without one the host falls back to\n * `completeSimple`. Every completion is cache-free (`cacheRetention: \"none\"`)\n * and routed through `routingSessionId` when the caller supplies one; callers\n * without a session (branch summaries) get a fresh routing id per call.\n */\nexport function createSummaryModelInvocationV1(\n\tsession: AgentSessionInternals,\n\tauth: {\n\t\tmodel: Model<Api>;\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t\tenv?: Record<string, string>;\n\t},\n\tretry: RetryPolicy,\n\tcallbacks?: RetryCallbacks,\n\toptions: { routingSessionId?: string } = {},\n): SummaryModelInvocationV1 {\n\treturn {\n\t\tinvoke: async (input, signal) => {\n\t\t\t// Host-owned output cap: never exceed the summary model's limit.\n\t\t\t// Models without a positive cap stay unclamped.\n\t\t\tconst maxTokens =\n\t\t\t\tinput.maxTokens === undefined || auth.model.maxTokens <= 0\n\t\t\t\t\t? input.maxTokens\n\t\t\t\t\t: Math.min(input.maxTokens, auth.model.maxTokens);\n\t\t\tconst requestOptions: SimpleStreamOptions = {\n\t\t\t\t...(maxTokens === undefined ? {} : { maxTokens }),\n\t\t\t\tsignal,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\theaders: auth.headers,\n\t\t\t\tenv: auth.env,\n\t\t\t\t// One-off summaries must never write the prompt cache.\n\t\t\t\tcacheRetention: \"none\",\n\t\t\t\tsessionId: options.routingSessionId ?? uuidv7(),\n\t\t\t};\n\t\t\tif (auth.model.reasoning && input.thinkingLevel && input.thinkingLevel !== \"off\") {\n\t\t\t\trequestOptions.reasoning = input.thinkingLevel;\n\t\t\t}\n\t\t\tconst context: Context = {\n\t\t\t\tsystemPrompt: input.systemPrompt,\n\t\t\t\tmessages: [{ role: \"user\", content: [{ type: \"text\", text: input.prompt }], timestamp: Date.now() }],\n\t\t\t};\n\t\t\tconst produce = async (): Promise<AssistantMessage> => {\n\t\t\t\tconst streamFn = session.agent.streamFunction;\n\t\t\t\treturn streamFn\n\t\t\t\t\t? (await streamFn(auth.model, context, requestOptions)).result()\n\t\t\t\t\t: completeSimple(auth.model, context, requestOptions);\n\t\t\t};\n\t\t\tlet response: AssistantMessage;\n\t\t\ttry {\n\t\t\t\tresponse = await retryAssistantCall(produce, retry, requestOptions.signal, callbacks);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof Error && error.name === \"AbortError\") {\n\t\t\t\t\treturn { version: 1, status: \"aborted\" };\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.transportError,\n\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (response.stopReason === \"aborted\") {\n\t\t\t\treturn { version: 1, status: \"aborted\" };\n\t\t\t}\n\t\t\tif (response.stopReason === \"error\") {\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.providerError,\n\t\t\t\t\t\tmessage: response.errorMessage || \"Unknown error\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (response.stopReason === \"length\") {\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.lengthCap,\n\t\t\t\t\t\tmessage: \"generation hit the token cap and the summary is incomplete\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (response.content.some((block) => block.type === \"toolCall\")) {\n\t\t\t\treturn {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: SUMMARY_MODEL_INVOCATION_ERROR_CODES.toolCall,\n\t\t\t\t\t\tmessage: \"attempted to call a tool\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tversion: 1,\n\t\t\t\tstatus: \"completed\",\n\t\t\t\ttext: contentText(response.content),\n\t\t\t\tusage: response.usage,\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport async function compactBeforeNextAssistantResponse(\n\tsession: AgentSessionInternals,\n\tcontext: AgentContext,\n\tsignal?: AbortSignal,\n): Promise<AgentContext> {\n\tconst model = session.model;\n\tconst settings = session.settingsManager.getCompactionSettings();\n\n\tif (!model || model.contextWindow <= 0) {\n\t\treturn context;\n\t}\n\tconst contextTokens = estimateContextTokens(context.messages).tokens;\n\tconst decision = await decideCompaction(session, {\n\t\treason: \"threshold\",\n\t\tcontextOverflow: false,\n\t\trecoverableLength: false,\n\t\tcontextUsage: {\n\t\t\tcontextTokens,\n\t\t\tcontextWindow: model.contextWindow,\n\t\t\treserveTokens: settings.reserveTokens,\n\t\t},\n\t\tcanContinue: false,\n\t\tsignal: signal ?? session._policyDecisionAbortController?.signal,\n\t});\n\tawait dispatchCompactionDecisionLifecycle(session, {\n\t\treason: \"threshold\",\n\t\tcompact: decision.compact,\n\t\t// The pre-check never retries the turn: the runAutoCompaction call below\n\t\t// passes a fixed false, so the policy's retry flag is not the fact here.\n\t\twillRetry: false,\n\t\tcontextTokens,\n\t\tcontextWindow: model.contextWindow,\n\t\treserveTokens: settings.reserveTokens,\n\t\t...(decision.failureMessage === undefined ? {} : { failureMessage: decision.failureMessage }),\n\t});\n\tif (!decision.compact) return context;\n\n\tawait session._runAutoCompaction(\"threshold\", false);\n\treturn {\n\t\t...context,\n\t\tmessages: session.agent.state.messages.slice(),\n\t};\n}\n\nexport function executionFacts(\n\tsession: AgentSessionInternals,\n\tphase: ExecutionFactsV1[\"phase\"],\n\toperationId?: string,\n\trevision?: ExecutionFactsV1[\"inputRevision\"],\n): ExecutionFactsV1 {\n\tconst inputRevision = detachedSessionSnapshot(revision ?? session.sessionManager.getRevision());\n\treturn Object.freeze({\n\t\tversion: 1 as const,\n\t\toperationId:\n\t\t\toperationId ??\n\t\t\t`${phase}:${inputRevision.sessionId}:${inputRevision.sequence}:${inputRevision.leafId ?? \"root\"}`,\n\t\tsessionId: session.sessionId,\n\t\tinputRevision,\n\t\tphase,\n\t});\n}\n\nexport async function runCompactionSummary(\n\tsession: AgentSessionInternals,\n\tpreparation: CompactionPreparation,\n\tauth: {\n\t\tmodel: Model<Api>;\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t\tenv?: Record<string, string>;\n\t},\n\tcustomInstructions: string | undefined,\n\tsignal: AbortSignal,\n\treason: \"manual\" | \"threshold\" | \"overflow\",\n\texecution: ExecutionFactsV1,\n): Promise<CompactionResult> {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\tconst result = await runtime.summarizeCompaction(session._compactionPolicyName, {\n\t\tpreparation,\n\t\tmodel: summaryModelFacts(auth.model),\n\t\tinvocation: createSummaryModelInvocationV1(\n\t\t\tsession,\n\t\t\tauth,\n\t\t\tsummarizationRetryPolicy(session, execution.operationId, execution.inputRevision, signal),\n\t\t\tsummarizationRetryCallbacks(session, { source: \"compaction\", reason }),\n\t\t\t{ routingSessionId: session.sessionId },\n\t\t),\n\t\tcustomInstructions,\n\t\tsignal,\n\t\treason,\n\t\tthinkingLevel: session.thinkingLevel,\n\t\tsessionId: session.sessionId,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"compaction\" },\n\t\texecution,\n\t});\n\t// 包内收窄:invocation 结果按 provider 原样透传 usage;压缩 usage 合并需要内部 Usage 形状。\n\treturn { ...result, usage: result.usage as Usage | undefined };\n}\n\nexport async function decideCompaction(\n\tsession: AgentSessionInternals,\n\tinput: {\n\t\treason: \"overflow\" | \"threshold\";\n\t\tcontextOverflow: boolean;\n\t\trecoverableLength: boolean;\n\t\tcontextUsage?: { contextTokens: number; contextWindow: number; reserveTokens: number };\n\t\tcanContinue: boolean;\n\t\t/** 便宜缓解的事实输入(threshold):分支快照与近期保护预算,仅策略消费。 */\n\t\tbranchEntries?: readonly SessionEntry[];\n\t\tkeepRecentTokens?: number;\n\t\texecution?: ExecutionFactsV1;\n\t\tsignal?: AbortSignal;\n\t},\n) {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\t// 内核机制兜底:默认策略名且无人注册(默认插件被禁用或失败隔离)时,机制层\n\t// 不做压缩决策(无执行者),回合继续;缺失经 warn 日志与手动 /compact 的显式\n\t// 错误暴露。默认插件不得成为隐藏前提(宪法 §2/§5)。显式配置的其他策略名缺\n\t// 失仍然显式失败——配置错误必须可见(宪法 §7)。\n\tif (\n\t\tsession._compactionPolicyName === DEFAULT_COMPACTION_POLICY_NAME &&\n\t\t!runtime.hasCompactionPolicy(session._compactionPolicyName)\n\t) {\n\t\tsession._hostLogger?.warn(\"compaction\", \"no compaction policy registered; skipping automatic compaction\", {\n\t\t\tfields: { policyName: session._compactionPolicyName, reason: input.reason },\n\t\t});\n\t\treturn { compact: false, retry: false };\n\t}\n\treturn runtime.decideCompaction(session._compactionPolicyName, {\n\t\tenabled: session.settingsManager.getCompactionSettings().enabled,\n\t\trecoveryAttempted: session._overflowRecoveryAttempted,\n\t\t...input,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"compaction\" },\n\t\texecution: input.execution ?? executionFacts(session, \"compaction\"),\n\t\tsignal: input.signal,\n\t});\n}\n\nexport async function prepareCompaction(\n\tsession: AgentSessionInternals,\n\tpathEntries: SessionEntry[],\n\tsettings: CompactionSettings,\n\texecution?: ExecutionFactsV1,\n\tsignal?: AbortSignal,\n): Promise<CompactionPreparation | undefined> {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\tconst preparation = await runtime.prepareCompaction(session._compactionPolicyName, {\n\t\tentries: pathEntries,\n\t\tsettings,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"compaction\" },\n\t\texecution: execution ?? executionFacts(session, \"compaction\"),\n\t\tsignal,\n\t});\n\tif (preparation === undefined) return undefined;\n\tconst value = preparation as Record<string, unknown>;\n\tif (\n\t\t!preparation ||\n\t\ttypeof preparation !== \"object\" ||\n\t\ttypeof value.firstKeptEntryId !== \"string\" ||\n\t\t!Array.isArray(value.messagesToSummarize) ||\n\t\t!Array.isArray(value.turnPrefixMessages) ||\n\t\ttypeof value.isSplitTurn !== \"boolean\" ||\n\t\t!Number.isFinite(value.tokensBefore) ||\n\t\ttypeof value.fileOps !== \"object\" ||\n\t\ttypeof value.settings !== \"object\"\n\t) {\n\t\tthrow new Error(`Compaction policy returned an invalid preparation: ${session._compactionPolicyName}`);\n\t}\n\treturn preparation as CompactionPreparation;\n}\n\n/** Capture the branch facts used by one optimistic compaction commit. */\nexport function captureCompactionCommitBoundary(\n\tsession: AgentSessionInternals,\n\treason: \"manual\" | \"threshold\" | \"overflow\",\n): {\n\tpathEntries: SessionEntry[];\n\texpectedRevision: ReturnType<SessionManager[\"getRevision\"]>;\n\toperationId: string;\n} {\n\tconst pathEntries = session.sessionManager.getBranch();\n\tconst expectedRevision = session.sessionManager.getRevision();\n\treturn {\n\t\tpathEntries,\n\t\texpectedRevision,\n\t\toperationId: `compaction:${reason}:${expectedRevision.sessionId}:${expectedRevision.sequence}:${expectedRevision.leafId ?? \"root\"}`,\n\t};\n}\n\n/** Queue compaction work so canonical commit and runtime projection stay ordered. */\nexport function enqueueCompaction<T>(session: AgentSessionInternals, operation: () => Promise<T>): Promise<T> {\n\tconst previous = session._compactionQueue;\n\tlet release!: () => void;\n\tsession._compactionQueue = new Promise<void>((resolve) => {\n\t\trelease = resolve;\n\t});\n\tconst queued = previous\n\t\t.then(() => {\n\t\t\tif (session._reloadInProgress) {\n\t\t\t\tthrow new Error(\"Session reload in progress; compaction was not started\");\n\t\t\t}\n\t\t\treturn operation();\n\t\t})\n\t\t.finally(release);\n\tsession._activeCompactionPromise = queued;\n\tvoid queued.then(\n\t\t() => {\n\t\t\tif (session._activeCompactionPromise === queued) session._activeCompactionPromise = undefined;\n\t\t},\n\t\t() => {\n\t\t\tif (session._activeCompactionPromise === queued) session._activeCompactionPromise = undefined;\n\t\t},\n\t);\n\treturn queued;\n}\n\n/**\n * Persist an optimistic compaction result before projecting its context.\n *\n * A revision conflict is a failed compaction, not a signal to recompute or\n * append against newer session history. A replay is a successful prior\n * commit and still requires the caller to apply the runtime projection.\n */\nexport function commitCompaction(\n\tsession: AgentSessionInternals,\n\tinput: {\n\t\texpectedRevision: ReturnType<SessionManager[\"getRevision\"]>;\n\t\toperationId: string;\n\t\tsummary: string;\n\t\tfirstKeptEntryId: string;\n\t\ttokensBefore: number;\n\t\tdetails: unknown;\n\t\tusage: Usage | undefined;\n\t},\n): {\n\tentry: CompactionEntry;\n\tstatus: \"committed\" | \"replayed\";\n\trevision: ReturnType<SessionManager[\"getRevision\"]>;\n} {\n\tconst commit = session.sessionManager.commitCompaction({\n\t\tversion: 1,\n\t\toperationId: input.operationId,\n\t\texpectedRevision: input.expectedRevision,\n\t\tsummary: input.summary,\n\t\tfirstKeptEntryId: input.firstKeptEntryId,\n\t\ttokensBefore: input.tokensBefore,\n\t\tdetails: input.details,\n\t\tfromHook: false,\n\t\tusage: input.usage,\n\t});\n\tif (commit.status === \"revision_conflict\") {\n\t\tthrow new Error(\n\t\t\t`Compaction commit revision conflict: expected ${input.expectedRevision.sequence}/${input.expectedRevision.leafId ?? \"root\"}, current ${commit.currentRevision.sequence}/${commit.currentRevision.leafId ?? \"root\"}`,\n\t\t);\n\t}\n\n\tconst entry = session.sessionManager.getEntry(commit.entryId);\n\tif (entry?.type !== \"compaction\" || entry.operationId !== input.operationId) {\n\t\tthrow new Error(`Compaction commit entry is unavailable: ${commit.entryId}`);\n\t}\n\treturn { entry, status: commit.status, revision: commit.revision };\n}\n\n/** Deliver committed compaction facts without making delivery part of the commit. */\nexport async function dispatchCompactionCommitLifecycle(\n\tsession: AgentSessionInternals,\n\treason: \"manual\" | \"threshold\" | \"overflow\",\n\tcommit: {\n\t\tentry: CompactionEntry;\n\t\tstatus: \"committed\" | \"replayed\";\n\t\trevision: ReturnType<SessionManager[\"getRevision\"]>;\n\t},\n): Promise<void> {\n\ttry {\n\t\tconst runtime = session._capabilityRuntime;\n\t\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\t\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\t\tconst entry = commit.entry;\n\t\tconst data: SessionCompactionCommitEventDataV1 = {\n\t\t\tversion: 1,\n\t\t\tstatus: commit.status,\n\t\t\tentryId: entry.id,\n\t\t\toperationId: entry.operationId ?? \"\",\n\t\t\trevision: commit.revision,\n\t\t\treason,\n\t\t\tsummary: entry.summary,\n\t\t\tfirstKeptEntryId: entry.firstKeptEntryId,\n\t\t\ttokensBefore: entry.tokensBefore,\n\t\t\tfromHook: entry.fromHook === true,\n\t\t\t...(entry.usage === undefined ? {} : { usage: entry.usage }),\n\t\t\t...(entry.details === undefined ? {} : { details: entry.details }),\n\t\t};\n\t\tconst delivery = await runtime.dispatchLifecycle({\n\t\t\ttype: SESSION_COMPACTION_COMMIT_EVENT_ID,\n\t\t\tversion: SESSION_COMPACTION_COMMIT_EVENT_VERSION,\n\t\t\tphase: \"committed\",\n\t\t\tdata,\n\t\t\toperationId: entry.operationId,\n\t\t\tsource: \"agent-session\",\n\t\t});\n\t\tfor (const failure of delivery.report.failures) {\n\t\t\tsession._observeLifecycleDeliveryError(\n\t\t\t\tfailure.error,\n\t\t\t\tfailure.pluginId,\n\t\t\t\t`${SESSION_COMPACTION_COMMIT_EVENT_ID}@${SESSION_COMPACTION_COMMIT_EVENT_VERSION}/${failure.kind}`,\n\t\t\t);\n\t\t}\n\t} catch (error) {\n\t\tconst report = (\n\t\t\terror as { report?: { failures?: readonly { error: unknown; pluginId: string; kind: string }[] } }\n\t\t).report;\n\t\t// v8 ignore next -- phase 常量合法且未传 signal,dispatchLifecycle 仅在 transform/execute 被禁止注册时拒绝,report 形态拒绝不可达\n\t\tif (report?.failures) {\n\t\t\tfor (const failure of report.failures) {\n\t\t\t\tsession._observeLifecycleDeliveryError(\n\t\t\t\t\tfailure.error,\n\t\t\t\t\tfailure.pluginId,\n\t\t\t\t\t`${SESSION_COMPACTION_COMMIT_EVENT_ID}@${SESSION_COMPACTION_COMMIT_EVENT_VERSION}/${failure.kind}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} else {\n\t\t\tsession._observeLifecycleDeliveryError(\n\t\t\t\terror,\n\t\t\t\t\"<capability-lifecycle>\",\n\t\t\t\t`${SESSION_COMPACTION_COMMIT_EVENT_ID}@${SESSION_COMPACTION_COMMIT_EVENT_VERSION}`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport async function decideOverflow(\n\tsession: AgentSessionInternals,\n\tmessage: AssistantMessage,\n\texecution?: ExecutionFactsV1,\n\tsignal?: AbortSignal,\n) {\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\treturn runtime.decideOverflow(session._overflowPolicyName, {\n\t\tmessage,\n\t\tcontextWindow: session.model?.contextWindow,\n\t\tdesiredMaxOutput: session.model?.maxTokens ?? 0,\n\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"overflow\" },\n\t\texecution: execution ?? executionFacts(session, \"overflow\"),\n\t\tsignal,\n\t});\n}\n\n/**\n * Clear the manual compaction controller and wake any `waitForIdle()` caller.\n * Mirrors the upstream `_clearManualCompactionState` helper: every manual\n * compaction exit path must drop the controller before listeners observe\n * `compaction_end`, so `isIdle` is already true when they run.\n */\nfunction clearManualCompactionState(session: AgentSessionInternals): void {\n\tsession._compactionAbortController = undefined;\n\tsession._resolveIdleWaitIfIdle();\n}\n\n/** Structured cancellation outcome for manual compaction (mirrors navigateTree's shape). */\nexport interface CompactionCancelledResult {\n\treadonly cancelled: true;\n\treadonly aborted: true;\n}\n\nexport async function compactInternal(\n\tsession: AgentSessionInternals,\n\tcustomInstructions?: string,\n): Promise<CompactionResult | CompactionCancelledResult> {\n\tawait session._abortInternal();\n\t// Reload can begin while abort() is waiting for the active agent turn. Do\n\t// not create a fresh controller or begin summary work after that barrier.\n\t// Nothing started, so there is nothing to notify: resolve cancelled.\n\tif (session._reloadInProgress) {\n\t\treturn { cancelled: true, aborted: true };\n\t}\n\tsession._compactionAbortController = new AbortController();\n\tsession._emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!session.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\tconst auth = await getSummarizationRequestAuth(session, session.model);\n\n\t\tconst { pathEntries, expectedRevision, operationId } = captureCompactionCommitBoundary(session, \"manual\");\n\t\tconst settings = session.settingsManager.getCompactionSettings();\n\n\t\tconst execution = executionFacts(session, \"compaction\", operationId);\n\t\tconst preparation = await prepareCompaction(\n\t\t\tsession,\n\t\t\tpathEntries,\n\t\t\tsettings,\n\t\t\texecution,\n\t\t\tsession._compactionAbortController.signal,\n\t\t);\n\t\tif (!preparation) {\n\t\t\t// Check why we can't compact\n\t\t\tconst lastEntry = pathEntries[pathEntries.length - 1];\n\t\t\tif (lastEntry?.type === \"compaction\") {\n\t\t\t\tthrow new Error(\"Already compacted\");\n\t\t\t}\n\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t}\n\n\t\t// Shared default summary generator, also used by automatic compaction.\n\t\tconst { summary, firstKeptEntryId, tokensBefore, usage, details } = await runCompactionSummary(\n\t\t\tsession,\n\t\t\tpreparation,\n\t\t\tauth,\n\t\t\tcustomInstructions,\n\t\t\tsession._compactionAbortController.signal,\n\t\t\t\"manual\",\n\t\t\texecution,\n\t\t);\n\n\t\tif (session._compactionAbortController.signal.aborted || session._reloadInProgress) {\n\t\t\tthrow new SessionCancelledError(\"signal\");\n\t\t}\n\n\t\tconst compactionCommit = commitCompaction(session, {\n\t\t\texpectedRevision,\n\t\t\toperationId,\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId,\n\t\t\ttokensBefore,\n\t\t\tdetails,\n\t\t\tusage,\n\t\t});\n\t\tawait dispatchCompactionCommitLifecycle(session, \"manual\", compactionCommit);\n\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\tawait session._applyContextTransition(sessionContext.messages, \"compaction\");\n\t\t// Context strategies may replace the runtime projection without rewriting\n\t\t// canonical session history. Report the immediately post-compaction runtime\n\t\t// projection, rather than the pre-projection session reconstruction.\n\t\tconst estimatedTokensAfter = estimateMessagesTokens(session.agent.state.messages);\n\n\t\tconst compactionResult: CompactionResult = {\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId,\n\t\t\ttokensBefore,\n\t\t\testimatedTokensAfter,\n\t\t\tusage,\n\t\t\tdetails,\n\t\t};\n\t\t// compaction_end listeners may submit queued prompts, so expose idle state before notifying them.\n\t\tclearManualCompactionState(session);\n\t\tsession._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: compactionResult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn compactionResult;\n\t} catch (error) {\n\t\tconst cancelled =\n\t\t\terror instanceof SessionCancelledError || (error instanceof Error && error.name === \"AbortError\");\n\t\tif (cancelled) {\n\t\t\t// Cancellation is a normal outcome: notify listeners and resolve with\n\t\t\t// the structured cancelled result instead of rejecting (matches\n\t\t\t// navigateTree).\n\t\t\tclearManualCompactionState(session);\n\t\t\tsession._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: true,\n\t\t\t\twillRetry: false,\n\t\t\t});\n\t\t\treturn { cancelled: true, aborted: true };\n\t\t}\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst errorMessage = `Compaction failed: ${message}`;\n\t\tclearManualCompactionState(session);\n\t\tsession._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t\terrorMessage,\n\t\t});\n\t\t// 压缩失败打点(会话轨迹第 2 期,设计 §5.2):lifecycle 事件之外的一行\n\t\t// 进程日志,缺 hostLogger 时为 no-op;取消路径不打点。\n\t\tsession._hostLogger?.error(\"compaction\", \"compaction failed\", { fields: { reason: \"manual\" }, cause: error });\n\t\tawait dispatchCompactionFailedLifecycle(session, { reason: \"manual\", errorMessage });\n\t\tthrow error;\n\t} finally {\n\t\tclearManualCompactionState(session);\n\t}\n}\n\n/**\n * Dispatch automatic compaction after `agent_end` or before prompt submission.\n * Manual compaction does not call this method; it enters through `compact()`.\n *\n * Automatic cases:\n * 1. Overflow with retry: a context-overflow error or recoverable length stop;\n * remove the failed assistant message, compact, and retry the turn once.\n * 2. Overflow without retry: a successful response exceeded the configured\n * context window; compact but preserve the completed response.\n * 3. Threshold without retry: valid or estimated context usage crossed the\n * configured threshold; compact without retrying the completed response.\n *\n * Each case calls `_runAutoCompaction()`. After preparation and the\n * `session_before_compact` hook, that method calls the lower-level `compact()`\n * function imported from `./compaction/index.ts`, unless the hook cancels or\n * supplies a custom result.\n *\n * @param assistantMessage The assistant message to check\n * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages\n */\nexport async function checkCompaction(\n\tsession: AgentSessionInternals,\n\tassistantMessage: AssistantMessage,\n\tskipAbortedCheck = true,\n\tsignal?: AbortSignal,\n): Promise<boolean> {\n\tif (signal?.aborted) return false;\n\tconst settings = session.settingsManager.getCompactionSettings();\n\n\t// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false\n\tif (skipAbortedCheck && assistantMessage.stopReason === \"aborted\") return false;\n\n\tconst contextWindow = session.model?.contextWindow ?? 0;\n\n\t// Skip overflow check if the message came from a different model.\n\t// This handles the case where user switched from a smaller-context model (e.g. opus)\n\t// to a larger-context model (e.g. codex) - the overflow error from the old model\n\t// shouldn't trigger compaction for the new model.\n\tconst sameModel =\n\t\tsession.model !== undefined &&\n\t\tassistantMessage.provider === session.model.provider &&\n\t\tassistantMessage.model === session.model.id;\n\n\t// Skip compaction checks if this assistant message is older than the latest\n\t// compaction boundary. This prevents a stale pre-compaction usage/error\n\t// from retriggering compaction on the first prompt after compaction.\n\tconst compactionEntry = getLatestCompactionEntry(session.sessionManager.getBranch());\n\tconst assistantIsFromBeforeCompaction =\n\t\tcompactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();\n\tif (assistantIsFromBeforeCompaction) {\n\t\treturn false;\n\t}\n\n\t// Automatic cases 1 and 2: context overflow.\n\t// A length stop is recoverable when output ended below the model's original desired limit,\n\t// independent of the configured context size or any context-clamped provider request limit.\n\tconst overflow = sameModel\n\t\t? await decideOverflow(session, assistantMessage, undefined, signal)\n\t\t: { contextOverflow: false, recoverableLength: false };\n\tconst { contextOverflow, recoverableLength } = overflow;\n\tif (contextOverflow || recoverableLength) {\n\t\tconst decision = await decideCompaction(session, {\n\t\t\treason: \"overflow\",\n\t\t\tcontextOverflow,\n\t\t\trecoverableLength,\n\t\t\tcanContinue: assistantMessage.stopReason !== \"stop\",\n\t\t\tsignal,\n\t\t});\n\t\t// The overflow policy request carries no contextUsage, so the threshold\n\t\t// triplet is omitted here rather than fabricated.\n\t\tawait dispatchCompactionDecisionLifecycle(session, {\n\t\t\treason: \"overflow\",\n\t\t\tcompact: decision.compact,\n\t\t\twillRetry: decision.retry === true,\n\t\t\t...(decision.failureMessage === undefined ? {} : { failureMessage: decision.failureMessage }),\n\t\t});\n\t\tif (!decision.compact) {\n\t\t\tif (decision.failureMessage) {\n\t\t\t\tconst errorMessage = decision.failureMessage;\n\t\t\t\tsession._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"overflow\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage,\n\t\t\t\t});\n\t\t\t\t// The policy reported a genuine failure (not a cancellation), so the\n\t\t\t\t// failed event carries the same fact. 进程日志打点与该事件同一事实,\n\t\t\t\t// 缺 hostLogger 时为 no-op。\n\t\t\t\tsession._hostLogger?.error(\"compaction\", \"compaction failed\", {\n\t\t\t\t\tfields: { reason: \"overflow\" },\n\t\t\t\t\tcause: errorMessage,\n\t\t\t\t});\n\t\t\t\tawait dispatchCompactionFailedLifecycle(session, { reason: \"overflow\", errorMessage });\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\tconst willRetry = decision.retry === true;\n\t\tif (willRetry) {\n\t\t\t// A retry excludes the failed assistant response from the next model context.\n\t\t\t// The actual context projection is performed inside the compaction queue so\n\t\t\t// it cannot race a manual compaction's summary, commit, or projection.\n\t\t\tsession._overflowRecoveryAttempted = true;\n\t\t}\n\t\treturn await session._runAutoCompaction(\"overflow\", willRetry);\n\t}\n\n\t// Case 3: threshold compaction without retry.\n\t// For error messages or all-zero usage messages, estimate from the last valid response.\n\t// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage\n\t// responses can still compact and do not reset context accounting.\n\tlet contextTokens: number;\n\tconst directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;\n\tif (assistantMessage.stopReason === \"error\" || directContextTokens === 0) {\n\t\tconst messages = session.agent.state.messages;\n\t\tconst estimate = estimateContextTokens(messages);\n\t\t// Without provider usage, estimate.tokens is the pure message-size estimate.\n\t\t// Only usage-backed estimates need the stale pre-compaction check.\n\t\tif (estimate.lastUsageIndex !== null) {\n\t\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t\t// trigger compaction right after one just finished.\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tcompactionEntry &&\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tcontextTokens = estimate.tokens;\n\t} else {\n\t\tcontextTokens = directContextTokens;\n\t}\n\n\tconst decision = await decideCompaction(session, {\n\t\treason: \"threshold\",\n\t\tcontextOverflow: false,\n\t\trecoverableLength: false,\n\t\tcontextUsage: {\n\t\t\tcontextTokens,\n\t\t\tcontextWindow,\n\t\t\treserveTokens: settings.reserveTokens,\n\t\t},\n\t\tcanContinue: false,\n\t\t// 便宜缓解的事实输入:策略可以据此先做零成本的本地上下文缓解(如\n\t\t// bootstrap 策略的 microcompact),把 LLM 摘要留作兜底。分支快照仅为\n\t\t// 策略读取,内核不解释其内容。\n\t\tbranchEntries: session.sessionManager.getBranch(),\n\t\tkeepRecentTokens: settings.keepRecentTokens,\n\t\tsignal,\n\t});\n\tawait dispatchCompactionDecisionLifecycle(session, {\n\t\treason: \"threshold\",\n\t\tcompact: decision.compact,\n\t\twillRetry: decision.retry === true,\n\t\tcontextTokens,\n\t\tcontextWindow,\n\t\treserveTokens: settings.reserveTokens,\n\t\t...(decision.failureMessage === undefined ? {} : { failureMessage: decision.failureMessage }),\n\t});\n\t// 策略便宜缓解(通用通道):策略通过 decision.relief 返回一个自定义条目,\n\t// 宿主原样追加并重投影上下文——内核不解释条目语义,投影由策略经公共\n\t// registerEntryProjection 通道自行注册(宪法 §4/§5:策略在插件,机制在内核)。\n\tif (decision.relief !== undefined && !decision.compact) {\n\t\tsession.sessionManager.appendCustomEntry(decision.relief.customType, decision.relief.data);\n\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\tawait session._applyContextTransition(sessionContext.messages, \"compaction\");\n\t\tsession._hostLogger?.info(\"compaction\", \"policy relief applied\", {\n\t\t\tfields: { customType: decision.relief.customType },\n\t\t});\n\t\treturn session.agent.hasQueuedMessages();\n\t}\n\treturn decision.compact ? session._runAutoCompaction(\"threshold\", decision.retry === true) : false;\n}\n\n/**\n * Execute threshold or overflow compaction. Manual compaction uses\n * `AgentSession.compact()` instead. Both paths use the selected compaction\n * policy after preparation and extension interception, then commit against the\n * branch revision captured before asynchronous work.\n *\n * @param reason Automatic trigger selected by `_checkCompaction()`\n * @param willRetry Whether to continue the interrupted turn after overflow compaction\n * @returns Whether the post-run loop should call `agent.continue()`\n */\nexport async function runAutoCompaction(\n\tsession: AgentSessionInternals,\n\treason: \"overflow\" | \"threshold\",\n\twillRetry: boolean,\n): Promise<boolean> {\n\tif (session._reloadInProgress) return false;\n\treturn enqueueCompaction(session, () => runAutoCompactionInternal(session, reason, willRetry));\n}\n\nexport async function runAutoCompactionInternal(\n\tsession: AgentSessionInternals,\n\treason: \"overflow\" | \"threshold\",\n\twillRetry: boolean,\n): Promise<boolean> {\n\t// Install the controller before asynchronous auth/preparation so callers can\n\t// cancel every stage, including the overflow-recovery projection.\n\tsession._autoCompactionAbortController = new AbortController();\n\tconst autoCompactionSignal = session._autoCompactionAbortController.signal;\n\t// One outer finally guards every exit path: the early overflow-recovery\n\t// return used to skip the summary-phase cleanup, leaking the aborted\n\t// controller (isCompacting stayed true) and hanging reload's waitForIdle().\n\ttry {\n\t\ttry {\n\t\t\tif (willRetry) {\n\t\t\t\t// Keep overflow recovery's runtime projection under the same queue as\n\t\t\t\t// summary generation, canonical commit, and post-commit projection. This\n\t\t\t\t// prevents a manual compaction that was already queued from observing or\n\t\t\t\t// being interleaved with a partially recovered context.\n\t\t\t\tconst messages = session.agent.state.messages;\n\t\t\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\t\tawait session._applyContextTransition(messages.slice(0, -1), \"overflow_recovery\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (autoCompactionSignal.aborted || session._reloadInProgress) return false;\n\t\t} catch (error) {\n\t\t\tsession._autoCompactionAbortController = undefined;\n\t\t\tthrow error;\n\t\t}\n\n\t\tconst settings = session.settingsManager.getCompactionSettings();\n\t\tlet started = false;\n\n\t\ttry {\n\t\t\tif (!session.model) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst auth = await getSummarizationRequestAuth(session, session.model);\n\t\t\tconst { pathEntries, expectedRevision, operationId } = captureCompactionCommitBoundary(session, reason);\n\n\t\t\tconst execution = executionFacts(session, \"compaction\", operationId);\n\t\t\tconst preparation = await prepareCompaction(session, pathEntries, settings, execution, autoCompactionSignal);\n\t\t\tif (!preparation) {\n\t\t\t\t// The policy decided to compact, but every entry sits inside the\n\t\t\t\t// keepRecentTokens budget, so there is no summarizable prefix.\n\t\t\t\t// Emit the skip so hosts can surface the \"context is full but\n\t\t\t\t// nothing compacts\" state; manual /compact reports the same\n\t\t\t\t// condition as its \"Nothing to compact\" error.\n\t\t\t\tsession._emit({ type: \"compaction_skipped\", reason, cause: \"nothing_to_compact\" });\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tsession._emit({ type: \"compaction_start\", reason });\n\t\t\tstarted = true;\n\n\t\t\t// Shared default summary generator, also used by manual compaction.\n\t\t\tconst { summary, firstKeptEntryId, tokensBefore, usage, details } = await runCompactionSummary(\n\t\t\t\tsession,\n\t\t\t\tpreparation,\n\t\t\t\tauth,\n\t\t\t\tundefined,\n\t\t\t\tautoCompactionSignal,\n\t\t\t\treason,\n\t\t\t\texecution,\n\t\t\t);\n\n\t\t\tif (autoCompactionSignal.aborted || session._reloadInProgress) {\n\t\t\t\tsession._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: true,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst compactionCommit = commitCompaction(session, {\n\t\t\t\texpectedRevision,\n\t\t\t\toperationId,\n\t\t\t\tsummary,\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\tdetails,\n\t\t\t\tusage,\n\t\t\t});\n\t\t\tawait dispatchCompactionCommitLifecycle(session, reason, compactionCommit);\n\t\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\t\tawait session._applyContextTransition(sessionContext.messages, \"compaction\");\n\t\t\t// See the manual-compaction path: this must describe the strategy-projected\n\t\t\t// runtime context, not canonical history retained by SessionManager.\n\t\t\tconst estimatedTokensAfter = estimateMessagesTokens(session.agent.state.messages);\n\n\t\t\tconst result: CompactionResult = {\n\t\t\t\tsummary,\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\testimatedTokensAfter,\n\t\t\t\tusage,\n\t\t\t\tdetails,\n\t\t\t};\n\t\t\tsession._emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry });\n\n\t\t\tif (willRetry) {\n\t\t\t\tconst messages = session.agent.state.messages;\n\t\t\t\tconst lastMsg = messages[messages.length - 1];\n\t\t\t\t// The overflow response was persisted on message_end before _checkCompaction() removed it\n\t\t\t\t// from agent state. Rebuilding state from the new compaction can restore that kept entry,\n\t\t\t\t// leaving an assistant as the final message. agent.continue() rejects that state, so remove\n\t\t\t\t// the retriable error or truncated-length response again before continuing the interrupted turn.\n\t\t\t\tif (lastMsg?.role === \"assistant\" && (lastMsg.stopReason === \"error\" || lastMsg.stopReason === \"length\")) {\n\t\t\t\t\tawait session._applyContextTransition(messages.slice(0, -1), \"overflow_recovery\");\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Auto-compaction can complete while follow-up/steering/custom messages are waiting.\n\t\t\t// Continue once so queued messages are delivered.\n\t\t\treturn session.agent.hasQueuedMessages();\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\t\tif (started) {\n\t\t\t\tconst formattedErrorMessage =\n\t\t\t\t\treason === \"overflow\"\n\t\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`;\n\t\t\t\tsession._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage: formattedErrorMessage,\n\t\t\t\t});\n\t\t\t\t// 压缩失败打点:与下方 failed 事件同一事实;started 前的取消路径不打点。\n\t\t\t\tsession._hostLogger?.error(\"compaction\", \"compaction failed\", { fields: { reason }, cause: error });\n\t\t\t\tawait dispatchCompactionFailedLifecycle(session, { reason, errorMessage: formattedErrorMessage });\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t} finally {\n\t\tsession._autoCompactionAbortController = undefined;\n\t\tsession._resolveIdleWaitIfIdle();\n\t}\n}\n\n/**\n * Retry policy + callbacks shared by compaction and branch-summary summarization calls.\n * Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient\n * stream drop no longer fails the whole operation. `source` carries the context\n * the TUI needs to render the retry and recreate the underlying indicator.\n */\nexport function summarizationRetryCallbacks(\n\tsession: AgentSessionInternals,\n\tsource: { source: \"branchSummary\" } | { source: \"compaction\"; reason: \"manual\" | \"threshold\" | \"overflow\" },\n): RetryCallbacks {\n\t// Structured trace mirroring SummaryAttemptRecordV1: one record per failed\n\t// attempt (with its backoff) plus the terminal attempt (success or final\n\t// failure) reported by onRetryFinished.\n\tconst attempts: SummaryAttemptRecordV1[] = [];\n\treturn {\n\t\tonRetryScheduled: (attempt, _maxAttempts, delayMs, errorMessage) => {\n\t\t\tattempts.push({ attempt, at: Date.now(), ok: false, failure: errorMessage, waitedMs: delayMs });\n\t\t\tsession._emit({\n\t\t\t\ttype: \"summarization_retry_scheduled\",\n\t\t\t\tattempt,\n\t\t\t\tmaxAttempts: _maxAttempts,\n\t\t\t\tdelayMs,\n\t\t\t\terrorMessage,\n\t\t\t});\n\t\t},\n\t\tonRetryAttemptStart: () => {\n\t\t\tsession._emit({\n\t\t\t\ttype: \"summarization_retry_attempt_start\",\n\t\t\t\t...source,\n\t\t\t});\n\t\t},\n\t\tonRetryFinished: (success, attempt, finalError) => {\n\t\t\t// onRetryFinished reports the number of the last FAILED attempt; the\n\t\t\t// terminal attempt it describes is the next one.\n\t\t\tattempts.push(\n\t\t\t\tsuccess\n\t\t\t\t\t? { attempt: attempt + 1, at: Date.now(), ok: true, waitedMs: 0 }\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tattempt: attempt + 1,\n\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\twaitedMs: 0,\n\t\t\t\t\t\t\t...(finalError === undefined ? {} : { failure: finalError }),\n\t\t\t\t\t\t},\n\t\t\t);\n\t\t\tsession._emit({ type: \"summarization_retry_finished\", source, attempts });\n\t\t},\n\t};\n}\n\n/** Adapt the selected capability retry policy to the shared summary executor. */\nexport function summarizationRetryPolicy(\n\tsession: AgentSessionInternals,\n\toperationId?: string,\n\tinputRevision?: ExecutionFactsV1[\"inputRevision\"],\n\tsignal?: AbortSignal,\n): RetryPolicy {\n\tconst settings = session.settingsManager.getRetrySettings();\n\tconst runtime = session._capabilityRuntime;\n\t// v8 ignore next -- capability runtime 恒由 _buildRuntime 创建,dispose 后不再调用,运行期恒存在\n\tif (!runtime) throw new Error(\"Capability runtime is not available\");\n\tconst execution = executionFacts(session, \"retry\", operationId ? `${operationId}:retry` : undefined, inputRevision);\n\treturn {\n\t\t...settings,\n\t\tclassify: (message) =>\n\t\t\truntime.classifyRetry(session._retryPolicyName, {\n\t\t\t\tmessage,\n\t\t\t\tsignal,\n\t\t\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"retry\" },\n\t\t\t\texecution,\n\t\t\t}),\n\t\tdecide: ({ message, retryable, attempt, maxAttempts, baseDelayMs }) =>\n\t\t\truntime.decideRetry(session._retryPolicyName, {\n\t\t\t\tretryable,\n\t\t\t\tenabled: settings.enabled,\n\t\t\t\tattempt,\n\t\t\t\tmaxAttempts,\n\t\t\t\tbaseDelayMs,\n\t\t\t\terrorMessage: message.errorMessage,\n\t\t\t\tsignal,\n\t\t\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"retry\" },\n\t\t\t\texecution,\n\t\t\t}),\n\t};\n}\n\n/**\n * Navigate to a different node in the session tree.\n * Unlike fork() which creates a new session file, this stays in the same file.\n *\n * @param session Internal session facade\n * @param targetId The entry ID to navigate to\n * @param options.summarize Whether user wants to summarize abandoned branch\n * @param options.customInstructions Custom instructions for summarizer\n * @param options.replaceInstructions If true, customInstructions replaces the default prompt\n * @param options.label Label to attach to the branch summary entry\n * @returns Result with editorText (if user message) and cancelled status\n */\nexport async function navigateTree(\n\tsession: AgentSessionInternals,\n\ttargetId: string,\n\toptions: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string } = {},\n): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry }> {\n\tif (session.isStreaming) {\n\t\tthrow new Error(\"Wait for the current response to finish before navigating the session tree.\");\n\t}\n\t// Compaction mutates the active branch; navigating mid-compaction would race the summary commit.\n\tif (session._compactionAbortController !== undefined || session._autoCompactionAbortController !== undefined) {\n\t\tthrow new Error(\n\t\t\t\"Wait for the current compaction or tree navigation to finish before navigating the session tree.\",\n\t\t);\n\t}\n\n\tconst oldLeafId = session.sessionManager.getLeafId();\n\n\t// No-op if already at target\n\tif (targetId === oldLeafId) {\n\t\treturn { cancelled: false };\n\t}\n\n\t// Model required for summarization\n\tif (options.summarize && !session.model) {\n\t\tthrow new Error(\"No model available for summarization\");\n\t}\n\n\tconst targetEntry = session.sessionManager.getEntry(targetId);\n\tif (!targetEntry) {\n\t\tthrow new Error(`Entry ${targetId} not found`);\n\t}\n\n\t// Collect entries to summarize (from old leaf to common ancestor)\n\tconst { entries: entriesToSummarize } = collectEntriesForBranchSummary(session.sessionManager, oldLeafId, targetId);\n\n\tconst customInstructions = options.customInstructions;\n\tconst replaceInstructions = options.replaceInstructions;\n\tconst label = options.label;\n\n\tif (session._reloadInProgress) {\n\t\tthrow new Error(\"Session reload in progress; tree navigation is unavailable\");\n\t}\n\tif (session._branchSummaryAbortController) {\n\t\tthrow new Error(\"Branch navigation is already in progress\");\n\t}\n\n\t// Set up abort controller for summarization\n\tsession._branchSummaryAbortController = new AbortController();\n\tconst branchSummaryPromise = new Promise<void>((resolve) => {\n\t\tsession._resolveBranchSummaryPromise = resolve;\n\t});\n\tsession._branchSummaryPromise = branchSummaryPromise;\n\n\ttry {\n\t\t// Run default summarizer if needed\n\t\tlet summaryText: string | undefined;\n\t\tlet summaryDetails: unknown;\n\t\tlet summaryUsage: Usage | undefined;\n\t\tif (options.summarize && entriesToSummarize.length > 0) {\n\t\t\tconst model = session.model!;\n\t\t\tconst auth = await getSummarizationRequestAuth(session, model);\n\t\t\tconst branchSummarySettings = session.settingsManager.getBranchSummarySettings();\n\t\t\tconst runtime = session._capabilityRuntime;\n\t\t\tif (!runtime || !runtime.hasBranchSummaryPolicy(session._branchSummaryPolicyName)) {\n\t\t\t\tthrow new Error(`Branch summary policy is not registered: ${session._branchSummaryPolicyName}`);\n\t\t\t}\n\t\t\tconst execution = executionFacts(\n\t\t\t\tsession,\n\t\t\t\t\"branch_summary\",\n\t\t\t\t`branch_summary:${session.sessionId}:${oldLeafId ?? \"root\"}:${targetId}`,\n\t\t\t);\n\t\t\tconst result = await runtime.summarizeBranch(session._branchSummaryPolicyName, {\n\t\t\t\tentries: entriesToSummarize,\n\t\t\t\tmodel: summaryModelFacts(auth.model),\n\t\t\t\tinvocation: createSummaryModelInvocationV1(\n\t\t\t\t\tsession,\n\t\t\t\t\tauth,\n\t\t\t\t\tsummarizationRetryPolicy(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\texecution.operationId,\n\t\t\t\t\t\texecution.inputRevision,\n\t\t\t\t\t\tsession._branchSummaryAbortController.signal,\n\t\t\t\t\t),\n\t\t\t\t\tsummarizationRetryCallbacks(session, { source: \"branchSummary\" }),\n\t\t\t\t),\n\t\t\t\tsignal: session._branchSummaryAbortController.signal,\n\t\t\t\tcustomInstructions,\n\t\t\t\treplaceInstructions,\n\t\t\t\treserveTokens: branchSummarySettings.reserveTokens,\n\t\t\t\tcontext: { version: 1, sessionId: session.sessionId, phase: \"branch_summary\" },\n\t\t\t\texecution,\n\t\t\t});\n\t\t\tif (result.aborted) {\n\t\t\t\treturn { cancelled: true, aborted: true };\n\t\t\t}\n\t\t\tif (result.error) {\n\t\t\t\tthrow new Error(result.error);\n\t\t\t}\n\t\t\tsummaryText = result.summary;\n\t\t\tsummaryUsage = result.usage as Usage | undefined;\n\t\t\tsummaryDetails = {\n\t\t\t\treadFiles: result.readFiles || [],\n\t\t\t\tmodifiedFiles: result.modifiedFiles || [],\n\t\t\t};\n\t\t}\n\t\tif (session._branchSummaryAbortController.signal.aborted || session._reloadInProgress) {\n\t\t\treturn { cancelled: true, aborted: true };\n\t\t}\n\n\t\t// Determine the new leaf position based on target type\n\t\tlet newLeafId: string | null;\n\t\tlet editorText: string | undefined;\n\n\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t// User message: leaf = parent (null if root), text goes to editor\n\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\teditorText = contentText(targetEntry.message.content, \"\");\n\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t// Custom message: leaf = parent (null if root), text goes to editor\n\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\teditorText = contentText(targetEntry.content, \"\");\n\t\t} else {\n\t\t\t// Non-user message: leaf = selected node\n\t\t\tnewLeafId = targetId;\n\t\t}\n\n\t\t// Switch leaf (with or without summary)\n\t\t// Summary is attached at the navigation target position (newLeafId), not the old branch\n\t\tlet summaryEntry: BranchSummaryEntry | undefined;\n\t\tif (summaryText) {\n\t\t\t// Create summary at target position (can be null for root)\n\t\t\tconst summaryId = session.sessionManager.branchWithSummary(\n\t\t\t\tnewLeafId,\n\t\t\t\tsummaryText,\n\t\t\t\tsummaryDetails,\n\t\t\t\tfalse,\n\t\t\t\tsummaryUsage,\n\t\t\t);\n\t\t\tsummaryEntry = session.sessionManager.getEntry(summaryId) as BranchSummaryEntry;\n\n\t\t\t// Attach label to the summary entry\n\t\t\tif (label) {\n\t\t\t\tsession.sessionManager.appendLabelChange(summaryId, label);\n\t\t\t}\n\t\t} else if (newLeafId === null) {\n\t\t\t// No summary, navigating to root - reset leaf\n\t\t\tsession.sessionManager.resetLeaf();\n\t\t} else {\n\t\t\t// No summary, navigating to non-root\n\t\t\tsession.sessionManager.branch(newLeafId);\n\t\t}\n\n\t\t// Attach label to target entry when not summarizing (no summary entry to label)\n\t\tif (label && !summaryText) {\n\t\t\tsession.sessionManager.appendLabelChange(targetId, label);\n\t\t}\n\n\t\t// Update agent state\n\t\tconst sessionContext = session.sessionManager.buildSessionContext();\n\t\tawait session._applyContextTransition(sessionContext.messages, \"tree\");\n\n\t\treturn { editorText, cancelled: false, summaryEntry };\n\t} finally {\n\t\tsession._branchSummaryAbortController = undefined;\n\t\tsession._resolveIdleWaitIfIdle();\n\t\tsession._resolveBranchSummaryPromise?.();\n\t\tsession._resolveBranchSummaryPromise = undefined;\n\t\tif (session._branchSummaryPromise === branchSummaryPromise) session._branchSummaryPromise = undefined;\n\t}\n}\n"]}
|
|
@@ -767,6 +767,12 @@ export async function runAutoCompactionInternal(session, reason, willRetry) {
|
|
|
767
767
|
const execution = executionFacts(session, "compaction", operationId);
|
|
768
768
|
const preparation = await prepareCompaction(session, pathEntries, settings, execution, autoCompactionSignal);
|
|
769
769
|
if (!preparation) {
|
|
770
|
+
// The policy decided to compact, but every entry sits inside the
|
|
771
|
+
// keepRecentTokens budget, so there is no summarizable prefix.
|
|
772
|
+
// Emit the skip so hosts can surface the "context is full but
|
|
773
|
+
// nothing compacts" state; manual /compact reports the same
|
|
774
|
+
// condition as its "Nothing to compact" error.
|
|
775
|
+
session._emit({ type: "compaction_skipped", reason, cause: "nothing_to_compact" });
|
|
770
776
|
return false;
|
|
771
777
|
}
|
|
772
778
|
session._emit({ type: "compaction_start", reason });
|