pi-pier 0.3.0 → 0.4.1

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 (103) hide show
  1. package/README.md +16 -12
  2. package/package.json +3 -3
  3. package/schemas/role-manifest.schema.json +7 -1
  4. package/skills/pi-herdr-workflow/SKILL.md +4 -5
  5. package/src/ansi-text.ts +62 -11
  6. package/src/ask-dialog.ts +308 -0
  7. package/src/ask-user.ts +113 -112
  8. package/src/bootstrap.ts +10 -13
  9. package/src/compact-coordinator.ts +279 -261
  10. package/src/compact-economics-core.ts +77 -145
  11. package/src/config-catalog-core.ts +77 -197
  12. package/src/config-command.ts +271 -64
  13. package/src/cordis-services.ts +64 -0
  14. package/src/dashboard-command.ts +77 -33
  15. package/src/delegation-core.ts +115 -0
  16. package/src/efficiency-config-core.ts +286 -486
  17. package/src/efficiency-store.ts +16 -29
  18. package/src/gc-core.ts +10 -36
  19. package/src/guards.ts +6 -0
  20. package/src/herdr-client.ts +202 -233
  21. package/src/history-store.ts +38 -23
  22. package/src/index-gates.ts +118 -0
  23. package/src/index-locks.ts +2 -7
  24. package/src/index-master.ts +31 -29
  25. package/src/index-notices.ts +28 -15
  26. package/src/index-pipe.ts +31 -12
  27. package/src/index-roles.ts +306 -0
  28. package/src/index-runtime.ts +16 -6
  29. package/src/index-worker.ts +8 -18
  30. package/src/index.ts +561 -570
  31. package/src/jev-client.ts +12 -17
  32. package/src/jev-core.ts +138 -90
  33. package/src/ledger.ts +13 -21
  34. package/src/lock-core.ts +28 -56
  35. package/src/manifest-compose.ts +35 -20
  36. package/src/observation-core.ts +52 -100
  37. package/src/pane-title.ts +19 -38
  38. package/src/panel-open.ts +47 -0
  39. package/src/pi-surface.ts +34 -22
  40. package/src/pier-options.ts +53 -50
  41. package/src/pipe-channel.ts +44 -59
  42. package/src/platform-paths.ts +19 -52
  43. package/src/{core → plugins}/grid-shape.ts +8 -10
  44. package/src/{core → plugins}/heat-plan.ts +20 -22
  45. package/src/plugins/observation.ts +475 -0
  46. package/src/plugins/subagent.ts +766 -0
  47. package/src/{core → plugins}/terminal.ts +198 -139
  48. package/src/{core → plugins}/todo.ts +73 -134
  49. package/src/progress-core.ts +16 -53
  50. package/src/reconcile-core.ts +24 -33
  51. package/src/reducer-core.ts +26 -64
  52. package/src/reducer-invoker.ts +266 -239
  53. package/src/renderers.ts +108 -84
  54. package/src/role-loader.ts +33 -28
  55. package/src/role-manifest.ts +25 -31
  56. package/src/role-state.ts +119 -0
  57. package/src/roles/worker-default.json +3 -3
  58. package/src/routing-telemetry.ts +136 -0
  59. package/src/runtime-policy.ts +25 -40
  60. package/src/session-tail.ts +65 -41
  61. package/src/settle-wake-core.ts +61 -22
  62. package/src/slim-frame.ts +25 -122
  63. package/src/stale-core.ts +17 -24
  64. package/src/storage-layout.ts +14 -43
  65. package/src/subagent-core.ts +423 -120
  66. package/src/subagent-gc.ts +109 -67
  67. package/src/subagent-poller.ts +383 -24
  68. package/src/subagent-session.ts +516 -0
  69. package/src/subagent-spawn.ts +577 -52
  70. package/src/swallow.ts +7 -17
  71. package/src/terminal-core.ts +187 -134
  72. package/src/todo-core.ts +57 -109
  73. package/src/todo-read-hook.ts +46 -39
  74. package/src/todo-reminder-core.ts +26 -51
  75. package/src/todo-window.ts +11 -12
  76. package/src/todos-service.ts +15 -35
  77. package/src/tool-error.ts +6 -13
  78. package/src/tool-gate.ts +38 -21
  79. package/src/tree-view.ts +162 -0
  80. package/src/vocab.ts +52 -25
  81. package/src/worker-marker.ts +76 -0
  82. package/src/ask-multi.ts +0 -260
  83. package/src/config-guide.ts +0 -361
  84. package/src/core/observation.ts +0 -555
  85. package/src/core/subagent.ts +0 -633
  86. package/src/efficiency-config-io.ts +0 -53
  87. package/src/focus-poller.ts +0 -244
  88. package/src/git-adapter.ts +0 -113
  89. package/src/notice-buffer.ts +0 -23
  90. package/src/subagent-execute.ts +0 -328
  91. package/src/subagent-git-io.ts +0 -80
  92. package/src/subagent-launch.ts +0 -114
  93. package/src/subagent-list-action.ts +0 -69
  94. package/src/subagent-output-action.ts +0 -104
  95. package/src/subagent-output-adapter.ts +0 -27
  96. package/src/subagent-output-core.ts +0 -263
  97. package/src/subagent-poll-loop.ts +0 -305
  98. package/src/subagent-port.ts +0 -27
  99. package/src/subagent-ready.ts +0 -89
  100. package/src/subagent-registry.ts +0 -70
  101. package/src/subagent-resolution.ts +0 -41
  102. package/src/subagent-scope.ts +0 -41
  103. package/src/subagent-session-io.ts +0 -230
@@ -1,21 +1,18 @@
1
1
  /**
2
- * D100-D103 Efficiency Configuration Core.
2
+ * D100-D103 efficiency configuration: schema, validation, multi-tier resolution and the two files
3
+ * pier itself reads for it.
3
4
  *
4
- * Provides zero-dependency parsing, validation, and multi-tier resolution
5
- * for pier efficiency mechanisms (OCC, ObservationPack, EPR).
6
- *
7
- * Contract:
8
- * - Unknown keys rejected to catch typos; all issues collected in one pass.
9
- * - Project-level configuration requires `isProjectTrusted: true` (security boundary).
10
- * - Workspace config shallow-replaces user config.
11
- * - Fail-open: configuration parse/validation errors disable the affected mechanism
12
- * and emit a single-line warning, never crashing normal agent sessions.
13
- * - Environment variables (PI_HERDR_*) take highest precedence.
5
+ * Contract: unknown keys are rejected (typo guard — all issues collected in one pass); a project-level
6
+ * config requires `isProjectTrusted: true` (security boundary); workspace config shallow-replaces user
7
+ * config while env (`PI_HERDR_*` / `PIER_JEV_*`) wins over both; and a config that fails validation
8
+ * disables the affected mechanism with a one-line warning instead of crashing the session (an unknown
9
+ * top-level key disables every mechanism).
14
10
  */
15
11
 
12
+ import { isPlainObject } from './guards.ts';
13
+ import { existsSync, readFileSync } from 'node:fs';
16
14
  import { homedir } from 'node:os';
17
15
  import { join } from 'node:path';
18
- import { loadPiNativeCompactionSettings, readJsonConfig } from './efficiency-config-io.ts';
19
16
 
20
17
  export interface OnlineContextCompactConfig {
21
18
  enabled: boolean;
@@ -47,9 +44,8 @@ export interface EvidencePreservingReducerConfig {
47
44
  }
48
45
 
49
46
  /**
50
- * Jev decision layer (RFC docs/rfc-jev-integration.md). Optional System One
51
- * classification calls that upgrade hand-written heuristics; every call site
52
- * fails open to the existing heuristic on any error, timeout, or low confidence.
47
+ * Jev decision layer (RFC docs/rfc-jev-integration.md): optional System One classification calls that
48
+ * upgrade hand-written heuristics; every call site fails open on any error, timeout, or low confidence.
53
49
  */
54
50
  export interface JevConfig {
55
51
  enabled: boolean;
@@ -77,89 +73,137 @@ export interface EfficiencyConfig {
77
73
  export const DEFAULT_EFFICIENCY_CONFIG: EfficiencyConfig = Object.freeze({
78
74
  version: 1,
79
75
  onlineContextCompact: Object.freeze({
80
- enabled: false,
81
- logEnabled: false,
82
- cacheWriteReadRatio: 'auto',
83
- keepRecentTokens: 20000,
84
- firstCompactionRequestScale: 2.0,
85
- subsequentCompactionMargin: 1.5,
76
+ enabled: false, logEnabled: false, cacheWriteReadRatio: 'auto', keepRecentTokens: 20000,
77
+ firstCompactionRequestScale: 2.0, subsequentCompactionMargin: 1.5,
86
78
  }),
87
79
  observationPack: Object.freeze({
88
- enabled: false,
89
- logEnabled: false,
90
- thresholdBytes: 10240,
91
- fullSends: 2,
92
- recallChunkBytes: 16384,
93
- excerptBytes: 1024,
80
+ enabled: false, logEnabled: false, thresholdBytes: 10240, fullSends: 2,
81
+ recallChunkBytes: 16384, excerptBytes: 1024,
94
82
  }),
95
83
  evidencePreservingReducer: Object.freeze({
96
- enabled: false,
97
- logEnabled: false,
98
- minBytes: 4096,
99
- maxChars: 600000,
100
- maxOutputTokens: 2048,
101
- timeoutMs: 5000,
102
- localOnly: false,
103
- }),
104
- jev: Object.freeze({
105
- enabled: false,
106
- logEnabled: false,
107
- model: 'jev-1.13.0',
108
- timeoutMs: 2000,
109
- minConfidence: 0.6,
84
+ enabled: false, logEnabled: false, minBytes: 4096, maxChars: 600000,
85
+ maxOutputTokens: 2048, timeoutMs: 5000, localOnly: false,
110
86
  }),
87
+ jev: Object.freeze({ enabled: false, logEnabled: false, model: 'jev-1.13.0', timeoutMs: 2000, minConfidence: 0.6 }),
111
88
  });
112
89
 
113
- function isPlainObject(v: unknown): v is Record<string, unknown> {
114
- return typeof v === 'object' && v !== null && !Array.isArray(v);
90
+ type FieldKind = 'bool' | 'int' | 'number' | 'ratio' | 'string' | 'optionalString';
91
+
92
+ interface FieldSpec {
93
+ readonly key: string;
94
+ readonly kind: FieldKind;
95
+ /** Lower bound for `int`/`number`; also printed in the issue message. */
96
+ readonly min?: number;
97
+ readonly max?: number;
98
+ /** Issue text for a bound that must not be printed as a plain JS number ("1.0"). */
99
+ readonly minText?: string;
100
+ }
101
+
102
+ /** A section is `{enabled, ...fields}`; field order fixes the order of reported issues. */
103
+ interface SectionSpec {
104
+ readonly name: 'onlineContextCompact' | 'observationPack' | 'evidencePreservingReducer' | 'jev';
105
+ readonly fields: readonly FieldSpec[];
106
+ }
107
+
108
+ const SECTIONS: readonly SectionSpec[] = [
109
+ { name: 'onlineContextCompact', fields: [
110
+ { key: 'enabled', kind: 'bool' },
111
+ { key: 'logEnabled', kind: 'bool' },
112
+ { key: 'cacheWriteReadRatio', kind: 'ratio' },
113
+ { key: 'keepRecentTokens', kind: 'int', min: 1000 },
114
+ { key: 'firstCompactionRequestScale', kind: 'number', min: 1, minText: '1.0' },
115
+ { key: 'subsequentCompactionMargin', kind: 'number', min: 1, minText: '1.0' },
116
+ ] },
117
+ { name: 'observationPack', fields: [
118
+ { key: 'enabled', kind: 'bool' },
119
+ { key: 'logEnabled', kind: 'bool' },
120
+ { key: 'thresholdBytes', kind: 'int', min: 1024 },
121
+ { key: 'fullSends', kind: 'int', min: 1 },
122
+ { key: 'recallChunkBytes', kind: 'int', min: 1024 },
123
+ { key: 'excerptBytes', kind: 'int', min: 128 },
124
+ ] },
125
+ { name: 'evidencePreservingReducer', fields: [
126
+ { key: 'enabled', kind: 'bool' },
127
+ { key: 'logEnabled', kind: 'bool' },
128
+ { key: 'model', kind: 'optionalString' },
129
+ { key: 'minBytes', kind: 'int', min: 512 },
130
+ { key: 'maxChars', kind: 'int', min: 1000 },
131
+ { key: 'maxOutputTokens', kind: 'int', min: 128 },
132
+ { key: 'timeoutMs', kind: 'int', min: 500 },
133
+ { key: 'localOnly', kind: 'bool' },
134
+ ] },
135
+ { name: 'jev', fields: [
136
+ { key: 'enabled', kind: 'bool' },
137
+ { key: 'logEnabled', kind: 'bool' },
138
+ { key: 'baseUrl', kind: 'optionalString' },
139
+ { key: 'model', kind: 'string' },
140
+ { key: 'timeoutMs', kind: 'int', min: 500 },
141
+ { key: 'minConfidence', kind: 'number', min: 0, max: 1 },
142
+ { key: 'apiKey', kind: 'optionalString' },
143
+ ] },
144
+ ];
145
+
146
+ function issueFor(section: string, field: FieldSpec): string {
147
+ const path = `${section}.${field.key}`;
148
+ switch (field.kind) {
149
+ case 'bool':
150
+ return `${path} 必须是 boolean`;
151
+ case 'int':
152
+ return `${path} 必须是 >= ${field.min} 的整数`;
153
+ case 'number':
154
+ return field.max === undefined
155
+ ? `${path} 必须是 >= ${field.minText ?? field.min} 的数字`
156
+ : `${path} 必须是 ${field.min} 到 ${field.max} 之间的数字`;
157
+ case 'ratio':
158
+ return `${path} 必须是 "auto" 或非负有限数`;
159
+ default:
160
+ return `${path} 必须是非空字符串`;
161
+ }
162
+ }
163
+
164
+ function applyField(target: Record<string, unknown>, field: FieldSpec, raw: unknown): boolean {
165
+ if (raw === undefined) return true;
166
+ const min = field.min ?? Number.NEGATIVE_INFINITY;
167
+ switch (field.kind) {
168
+ case 'bool':
169
+ if (typeof raw !== 'boolean') return false;
170
+ break;
171
+ case 'int':
172
+ if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < min) return false;
173
+ break;
174
+ case 'number':
175
+ if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < min) return false;
176
+ if (field.max !== undefined && raw > field.max) return false;
177
+ break;
178
+ case 'ratio':
179
+ if (raw !== 'auto' && (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0)) return false;
180
+ break;
181
+ case 'string':
182
+ if (typeof raw !== 'string' || !raw.trim()) return false;
183
+ target[field.key] = raw.trim();
184
+ return true;
185
+ case 'optionalString':
186
+ if (raw === '' || raw === null) {
187
+ target[field.key] = undefined;
188
+ return true;
189
+ }
190
+ if (typeof raw !== 'string' || !raw.trim()) return false;
191
+ target[field.key] = raw.trim();
192
+ return true;
193
+ }
194
+ target[field.key] = raw;
195
+ return true;
115
196
  }
116
197
 
117
- const TOP_LEVEL_KEYS = new Set([
118
- 'version',
119
- 'onlineContextCompact',
120
- 'observationPack',
121
- 'evidencePreservingReducer',
122
- 'jev',
123
- ]);
124
-
125
- const OCC_KEYS = new Set([
126
- 'enabled',
127
- 'logEnabled',
128
- 'cacheWriteReadRatio',
129
- 'keepRecentTokens',
130
- 'firstCompactionRequestScale',
131
- 'subsequentCompactionMargin',
132
- ]);
133
-
134
- const OBS_KEYS = new Set([
135
- 'enabled',
136
- 'logEnabled',
137
- 'thresholdBytes',
138
- 'fullSends',
139
- 'recallChunkBytes',
140
- 'excerptBytes',
141
- ]);
142
-
143
- const EPR_KEYS = new Set([
144
- 'enabled',
145
- 'logEnabled',
146
- 'model',
147
- 'minBytes',
148
- 'maxChars',
149
- 'maxOutputTokens',
150
- 'timeoutMs',
151
- 'localOnly',
152
- ]);
153
-
154
- const JEV_KEYS: Record<string, true> = {
155
- enabled: true,
156
- logEnabled: true,
157
- baseUrl: true,
158
- model: true,
159
- timeoutMs: true,
160
- minConfidence: true,
161
- apiKey: true,
162
- };
198
+ function defaults(): EfficiencyConfig {
199
+ return {
200
+ version: 1,
201
+ onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact },
202
+ observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack },
203
+ evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer },
204
+ jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev },
205
+ };
206
+ }
163
207
 
164
208
  export interface ValidateConfigResult {
165
209
  ok: boolean;
@@ -168,301 +212,124 @@ export interface ValidateConfigResult {
168
212
  }
169
213
 
170
214
  export function validateEfficiencyConfig(raw: unknown): ValidateConfigResult {
215
+ const config = defaults();
171
216
  if (!isPlainObject(raw)) {
172
- return {
173
- ok: false,
174
- config: {
175
- version: 1,
176
- onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact, enabled: false },
177
- observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack, enabled: false },
178
- evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer, enabled: false },
179
- jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev, enabled: false },
180
- },
181
- issues: ['efficiency config 必须是 JSON 对象'],
182
- };
217
+ for (const section of SECTIONS) config[section.name].enabled = false;
218
+ return { ok: false, config, issues: ['efficiency config 必须是 JSON 对象'] };
183
219
  }
184
220
 
185
- const topIssues: string[] = [];
186
- const occIssues: string[] = [];
187
- const obsIssues: string[] = [];
188
- const eprIssues: string[] = [];
189
- const jevIssues: string[] = [];
190
-
191
- for (const k of Object.keys(raw)) {
192
- if (!TOP_LEVEL_KEYS.has(k)) {
193
- topIssues.push(`未知顶层配置项: "${k}"`);
194
- }
221
+ const issues: string[] = [];
222
+ for (const key of Object.keys(raw)) {
223
+ if (key !== 'version' && !SECTIONS.some((s) => s.name === key)) issues.push(`未知顶层配置项: "${key}"`);
195
224
  }
196
-
197
225
  if (raw.version !== undefined && raw.version !== 1) {
198
- topIssues.push(`version 必须是 1 (当前: ${JSON.stringify(raw.version)})`);
199
- }
200
-
201
- // Section 1: onlineContextCompact
202
- let occ: OnlineContextCompactConfig = { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact };
203
- if (raw.onlineContextCompact !== undefined) {
204
- if (!isPlainObject(raw.onlineContextCompact)) {
205
- occIssues.push('onlineContextCompact 必须是 JSON 对象');
206
- } else {
207
- const o = raw.onlineContextCompact;
208
- for (const k of Object.keys(o)) {
209
- if (!OCC_KEYS.has(k)) occIssues.push(`onlineContextCompact 未知配置项: "${k}"`);
210
- }
211
- if (o.enabled !== undefined) {
212
- if (typeof o.enabled !== 'boolean') occIssues.push('onlineContextCompact.enabled 必须是 boolean');
213
- else occ.enabled = o.enabled;
214
- }
215
- if (o.logEnabled !== undefined) {
216
- if (typeof o.logEnabled !== 'boolean') occIssues.push('onlineContextCompact.logEnabled 必须是 boolean');
217
- else occ.logEnabled = o.logEnabled;
218
- }
219
- if (o.cacheWriteReadRatio !== undefined) {
220
- if (o.cacheWriteReadRatio === 'auto') {
221
- occ.cacheWriteReadRatio = 'auto';
222
- } else if (typeof o.cacheWriteReadRatio === 'number' && Number.isFinite(o.cacheWriteReadRatio) && o.cacheWriteReadRatio >= 0) {
223
- occ.cacheWriteReadRatio = o.cacheWriteReadRatio;
224
- } else {
225
- occIssues.push('onlineContextCompact.cacheWriteReadRatio 必须是 "auto" 或非负有限数');
226
- }
227
- }
228
- if (o.keepRecentTokens !== undefined) {
229
- if (typeof o.keepRecentTokens === 'number' && Number.isInteger(o.keepRecentTokens) && o.keepRecentTokens >= 1000) {
230
- occ.keepRecentTokens = o.keepRecentTokens;
231
- } else {
232
- occIssues.push('onlineContextCompact.keepRecentTokens 必须是 >= 1000 的整数');
233
- }
234
- }
235
- if (o.firstCompactionRequestScale !== undefined) {
236
- if (typeof o.firstCompactionRequestScale === 'number' && Number.isFinite(o.firstCompactionRequestScale) && o.firstCompactionRequestScale >= 1.0) {
237
- occ.firstCompactionRequestScale = o.firstCompactionRequestScale;
238
- } else {
239
- occIssues.push('onlineContextCompact.firstCompactionRequestScale 必须是 >= 1.0 的数字');
240
- }
241
- }
242
- if (o.subsequentCompactionMargin !== undefined) {
243
- if (typeof o.subsequentCompactionMargin === 'number' && Number.isFinite(o.subsequentCompactionMargin) && o.subsequentCompactionMargin >= 1.0) {
244
- occ.subsequentCompactionMargin = o.subsequentCompactionMargin;
245
- } else {
246
- occIssues.push('onlineContextCompact.subsequentCompactionMargin 必须是 >= 1.0 的数字');
247
- }
248
- }
249
- }
226
+ issues.push(`version 必须是 1 (当前: ${JSON.stringify(raw.version)})`);
250
227
  }
251
-
252
- // Section 2: observationPack
253
- let obs: ObservationPackConfig = { ...DEFAULT_EFFICIENCY_CONFIG.observationPack };
254
- if (raw.observationPack !== undefined) {
255
- if (!isPlainObject(raw.observationPack)) {
256
- obsIssues.push('observationPack 必须是 JSON 对象');
257
- } else {
258
- const o = raw.observationPack;
259
- for (const k of Object.keys(o)) {
260
- if (!OBS_KEYS.has(k)) obsIssues.push(`observationPack 未知配置项: "${k}"`);
261
- }
262
- if (o.enabled !== undefined) {
263
- if (typeof o.enabled !== 'boolean') obsIssues.push('observationPack.enabled 必须是 boolean');
264
- else obs.enabled = o.enabled;
265
- }
266
- if (o.logEnabled !== undefined) {
267
- if (typeof o.logEnabled !== 'boolean') obsIssues.push('observationPack.logEnabled 必须是 boolean');
268
- else obs.logEnabled = o.logEnabled;
269
- }
270
- if (o.thresholdBytes !== undefined) {
271
- if (typeof o.thresholdBytes === 'number' && Number.isInteger(o.thresholdBytes) && o.thresholdBytes >= 1024) {
272
- obs.thresholdBytes = o.thresholdBytes;
273
- } else {
274
- obsIssues.push('observationPack.thresholdBytes 必须是 >= 1024 的整数');
275
- }
276
- }
277
- if (o.fullSends !== undefined) {
278
- if (typeof o.fullSends === 'number' && Number.isInteger(o.fullSends) && o.fullSends >= 1) {
279
- obs.fullSends = o.fullSends;
280
- } else {
281
- obsIssues.push('observationPack.fullSends 必须是 >= 1 的整数');
282
- }
283
- }
284
- if (o.recallChunkBytes !== undefined) {
285
- if (typeof o.recallChunkBytes === 'number' && Number.isInteger(o.recallChunkBytes) && o.recallChunkBytes >= 1024) {
286
- obs.recallChunkBytes = o.recallChunkBytes;
287
- } else {
288
- obsIssues.push('observationPack.recallChunkBytes 必须是 >= 1024 的整数');
289
- }
290
- }
291
- if (o.excerptBytes !== undefined) {
292
- if (typeof o.excerptBytes === 'number' && Number.isInteger(o.excerptBytes) && o.excerptBytes >= 128) {
293
- obs.excerptBytes = o.excerptBytes;
294
- } else {
295
- obsIssues.push('observationPack.excerptBytes 必须是 >= 128 的整数');
296
- }
297
- }
298
- }
299
- }
300
-
301
- // Section 3: evidencePreservingReducer
302
- let epr: EvidencePreservingReducerConfig = { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer };
303
- if (raw.evidencePreservingReducer !== undefined) {
304
- if (!isPlainObject(raw.evidencePreservingReducer)) {
305
- eprIssues.push('evidencePreservingReducer 必须是 JSON 对象');
306
- } else {
307
- const o = raw.evidencePreservingReducer;
308
- for (const k of Object.keys(o)) {
309
- if (!EPR_KEYS.has(k)) eprIssues.push(`evidencePreservingReducer 未知配置项: "${k}"`);
310
- }
311
- if (o.enabled !== undefined) {
312
- if (typeof o.enabled !== 'boolean') eprIssues.push('evidencePreservingReducer.enabled 必须是 boolean');
313
- else epr.enabled = o.enabled;
314
- }
315
- if (o.logEnabled !== undefined) {
316
- if (typeof o.logEnabled !== 'boolean') eprIssues.push('evidencePreservingReducer.logEnabled 必须是 boolean');
317
- else epr.logEnabled = o.logEnabled;
318
- }
319
- if (o.model !== undefined) {
320
- if (typeof o.model === 'string' && o.model.trim()) {
321
- epr.model = o.model.trim();
322
- } else if (o.model === '' || o.model === null) {
323
- epr.model = undefined;
324
- } else {
325
- eprIssues.push('evidencePreservingReducer.model 必须是非空字符串');
326
- }
327
- }
328
- if (o.minBytes !== undefined) {
329
- if (typeof o.minBytes === 'number' && Number.isInteger(o.minBytes) && o.minBytes >= 512) {
330
- epr.minBytes = o.minBytes;
331
- } else {
332
- eprIssues.push('evidencePreservingReducer.minBytes 必须是 >= 512 的整数');
333
- }
334
- }
335
- if (o.maxChars !== undefined) {
336
- if (typeof o.maxChars === 'number' && Number.isInteger(o.maxChars) && o.maxChars >= 1000) {
337
- epr.maxChars = o.maxChars;
338
- } else {
339
- eprIssues.push('evidencePreservingReducer.maxChars 必须是 >= 1000 的整数');
340
- }
341
- }
342
- if (o.maxOutputTokens !== undefined) {
343
- if (typeof o.maxOutputTokens === 'number' && Number.isInteger(o.maxOutputTokens) && o.maxOutputTokens >= 128) {
344
- epr.maxOutputTokens = o.maxOutputTokens;
345
- } else {
346
- eprIssues.push('evidencePreservingReducer.maxOutputTokens 必须是 >= 128 的整数');
347
- }
348
- }
349
- if (o.timeoutMs !== undefined) {
350
- if (typeof o.timeoutMs === 'number' && Number.isInteger(o.timeoutMs) && o.timeoutMs >= 500) {
351
- epr.timeoutMs = o.timeoutMs;
352
- } else {
353
- eprIssues.push('evidencePreservingReducer.timeoutMs 必须是 >= 500 的整数');
354
- }
355
- }
356
- if (o.localOnly !== undefined) {
357
- if (typeof o.localOnly !== 'boolean') eprIssues.push('evidencePreservingReducer.localOnly 必须是 boolean');
358
- else epr.localOnly = o.localOnly;
359
- }
360
- }
361
- }
362
-
363
- // Section 4: jev decision layer
364
- let jev: JevConfig = { ...DEFAULT_EFFICIENCY_CONFIG.jev };
365
- if (raw.jev !== undefined) {
366
- if (!isPlainObject(raw.jev)) {
367
- jevIssues.push('jev 必须是 JSON 对象');
368
- } else {
369
- const o = raw.jev;
370
- for (const k of Object.keys(o)) {
371
- if (JEV_KEYS[k] !== true) jevIssues.push(`jev 未知配置项: "${k}"`);
372
- }
373
- if (o.enabled !== undefined) {
374
- if (typeof o.enabled !== 'boolean') jevIssues.push('jev.enabled 必须是 boolean');
375
- else jev.enabled = o.enabled;
376
- }
377
- if (o.logEnabled !== undefined) {
378
- if (typeof o.logEnabled !== 'boolean') jevIssues.push('jev.logEnabled 必须是 boolean');
379
- else jev.logEnabled = o.logEnabled;
380
- }
381
- if (o.baseUrl !== undefined) {
382
- if (typeof o.baseUrl === 'string' && o.baseUrl.trim()) {
383
- jev.baseUrl = o.baseUrl.trim();
384
- } else if (o.baseUrl === '' || o.baseUrl === null) {
385
- jev.baseUrl = undefined;
386
- } else {
387
- jevIssues.push('jev.baseUrl 必须是非空字符串');
388
- }
389
- }
390
- if (o.model !== undefined) {
391
- if (typeof o.model === 'string' && o.model.trim()) {
392
- jev.model = o.model.trim();
393
- } else {
394
- jevIssues.push('jev.model 必须是非空字符串');
395
- }
396
- }
397
- if (o.timeoutMs !== undefined) {
398
- if (typeof o.timeoutMs === 'number' && Number.isInteger(o.timeoutMs) && o.timeoutMs >= 500) {
399
- jev.timeoutMs = o.timeoutMs;
400
- } else {
401
- jevIssues.push('jev.timeoutMs 必须是 >= 500 的整数');
402
- }
403
- }
404
- if (o.minConfidence !== undefined) {
405
- if (typeof o.minConfidence === 'number' && Number.isFinite(o.minConfidence) && o.minConfidence >= 0 && o.minConfidence <= 1) {
406
- jev.minConfidence = o.minConfidence;
407
- } else {
408
- jevIssues.push('jev.minConfidence 必须是 0 到 1 之间的数字');
228
+ const topLevelInvalid = issues.length > 0;
229
+
230
+ for (const section of SECTIONS) {
231
+ const issueCountBefore = issues.length;
232
+ const target = config[section.name] as unknown as Record<string, unknown>;
233
+ const rawSection = raw[section.name];
234
+ if (rawSection !== undefined) {
235
+ if (!isPlainObject(rawSection)) {
236
+ issues.push(`${section.name} 必须是 JSON 对象`);
237
+ } else {
238
+ for (const key of Object.keys(rawSection)) {
239
+ if (!section.fields.some((f) => f.key === key)) issues.push(`${section.name} 未知配置项: "${key}"`);
409
240
  }
410
- }
411
- if (o.apiKey !== undefined) {
412
- if (typeof o.apiKey === 'string' && o.apiKey.trim()) {
413
- jev.apiKey = o.apiKey.trim();
414
- } else if (o.apiKey === '' || o.apiKey === null) {
415
- jev.apiKey = undefined;
416
- } else {
417
- jevIssues.push('jev.apiKey 必须是非空字符串');
241
+ for (const field of section.fields) {
242
+ if (!applyField(target, field, rawSection[field.key])) issues.push(issueFor(section.name, field));
418
243
  }
419
244
  }
420
245
  }
246
+ // Fail-open: an invalid section (or an invalid top level) can never enable a mechanism.
247
+ if (topLevelInvalid || issues.length > issueCountBefore) config[section.name].enabled = false;
421
248
  }
422
249
 
423
- // Fail-open: any section with validation issues has its enabled flag forced to false
424
- if (topIssues.length > 0) {
425
- occ.enabled = false;
426
- obs.enabled = false;
427
- epr.enabled = false;
428
- jev.enabled = false;
429
- }
430
- if (occIssues.length > 0) {
431
- occ.enabled = false;
432
- }
433
- if (obsIssues.length > 0) {
434
- obs.enabled = false;
435
- }
436
- if (eprIssues.length > 0) {
437
- epr.enabled = false;
438
- }
439
- if (jevIssues.length > 0) {
440
- jev.enabled = false;
441
- }
442
-
443
- const issues = [...topIssues, ...occIssues, ...obsIssues, ...eprIssues, ...jevIssues];
444
-
445
- return {
446
- ok: issues.length === 0,
447
- config: {
448
- version: 1,
449
- onlineContextCompact: occ,
450
- observationPack: obs,
451
- evidencePreservingReducer: epr,
452
- jev,
453
- },
454
- issues,
455
- };
250
+ return { ok: issues.length === 0, config, issues };
456
251
  }
457
252
 
458
- function parseEnvBool(val: string | undefined): boolean | undefined {
459
- if (!val) return undefined;
253
+ function parseEnvBool(val: string): boolean | undefined {
460
254
  const s = val.trim().toLowerCase();
461
255
  if (s === '1' || s === 'true' || s === 'on') return true;
462
256
  if (s === '0' || s === 'false' || s === 'off') return false;
463
257
  return undefined;
464
258
  }
465
259
 
260
+ interface EnvOverride {
261
+ readonly env: string;
262
+ readonly section: SectionSpec['name'];
263
+ readonly key: string;
264
+ readonly kind: 'bool' | 'string' | 'ratio' | 'int' | 'unit';
265
+ readonly min?: number;
266
+ /** Set only when the config left the key unset (foreign-convention fallback). */
267
+ readonly fallbackOnly?: boolean;
268
+ }
269
+
270
+ /** Highest-precedence layer; an empty value counts as unset everywhere. */
271
+ const ENV_OVERRIDES: readonly EnvOverride[] = [
272
+ { env: 'PI_HERDR_COMPACT_ENABLE', section: 'onlineContextCompact', key: 'enabled', kind: 'bool' },
273
+ { env: 'PI_HERDR_COMPACT_LOG', section: 'onlineContextCompact', key: 'logEnabled', kind: 'bool' },
274
+ { env: 'PI_HERDR_CACHE_RATIO', section: 'onlineContextCompact', key: 'cacheWriteReadRatio', kind: 'ratio' },
275
+ { env: 'PI_HERDR_OBS_PACK_ENABLE', section: 'observationPack', key: 'enabled', kind: 'bool' },
276
+ { env: 'PI_HERDR_OBS_PACK_LOG', section: 'observationPack', key: 'logEnabled', kind: 'bool' },
277
+ { env: 'PI_HERDR_REDUCER_ENABLE', section: 'evidencePreservingReducer', key: 'enabled', kind: 'bool' },
278
+ { env: 'PI_HERDR_REDUCER_LOG', section: 'evidencePreservingReducer', key: 'logEnabled', kind: 'bool' },
279
+ { env: 'PI_HERDR_REDUCER_MODEL', section: 'evidencePreservingReducer', key: 'model', kind: 'string' },
280
+ // jev has no legacy spelling: PIER_JEV_* is canonical from the start.
281
+ { env: 'PIER_JEV_ENABLE', section: 'jev', key: 'enabled', kind: 'bool' },
282
+ { env: 'PIER_JEV_LOG', section: 'jev', key: 'logEnabled', kind: 'bool' },
283
+ { env: 'PIER_JEV_MODEL', section: 'jev', key: 'model', kind: 'string' },
284
+ { env: 'PIER_JEV_BASE_URL', section: 'jev', key: 'baseUrl', kind: 'string' },
285
+ { env: 'PIER_JEV_TIMEOUT_MS', section: 'jev', key: 'timeoutMs', kind: 'int', min: 500 },
286
+ { env: 'PIER_JEV_MIN_CONFIDENCE', section: 'jev', key: 'minConfidence', kind: 'unit' },
287
+ { env: 'PIER_JEV_API_KEY', section: 'jev', key: 'apiKey', kind: 'string' },
288
+ // TYPESAFE_API_KEY is a foreign (SDK) convention, so it ranks below the config file too.
289
+ { env: 'TYPESAFE_API_KEY', section: 'jev', key: 'apiKey', kind: 'string', fallbackOnly: true },
290
+ ];
291
+
292
+ function applyEnvOverrides(
293
+ config: EfficiencyConfig,
294
+ env: Record<string, string | undefined>,
295
+ warn: (message: string) => void,
296
+ ): void {
297
+ for (const override of ENV_OVERRIDES) {
298
+ const raw = env[override.env];
299
+ if (raw === undefined || raw === '') continue;
300
+ const target = config[override.section] as unknown as Record<string, unknown>;
301
+ if (override.fallbackOnly && target[override.key] !== undefined) continue;
302
+
303
+ if (override.kind === 'bool') {
304
+ const parsed = parseEnvBool(raw);
305
+ if (parsed !== undefined) target[override.key] = parsed;
306
+ continue;
307
+ }
308
+ if (override.kind === 'string') {
309
+ if (raw.trim()) target[override.key] = raw.trim();
310
+ continue;
311
+ }
312
+ if (override.kind === 'ratio' && raw.trim().toLowerCase() === 'auto') {
313
+ target[override.key] = 'auto';
314
+ continue;
315
+ }
316
+ const num = Number(raw);
317
+ const valid =
318
+ override.kind === 'ratio' ? Number.isFinite(num) && num >= 0
319
+ : override.kind === 'unit' ? Number.isFinite(num) && num >= 0 && num <= 1
320
+ : Number.isInteger(num) && num >= (override.min ?? 0);
321
+ if (valid) {
322
+ target[override.key] = num;
323
+ continue;
324
+ }
325
+ const requirement =
326
+ override.kind === 'unit' ? '(需 0 到 1)'
327
+ : override.kind === 'int' ? `(需 >= ${override.min} 的整数)`
328
+ : '';
329
+ warn(`无效环境变量 ${override.env}="${raw}"${requirement},忽略`);
330
+ }
331
+ }
332
+
466
333
  export interface ResolveConfigOptions {
467
334
  workspaceConfig?: unknown;
468
335
  userConfig?: unknown;
@@ -473,128 +340,28 @@ export interface ResolveConfigOptions {
473
340
 
474
341
  export function resolveEfficiencyConfig(opts: ResolveConfigOptions = {}): EfficiencyConfig {
475
342
  const env = opts.env ?? process.env;
476
- const warn = opts.onWarning ?? ((msg) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
343
+ const warn = opts.onWarning ?? ((msg: string) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
477
344
  const isTrusted = opts.isProjectTrusted ?? false;
478
345
 
479
- let baseRaw: unknown = undefined;
480
-
346
+ // An untrusted project's file is ignored wholesale (never merged with the user layer).
347
+ let baseRaw: unknown = opts.userConfig;
481
348
  if (opts.workspaceConfig !== undefined) {
482
- if (isTrusted) {
483
- baseRaw = opts.workspaceConfig;
484
- } else {
485
- warn('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
486
- baseRaw = opts.userConfig;
487
- }
488
- } else {
489
- baseRaw = opts.userConfig;
349
+ if (isTrusted) baseRaw = opts.workspaceConfig;
350
+ else warn('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
490
351
  }
491
352
 
492
- let resolved: EfficiencyConfig;
353
+ let resolved = defaults();
493
354
  if (baseRaw !== undefined) {
494
355
  const validated = validateEfficiencyConfig(baseRaw);
495
- if (!validated.ok) {
496
- warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
497
- }
356
+ if (!validated.ok) warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
498
357
  resolved = validated.config;
499
- } else {
500
- resolved = {
501
- version: 1,
502
- onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact },
503
- observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack },
504
- evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer },
505
- jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev },
506
- };
507
- }
508
-
509
- // Apply environment variable overrides (highest precedence)
510
- const occEnabled = parseEnvBool(env.PI_HERDR_COMPACT_ENABLE);
511
- if (occEnabled !== undefined) resolved.onlineContextCompact.enabled = occEnabled;
512
-
513
- const occLog = parseEnvBool(env.PI_HERDR_COMPACT_LOG);
514
- if (occLog !== undefined) resolved.onlineContextCompact.logEnabled = occLog;
515
-
516
- const cacheRatioRaw = env.PI_HERDR_CACHE_RATIO;
517
- if (cacheRatioRaw !== undefined) {
518
- if (cacheRatioRaw.trim().toLowerCase() === 'auto') {
519
- resolved.onlineContextCompact.cacheWriteReadRatio = 'auto';
520
- } else {
521
- const num = Number(cacheRatioRaw);
522
- if (Number.isFinite(num) && num >= 0) {
523
- resolved.onlineContextCompact.cacheWriteReadRatio = num;
524
- } else {
525
- warn(`无效环境变量 PI_HERDR_CACHE_RATIO="${cacheRatioRaw}",忽略`);
526
- }
527
- }
528
- }
529
-
530
- const obsEnabled = parseEnvBool(env.PI_HERDR_OBS_PACK_ENABLE);
531
- if (obsEnabled !== undefined) resolved.observationPack.enabled = obsEnabled;
532
-
533
- const obsLog = parseEnvBool(env.PI_HERDR_OBS_PACK_LOG);
534
- if (obsLog !== undefined) resolved.observationPack.logEnabled = obsLog;
535
-
536
- const redEnabled = parseEnvBool(env.PI_HERDR_REDUCER_ENABLE);
537
- if (redEnabled !== undefined) resolved.evidencePreservingReducer.enabled = redEnabled;
538
-
539
- const redLog = parseEnvBool(env.PI_HERDR_REDUCER_LOG);
540
- if (redLog !== undefined) resolved.evidencePreservingReducer.logEnabled = redLog;
541
-
542
- const redModel = env.PI_HERDR_REDUCER_MODEL;
543
- if (redModel !== undefined && redModel.trim()) {
544
- resolved.evidencePreservingReducer.model = redModel.trim();
545
- }
546
-
547
- // jev decision layer (new mechanism: canonical PIER_JEV_* only, no legacy spelling exists).
548
- // TYPESAFE_API_KEY is a foreign-convention fallback (TypeSafe SDK reads it), so it ranks
549
- // below pier's own env and below an explicit config-file key.
550
- const jevEnabled = parseEnvBool(env.PIER_JEV_ENABLE);
551
- if (jevEnabled !== undefined) resolved.jev.enabled = jevEnabled;
552
-
553
- const jevLog = parseEnvBool(env.PIER_JEV_LOG);
554
- if (jevLog !== undefined) resolved.jev.logEnabled = jevLog;
555
-
556
- const jevModel = env.PIER_JEV_MODEL;
557
- if (jevModel !== undefined && jevModel.trim()) {
558
- resolved.jev.model = jevModel.trim();
559
- }
560
-
561
- const jevBase = env.PIER_JEV_BASE_URL;
562
- if (jevBase !== undefined && jevBase.trim()) {
563
- resolved.jev.baseUrl = jevBase.trim();
564
- }
565
-
566
- const jevTimeoutRaw = env.PIER_JEV_TIMEOUT_MS;
567
- if (jevTimeoutRaw !== undefined) {
568
- const num = Number(jevTimeoutRaw);
569
- if (Number.isInteger(num) && num >= 500) {
570
- resolved.jev.timeoutMs = num;
571
- } else {
572
- warn(`无效环境变量 PIER_JEV_TIMEOUT_MS="${jevTimeoutRaw}"(需 >= 500 的整数),忽略`);
573
- }
574
- }
575
-
576
- const jevMinConfRaw = env.PIER_JEV_MIN_CONFIDENCE;
577
- if (jevMinConfRaw !== undefined) {
578
- const num = Number(jevMinConfRaw);
579
- if (Number.isFinite(num) && num >= 0 && num <= 1) {
580
- resolved.jev.minConfidence = num;
581
- } else {
582
- warn(`无效环境变量 PIER_JEV_MIN_CONFIDENCE="${jevMinConfRaw}"(需 0 到 1),忽略`);
583
- }
584
- }
585
-
586
- const jevKey = env.PIER_JEV_API_KEY;
587
- if (jevKey !== undefined && jevKey.trim()) {
588
- resolved.jev.apiKey = jevKey.trim();
589
- }
590
- if (!resolved.jev.apiKey && env.TYPESAFE_API_KEY && env.TYPESAFE_API_KEY.trim()) {
591
- resolved.jev.apiKey = env.TYPESAFE_API_KEY.trim();
592
358
  }
593
359
 
360
+ applyEnvOverrides(resolved, env, warn);
594
361
  return resolved;
595
362
  }
596
363
 
597
- export function defaultUserEfficiencyConfigDir(): string {
364
+ function defaultUserEfficiencyConfigDir(): string {
598
365
  return join(homedir(), '.pi', 'agent', 'herdr-pi');
599
366
  }
600
367
 
@@ -606,6 +373,44 @@ export function defaultUserEfficiencyConfigFile(): string {
606
373
  return join(defaultUserEfficiencyConfigDir(), 'config.json');
607
374
  }
608
375
 
376
+ interface PiCompactionSettings {
377
+ enabled?: boolean;
378
+ keepRecentTokens?: number;
379
+ }
380
+
381
+ function readCompactionSettings(filePath: string): PiCompactionSettings {
382
+ const parsed = readJsonConfig(filePath);
383
+ if (!isPlainObject(parsed) || !isPlainObject(parsed.compaction)) return {};
384
+ const { compaction } = parsed;
385
+ const result: PiCompactionSettings = {};
386
+ if (typeof compaction.enabled === 'boolean') result.enabled = compaction.enabled;
387
+ if (typeof compaction.keepRecentTokens === 'number') result.keepRecentTokens = compaction.keepRecentTokens;
388
+ return result;
389
+ }
390
+
391
+ function readJsonConfig(filePath: string, onError?: (error: unknown) => void): unknown {
392
+ if (!existsSync(filePath)) return undefined;
393
+ try {
394
+ return JSON.parse(readFileSync(filePath, 'utf8')) as unknown;
395
+ } catch (error) {
396
+ onError?.(error);
397
+ return undefined;
398
+ }
399
+ }
400
+
401
+ /** Pi's own compaction settings: global, plus the project file when the project is trusted. */
402
+ export function loadPiNativeCompactionSettings(opts: {
403
+ cwd?: string;
404
+ isProjectTrusted?: boolean;
405
+ agentDir?: string;
406
+ } = {}): PiCompactionSettings {
407
+ const cwd = opts.cwd ?? process.cwd();
408
+ const agentDir = opts.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
409
+ const globalSettings = readCompactionSettings(join(agentDir, 'settings.json'));
410
+ if (!opts.isProjectTrusted) return globalSettings;
411
+ return { ...globalSettings, ...readCompactionSettings(join(cwd, '.pi', 'settings.json')) };
412
+ }
413
+
609
414
  export function loadEfficiencyConfigFromDisk(opts: {
610
415
  cwd?: string;
611
416
  isProjectTrusted?: boolean;
@@ -613,13 +418,11 @@ export function loadEfficiencyConfigFromDisk(opts: {
613
418
  onWarning?: (msg: string) => void;
614
419
  /** Override the Pi agent dir used to read native `settings.json` (tests/isolation). */
615
420
  agentDir?: string;
616
- /** Override the user-level efficiency config file path (tests/isolation). */
617
421
  userConfigPath?: string;
618
422
  } = {}): EfficiencyConfig {
619
423
  const cwd = opts.cwd ?? process.cwd();
620
424
  const wsPath = defaultWorkspaceEfficiencyConfigFile(cwd);
621
425
  const userPath = opts.userConfigPath ?? defaultUserEfficiencyConfigFile();
622
-
623
426
  const warnFile = (label: string, filePath: string, error: unknown): void => {
624
427
  opts.onWarning?.(`${label} ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
625
428
  };
@@ -639,23 +442,20 @@ export function loadEfficiencyConfigFromDisk(opts: {
639
442
  agentDir: opts.agentDir,
640
443
  });
641
444
 
642
- // If user disabled compaction globally in pi settings, and env didn't explicitly force OCC enable, respect it!
445
+ // pi's global compaction switch wins over an explicitly enabled OCC — only an env force overrides it.
643
446
  const env = opts.env ?? process.env;
644
447
  if (piSettings.enabled === false && !env.PI_HERDR_COMPACT_ENABLE) {
645
448
  resolved.onlineContextCompact.enabled = false;
646
449
  }
647
-
648
- const hasExplicitKeepRecent = [
649
- opts.isProjectTrusted ? workspaceConfig : undefined,
650
- userConfig,
651
- ].some((config) => isPlainObject(config)
652
- && isPlainObject(config.onlineContextCompact)
653
- && config.onlineContextCompact.keepRecentTokens !== undefined);
654
-
450
+ const hasExplicitKeepRecent = [opts.isProjectTrusted ? workspaceConfig : undefined, userConfig].some(
451
+ (config) =>
452
+ isPlainObject(config)
453
+ && isPlainObject(config.onlineContextCompact)
454
+ && config.onlineContextCompact.keepRecentTokens !== undefined,
455
+ );
456
+ // pi's keepRecentTokens is inherited only while the efficiency config does not set its own.
655
457
  if (!hasExplicitKeepRecent && typeof piSettings.keepRecentTokens === 'number') {
656
458
  resolved.onlineContextCompact.keepRecentTokens = piSettings.keepRecentTokens;
657
459
  }
658
-
659
460
  return resolved;
660
461
  }
661
-