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.
- package/README.md +16 -12
- package/package.json +3 -3
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +10 -13
- package/src/compact-coordinator.ts +279 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +77 -33
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +286 -486
- package/src/efficiency-store.ts +16 -29
- package/src/gc-core.ts +10 -36
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +202 -233
- package/src/history-store.ts +38 -23
- package/src/index-gates.ts +118 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +31 -29
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +31 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +16 -6
- package/src/index-worker.ts +8 -18
- package/src/index.ts +561 -570
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +138 -90
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/panel-open.ts +47 -0
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +53 -50
- package/src/pipe-channel.ts +44 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +766 -0
- package/src/{core → plugins}/terminal.ts +198 -139
- package/src/{core → plugins}/todo.ts +73 -134
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +26 -64
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +108 -84
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +25 -31
- package/src/role-state.ts +119 -0
- package/src/roles/worker-default.json +3 -3
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +61 -22
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +423 -120
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +383 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +577 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +187 -134
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +46 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +52 -25
- package/src/worker-marker.ts +76 -0
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/focus-poller.ts +0 -244
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/core/observation.ts
DELETED
|
@@ -1,555 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* D101 ObservationPack Plugin & Context Interceptor.
|
|
3
|
-
*
|
|
4
|
-
* Provides:
|
|
5
|
-
* - `obs_recall` tool for paged retrieval of replaced large tool results.
|
|
6
|
-
* - `context` lifecycle interception: projects placeholders for large tool results
|
|
7
|
-
* that have exceeded fullSends, keeping underlying JSONL intact.
|
|
8
|
-
* - Cache-aware packing decisions and role visibility guards.
|
|
9
|
-
* - Append-only telemetry auditing.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
13
|
-
import { Type } from 'typebox';
|
|
14
|
-
import {
|
|
15
|
-
containsReducerReceipt,
|
|
16
|
-
countLines,
|
|
17
|
-
deriveObservationId,
|
|
18
|
-
estimateTokens,
|
|
19
|
-
formatObservationPlaceholder,
|
|
20
|
-
isObservationId,
|
|
21
|
-
sha256Hex,
|
|
22
|
-
shouldPackForCache,
|
|
23
|
-
} from '../observation-core.ts';
|
|
24
|
-
import {
|
|
25
|
-
appendEfficiencyLog,
|
|
26
|
-
efficiencyLogPath,
|
|
27
|
-
observationObjectPath,
|
|
28
|
-
readStoredObjectChunk,
|
|
29
|
-
resolveSessionRoot,
|
|
30
|
-
storeContentAddressedObject,
|
|
31
|
-
} from '../efficiency-store.ts';
|
|
32
|
-
import {
|
|
33
|
-
resolveEfficiencyConfig,
|
|
34
|
-
type EfficiencyConfig,
|
|
35
|
-
type ObservationPackConfig,
|
|
36
|
-
} from '../efficiency-config-core.ts';
|
|
37
|
-
import { resolveCacheRatioFromCost } from '../compact-economics-core.ts';
|
|
38
|
-
import { planToolGate, type RuntimeRoleManifest } from '../tool-gate.ts';
|
|
39
|
-
import { toolError } from '../tool-error.ts';
|
|
40
|
-
|
|
41
|
-
export const RECALL_TOOL_NAME = 'obs_recall';
|
|
42
|
-
export const RECALL_MAX_BYTES = 16 * 1024;
|
|
43
|
-
export const RECALL_MAX_LINES = 400;
|
|
44
|
-
|
|
45
|
-
const loggedPackedObsIds = new Set<string>();
|
|
46
|
-
|
|
47
|
-
interface MemoizedPlaceholder {
|
|
48
|
-
placeholder: string;
|
|
49
|
-
obsId: string;
|
|
50
|
-
bytes: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const placeholderMemo = new Map<string, MemoizedPlaceholder>();
|
|
54
|
-
const MAX_MEMO_ENTRIES = 256;
|
|
55
|
-
|
|
56
|
-
export function contentJoinedLength(content: Array<{ text?: string }>): number {
|
|
57
|
-
if (content.length === 0) return 0;
|
|
58
|
-
let len = content.length - 1; // newline separators between blocks
|
|
59
|
-
for (let i = 0; i < content.length; i++) {
|
|
60
|
-
len += content[i]?.text?.length ?? 0;
|
|
61
|
-
}
|
|
62
|
-
return len;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function memoKeyFor(sessionRoot: string, toolCallId: string, charLength: number): string {
|
|
66
|
-
return `${sessionRoot}:${toolCallId}:${charLength}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function invalidateObservationMemo(obsId: string): void {
|
|
70
|
-
for (const [k, v] of placeholderMemo.entries()) {
|
|
71
|
-
if (v.obsId === obsId) {
|
|
72
|
-
placeholderMemo.delete(k);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function clearObservationMemoForTest(): void {
|
|
78
|
-
placeholderMemo.clear();
|
|
79
|
-
loggedPackedObsIds.clear();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const MAX_BATCH_PACK_ITEMS = 20;
|
|
83
|
-
export const MAX_BATCH_PACK_BYTES = 10 * 1024 * 1024; // 10MB limit per batch
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Build the OCC `onBeforeCompact` hook: role-gated, config-gated batch packing of the
|
|
87
|
-
* branch's large tool results before `ctx.compact()` rewrites the prefix.
|
|
88
|
-
* Exported (rather than inlined in index.ts) so the gate behaviour is unit-testable.
|
|
89
|
-
*/
|
|
90
|
-
export interface ExcerptMiddlePick {
|
|
91
|
-
text: string;
|
|
92
|
-
label: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** P0-3 seam: async middle-window picker (jev-backed); null keeps the legacy halves. */
|
|
96
|
-
export type PickMiddleExcerpt = (text: string, excerptBudgetBytes: number) => Promise<ExcerptMiddlePick | null>;
|
|
97
|
-
|
|
98
|
-
export function createCompactionBatchPackHook(deps: {
|
|
99
|
-
getObsConfig: () => ObservationPackConfig;
|
|
100
|
-
getManifest?: () => RuntimeRoleManifest | null;
|
|
101
|
-
getSessionId?: () => string | undefined;
|
|
102
|
-
pickMiddleExcerpt?: PickMiddleExcerpt;
|
|
103
|
-
}): (sessionRoot: string, ctx: unknown) => Promise<void> {
|
|
104
|
-
return async (sessionRoot: string, ctx: unknown): Promise<void> => {
|
|
105
|
-
const obsConfig = deps.getObsConfig();
|
|
106
|
-
if (!obsConfig.enabled) return;
|
|
107
|
-
|
|
108
|
-
// Role gate: a role that cannot call obs_recall must not receive placeholders.
|
|
109
|
-
const manifest = deps.getManifest?.() ?? null;
|
|
110
|
-
if (manifest && planToolGate(RECALL_TOOL_NAME, manifest).kind === 'deny') return;
|
|
111
|
-
|
|
112
|
-
const branch = (ctx as { sessionManager?: { getBranch?: () => readonly unknown[] } })?.sessionManager?.getBranch?.() ?? [];
|
|
113
|
-
const messages = branch.flatMap((entry) => {
|
|
114
|
-
if (!entry || typeof entry !== 'object') return [];
|
|
115
|
-
const e = entry as { type?: unknown; message?: unknown };
|
|
116
|
-
return e.type === 'message' && e.message ? [e.message] : [];
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const sessionId = deps.getSessionId?.();
|
|
120
|
-
await batchPackObservations({
|
|
121
|
-
sessionRoot,
|
|
122
|
-
sessionId,
|
|
123
|
-
messages,
|
|
124
|
-
obsConfig,
|
|
125
|
-
pickMiddleExcerpt: deps.pickMiddleExcerpt,
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface PackMessageResult {
|
|
131
|
-
obsId: string;
|
|
132
|
-
placeholder: string;
|
|
133
|
-
originalBytes: number;
|
|
134
|
-
originalTokens: number;
|
|
135
|
-
placeholderTokens: number;
|
|
136
|
-
grossSavedTokens: number;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface PackLogOptions {
|
|
140
|
-
logEnabled: boolean;
|
|
141
|
-
event: 'packed' | 'packed-batch';
|
|
142
|
-
sessionId?: string;
|
|
143
|
-
/** Extra audit fields (e.g. `source: 'compaction'`, `sendCount`, `tailTokensAfter`). */
|
|
144
|
-
extra?: Record<string, unknown>;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Shared packing primitive: derive id/placeholder, archive the object, memoize it, and append
|
|
149
|
-
* the `observation.jsonl` record exactly once. Keeping memo + telemetry in one place prevents the
|
|
150
|
-
* "wrote the memo but forgot the log" (or vice versa) divergence between the projection path and
|
|
151
|
-
* the compaction batch path.
|
|
152
|
-
*/
|
|
153
|
-
export async function packOneMessage(opts: {
|
|
154
|
-
sessionRoot: string;
|
|
155
|
-
toolName: string;
|
|
156
|
-
toolCallId: string;
|
|
157
|
-
text: string;
|
|
158
|
-
textBytes: number;
|
|
159
|
-
charLength: number;
|
|
160
|
-
obsConfig: ObservationPackConfig;
|
|
161
|
-
log?: PackLogOptions;
|
|
162
|
-
pickMiddleExcerpt?: PickMiddleExcerpt;
|
|
163
|
-
}): Promise<PackMessageResult | null> {
|
|
164
|
-
const { sessionRoot, toolName, toolCallId, text, textBytes, charLength, obsConfig, log, pickMiddleExcerpt } = opts;
|
|
165
|
-
const contentHash = sha256Hex(text);
|
|
166
|
-
const obsId = deriveObservationId(toolName, toolCallId, contentHash);
|
|
167
|
-
const originalTokens = estimateTokens(text);
|
|
168
|
-
const lines = countLines(text);
|
|
169
|
-
const middle = pickMiddleExcerpt
|
|
170
|
-
? (await pickMiddleExcerpt(text, obsConfig.excerptBytes)) ?? undefined
|
|
171
|
-
: undefined;
|
|
172
|
-
const placeholder = formatObservationPlaceholder({
|
|
173
|
-
id: obsId,
|
|
174
|
-
toolName,
|
|
175
|
-
bytes: textBytes,
|
|
176
|
-
lines,
|
|
177
|
-
tokens: originalTokens,
|
|
178
|
-
text,
|
|
179
|
-
fullSends: obsConfig.fullSends,
|
|
180
|
-
excerptBudget: obsConfig.excerptBytes,
|
|
181
|
-
middle,
|
|
182
|
-
});
|
|
183
|
-
const placeholderTokens = estimateTokens(placeholder);
|
|
184
|
-
const removedTokens = Math.max(0, originalTokens - placeholderTokens);
|
|
185
|
-
|
|
186
|
-
const objPath = observationObjectPath(sessionRoot, obsId);
|
|
187
|
-
try {
|
|
188
|
-
await storeContentAddressedObject(objPath, text, {
|
|
189
|
-
bytes: textBytes,
|
|
190
|
-
hash: contentHash,
|
|
191
|
-
lines,
|
|
192
|
-
});
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const memoKey = memoKeyFor(sessionRoot, toolCallId, charLength);
|
|
198
|
-
if (placeholderMemo.size >= MAX_MEMO_ENTRIES) {
|
|
199
|
-
const oldest = placeholderMemo.keys().next().value;
|
|
200
|
-
if (oldest) placeholderMemo.delete(oldest);
|
|
201
|
-
}
|
|
202
|
-
placeholderMemo.set(memoKey, {
|
|
203
|
-
placeholder,
|
|
204
|
-
obsId,
|
|
205
|
-
bytes: textBytes,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
if (log?.logEnabled && !loggedPackedObsIds.has(obsId)) {
|
|
209
|
-
loggedPackedObsIds.add(obsId);
|
|
210
|
-
try {
|
|
211
|
-
await appendEfficiencyLog(efficiencyLogPath(sessionRoot, 'observation'), {
|
|
212
|
-
schema: 'pier-efficiency/1',
|
|
213
|
-
mechanism: 'observationPack',
|
|
214
|
-
event: log.event,
|
|
215
|
-
ts: new Date().toISOString(),
|
|
216
|
-
sessionId: log.sessionId ?? 'unknown',
|
|
217
|
-
obsId,
|
|
218
|
-
toolName,
|
|
219
|
-
originalBytes: textBytes,
|
|
220
|
-
originalTokens,
|
|
221
|
-
placeholderTokens,
|
|
222
|
-
grossSavedTokens: removedTokens,
|
|
223
|
-
...(log.extra ?? {}),
|
|
224
|
-
});
|
|
225
|
-
} catch {
|
|
226
|
-
/* telemetry is best-effort */
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
obsId,
|
|
232
|
-
placeholder,
|
|
233
|
-
originalBytes: textBytes,
|
|
234
|
-
originalTokens,
|
|
235
|
-
placeholderTokens,
|
|
236
|
-
grossSavedTokens: removedTokens,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export async function batchPackObservations(opts: {
|
|
241
|
-
sessionRoot: string;
|
|
242
|
-
sessionId?: string;
|
|
243
|
-
messages: readonly unknown[];
|
|
244
|
-
obsConfig: ObservationPackConfig;
|
|
245
|
-
limits?: { maxItems?: number; maxBytes?: number };
|
|
246
|
-
pickMiddleExcerpt?: PickMiddleExcerpt;
|
|
247
|
-
}): Promise<number> {
|
|
248
|
-
const { sessionRoot, sessionId, messages, obsConfig } = opts;
|
|
249
|
-
if (!obsConfig.enabled || !sessionRoot || !Array.isArray(messages)) return 0;
|
|
250
|
-
|
|
251
|
-
const maxItems = opts.limits?.maxItems ?? MAX_BATCH_PACK_ITEMS;
|
|
252
|
-
const maxBytes = opts.limits?.maxBytes ?? MAX_BATCH_PACK_BYTES;
|
|
253
|
-
|
|
254
|
-
let packedCount = 0;
|
|
255
|
-
let accumulatedBytes = 0;
|
|
256
|
-
|
|
257
|
-
for (let i = 0; i < messages.length; i++) {
|
|
258
|
-
if (packedCount >= maxItems) break;
|
|
259
|
-
const msg = messages[i];
|
|
260
|
-
if (!msg || msg.role !== 'toolResult' || (msg as { isError?: boolean }).isError) continue;
|
|
261
|
-
const content = (msg as { content?: Array<{ type: string; text?: string }> }).content;
|
|
262
|
-
if (!Array.isArray(content) || content.length === 0) continue;
|
|
263
|
-
if (!content.every((b) => b && b.type === 'text' && typeof b.text === 'string')) continue;
|
|
264
|
-
|
|
265
|
-
const toolName = (msg as { toolName?: string }).toolName ?? 'tool';
|
|
266
|
-
const toolCallId = (msg as { toolCallId?: string }).toolCallId ?? `call_${i}`;
|
|
267
|
-
const charLength = contentJoinedLength(content);
|
|
268
|
-
|
|
269
|
-
const memoKey = memoKeyFor(sessionRoot, toolCallId, charLength);
|
|
270
|
-
if (placeholderMemo.has(memoKey)) continue;
|
|
271
|
-
|
|
272
|
-
const text = content.map((b) => b.text ?? '').join('\n');
|
|
273
|
-
if (containsReducerReceipt(text)) continue;
|
|
274
|
-
|
|
275
|
-
const textBytes = Buffer.byteLength(text, 'utf8');
|
|
276
|
-
if (textBytes < obsConfig.thresholdBytes) continue;
|
|
277
|
-
|
|
278
|
-
// Check soft maxBytes limit before packing
|
|
279
|
-
if (accumulatedBytes + textBytes > maxBytes && packedCount > 0) break;
|
|
280
|
-
|
|
281
|
-
const packed = await packOneMessage({
|
|
282
|
-
sessionRoot,
|
|
283
|
-
toolName,
|
|
284
|
-
toolCallId,
|
|
285
|
-
text,
|
|
286
|
-
textBytes,
|
|
287
|
-
charLength,
|
|
288
|
-
obsConfig,
|
|
289
|
-
log: { logEnabled: obsConfig.logEnabled, event: 'packed-batch', sessionId, extra: { source: 'compaction' } },
|
|
290
|
-
pickMiddleExcerpt: opts.pickMiddleExcerpt,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (packed) {
|
|
294
|
-
packedCount++;
|
|
295
|
-
accumulatedBytes += packed.originalBytes;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return packedCount;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export interface ObservationPackDeps {
|
|
302
|
-
pi: ExtensionAPI;
|
|
303
|
-
getConfig?: (ctx: ExtensionContext) => EfficiencyConfig;
|
|
304
|
-
getRuntimeManifest?: () => RuntimeRoleManifest | null;
|
|
305
|
-
getRemainingHorizon?: () => number;
|
|
306
|
-
pickMiddleExcerpt?: PickMiddleExcerpt;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function registerObservationPack(deps: ObservationPackDeps): void {
|
|
310
|
-
const { pi, getConfig, getRuntimeManifest, pickMiddleExcerpt } = deps;
|
|
311
|
-
|
|
312
|
-
// 1. Register obs_recall tool
|
|
313
|
-
pi.registerTool({
|
|
314
|
-
name: RECALL_TOOL_NAME,
|
|
315
|
-
label: 'Recall Observation',
|
|
316
|
-
description:
|
|
317
|
-
'Recall a paged slice of a previously replaced large tool result by observation id and byte offset.',
|
|
318
|
-
promptGuidelines: [
|
|
319
|
-
'Call obs_recall with id and offset to inspect specific parts of large tool outputs.',
|
|
320
|
-
'Check the returned next_offset and eof to page through long logs.',
|
|
321
|
-
],
|
|
322
|
-
parameters: Type.Object({
|
|
323
|
-
id: Type.String({ description: 'Observation ID from placeholder (e.g. obs_...)' }),
|
|
324
|
-
offset: Type.Optional(
|
|
325
|
-
Type.Integer({ minimum: 0, description: 'Byte offset to recall from (default 0)' }),
|
|
326
|
-
),
|
|
327
|
-
}),
|
|
328
|
-
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
329
|
-
void toolCallId;
|
|
330
|
-
const id = params?.id;
|
|
331
|
-
const offset = params?.offset ?? 0;
|
|
332
|
-
|
|
333
|
-
if (!isObservationId(id)) {
|
|
334
|
-
// A1: hard failures throw so pi flags isError for the model.
|
|
335
|
-
return toolError(`invalid observation id format: "${id}"`);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const sessionDir = ctx?.sessionManager?.getSessionDir?.();
|
|
339
|
-
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
340
|
-
const sessionRoot = resolveSessionRoot(sessionDir, sessionId);
|
|
341
|
-
|
|
342
|
-
if (!sessionRoot) {
|
|
343
|
-
return toolError('session storage is unavailable for observation recall.');
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const filePath = observationObjectPath(sessionRoot, id);
|
|
347
|
-
const startMs = Date.now();
|
|
348
|
-
const effConfig = getConfig ? getConfig(ctx) : resolveEfficiencyConfig();
|
|
349
|
-
const recallMaxBytes = effConfig.observationPack.recallChunkBytes ?? RECALL_MAX_BYTES;
|
|
350
|
-
try {
|
|
351
|
-
const chunk = await readStoredObjectChunk(filePath, offset, {
|
|
352
|
-
maxBytes: recallMaxBytes,
|
|
353
|
-
maxLines: RECALL_MAX_LINES,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const header = [
|
|
357
|
-
`[obs_recall id=${id} offset=${offset} next_offset=${chunk.nextOffset} eof=${chunk.eof}]`,
|
|
358
|
-
`[chunk_bytes=${chunk.bytes} chunk_lines=${chunk.lines}; use next_offset to continue]`,
|
|
359
|
-
].join('\n');
|
|
360
|
-
|
|
361
|
-
if (effConfig.observationPack.logEnabled) {
|
|
362
|
-
const logPath = efficiencyLogPath(
|
|
363
|
-
sessionRoot,
|
|
364
|
-
'observation',
|
|
365
|
-
undefined,
|
|
366
|
-
);
|
|
367
|
-
void appendEfficiencyLog(logPath, {
|
|
368
|
-
schema: 'pier-efficiency/1',
|
|
369
|
-
mechanism: 'observationPack',
|
|
370
|
-
event: 'recall',
|
|
371
|
-
ts: new Date().toISOString(),
|
|
372
|
-
sessionId: sessionId ?? 'unknown',
|
|
373
|
-
obsId: id,
|
|
374
|
-
offset,
|
|
375
|
-
chunkBytes: chunk.bytes,
|
|
376
|
-
lines: chunk.lines,
|
|
377
|
-
nextOffset: chunk.nextOffset,
|
|
378
|
-
eof: chunk.eof,
|
|
379
|
-
durationMs: Date.now() - startMs,
|
|
380
|
-
}).catch(() => {});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
content: [{ type: 'text', text: `${header}\n${chunk.text}` }],
|
|
385
|
-
details: {
|
|
386
|
-
id,
|
|
387
|
-
offset,
|
|
388
|
-
bytes: chunk.bytes,
|
|
389
|
-
lines: chunk.lines,
|
|
390
|
-
nextOffset: chunk.nextOffset,
|
|
391
|
-
eof: chunk.eof,
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
} catch (err) {
|
|
395
|
-
// Self-healing: if disk file is missing or corrupted, invalidate memo to repack if needed
|
|
396
|
-
invalidateObservationMemo(id);
|
|
397
|
-
return {
|
|
398
|
-
content: [
|
|
399
|
-
{
|
|
400
|
-
type: 'text',
|
|
401
|
-
text: `Error: failed to recall observation ${id}: ${err instanceof Error ? err.message : String(err)}`,
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
details: {},
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// 2. Intercept context event to project placeholders
|
|
411
|
-
pi.on('context', async (event, ctx: ExtensionContext) => {
|
|
412
|
-
if (!event || !Array.isArray(event.messages)) return;
|
|
413
|
-
|
|
414
|
-
const effConfig = getConfig ? getConfig(ctx) : resolveEfficiencyConfig();
|
|
415
|
-
const obsConfig: ObservationPackConfig = effConfig.observationPack;
|
|
416
|
-
if (!obsConfig.enabled) return;
|
|
417
|
-
|
|
418
|
-
// Role visibility gate check: if current role denies obs_recall, skip packing completely!
|
|
419
|
-
const manifest = getRuntimeManifest ? getRuntimeManifest() : null;
|
|
420
|
-
if (manifest) {
|
|
421
|
-
const gate = planToolGate(RECALL_TOOL_NAME, manifest);
|
|
422
|
-
if (gate.kind === 'deny') {
|
|
423
|
-
return; // Skip packing: agent would not be permitted to call obs_recall
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const sessionDir = ctx?.sessionManager?.getSessionDir?.();
|
|
428
|
-
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
429
|
-
const sessionRoot = resolveSessionRoot(sessionDir, sessionId);
|
|
430
|
-
if (!sessionRoot) return; // Fail-open if session directory is absent
|
|
431
|
-
|
|
432
|
-
const projected = [...event.messages];
|
|
433
|
-
const len = projected.length;
|
|
434
|
-
|
|
435
|
-
// Calculate how many assistant responses followed each message
|
|
436
|
-
const priorAssistantCounts = new Array<number>(len);
|
|
437
|
-
let assistantCount = 0;
|
|
438
|
-
for (let i = len - 1; i >= 0; i--) {
|
|
439
|
-
priorAssistantCounts[i] = assistantCount;
|
|
440
|
-
if (projected[i]?.role === 'assistant') {
|
|
441
|
-
assistantCount++;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
for (let i = 0; i < len; i++) {
|
|
446
|
-
const msg = projected[i];
|
|
447
|
-
if (!msg || msg.role !== 'toolResult' || (msg as { isError?: boolean }).isError) continue;
|
|
448
|
-
const content = (msg as { content?: Array<{ type: string; text?: string }> }).content;
|
|
449
|
-
if (!Array.isArray(content) || content.length === 0) continue;
|
|
450
|
-
if (!content.every((b) => b && b.type === 'text' && typeof b.text === 'string')) continue;
|
|
451
|
-
|
|
452
|
-
const toolName = (msg as { toolName?: string }).toolName ?? 'tool';
|
|
453
|
-
const toolCallId = (msg as { toolCallId?: string }).toolCallId ?? `call_${i}`;
|
|
454
|
-
const charLength = contentJoinedLength(content);
|
|
455
|
-
|
|
456
|
-
// N1' Fast Path: check memo BEFORE text join, containsReducerReceipt, and byteLength!
|
|
457
|
-
// Once packed, observation remains sticky until memo eviction.
|
|
458
|
-
const memoKey = memoKeyFor(sessionRoot, toolCallId, charLength);
|
|
459
|
-
const memoized = placeholderMemo.get(memoKey);
|
|
460
|
-
if (memoized) {
|
|
461
|
-
// Refresh LRU position
|
|
462
|
-
placeholderMemo.delete(memoKey);
|
|
463
|
-
placeholderMemo.set(memoKey, memoized);
|
|
464
|
-
projected[i] = {
|
|
465
|
-
...msg,
|
|
466
|
-
content: [{ type: 'text', text: memoized.placeholder }],
|
|
467
|
-
};
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const sendCount = priorAssistantCounts[i] ?? 0;
|
|
472
|
-
// Active use window: do not touch and do not store to disk prematurely!
|
|
473
|
-
if (sendCount < obsConfig.fullSends) {
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const text = content.map((b) => b.text ?? '').join('\n');
|
|
478
|
-
if (containsReducerReceipt(text)) continue;
|
|
479
|
-
|
|
480
|
-
const textBytes = Buffer.byteLength(text, 'utf8');
|
|
481
|
-
if (textBytes < obsConfig.thresholdBytes) continue;
|
|
482
|
-
|
|
483
|
-
// Calculate tail tokens after this message
|
|
484
|
-
let tailTokensAfter = 0;
|
|
485
|
-
for (let j = i + 1; j < len; j++) {
|
|
486
|
-
const afterMsg = projected[j];
|
|
487
|
-
if (afterMsg && Array.isArray((afterMsg as { content?: unknown[] }).content)) {
|
|
488
|
-
for (const b of (afterMsg as { content: Array<{ type: string; text?: string }> }).content) {
|
|
489
|
-
if (b && b.type === 'text' && typeof b.text === 'string') {
|
|
490
|
-
tailTokensAfter += estimateTokens(b.text);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const contentHash = sha256Hex(text);
|
|
497
|
-
const obsId = deriveObservationId(toolName, toolCallId, contentHash);
|
|
498
|
-
const originalTokens = estimateTokens(text);
|
|
499
|
-
const lines = countLines(text);
|
|
500
|
-
const placeholder = formatObservationPlaceholder({
|
|
501
|
-
id: obsId,
|
|
502
|
-
toolName,
|
|
503
|
-
bytes: textBytes,
|
|
504
|
-
lines,
|
|
505
|
-
tokens: originalTokens,
|
|
506
|
-
text,
|
|
507
|
-
fullSends: obsConfig.fullSends,
|
|
508
|
-
excerptBudget: obsConfig.excerptBytes,
|
|
509
|
-
});
|
|
510
|
-
const placeholderTokens = estimateTokens(placeholder);
|
|
511
|
-
const removedTokens = Math.max(0, originalTokens - placeholderTokens);
|
|
512
|
-
|
|
513
|
-
const remainingHorizon = deps.getRemainingHorizon ? deps.getRemainingHorizon() : 4;
|
|
514
|
-
// Same resolution as OCC (provider family / token-account fallback); the old
|
|
515
|
-
// raw pass-through of 'auto' let shouldPackForCache hardcode 12.5 internally.
|
|
516
|
-
const cacheRatio = resolveCacheRatioFromCost(
|
|
517
|
-
effConfig.onlineContextCompact.cacheWriteReadRatio,
|
|
518
|
-
ctx.model?.cost,
|
|
519
|
-
{ provider: ctx.model?.provider, modelId: ctx.model?.id },
|
|
520
|
-
);
|
|
521
|
-
const canPack = shouldPackForCache({
|
|
522
|
-
removedTokens,
|
|
523
|
-
tailTokensAfter,
|
|
524
|
-
expectedRemainingRequests: remainingHorizon,
|
|
525
|
-
cacheWriteReadRatio: cacheRatio,
|
|
526
|
-
});
|
|
527
|
-
if (canPack) {
|
|
528
|
-
const packed = await packOneMessage({
|
|
529
|
-
sessionRoot,
|
|
530
|
-
toolName,
|
|
531
|
-
toolCallId,
|
|
532
|
-
text,
|
|
533
|
-
textBytes,
|
|
534
|
-
charLength,
|
|
535
|
-
obsConfig,
|
|
536
|
-
log: {
|
|
537
|
-
logEnabled: obsConfig.logEnabled,
|
|
538
|
-
event: 'packed',
|
|
539
|
-
sessionId,
|
|
540
|
-
extra: { sendCount, tailTokensAfter },
|
|
541
|
-
},
|
|
542
|
-
pickMiddleExcerpt,
|
|
543
|
-
});
|
|
544
|
-
if (!packed) continue;
|
|
545
|
-
|
|
546
|
-
projected[i] = {
|
|
547
|
-
...msg,
|
|
548
|
-
content: [{ type: 'text', text: packed.placeholder }],
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return { messages: projected };
|
|
554
|
-
});
|
|
555
|
-
}
|