@wangzhizhi/remi 0.1.225 → 0.1.244

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.
Files changed (36) hide show
  1. package/README.md +206 -2
  2. package/dist/help.js +1 -1
  3. package/dist/initPrompt.js +1 -1
  4. package/dist/setup.js +5 -4
  5. package/dist/statusCard.js +1 -1
  6. package/dist/statusline.js +1 -1
  7. package/dist/tui/RemiApp.js +55 -7
  8. package/dist/tui/hooksPanel.js +2 -2
  9. package/dist/tui/renderers/MessageList.js +10 -7
  10. package/dist/version.js +1 -1
  11. package/node_modules/@remi/compact/dist/index.js +13 -5
  12. package/node_modules/@remi/compact/package.json +1 -1
  13. package/node_modules/@remi/config/dist/index.js +816 -38
  14. package/node_modules/@remi/config/package.json +1 -1
  15. package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
  16. package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
  17. package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
  18. package/node_modules/@remi/core/dist/index.js +630 -33
  19. package/node_modules/@remi/core/dist/stableHash.js +30 -0
  20. package/node_modules/@remi/core/package.json +1 -1
  21. package/node_modules/@remi/hooks/package.json +1 -1
  22. package/node_modules/@remi/llm/package.json +1 -1
  23. package/node_modules/@remi/mcp/dist/index.js +32 -18
  24. package/node_modules/@remi/mcp/package.json +1 -1
  25. package/node_modules/@remi/memory/package.json +1 -1
  26. package/node_modules/@remi/permissions/package.json +1 -1
  27. package/node_modules/@remi/sessions/dist/index.js +38 -0
  28. package/node_modules/@remi/sessions/package.json +1 -1
  29. package/node_modules/@remi/skills/dist/index.js +2 -2
  30. package/node_modules/@remi/skills/package.json +1 -1
  31. package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
  32. package/node_modules/@remi/terminal-markdown/package.json +1 -1
  33. package/node_modules/@remi/tools/dist/index.js +1 -1
  34. package/node_modules/@remi/tools/package.json +1 -1
  35. package/package.json +13 -13
  36. package/prompt/tool-use-system.md +19 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remi/config",
3
- "version": "0.1.225",
3
+ "version": "0.1.244",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -0,0 +1,217 @@
1
+ import { createHeuristicTokenEstimator } from '@remi/llm';
2
+ import { buildProviderContext } from './contextBuilder.js';
3
+ import { stableHash, stableStringify } from './stableHash.js';
4
+ const defaultTokenEstimator = createHeuristicTokenEstimator();
5
+ export function buildCacheShapeBenchmark(requests, options = {}) {
6
+ const tokenEstimator = options.tokenEstimator ?? defaultTokenEstimator;
7
+ const tokenEstimateProfile = options.tokenEstimateProfile ?? 'default';
8
+ const shapedRequests = requests.map(request => buildShapedRequest(request, tokenEstimator, tokenEstimateProfile));
9
+ const pairs = [];
10
+ for (let index = 1; index < shapedRequests.length; index += 1) {
11
+ const before = shapedRequests[index - 1];
12
+ const after = shapedRequests[index];
13
+ if (before && after) {
14
+ pairs.push(compareShapedRequests(before, after));
15
+ }
16
+ }
17
+ return {
18
+ requestCount: shapedRequests.length,
19
+ requests: shapedRequests.map(request => request.summary),
20
+ pairs,
21
+ };
22
+ }
23
+ export function buildRemiCacheShapeBenchmarkFixture() {
24
+ return buildCacheShapeBenchmark([
25
+ buildRemiCacheShapeBenchmarkRequest({
26
+ label: 'turn-1',
27
+ runtimePrompt: 'cwd=/Users/example/project\nlanguage=zh-Hans\nturn=1',
28
+ currentUserMessage: '优化缓存命中率',
29
+ }),
30
+ buildRemiCacheShapeBenchmarkRequest({
31
+ label: 'turn-2',
32
+ runtimePrompt: 'cwd=/Users/example/project\nlanguage=zh-Hans\nturn=2',
33
+ currentUserMessage: '继续验证 benchmark',
34
+ }),
35
+ ]);
36
+ }
37
+ export function buildRemiCacheShapeBenchmarkRequest(options) {
38
+ const context = buildProviderContext({
39
+ systemPrompt: 'Remi stable core behavior.',
40
+ toolPrompt: 'Remi stable tool-use policy.',
41
+ projectInstructions: 'Follow PLAN.md and update checklist after each step.',
42
+ projectInstructionsSource: 'AGENTS.md + PLAN.md',
43
+ memories: [
44
+ {
45
+ id: 'cache-memory',
46
+ type: 'project',
47
+ scope: 'project',
48
+ description: 'Cache optimization memory',
49
+ createdAt: '2026-06-04T00:00:00.000Z',
50
+ updatedAt: '2026-06-04T00:00:00.000Z',
51
+ useCount: 1,
52
+ body: 'Keep stable context layers before dynamic runtime facts.',
53
+ reason: 'benchmark fixture',
54
+ },
55
+ ],
56
+ sessionMemorySummary: 'Stable session memory summary for cache benchmark.',
57
+ compactSummary: 'Stable compact summary for cache benchmark.',
58
+ recentMessages: [
59
+ { role: 'user', content: '上一轮请求' },
60
+ { role: 'assistant', content: '上一轮已完成观测。' },
61
+ ],
62
+ runtimePrompt: options.runtimePrompt,
63
+ currentTurnPrompt: 'Current turn reminder stays near the active user message.',
64
+ currentUserMessage: options.currentUserMessage,
65
+ });
66
+ return {
67
+ label: options.label,
68
+ messages: context.messages,
69
+ trace: context.trace,
70
+ toolDefinitions: options.toolDefinitions ?? benchmarkToolDefinitions,
71
+ };
72
+ }
73
+ function buildShapedRequest(request, tokenEstimator, tokenEstimateProfile) {
74
+ const toolSchema = stableStringify(canonicalToolDefinitions(request.toolDefinitions));
75
+ const toolSchemaUnit = unitForText('tool-schema', toolSchema, tokenEstimator, tokenEstimateProfile);
76
+ const layerIds = messageLayerIds(request.trace);
77
+ const messageUnits = request.messages.map((message, index) => {
78
+ const serialized = stableStringify({ role: message.role, content: message.content });
79
+ return unitForText('message', serialized, tokenEstimator, tokenEstimateProfile, layerIds[index]);
80
+ });
81
+ const units = [toolSchemaUnit, ...messageUnits];
82
+ return {
83
+ request,
84
+ units,
85
+ summary: {
86
+ label: request.label,
87
+ messageCount: request.messages.length,
88
+ toolCount: request.toolDefinitions.length,
89
+ toolSchemaHash: toolSchemaUnit.hash,
90
+ toolSchemaTokens: toolSchemaUnit.tokenCount,
91
+ layerOrder: request.trace.layers.map(layer => layer.id),
92
+ currentUserMessageLast: request.messages.at(-1)?.role === 'user',
93
+ totalBytes: units.reduce((sum, unit) => sum + unit.byteCount, 0),
94
+ totalTokens: units.reduce((sum, unit) => sum + unit.tokenCount, 0),
95
+ },
96
+ };
97
+ }
98
+ function compareShapedRequests(before, after) {
99
+ let sharedUnitCount = 0;
100
+ let sharedByteCount = 0;
101
+ let sharedTokenCount = 0;
102
+ const sharedLayerIds = [];
103
+ const maxSharedUnits = Math.min(before.units.length, after.units.length);
104
+ for (let index = 0; index < maxSharedUnits; index += 1) {
105
+ const beforeUnit = before.units[index];
106
+ const afterUnit = after.units[index];
107
+ if (!beforeUnit || !afterUnit || beforeUnit.kind !== afterUnit.kind || beforeUnit.layerId !== afterUnit.layerId || beforeUnit.hash !== afterUnit.hash) {
108
+ break;
109
+ }
110
+ sharedUnitCount += 1;
111
+ sharedByteCount += afterUnit.byteCount;
112
+ sharedTokenCount += afterUnit.tokenCount;
113
+ if (afterUnit.layerId && !sharedLayerIds.includes(afterUnit.layerId)) {
114
+ sharedLayerIds.push(afterUnit.layerId);
115
+ }
116
+ }
117
+ const firstChanged = after.units[sharedUnitCount];
118
+ const firstChangedLayer = firstChanged?.layerId ? layerIndexAndId(after.request.trace, firstChanged.layerId) : undefined;
119
+ const changedLayers = changedLayerReports(before.request.trace, after.request.trace);
120
+ const stableLayers = after.request.trace.layers
121
+ .filter(layer => layer.included && !changedLayers.some(changed => changed.id === layer.id))
122
+ .map(layer => ({
123
+ id: layer.id,
124
+ tokenCount: layer.tokenCount,
125
+ ...(layer.hash ? { hash: layer.hash } : {}),
126
+ }));
127
+ return {
128
+ from: before.request.label,
129
+ to: after.request.label,
130
+ toolSchemaChanged: before.summary.toolSchemaHash !== after.summary.toolSchemaHash,
131
+ toolSchemaHashBefore: before.summary.toolSchemaHash,
132
+ toolSchemaHashAfter: after.summary.toolSchemaHash,
133
+ toolSchemaTokensBefore: before.summary.toolSchemaTokens,
134
+ toolSchemaTokensAfter: after.summary.toolSchemaTokens,
135
+ firstChangedUnit: firstChanged ? firstChanged.kind : 'none',
136
+ ...(firstChanged && firstChanged.kind === 'message' ? { firstChangedMessageIndex: sharedUnitCount - 1 } : {}),
137
+ ...(firstChangedLayer ? { firstChangedLayerIndex: firstChangedLayer.index, firstChangedLayerId: firstChangedLayer.id } : {}),
138
+ sharedPrefix: {
139
+ unitCount: sharedUnitCount,
140
+ byteCount: sharedByteCount,
141
+ tokenCount: sharedTokenCount,
142
+ percentOfNextBytes: after.summary.totalBytes > 0 ? sharedByteCount / after.summary.totalBytes : 0,
143
+ layerIds: sharedLayerIds,
144
+ },
145
+ stableLayers,
146
+ changedLayers,
147
+ };
148
+ }
149
+ function canonicalToolDefinitions(tools) {
150
+ return [...tools]
151
+ .map(tool => ({
152
+ name: tool.name,
153
+ description: tool.description,
154
+ parameters: tool.parameters,
155
+ }))
156
+ .sort((left, right) => left.name.localeCompare(right.name) || left.description.localeCompare(right.description));
157
+ }
158
+ function messageLayerIds(trace) {
159
+ return trace.layers.flatMap(layer => Array.from({ length: layer.messageCount }, () => layer.id));
160
+ }
161
+ function unitForText(kind, text, tokenEstimator, tokenEstimateProfile, layerId) {
162
+ return {
163
+ kind,
164
+ hash: stableHash(text),
165
+ byteCount: Buffer.byteLength(text, 'utf8'),
166
+ tokenCount: tokenEstimator.estimateText({ text, profile: tokenEstimateProfile }).tokens,
167
+ ...(layerId ? { layerId } : {}),
168
+ };
169
+ }
170
+ function layerIndexAndId(trace, layerId) {
171
+ const index = trace.layers.findIndex(layer => layer.id === layerId);
172
+ return index >= 0 ? { index, id: layerId } : undefined;
173
+ }
174
+ function changedLayerReports(before, after) {
175
+ const beforeById = new Map(before.layers.map(layer => [layer.id, layer]));
176
+ return after.layers
177
+ .filter(layer => {
178
+ const previous = beforeById.get(layer.id);
179
+ return !previous || previous.included !== layer.included || previous.hash !== layer.hash;
180
+ })
181
+ .map(layer => {
182
+ const previous = beforeById.get(layer.id);
183
+ return {
184
+ id: layer.id,
185
+ ...(previous?.hash ? { beforeHash: previous.hash } : {}),
186
+ ...(layer.hash ? { afterHash: layer.hash } : {}),
187
+ includedBefore: previous?.included ?? false,
188
+ includedAfter: layer.included,
189
+ };
190
+ });
191
+ }
192
+ const benchmarkToolDefinitions = [
193
+ {
194
+ name: 'read_file',
195
+ description: 'Read a local file.',
196
+ parameters: {
197
+ type: 'object',
198
+ properties: {
199
+ path: { type: 'string' },
200
+ },
201
+ required: ['path'],
202
+ additionalProperties: false,
203
+ },
204
+ },
205
+ {
206
+ name: 'search_text',
207
+ description: 'Search text in local files.',
208
+ parameters: {
209
+ type: 'object',
210
+ properties: {
211
+ query: { type: 'string' },
212
+ },
213
+ required: ['query'],
214
+ additionalProperties: false,
215
+ },
216
+ },
217
+ ];
@@ -0,0 +1,300 @@
1
+ import { stableHash, stableStringify } from './stableHash.js';
2
+ export function latestCacheDiagnostic(events) {
3
+ for (let index = events.length - 1; index >= 0; index -= 1) {
4
+ const event = events[index];
5
+ if (event?.type === 'cache_diagnostic') {
6
+ return event;
7
+ }
8
+ }
9
+ return undefined;
10
+ }
11
+ export function buildCacheDiagnosticReport(events) {
12
+ const diagnostics = events.filter((event) => event.type === 'cache_diagnostic');
13
+ if (diagnostics.length === 0) {
14
+ return emptyCacheDiagnosticReport();
15
+ }
16
+ const missReasons = new Map();
17
+ const changedLayers = new Map();
18
+ let inputTokens = 0;
19
+ let cachedInputTokens = 0;
20
+ let outputTokens = 0;
21
+ let totalTokens = 0;
22
+ let toolSchemaChanges = 0;
23
+ let prefixChanges = 0;
24
+ for (const diagnostic of diagnostics) {
25
+ inputTokens += diagnostic.usage.inputTokens;
26
+ cachedInputTokens += diagnostic.usage.cachedInputTokens;
27
+ outputTokens += diagnostic.usage.outputTokens;
28
+ totalTokens += diagnostic.usage.totalTokens;
29
+ missReasons.set(diagnostic.inference.missReason, (missReasons.get(diagnostic.inference.missReason) ?? 0) + 1);
30
+ if (diagnostic.inference.toolSchemaChanged) {
31
+ toolSchemaChanges += 1;
32
+ }
33
+ if (diagnostic.inference.prefixChanged) {
34
+ prefixChanges += 1;
35
+ }
36
+ for (const layer of diagnostic.inference.changedLayers) {
37
+ changedLayers.set(layer, (changedLayers.get(layer) ?? 0) + 1);
38
+ }
39
+ }
40
+ const latest = diagnostics.at(-1);
41
+ const totalInputTokens = inputTokens + cachedInputTokens;
42
+ return {
43
+ diagnosticCount: diagnostics.length,
44
+ inputTokens,
45
+ cachedInputTokens,
46
+ outputTokens,
47
+ totalTokens,
48
+ totalInputTokens,
49
+ cacheHitRate: totalInputTokens > 0 ? cachedInputTokens / totalInputTokens : 0,
50
+ missReasons: reasonCountEntries(missReasons),
51
+ changedLayers: idCountEntries(changedLayers),
52
+ toolSchemaChanges,
53
+ prefixChanges,
54
+ ...(latest
55
+ ? {
56
+ latest: {
57
+ turn: latest.request.turn,
58
+ model: latest.model.displayName ?? latest.model.alias,
59
+ missReason: latest.inference.missReason,
60
+ changedLayers: latest.inference.changedLayers,
61
+ toolSchemaChanged: latest.inference.toolSchemaChanged,
62
+ prefixChanged: latest.inference.prefixChanged,
63
+ prefixHash: latest.request.prefixHash,
64
+ toolSchemaHash: latest.request.toolSchemaHash,
65
+ toolSchemaTokens: latest.request.toolSchemaTokens,
66
+ toolCount: latest.request.toolCount,
67
+ toolNames: latest.request.toolNames,
68
+ cacheHitRate: latest.usage.cacheHitRate,
69
+ },
70
+ }
71
+ : {}),
72
+ };
73
+ }
74
+ export function formatCacheDiagnosticStatus(report, language = 'en') {
75
+ if (report.diagnosticCount === 0 || !report.latest) {
76
+ return language === 'zh-Hans' ? '暂无诊断' : 'no diagnostics yet';
77
+ }
78
+ const latest = report.latest;
79
+ const parts = language === 'zh-Hans'
80
+ ? [
81
+ `命中 ${formatPercent(report.cacheHitRate)}`,
82
+ `cached ${formatTokenCount(report.cachedInputTokens)} / input ${formatTokenCount(report.totalInputTokens)}`,
83
+ `最近 ${formatMissReason(latest.missReason, language)}`,
84
+ ]
85
+ : [
86
+ `${formatPercent(report.cacheHitRate)} hit`,
87
+ `cached ${formatTokenCount(report.cachedInputTokens)} / input ${formatTokenCount(report.totalInputTokens)}`,
88
+ `last ${formatMissReason(latest.missReason, language)}`,
89
+ ];
90
+ if (latest.changedLayers.length > 0) {
91
+ parts.push(language === 'zh-Hans' ? `层 ${latest.changedLayers.join(',')}` : `changed ${latest.changedLayers.join(',')}`);
92
+ }
93
+ if (latest.toolSchemaChanged) {
94
+ parts.push(language === 'zh-Hans' ? '工具 schema 变更' : 'tools changed');
95
+ }
96
+ if (latest.prefixChanged) {
97
+ parts.push(language === 'zh-Hans' ? 'prefix 变更' : 'prefix changed');
98
+ }
99
+ return parts.join(' · ');
100
+ }
101
+ export function buildCacheDiagnostic(options) {
102
+ const canonicalTools = canonicalToolDefinitions(options.toolDefinitions);
103
+ const toolSchemaText = stableStringify(canonicalTools);
104
+ const toolSchemaHash = stableHash(canonicalTools);
105
+ const messageShape = options.messages.map(cacheMessageShape);
106
+ const messageHash = stableHash(messageShape);
107
+ const prefixHash = stableHash({ messages: messageShape, tools: canonicalTools });
108
+ const cachedInputTokens = options.usage.cachedInputTokens ?? 0;
109
+ const inputTokens = options.usage.inputTokens;
110
+ const inputDenominator = inputTokens + cachedInputTokens;
111
+ const toolNames = canonicalTools.map(tool => tool.name);
112
+ const layers = options.contextTrace.layers.map(layer => ({
113
+ id: layer.id,
114
+ included: layer.included,
115
+ ...(layer.hash ? { hash: layer.hash } : {}),
116
+ tokenCount: layer.tokenCount,
117
+ messageCount: layer.messageCount,
118
+ }));
119
+ const changedLayers = changedLayerIds(options.previous, layers);
120
+ const toolSchemaChanged = options.previous ? options.previous.request.toolSchemaHash !== toolSchemaHash : false;
121
+ const prefixChanged = options.previous ? options.previous.request.prefixHash !== prefixHash : false;
122
+ return {
123
+ type: 'cache_diagnostic',
124
+ model: {
125
+ profile: options.model.profile,
126
+ role: options.model.role,
127
+ alias: options.model.alias,
128
+ displayName: options.model.displayName,
129
+ provider: options.model.providerAlias,
130
+ model: options.model.model,
131
+ ...(options.model.effort ? { effort: options.model.effort } : {}),
132
+ },
133
+ request: {
134
+ turn: options.turn,
135
+ messageCount: options.messages.length,
136
+ prefixHash,
137
+ messageHash,
138
+ toolSchemaHash,
139
+ toolSchemaTokens: options.tokenEstimator.estimateText({ text: toolSchemaText, profile: options.tokenEstimateProfile }).tokens,
140
+ toolCount: canonicalTools.length,
141
+ toolNames,
142
+ },
143
+ layers,
144
+ usage: {
145
+ inputTokens,
146
+ cachedInputTokens,
147
+ outputTokens: options.usage.outputTokens,
148
+ totalTokens: Math.max(options.usage.totalTokens, inputTokens + cachedInputTokens + options.usage.outputTokens),
149
+ cacheHitRate: inputDenominator > 0 ? cachedInputTokens / inputDenominator : 0,
150
+ },
151
+ inference: {
152
+ inferred: true,
153
+ missReason: inferMissReason({
154
+ missTokens: inputTokens,
155
+ ...(options.previous ? { previous: options.previous } : {}),
156
+ changedLayers,
157
+ toolSchemaChanged,
158
+ prefixChanged,
159
+ }),
160
+ changedLayers,
161
+ toolSchemaChanged,
162
+ prefixChanged,
163
+ },
164
+ };
165
+ }
166
+ function canonicalToolDefinitions(tools) {
167
+ return [...tools]
168
+ .map(tool => ({
169
+ name: tool.name,
170
+ description: tool.description,
171
+ parameters: tool.parameters,
172
+ }))
173
+ .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
174
+ }
175
+ function cacheMessageShape(message) {
176
+ if (message.role === 'tool') {
177
+ return {
178
+ role: 'tool',
179
+ toolCallId: message.toolCallId,
180
+ contentHash: stableHash(message.content),
181
+ };
182
+ }
183
+ if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
184
+ return {
185
+ role: 'assistant',
186
+ contentHash: stableHash(message.content),
187
+ ...(message.reasoningContent ? { reasoningHash: stableHash(message.reasoningContent) } : {}),
188
+ toolCallsHash: stableHash(message.toolCalls),
189
+ };
190
+ }
191
+ return {
192
+ role: message.role,
193
+ contentHash: stableHash(message.content),
194
+ };
195
+ }
196
+ function changedLayerIds(previous, currentLayers) {
197
+ if (!previous) {
198
+ return [];
199
+ }
200
+ const previousById = new Map(previous.layers.map(layer => [layer.id, layer]));
201
+ return currentLayers
202
+ .filter(layer => {
203
+ const before = previousById.get(layer.id);
204
+ return !before || before.hash !== layer.hash || before.included !== layer.included;
205
+ })
206
+ .map(layer => layer.id);
207
+ }
208
+ function inferMissReason(options) {
209
+ if (options.missTokens <= 0) {
210
+ return 'no-miss';
211
+ }
212
+ if (!options.previous) {
213
+ return 'cold-start';
214
+ }
215
+ if (options.toolSchemaChanged) {
216
+ return 'tool-schema-changed';
217
+ }
218
+ if (options.changedLayers.length > 0) {
219
+ return 'layer-changed';
220
+ }
221
+ if (!options.prefixChanged) {
222
+ return 'prefix-stable-provider-side';
223
+ }
224
+ return 'unknown';
225
+ }
226
+ function emptyCacheDiagnosticReport() {
227
+ return {
228
+ diagnosticCount: 0,
229
+ inputTokens: 0,
230
+ cachedInputTokens: 0,
231
+ outputTokens: 0,
232
+ totalTokens: 0,
233
+ totalInputTokens: 0,
234
+ cacheHitRate: 0,
235
+ missReasons: [],
236
+ changedLayers: [],
237
+ toolSchemaChanges: 0,
238
+ prefixChanges: 0,
239
+ };
240
+ }
241
+ function reasonCountEntries(counts) {
242
+ const orderByValue = new Map(reasonOrder.map((value, index) => [value, index]));
243
+ const entries = [...counts.entries()].sort(([leftId, leftCount], [rightId, rightCount]) => {
244
+ if (leftCount !== rightCount) {
245
+ return rightCount - leftCount;
246
+ }
247
+ const leftOrder = orderByValue.get(leftId) ?? Number.MAX_SAFE_INTEGER;
248
+ const rightOrder = orderByValue.get(rightId) ?? Number.MAX_SAFE_INTEGER;
249
+ if (leftOrder !== rightOrder) {
250
+ return leftOrder - rightOrder;
251
+ }
252
+ return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
253
+ });
254
+ return entries.map(([reason, count]) => ({ reason, count }));
255
+ }
256
+ function idCountEntries(counts) {
257
+ const entries = [...counts.entries()].sort(([leftId, leftCount], [rightId, rightCount]) => {
258
+ if (leftCount !== rightCount) {
259
+ return rightCount - leftCount;
260
+ }
261
+ return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
262
+ });
263
+ return entries.map(([id, count]) => ({ id, count }));
264
+ }
265
+ const reasonOrder = [
266
+ 'no-miss',
267
+ 'cold-start',
268
+ 'tool-schema-changed',
269
+ 'layer-changed',
270
+ 'prefix-stable-provider-side',
271
+ 'unknown',
272
+ ];
273
+ function formatMissReason(reason, language) {
274
+ const labels = {
275
+ 'no-miss': { en: 'no miss', 'zh-Hans': '无 miss' },
276
+ 'cold-start': { en: 'cold start', 'zh-Hans': '冷启动' },
277
+ 'tool-schema-changed': { en: 'tool schema changed', 'zh-Hans': '工具 schema 变化' },
278
+ 'layer-changed': { en: 'layer changed', 'zh-Hans': '上下文层变化' },
279
+ 'prefix-stable-provider-side': { en: 'provider-side miss', 'zh-Hans': 'prefix 稳定但 provider miss' },
280
+ unknown: { en: 'unknown', 'zh-Hans': '未知' },
281
+ };
282
+ return labels[reason][language];
283
+ }
284
+ function formatPercent(value) {
285
+ const percent = value * 100;
286
+ const precision = percent >= 99 && percent < 100 ? 1 : 0;
287
+ return `${percent.toFixed(precision).replace(/\.0$/, '')}%`;
288
+ }
289
+ function formatTokenCount(value) {
290
+ if (value >= 1_000_000) {
291
+ return `${trimFixed(value / 1_000_000)}m`;
292
+ }
293
+ if (value >= 1_000) {
294
+ return `${trimFixed(value / 1_000)}k`;
295
+ }
296
+ return String(value);
297
+ }
298
+ function trimFixed(value) {
299
+ return value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, '');
300
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHeuristicTokenEstimator } from '@remi/llm';
2
+ import { stableHash } from './stableHash.js';
2
3
  const defaultTokenEstimator = createHeuristicTokenEstimator();
3
4
  // Global "收口": per-layer budgets are allocated with floors, so for a small
4
5
  // effective input budget their sum can exceed the budget. Shrink low-priority
@@ -221,6 +222,17 @@ export function buildProviderContext(options) {
221
222
  tokenCount: 0,
222
223
  }, charBudgets.compact, tokenBudgets.compact);
223
224
  }
225
+ addMessageLayer({
226
+ id: 'recent',
227
+ source: '~/.remi sessions recent user/assistant transcript',
228
+ messages,
229
+ layers,
230
+ layerMessages: applyMessagesBudget(options.recentMessages ?? [], charBudgets.recent, tokenBudgets.recent, tokenEstimator, tokenEstimateProfile),
231
+ budgetChars: charBudgets.recent,
232
+ budgetTokens: tokenBudgets.recent,
233
+ tokenEstimator,
234
+ tokenEstimateProfile,
235
+ });
224
236
  if (options.runtimePrompt && options.runtimePrompt.trim().length > 0) {
225
237
  addMessageLayer({
226
238
  id: 'runtime',
@@ -249,17 +261,6 @@ export function buildProviderContext(options) {
249
261
  tokenCount: 0,
250
262
  }, charBudgets.runtime, tokenBudgets.runtime);
251
263
  }
252
- addMessageLayer({
253
- id: 'recent',
254
- source: '~/.remi sessions recent user/assistant transcript',
255
- messages,
256
- layers,
257
- layerMessages: applyMessagesBudget(options.recentMessages ?? [], charBudgets.recent, tokenBudgets.recent, tokenEstimator, tokenEstimateProfile),
258
- budgetChars: charBudgets.recent,
259
- budgetTokens: tokenBudgets.recent,
260
- tokenEstimator,
261
- tokenEstimateProfile,
262
- });
263
264
  const currentLayerMessages = [];
264
265
  if (options.currentTurnPrompt && options.currentTurnPrompt.trim().length > 0) {
265
266
  currentLayerMessages.push({
@@ -323,6 +324,7 @@ function addMessageLayer(options) {
323
324
  messageCount: options.layerMessages.length,
324
325
  charCount: content.length,
325
326
  tokenCount: estimateTextTokens(content, options.tokenEstimator, options.tokenEstimateProfile),
327
+ ...(included ? { hash: stableHash(layerMessages.map(message => ({ role: message.role, content: message.content }))) } : {}),
326
328
  }, options.budgetChars, options.budgetTokens);
327
329
  }
328
330
  function addTrace(layers, trace, budgetChars, budgetTokens) {