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