pi-pier 0.2.7 → 0.3.0

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 (72) hide show
  1. package/README.md +20 -7
  2. package/package.json +7 -2
  3. package/schemas/efficiency-config.schema.json +77 -0
  4. package/skills/pi-herdr-workflow/SKILL.md +12 -0
  5. package/src/ansi-text.ts +83 -0
  6. package/src/ask-multi.ts +260 -0
  7. package/src/ask-user.ts +60 -10
  8. package/src/bootstrap.ts +5 -2
  9. package/src/compact-coordinator.ts +454 -0
  10. package/src/compact-economics-core.ts +396 -0
  11. package/src/config-catalog-core.ts +452 -0
  12. package/src/config-command.ts +181 -0
  13. package/src/config-guide.ts +361 -0
  14. package/src/core/grid-shape.ts +4 -2
  15. package/src/core/heat-plan.ts +160 -0
  16. package/src/core/observation.ts +555 -0
  17. package/src/core/subagent.ts +187 -393
  18. package/src/core/terminal.ts +121 -47
  19. package/src/core/todo.ts +25 -11
  20. package/src/dashboard-command.ts +89 -0
  21. package/src/efficiency-config-core.ts +661 -0
  22. package/src/efficiency-config-io.ts +53 -0
  23. package/src/efficiency-store.ts +312 -0
  24. package/src/focus-poller.ts +244 -0
  25. package/src/gc-core.ts +85 -0
  26. package/src/git-adapter.ts +12 -3
  27. package/src/herdr-client.ts +292 -36
  28. package/src/index-locks.ts +27 -9
  29. package/src/index-master.ts +5 -1
  30. package/src/index-notices.ts +13 -2
  31. package/src/index-pipe.ts +5 -0
  32. package/src/index-worker.ts +3 -1
  33. package/src/index.ts +330 -34
  34. package/src/jev-client.ts +146 -0
  35. package/src/jev-core.ts +381 -0
  36. package/src/lock-core.ts +105 -16
  37. package/src/observation-core.ts +248 -0
  38. package/src/pier-options.ts +96 -0
  39. package/src/pipe-channel.ts +52 -7
  40. package/src/reducer-core.ts +358 -0
  41. package/src/reducer-invoker.ts +392 -0
  42. package/src/renderers.ts +198 -0
  43. package/src/role-loader.ts +2 -1
  44. package/src/role-manifest.ts +0 -4
  45. package/src/runtime-policy.ts +5 -8
  46. package/src/session-tail.ts +60 -5
  47. package/src/settle-wake-core.ts +3 -1
  48. package/src/slim-frame.ts +4 -2
  49. package/src/storage-layout.ts +23 -0
  50. package/src/subagent-core.ts +3 -1
  51. package/src/subagent-execute.ts +328 -0
  52. package/src/subagent-gc.ts +33 -13
  53. package/src/subagent-launch.ts +4 -0
  54. package/src/subagent-list-action.ts +69 -0
  55. package/src/subagent-output-action.ts +104 -0
  56. package/src/subagent-output-adapter.ts +27 -0
  57. package/src/subagent-output-core.ts +263 -0
  58. package/src/subagent-poll-loop.ts +59 -39
  59. package/src/subagent-poller.ts +46 -0
  60. package/src/subagent-ready.ts +89 -0
  61. package/src/subagent-registry.ts +70 -0
  62. package/src/subagent-resolution.ts +41 -0
  63. package/src/subagent-scope.ts +3 -1
  64. package/src/subagent-session-io.ts +115 -32
  65. package/src/subagent-spawn.ts +153 -8
  66. package/src/swallow.ts +75 -0
  67. package/src/terminal-core.ts +82 -10
  68. package/src/todo-core.ts +5 -4
  69. package/src/todo-reminder-core.ts +8 -1
  70. package/src/todos-service.ts +18 -6
  71. package/src/tool-error.ts +26 -0
  72. package/src/vocab.ts +7 -4
@@ -0,0 +1,396 @@
1
+ /**
2
+ * D100 Online Context Compact Economics & Feasibility Core.
3
+ *
4
+ * Implements pure economics decision-making, request horizon estimation,
5
+ * cache debt tracking, and native compaction feasibility preflights.
6
+ *
7
+ * Zero-dependency / pure algorithm core (except Pi's public cut point helper).
8
+ */
9
+
10
+ import {
11
+ findCutPoint,
12
+ sessionEntryToContextMessages,
13
+ type SessionEntry,
14
+ } from '@earendil-works/pi-coding-agent';
15
+
16
+ export interface CompactionEconomics {
17
+ readonly remainingRequestScale: number;
18
+ readonly remainingRequestStddevK: number;
19
+ readonly windowReserveTokens: number;
20
+ readonly firstCompactionRequestScale: number;
21
+ readonly subsequentCompactionMargin: number;
22
+ }
23
+
24
+ export const DEFAULT_COMPACTION_ECONOMICS: CompactionEconomics = Object.freeze({
25
+ remainingRequestScale: 1.0,
26
+ remainingRequestStddevK: 0.0,
27
+ windowReserveTokens: 16_384,
28
+ firstCompactionRequestScale: 2.0,
29
+ subsequentCompactionMargin: 1.5,
30
+ });
31
+
32
+ /**
33
+ * Token-account cache ratio: a compaction request re-reads the whole current
34
+ * context once (writeTokens), so breakeven = writeTokens / savingTokens ⇔ ratio 2.
35
+ * `auto` fallback when no usable cache pricing exists (2026-09-17 review: every
36
+ * model actually in use reports zero cacheWrite; the old `null` disabled OCC
37
+ * for all of them — 61/61 decisions `cache_ratio_unavailable`, zero compactions).
38
+ */
39
+ export const TOKEN_ACCOUNT_CACHE_RATIO = 2.0;
40
+
41
+ /** Provider-family `auto` fallbacks (implicit-cache families without a write SKU). */
42
+ const PROVIDER_FAMILY_CACHE_RATIOS: Readonly<Record<string, number>> = Object.freeze({
43
+ gemini: 4,
44
+ grok: 10,
45
+ deepseek: 10,
46
+ });
47
+
48
+ export type CompactionReason =
49
+ | 'economic'
50
+ | 'window_protection'
51
+ | 'deferred_economic'
52
+ | 'deferred_subsequent_margin'
53
+ | 'deferred_carried_debt'
54
+ | 'horizon_unavailable'
55
+ | 'cache_ratio_unavailable'
56
+ | 'native_not_compactable'
57
+ | 'non_positive_saving';
58
+
59
+ export interface RequestHorizonEstimate {
60
+ readonly completedBoundaryRequestCounts: readonly number[];
61
+ readonly requestsPerBoundaryMean: number;
62
+ readonly requestsPerBoundaryLowerBound: number;
63
+ readonly unboundedExpectedRemainingRequests: number;
64
+ readonly averageContextTokenIncrement: number | null;
65
+ readonly windowRequestUpperBound: number | null;
66
+ readonly expectedRemainingRequests: number;
67
+ }
68
+
69
+ export interface CompactionDecision {
70
+ readonly writeTokens: number;
71
+ readonly archiveTokens: number;
72
+ readonly memoTokens: number;
73
+ readonly contextTokens: number;
74
+ readonly completedBoundaryRequestCounts: readonly number[] | null;
75
+ readonly requestsPerBoundaryMean: number | null;
76
+ readonly requestsPerBoundaryLowerBound: number | null;
77
+ readonly unboundedExpectedRemainingRequests: number | null;
78
+ readonly averageContextTokenIncrement: number | null;
79
+ readonly windowRequestUpperBound: number | null;
80
+ readonly expectedRemainingRequests: number | null;
81
+ readonly breakevenRequests: number | null;
82
+ readonly combinedBreakevenRequests: number | null;
83
+ readonly effectiveHorizonRequests: number | null;
84
+ readonly remainingBoundaries: number;
85
+ readonly cacheWriteReadRatio: number | null;
86
+ readonly incrementalCacheCostRatio: number | null;
87
+ readonly priorCompactionCount: number;
88
+ readonly carriedDebtTokens: number;
89
+ readonly cacheDebtRepaymentTokens: number;
90
+ readonly compact: boolean;
91
+ readonly reason: CompactionReason;
92
+ }
93
+
94
+ const MINIMUM_VARIANCE_SAMPLES = 3;
95
+ const SMALL_SAMPLE_SCALE = 0.5;
96
+ /** Retained boundary-request sample cap so the mean tracks recent pacing. */
97
+ export const MAX_BOUNDARY_SAMPLES = 16;
98
+
99
+ export function estimateRemainingRequests(input: {
100
+ readonly completedBoundaryRequestCounts: readonly number[];
101
+ readonly remainingBoundaries: number;
102
+ readonly scale: number;
103
+ readonly standardDeviationK: number;
104
+ readonly contextTokens: number;
105
+ readonly contextWindowTokens: number | null;
106
+ readonly averageContextTokenIncrement: number | null;
107
+ }): RequestHorizonEstimate {
108
+ const counts = input.completedBoundaryRequestCounts;
109
+ const mean =
110
+ counts.reduce((total, count) => total + count, 0) /
111
+ Math.max(1, counts.length);
112
+ let lowerBound = mean;
113
+ if (input.standardDeviationK !== 0) {
114
+ if (counts.length < MINIMUM_VARIANCE_SAMPLES) {
115
+ lowerBound *= SMALL_SAMPLE_SCALE;
116
+ } else {
117
+ const variance = counts.reduce((total, count) => total + (count - mean) ** 2, 0);
118
+ const deviation = Math.sqrt(variance / (counts.length - 1));
119
+ lowerBound = Math.max(0, mean - input.standardDeviationK * deviation);
120
+ }
121
+ }
122
+
123
+ const unboundedExpectedRemainingRequests =
124
+ 1 + Math.floor(lowerBound * Math.max(0, input.remainingBoundaries) * input.scale);
125
+ const windowRequestUpperBound =
126
+ input.contextWindowTokens === null ||
127
+ input.averageContextTokenIncrement === null ||
128
+ input.averageContextTokenIncrement <= 0
129
+ ? null
130
+ : Math.max(
131
+ 0,
132
+ Math.floor((input.contextWindowTokens - input.contextTokens) / input.averageContextTokenIncrement),
133
+ );
134
+
135
+ return {
136
+ completedBoundaryRequestCounts: [...counts],
137
+ requestsPerBoundaryMean: mean,
138
+ requestsPerBoundaryLowerBound: lowerBound,
139
+ unboundedExpectedRemainingRequests,
140
+ averageContextTokenIncrement: input.averageContextTokenIncrement,
141
+ windowRequestUpperBound,
142
+ expectedRemainingRequests:
143
+ windowRequestUpperBound === null
144
+ ? unboundedExpectedRemainingRequests
145
+ : Math.min(unboundedExpectedRemainingRequests, windowRequestUpperBound),
146
+ };
147
+ }
148
+
149
+ export function decideCompaction(input: {
150
+ readonly writeTokens: number;
151
+ readonly archiveTokens: number;
152
+ readonly memoTokens: number;
153
+ readonly contextTokens: number;
154
+ readonly completedBoundaryRequestCounts: readonly number[] | null;
155
+ readonly remainingBoundaries: number;
156
+ readonly averageContextTokenIncrement: number | null;
157
+ readonly contextWindowTokens: number | null;
158
+ readonly priorCompactionCount: number;
159
+ readonly carriedDebtTokens: number;
160
+ readonly cacheDebtRepaymentTokens: number;
161
+ readonly cacheWriteReadRatio: number | null;
162
+ readonly economics?: CompactionEconomics;
163
+ }): CompactionDecision {
164
+ const economics = input.economics ?? DEFAULT_COMPACTION_ECONOMICS;
165
+ const horizon =
166
+ input.completedBoundaryRequestCounts === null
167
+ ? null
168
+ : estimateRemainingRequests({
169
+ completedBoundaryRequestCounts: input.completedBoundaryRequestCounts,
170
+ remainingBoundaries: input.remainingBoundaries,
171
+ scale: economics.remainingRequestScale,
172
+ standardDeviationK: economics.remainingRequestStddevK,
173
+ contextTokens: input.contextTokens,
174
+ contextWindowTokens: input.contextWindowTokens,
175
+ averageContextTokenIncrement: input.averageContextTokenIncrement,
176
+ });
177
+
178
+ const savingTokens = input.archiveTokens - input.memoTokens;
179
+ const incrementalCacheCostRatio =
180
+ input.cacheWriteReadRatio === null ? null : Math.max(0, input.cacheWriteReadRatio - 1);
181
+
182
+ const breakevenRequests =
183
+ savingTokens > 0 && incrementalCacheCostRatio !== null
184
+ ? (input.writeTokens * incrementalCacheCostRatio) / savingTokens
185
+ : null;
186
+
187
+ const combinedBreakevenRequests =
188
+ savingTokens > 0 && incrementalCacheCostRatio !== null
189
+ ? (input.carriedDebtTokens + input.writeTokens * incrementalCacheCostRatio) / savingTokens
190
+ : null;
191
+ const firstCompaction = input.priorCompactionCount === 0;
192
+ // The first-compaction relaxation survives cold-start sample noise when real work
193
+ // remains. With zero remaining boundaries the horizon is 1 (nothing left to amortize
194
+ // over); scaling it by firstCompactionRequestScale would "discover" a horizon of 2
195
+ // and burn a full-context summarization for zero future benefit (2026-09-17 review).
196
+ const relaxFirstCompaction = firstCompaction && input.remainingBoundaries > 0;
197
+ const effectiveHorizonRequests =
198
+ horizon === null
199
+ ? null
200
+ : relaxFirstCompaction
201
+ ? Math.min(
202
+ horizon.expectedRemainingRequests * economics.firstCompactionRequestScale,
203
+ horizon.windowRequestUpperBound ?? Number.POSITIVE_INFINITY,
204
+ )
205
+ : horizon.expectedRemainingRequests;
206
+
207
+ const windowProtection =
208
+ input.contextWindowTokens !== null &&
209
+ input.contextTokens >= input.contextWindowTokens - economics.windowReserveTokens;
210
+
211
+ const baseEconomic =
212
+ horizon !== null &&
213
+ horizon.expectedRemainingRequests > 0 &&
214
+ breakevenRequests !== null &&
215
+ breakevenRequests <= horizon.expectedRemainingRequests;
216
+
217
+ const firstEconomic =
218
+ firstCompaction &&
219
+ effectiveHorizonRequests !== null &&
220
+ effectiveHorizonRequests > 0 &&
221
+ breakevenRequests !== null &&
222
+ breakevenRequests <= effectiveHorizonRequests;
223
+
224
+ const subsequentMarginOpen =
225
+ !firstCompaction &&
226
+ horizon !== null &&
227
+ breakevenRequests !== null &&
228
+ breakevenRequests * economics.subsequentCompactionMargin <= horizon.expectedRemainingRequests;
229
+
230
+ const carriedDebtGateOpen =
231
+ !firstCompaction &&
232
+ horizon !== null &&
233
+ combinedBreakevenRequests !== null &&
234
+ combinedBreakevenRequests <= horizon.expectedRemainingRequests;
235
+
236
+ const economic = firstCompaction ? firstEconomic : baseEconomic && subsequentMarginOpen && carriedDebtGateOpen;
237
+ const compressible = savingTokens > 0;
238
+ const compact = compressible && (windowProtection || economic);
239
+
240
+ return {
241
+ writeTokens: input.writeTokens,
242
+ archiveTokens: input.archiveTokens,
243
+ memoTokens: input.memoTokens,
244
+ contextTokens: input.contextTokens,
245
+ ...(horizon ?? {
246
+ completedBoundaryRequestCounts: null,
247
+ requestsPerBoundaryMean: null,
248
+ requestsPerBoundaryLowerBound: null,
249
+ unboundedExpectedRemainingRequests: null,
250
+ averageContextTokenIncrement: input.averageContextTokenIncrement,
251
+ windowRequestUpperBound: null,
252
+ expectedRemainingRequests: null,
253
+ }),
254
+ breakevenRequests,
255
+ combinedBreakevenRequests,
256
+ effectiveHorizonRequests,
257
+ remainingBoundaries: input.remainingBoundaries,
258
+ cacheWriteReadRatio: input.cacheWriteReadRatio,
259
+ incrementalCacheCostRatio,
260
+ priorCompactionCount: input.priorCompactionCount,
261
+ carriedDebtTokens: input.carriedDebtTokens,
262
+ cacheDebtRepaymentTokens: input.cacheDebtRepaymentTokens,
263
+ compact,
264
+ reason: !compressible
265
+ ? 'non_positive_saving'
266
+ : windowProtection
267
+ ? 'window_protection'
268
+ : economic
269
+ ? 'economic'
270
+ : horizon === null
271
+ ? 'horizon_unavailable'
272
+ : breakevenRequests === null
273
+ ? 'cache_ratio_unavailable'
274
+ : !firstCompaction && baseEconomic && !subsequentMarginOpen
275
+ ? 'deferred_subsequent_margin'
276
+ : !firstCompaction && baseEconomic && !carriedDebtGateOpen
277
+ ? 'deferred_carried_debt'
278
+ : 'deferred_economic',
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Resolve the cache write/read cost ratio for economic compaction decisions.
284
+ *
285
+ * Priority:
286
+ * 1. Explicit numeric config → use as-is (e.g. 12.5 for Anthropic-style pricing)
287
+ * 2. Model cost metadata:
288
+ * a. cacheWrite > 0 && cacheRead > 0 → write/read (explicit cache SKU)
289
+ * b. cacheWrite == 0 && cacheRead > 0 && input > 0 → input/cacheRead
290
+ * (implicit cache: rewriting the prefix is billed at the input price)
291
+ * 3. Provider-family fallback (gemini≈4, grok/deepseek≈10 — implicit-cache
292
+ * families without a local cacheWrite SKU)
293
+ * 4. Token-account fallback 2.0: a compaction re-reads writeTokens once,
294
+ * so breakeven = writeTokens/savingTokens ⇔ ratio 2
295
+ *
296
+ * Returns null only for an explicitly invalid numeric config.
297
+ *
298
+ * History (P0-1): `auto` used to return null when cacheWrite==0, reading that
299
+ * as "no KV cache at all". The 2026-09-17 trial disproved it on this machine:
300
+ * grok/deepseek/gemini sessions all show non-zero cacheRead usage while
301
+ * cacheWrite stays 0 — there is no separate write SKU, not "no cache". null
302
+ * disabled OCC for every model actually used (61/61 decisions
303
+ * `cache_ratio_unavailable`, zero compactions). The floor matters as much as
304
+ * the fix: never implicitly fall back below 2.0, because ratio <= 1 zeroes the
305
+ * incremental cost and would compact even on the last boundary with nothing
306
+ * left to amortize (the original P0-1 failure mode).
307
+ */
308
+ export function resolveCacheRatioFromCost(
309
+ ratioConfig: number | 'auto',
310
+ cost?: { input?: number; cacheRead?: number; cacheWrite?: number } | null,
311
+ identity?: { provider?: string; modelId?: string } | null,
312
+ ): number | null {
313
+ // Explicit config takes absolute precedence
314
+ if (typeof ratioConfig === 'number') {
315
+ return Number.isFinite(ratioConfig) && ratioConfig >= 0 ? ratioConfig : null;
316
+ }
317
+
318
+ if (cost) {
319
+ const read = cost.cacheRead ?? 0;
320
+ const write = cost.cacheWrite ?? 0;
321
+ const input = cost.input ?? 0;
322
+
323
+ if (read > 0 && write > 0) return write / read;
324
+ if (read > 0 && write === 0 && input > 0) return input / read;
325
+ }
326
+
327
+ const familyKey = `${identity?.provider ?? ''}/${identity?.modelId ?? ''}`.toLowerCase();
328
+ for (const [family, ratio] of Object.entries(PROVIDER_FAMILY_CACHE_RATIOS)) {
329
+ if (familyKey.includes(family)) return ratio;
330
+ }
331
+ return TOKEN_ACCOUNT_CACHE_RATIO;
332
+ }
333
+
334
+ function compactionMessageCount(entries: readonly SessionEntry[], startIndex: number, endIndex: number): number {
335
+ let count = 0;
336
+ for (let index = startIndex; index < endIndex; index++) {
337
+ const entry = entries[index];
338
+ if (entry && entry.type !== 'compaction' && sessionEntryToContextMessages(entry).length > 0) {
339
+ count++;
340
+ }
341
+ }
342
+ return count;
343
+ }
344
+
345
+ function branchAfterAbort(entries: readonly SessionEntry[]): SessionEntry[] {
346
+ const last = entries.at(-1);
347
+ const markerProvider = 'pi-herdr';
348
+ return [
349
+ ...entries,
350
+ {
351
+ type: 'message',
352
+ id: 'pi-herdr-online-context-compact-abort-marker',
353
+ parentId: last?.id ?? null,
354
+ timestamp: new Date(0).toISOString(),
355
+ message: {
356
+ role: 'assistant',
357
+ content: [],
358
+ api: markerProvider,
359
+ provider: markerProvider,
360
+ model: 'aborted',
361
+ usage: {
362
+ input: 0,
363
+ output: 0,
364
+ cacheRead: 0,
365
+ cacheWrite: 0,
366
+ totalTokens: 0,
367
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
368
+ },
369
+ stopReason: 'aborted',
370
+ timestamp: 0,
371
+ },
372
+ } as SessionEntry,
373
+ ];
374
+ }
375
+
376
+ export function nativeCompactionFeasible(entries: readonly SessionEntry[], keepRecentTokens: number): boolean {
377
+ if (!Array.isArray(entries) || entries.length === 0) return false;
378
+ const path = branchAfterAbort(entries);
379
+ let startIndex = 0;
380
+ for (let index = path.length - 1; index >= 0; index--) {
381
+ const entry = path[index];
382
+ if (entry?.type !== 'compaction') continue;
383
+ const keptIndex = path.findIndex((item) => item.id === (entry as { firstKeptEntryId?: string }).firstKeptEntryId);
384
+ startIndex = keptIndex >= 0 ? keptIndex : index + 1;
385
+ break;
386
+ }
387
+
388
+ const cut = findCutPoint(path, startIndex, path.length, keepRecentTokens);
389
+ const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
390
+ const historyMessages = historyEnd > startIndex ? compactionMessageCount(path, startIndex, historyEnd) : 0;
391
+ const prefixMessages =
392
+ cut.isSplitTurn && cut.turnStartIndex >= 0
393
+ ? compactionMessageCount(path, cut.turnStartIndex, cut.firstKeptEntryIndex)
394
+ : 0;
395
+ return historyMessages > 0 || prefixMessages > 0;
396
+ }