pi-tuck 1.0.26 → 1.0.28
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/CHANGELOG.md +47 -0
- package/README.md +9 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +37 -24
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +4 -0
- package/dist/src/compaction.js +156 -172
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/request-capacity.d.ts +8 -3
- package/dist/src/request-capacity.js +19 -14
- package/dist/src/run.d.ts +52 -0
- package/dist/src/run.js +89 -0
- package/dist/src/semantic-recall.d.ts +72 -2
- package/dist/src/semantic-recall.js +58 -12
- package/dist/src/status.d.ts +1 -0
- package/dist/src/status.js +2 -1
- package/dist/src/tuck.js +33 -27
- package/index.ts +31 -22
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +137 -155
- package/src/request-capacity.ts +18 -12
- package/src/run.ts +91 -0
- package/src/semantic-recall.ts +46 -14
- package/src/status.ts +3 -1
- package/src/tuck.ts +32 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RecallPolicy } from './recall.js';
|
|
2
2
|
import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
export declare const DIAGNOSTIC_BUILD = "
|
|
3
|
+
export declare const DIAGNOSTIC_BUILD = "e9c750b757776bf44ad79939b633f178ef9a8f14acfab1c8bb6311d692c79e61";
|
|
4
4
|
export interface Receipt {
|
|
5
5
|
id: string;
|
|
6
6
|
build: string;
|
|
@@ -22,10 +22,15 @@ export declare const REPLY_FLOOR_MIN = 3584;
|
|
|
22
22
|
export declare const thinkingOnlyLength: (message: AgentMessage) => boolean;
|
|
23
23
|
/** Thinking-only exhaustion uses the observed largest reply, not the full reserve. Every other failed length/overflow keeps its override. */
|
|
24
24
|
export declare const needsFullReplyReserve: (message: AgentMessage) => boolean;
|
|
25
|
-
/**
|
|
25
|
+
/** One branch reader for the reserve override and reply floor, including history before cuts. */
|
|
26
|
+
export declare function replyFit(branch: readonly SessionEntry[]): {
|
|
27
|
+
failed: boolean;
|
|
28
|
+
largestOutput: number;
|
|
29
|
+
thinkingStop: boolean;
|
|
30
|
+
};
|
|
26
31
|
export declare function branchReplyFloor(branch: readonly SessionEntry[], reserve: number): number;
|
|
27
32
|
/** Admission never promises more output than a model with a positive reply cap can produce. */
|
|
28
33
|
export declare function requestReplyFloor(branch: readonly SessionEntry[], reserve: number, maxTokens?: number): number;
|
|
29
|
-
/** Check decoded text,
|
|
30
|
-
export declare function carriesPrompt(messages: Messages, prompt: string): boolean;
|
|
34
|
+
/** Check decoded starting text, native or quoted whole with its original role; never JSON-escaped substring matching. */
|
|
35
|
+
export declare function carriesPrompt(messages: Messages, prompt: string, role?: 'user' | 'custom'): boolean;
|
|
31
36
|
export {};
|
|
@@ -64,41 +64,46 @@ export const thinkingOnlyLength = (message) => message.role === 'assistant' && m
|
|
|
64
64
|
/** Thinking-only exhaustion uses the observed largest reply, not the full reserve. Every other failed length/overflow keeps its override. */
|
|
65
65
|
export const needsFullReplyReserve = (message) => message.role === 'assistant' &&
|
|
66
66
|
(message.stopReason === 'length' && !thinkingOnlyLength(message) || message.stopReason === 'error' && isContextOverflow(message));
|
|
67
|
-
/**
|
|
68
|
-
export function
|
|
69
|
-
let
|
|
67
|
+
/** One branch reader for the reserve override and reply floor, including history before cuts. */
|
|
68
|
+
export function replyFit(branch) {
|
|
69
|
+
let failed = false, largestOutput = 0, thinkingStop = false;
|
|
70
70
|
for (const entry of branch) {
|
|
71
71
|
if (entry.type !== 'message' || entry.message.role !== 'assistant')
|
|
72
72
|
continue;
|
|
73
73
|
const message = entry.message;
|
|
74
|
-
|
|
75
|
-
return reserve;
|
|
74
|
+
failed ||= needsFullReplyReserve(message);
|
|
76
75
|
thinkingStop ||= thinkingOnlyLength(message);
|
|
77
76
|
const output = message.usage?.output;
|
|
78
|
-
if (typeof output === 'number' && Number.isFinite(output) && output >
|
|
79
|
-
|
|
77
|
+
if (typeof output === 'number' && Number.isFinite(output) && output > largestOutput)
|
|
78
|
+
largestOutput = output;
|
|
80
79
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
return { failed, largestOutput, thinkingStop };
|
|
81
|
+
}
|
|
82
|
+
export function branchReplyFloor(branch, reserve) {
|
|
83
|
+
const { failed, largestOutput, thinkingStop } = replyFit(branch);
|
|
84
|
+
if (failed)
|
|
85
|
+
return reserve;
|
|
86
|
+
// Include exhausted thinking output across cuts/restarts; requestReplyFloor applies the model cap.
|
|
87
|
+
const largest = Math.ceil(Math.max(REPLY_FLOOR_MIN, largestOutput));
|
|
88
|
+
return thinkingStop ? largest : Math.min(reserve, largest);
|
|
84
89
|
}
|
|
85
90
|
/** Admission never promises more output than a model with a positive reply cap can produce. */
|
|
86
91
|
export function requestReplyFloor(branch, reserve, maxTokens) {
|
|
87
92
|
const floor = branchReplyFloor(branch, reserve);
|
|
88
93
|
return maxTokens !== undefined && maxTokens > 0 ? Math.min(floor, maxTokens) : floor;
|
|
89
94
|
}
|
|
90
|
-
/** Check decoded text,
|
|
91
|
-
export function carriesPrompt(messages, prompt) {
|
|
95
|
+
/** Check decoded starting text, native or quoted whole with its original role; never JSON-escaped substring matching. */
|
|
96
|
+
export function carriesPrompt(messages, prompt, role = 'user') {
|
|
92
97
|
if (!prompt.length)
|
|
93
98
|
return true;
|
|
94
99
|
return messages.some(message => {
|
|
95
|
-
if (message.role ===
|
|
100
|
+
if (message.role === role) {
|
|
96
101
|
const text = typeof message.content === 'string' ? message.content : message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n');
|
|
97
102
|
return text.includes(prompt);
|
|
98
103
|
}
|
|
99
104
|
if (message.role !== 'compactionSummary')
|
|
100
105
|
return false;
|
|
101
106
|
const split = splitSummary(message.summary);
|
|
102
|
-
return split !== null && split !== undefined && parseRecords(split.body, split.version).some(record => record.kind === 'excerpt' && record.role ===
|
|
107
|
+
return split !== null && split !== undefined && parseRecords(split.body, split.version).some(record => record.kind === 'excerpt' && record.role === role && record.text.includes(prompt));
|
|
103
108
|
});
|
|
104
109
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ExtensionContext, type SessionEntry, type SessionBoundaryDraft } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { Clip } from './clip.js';
|
|
3
|
+
export type CutEntry = {
|
|
4
|
+
type: 'compaction';
|
|
5
|
+
summary: string;
|
|
6
|
+
firstKeptEntryId: string;
|
|
7
|
+
tokensBefore: number;
|
|
8
|
+
details?: unknown;
|
|
9
|
+
};
|
|
10
|
+
type Context = Pick<ExtensionContext, 'cwd' | 'sessionManager'>;
|
|
11
|
+
export interface CutView {
|
|
12
|
+
session: ExtensionContext['sessionManager'];
|
|
13
|
+
keptFromId: string | undefined;
|
|
14
|
+
viewStart: number;
|
|
15
|
+
hasTuckCut: boolean;
|
|
16
|
+
pending: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The only owner of a cut that requests carry before Pi saves it. */
|
|
19
|
+
export declare class PendingCut {
|
|
20
|
+
private cut?;
|
|
21
|
+
private branchLength;
|
|
22
|
+
private preview?;
|
|
23
|
+
plan(cut: CutEntry, branchLength: number): void;
|
|
24
|
+
clear(): void;
|
|
25
|
+
take(): CutEntry | undefined;
|
|
26
|
+
view(ctx: Context, entries?: readonly SessionBoundaryDraft[]): CutView;
|
|
27
|
+
}
|
|
28
|
+
/** Text bridges before Pi appends the starting message. Once visible, every reader uses its entry id.
|
|
29
|
+
* Bind on the first request/compaction read after message_end (which Pi emits before appending), before steering can replace it. */
|
|
30
|
+
export declare class StartingPrompt {
|
|
31
|
+
private readonly branchLength;
|
|
32
|
+
readonly role: 'user' | 'custom';
|
|
33
|
+
private entryId?;
|
|
34
|
+
private bridge?;
|
|
35
|
+
constructor(bridge: string, branchLength: number, role?: 'user' | 'custom');
|
|
36
|
+
id(branch: readonly SessionEntry[]): string | undefined;
|
|
37
|
+
text(branch: readonly SessionEntry[]): string;
|
|
38
|
+
}
|
|
39
|
+
export interface Run {
|
|
40
|
+
phase: 'decided' | 'starting' | 'admitted' | 'failed';
|
|
41
|
+
claimed: boolean;
|
|
42
|
+
expired: boolean;
|
|
43
|
+
prompt?: StartingPrompt;
|
|
44
|
+
cut: PendingCut;
|
|
45
|
+
clips: Map<string, Clip>;
|
|
46
|
+
pendingBoundaryCommit: boolean;
|
|
47
|
+
thinkingRetryPending: boolean;
|
|
48
|
+
failure?: Error;
|
|
49
|
+
refusalReceipted: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare const createRun: (phase: Run["phase"]) => Run;
|
|
52
|
+
export {};
|
package/dist/src/run.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SessionManager } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { hasTuckCut } from './history-tools.js';
|
|
3
|
+
import { viewStart } from './semantic-recall.js';
|
|
4
|
+
import { textOf } from './tuck/core/content.js';
|
|
5
|
+
/** The only owner of a cut that requests carry before Pi saves it. */
|
|
6
|
+
export class PendingCut {
|
|
7
|
+
cut;
|
|
8
|
+
branchLength = 0;
|
|
9
|
+
preview;
|
|
10
|
+
plan(cut, branchLength) { this.cut = cut; this.branchLength = branchLength; this.preview = undefined; }
|
|
11
|
+
clear() { this.cut = undefined; this.branchLength = 0; this.preview = undefined; }
|
|
12
|
+
take() { const cut = this.cut; this.clear(); return cut; }
|
|
13
|
+
view(ctx, entries = []) {
|
|
14
|
+
const live = ctx.sessionManager, leaf = live.getLeafId?.(), held = this.preview;
|
|
15
|
+
// Native entries are append-only; a leaf move invalidates both the saved and pending views, as in the eligibility cache.
|
|
16
|
+
if (!entries.length && leaf !== undefined && held && held.manager === live && held.cwd === ctx.cwd && held.leaf === leaf)
|
|
17
|
+
return held.view;
|
|
18
|
+
const branch = live.getBranch(), cut = this.cut;
|
|
19
|
+
const remember = (view) => {
|
|
20
|
+
if (!entries.length && leaf !== undefined)
|
|
21
|
+
this.preview = { manager: live, cwd: ctx.cwd, leaf, view };
|
|
22
|
+
return view;
|
|
23
|
+
};
|
|
24
|
+
if (!cut && !entries.length) {
|
|
25
|
+
const last = branch.findLast(entry => entry.type === 'compaction');
|
|
26
|
+
return remember({ session: live, keptFromId: last?.type === 'compaction' ? last.firstKeptEntryId : undefined,
|
|
27
|
+
viewStart: viewStart(branch), hasTuckCut: hasTuckCut(branch), pending: false });
|
|
28
|
+
}
|
|
29
|
+
// A compact-all plan predates the prompt. Keep from the first appended entry, skipping our capacity receipts.
|
|
30
|
+
const keptFromId = cut && (cut.firstKeptEntryId || branch.slice(this.branchLength)
|
|
31
|
+
.find(entry => entry.type !== 'custom' || entry.customType !== 'pi-tuck-capacity')?.id || '');
|
|
32
|
+
const header = live.getHeader();
|
|
33
|
+
if (!header)
|
|
34
|
+
throw new Error("Missing native session header for the prompt's cut");
|
|
35
|
+
const session = SessionManager.inMemory(ctx.cwd, undefined, [header, ...branch]);
|
|
36
|
+
if (cut)
|
|
37
|
+
session.appendCompaction(cut.summary, keptFromId, 0, cut.details, true);
|
|
38
|
+
// Retry/settlement drafts model the same request view, including an omitted failed reply.
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.type === 'context_edit')
|
|
41
|
+
session.appendContextEdit(entry.targetId, entry.replacement);
|
|
42
|
+
else if (entry.type === 'compaction')
|
|
43
|
+
session.appendCompaction(entry.summary, entry.firstKeptEntryId, 0, entry.details, true, entry.usage);
|
|
44
|
+
else if (entry.type === 'custom')
|
|
45
|
+
session.appendCustomEntry(entry.customType, entry.data);
|
|
46
|
+
else
|
|
47
|
+
session.appendCustomMessageEntry(entry.customType, entry.content, entry.display, entry.details);
|
|
48
|
+
}
|
|
49
|
+
const previewBranch = session.getBranch(), last = previewBranch.findLast(entry => entry.type === 'compaction');
|
|
50
|
+
return remember({ session, keptFromId: last?.type === 'compaction' ? last.firstKeptEntryId : undefined,
|
|
51
|
+
viewStart: viewStart(previewBranch), hasTuckCut: hasTuckCut(previewBranch), pending: Boolean(cut) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Text bridges before Pi appends the starting message. Once visible, every reader uses its entry id.
|
|
55
|
+
* Bind on the first request/compaction read after message_end (which Pi emits before appending), before steering can replace it. */
|
|
56
|
+
export class StartingPrompt {
|
|
57
|
+
branchLength;
|
|
58
|
+
role;
|
|
59
|
+
entryId;
|
|
60
|
+
bridge;
|
|
61
|
+
constructor(bridge, branchLength, role = 'user') {
|
|
62
|
+
this.branchLength = branchLength;
|
|
63
|
+
this.role = role;
|
|
64
|
+
this.bridge = bridge;
|
|
65
|
+
}
|
|
66
|
+
id(branch) {
|
|
67
|
+
if (!this.entryId && this.bridge !== undefined) {
|
|
68
|
+
const text = this.bridge;
|
|
69
|
+
this.entryId = branch.slice(this.branchLength).find(entry => this.role === 'custom'
|
|
70
|
+
? entry.type === 'custom_message' && textOf(entry.content) === text
|
|
71
|
+
: entry.type === 'message' && entry.message.role === 'user' && textOf(entry.message.content).includes(text))?.id;
|
|
72
|
+
if (this.entryId)
|
|
73
|
+
this.bridge = undefined;
|
|
74
|
+
}
|
|
75
|
+
return this.entryId;
|
|
76
|
+
}
|
|
77
|
+
text(branch) {
|
|
78
|
+
const id = this.id(branch), entry = branch.find(entry => entry.id === id);
|
|
79
|
+
if (entry?.type === 'message' && entry.message.role === 'user')
|
|
80
|
+
return textOf(entry.message.content);
|
|
81
|
+
if (entry?.type === 'custom_message')
|
|
82
|
+
return textOf(entry.content);
|
|
83
|
+
if (this.bridge !== undefined)
|
|
84
|
+
return this.bridge;
|
|
85
|
+
throw new Error('The starting prompt entry is no longer on this branch');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const createRun = (phase) => ({ phase, claimed: false, expired: false,
|
|
89
|
+
cut: new PendingCut(), clips: new Map(), pendingBoundaryCommit: false, thinkingRetryPending: false, refusalReceipted: false });
|
|
@@ -101,7 +101,7 @@ export interface SemanticGroup {
|
|
|
101
101
|
export interface SemanticPage {
|
|
102
102
|
status: string;
|
|
103
103
|
notice: string;
|
|
104
|
-
coverage: ReturnType<SemanticRecallController["
|
|
104
|
+
coverage: ReturnType<SemanticRecallController["requestCoverage"]>;
|
|
105
105
|
groups: SemanticGroup[];
|
|
106
106
|
method?: string;
|
|
107
107
|
}
|
|
@@ -127,6 +127,20 @@ export declare const MAX_INDEX_UNITS = 8000;
|
|
|
127
127
|
/** The wait before the next automatic attempt, given how many were already armed since the last success. */
|
|
128
128
|
export declare function healDelayMs(attempts: number, integrity: boolean, delays?: readonly number[], steady?: number): number;
|
|
129
129
|
export type SemanticState = 'disabled' | 'unavailable' | 'preparing' | 'ready' | 'waiting';
|
|
130
|
+
export type SemanticSetup = 'off-settings' | 'off-user' | 'not-installed' | 'off-platform' | 'off-project' | 'starting' | 'failed' | 'ready';
|
|
131
|
+
interface SetupDetail {
|
|
132
|
+
installed?: boolean;
|
|
133
|
+
reason?: string;
|
|
134
|
+
stage?: 'setup' | 'start';
|
|
135
|
+
notice?: string;
|
|
136
|
+
announced?: boolean;
|
|
137
|
+
}
|
|
138
|
+
/** Setup and operator wording have one owner; runtime indexing state remains separate. */
|
|
139
|
+
export declare function semanticSetupMessage(setup: SemanticSetup, detail?: SetupDetail): string | null;
|
|
140
|
+
export declare function semanticSetupNotice(setup: SemanticSetup, detail?: SetupDetail): {
|
|
141
|
+
message: string;
|
|
142
|
+
level: 'info' | 'warning';
|
|
143
|
+
} | undefined;
|
|
130
144
|
/** Where the current view starts, as a branch position: the latest compaction entry's first kept entry. Earlier entries were compacted away. With no compaction, nothing is out of view. */
|
|
131
145
|
export declare function viewStart(branch: SessionEntry[]): number;
|
|
132
146
|
/** `pendingKeptId`: the first kept entry of a cut pi-tuck has applied to the requests but Pi has not yet saved (a prompt's cut is saved at the end of
|
|
@@ -147,6 +161,8 @@ export declare class SemanticRecallController {
|
|
|
147
161
|
private cleanupError;
|
|
148
162
|
private dirty;
|
|
149
163
|
private operations;
|
|
164
|
+
private setup;
|
|
165
|
+
private setupDetail;
|
|
150
166
|
private _enabled;
|
|
151
167
|
private _current?;
|
|
152
168
|
private _task?;
|
|
@@ -190,7 +206,13 @@ export declare class SemanticRecallController {
|
|
|
190
206
|
onStateChange(listener: (state: SemanticState, reason: string | null) => void): () => void;
|
|
191
207
|
private touch;
|
|
192
208
|
/** A host that could not configure the runtime records why a requested start failed. The controller stays disabled and `lastError` carries the reason until the next `enable`. */
|
|
209
|
+
setSetup(setup: SemanticSetup, detail?: SetupDetail): void;
|
|
210
|
+
/** Compatibility for hosts that already record an external start failure. */
|
|
193
211
|
refuse(reason: string): void;
|
|
212
|
+
setupNotice(): {
|
|
213
|
+
message: string;
|
|
214
|
+
level: "info" | "warning";
|
|
215
|
+
} | undefined;
|
|
194
216
|
configure(config: SemanticRecallConfig): void;
|
|
195
217
|
/** Ordinary external settings remain caller-selected/outside-project. The
|
|
196
218
|
* optional override is a current-process verified dependency, not Task data. */
|
|
@@ -199,6 +221,52 @@ export declare class SemanticRecallController {
|
|
|
199
221
|
}): Promise<void>;
|
|
200
222
|
enable(): void;
|
|
201
223
|
status(): {
|
|
224
|
+
setup: SemanticSetup;
|
|
225
|
+
setupMessage: string | null;
|
|
226
|
+
enabled: boolean;
|
|
227
|
+
configured: boolean;
|
|
228
|
+
cleanupError: string | null;
|
|
229
|
+
state: SemanticState;
|
|
230
|
+
snapshotId: string | null;
|
|
231
|
+
throughEntryId: string | null;
|
|
232
|
+
eligibleSources: number;
|
|
233
|
+
totalUnits: number;
|
|
234
|
+
coveredUnits: number;
|
|
235
|
+
indexingComplete: boolean;
|
|
236
|
+
lastError: string | null;
|
|
237
|
+
window: {
|
|
238
|
+
indexedCharacters: number;
|
|
239
|
+
windowCharacters: number;
|
|
240
|
+
outOfViewCharacters: number;
|
|
241
|
+
eligibleCharacters: number;
|
|
242
|
+
oldestCoveredEntryId: string | null;
|
|
243
|
+
oldestCoveredTime: string | null;
|
|
244
|
+
conversation: {
|
|
245
|
+
indexedCharacters: number;
|
|
246
|
+
outOfViewCharacters: number;
|
|
247
|
+
};
|
|
248
|
+
toolResults: {
|
|
249
|
+
indexedCharacters: number;
|
|
250
|
+
outOfViewCharacters: number;
|
|
251
|
+
oldestCoveredTime: string | null;
|
|
252
|
+
};
|
|
253
|
+
} | null;
|
|
254
|
+
recovery: {
|
|
255
|
+
attempts: number;
|
|
256
|
+
integrity: boolean;
|
|
257
|
+
retryAt: string | null;
|
|
258
|
+
} | null;
|
|
259
|
+
operations: {
|
|
260
|
+
preparationEmbeddings: number;
|
|
261
|
+
queryEmbeddings: number;
|
|
262
|
+
};
|
|
263
|
+
limits: {
|
|
264
|
+
keyword: number;
|
|
265
|
+
broad: number;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
/** The existing model-facing coverage contract: setup metadata belongs to operator status, not the request. */
|
|
269
|
+
requestCoverage(): {
|
|
202
270
|
enabled: boolean;
|
|
203
271
|
configured: boolean;
|
|
204
272
|
cleanupError: string | null;
|
|
@@ -251,6 +319,8 @@ export declare class SemanticRecallController {
|
|
|
251
319
|
private index;
|
|
252
320
|
private prepare;
|
|
253
321
|
waitForReady(timeoutMs?: number, signal?: AbortSignal, expectedSnapshotId?: string, forSearch?: boolean): Promise<{
|
|
322
|
+
setup: SemanticSetup;
|
|
323
|
+
setupMessage: string | null;
|
|
254
324
|
enabled: boolean;
|
|
255
325
|
configured: boolean;
|
|
256
326
|
cleanupError: string | null;
|
|
@@ -306,7 +376,7 @@ export declare class SemanticRecallController {
|
|
|
306
376
|
private assemble;
|
|
307
377
|
private focus;
|
|
308
378
|
private evidence;
|
|
309
|
-
disable(joinSearches?: boolean): Promise<void>;
|
|
379
|
+
disable(joinSearches?: boolean, userOff?: boolean): Promise<void>;
|
|
310
380
|
private closeResources;
|
|
311
381
|
private stopOwner;
|
|
312
382
|
/** A search, or a wait for readiness, is demand: it lets background work start and keeps it alive for IDLE_STOP_MS after the last one. */
|
|
@@ -188,6 +188,44 @@ const SCORE_CHUNK = 1024;
|
|
|
188
188
|
export function healDelayMs(attempts, integrity, delays = HEAL_DELAYS_MS, steady = HEAL_STEADY_MS) { return integrity ? steady : delays[attempts] ?? steady; }
|
|
189
189
|
/** Errors that mean "the history moved or a slice ended; try again", not that the runtime failed. They do not make the state `unavailable`. */
|
|
190
190
|
const CHURN = /indexing incomplete|history changed|source changed before|branch or source changed|cancelled/i;
|
|
191
|
+
/** Setup and operator wording have one owner; runtime indexing state remains separate. */
|
|
192
|
+
export function semanticSetupMessage(setup, detail = {}) {
|
|
193
|
+
switch (setup) {
|
|
194
|
+
case 'not-installed': return 'Semantic recall is not set up yet; start Pi once in an interactive session to set it up. Word recall works meanwhile';
|
|
195
|
+
case 'starting': return detail.installed ? 'Semantic recall is starting; word recall works meanwhile' : 'Semantic recall is being set up; word recall works meanwhile';
|
|
196
|
+
case 'failed': return detail.stage === 'start' ? detail.reason ?? 'Semantic recall could not start' : `Semantic recall setup failed: ${detail.reason}`;
|
|
197
|
+
default: return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function semanticSetupNotice(setup, detail = {}) {
|
|
201
|
+
let message, level = 'info';
|
|
202
|
+
switch (setup) {
|
|
203
|
+
case 'off-platform':
|
|
204
|
+
message = 'pi-tuck: semantic recall needs macOS on Apple silicon; compaction and word recall work.';
|
|
205
|
+
break;
|
|
206
|
+
case 'off-project':
|
|
207
|
+
message = "pi-tuck: semantic recall is off here because Pi's agent folder is inside this folder; open Pi in a project folder to use it.";
|
|
208
|
+
break;
|
|
209
|
+
case 'not-installed':
|
|
210
|
+
message = 'pi-tuck: semantic recall is not set up yet; start Pi once in an interactive session to set it up. Word recall works.';
|
|
211
|
+
level = 'warning';
|
|
212
|
+
break;
|
|
213
|
+
case 'starting':
|
|
214
|
+
message = detail.notice;
|
|
215
|
+
break;
|
|
216
|
+
case 'ready':
|
|
217
|
+
if (detail.announced)
|
|
218
|
+
message = 'pi-tuck: semantic recall is set up. Search by meaning works from the first cut on.';
|
|
219
|
+
break;
|
|
220
|
+
case 'failed':
|
|
221
|
+
message = detail.stage === 'start'
|
|
222
|
+
? `pi-tuck: semantic recall could not start (${detail.reason}); it tries again when a session starts.`
|
|
223
|
+
: `pi-tuck: semantic recall setup failed (${detail.reason}). Word recall works; setup retries when a session starts.`;
|
|
224
|
+
level = 'warning';
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
return message ? { message, level } : undefined;
|
|
228
|
+
}
|
|
191
229
|
const PROBE_TEXT = 'pi-tuck semantic recall status probe';
|
|
192
230
|
class LocalWorker {
|
|
193
231
|
config;
|
|
@@ -497,7 +535,7 @@ export function semanticEligibleSources(ctx, policy, access, pendingKeptId) {
|
|
|
497
535
|
const orders = new Map(branch.map((entry, order) => [entry.id, order]));
|
|
498
536
|
const sources = eligible.flatMap(entry => { const order = orders.get(entry.id); const row = recall.discoveryEntry(entry, attempts, links); if (!row)
|
|
499
537
|
return []; const { sourceText: unusedText, ranges: unusedRanges, ...readable } = row; return [{ entry, row: readable, sourceText: row.sourceText, ranges: row.ranges, order, sha: hash({ entry, order, row: readable, ranges: row.ranges, index: indices.get(entry.id) }) }]; });
|
|
500
|
-
const pending = pendingKeptId === undefined ? -1 : branch.findIndex(e => e.id === pendingKeptId), start = Math.max(viewStart(branch), pending);
|
|
538
|
+
const pending = pendingKeptId === undefined ? -1 : pendingKeptId === '' ? branch.length : branch.findIndex(e => e.id === pendingKeptId), start = Math.max(viewStart(branch), pending);
|
|
501
539
|
const session = ctx.sessionManager.getSessionId(), id = hash({ session, project: resolve(ctx.cwd), branch: branch.map(e => [e.id, e.parentId]), sources: sources.map(s => [s.entry.id, s.sha]), ...(pending >= 0 ? { pendingKeptId } : {}) });
|
|
502
540
|
return { id, session, project: ctx.cwd, sources, units: [], complete: false, throughEntryId: sources.at(-1)?.entry.id ?? null, viewStart: start, indexed: new Set() };
|
|
503
541
|
}
|
|
@@ -517,6 +555,8 @@ export class SemanticRecallController {
|
|
|
517
555
|
dirty = false;
|
|
518
556
|
operations = { preparationEmbeddings: 0, queryEmbeddings: 0 };
|
|
519
557
|
// The fields below decide `status().state`; their setters announce a change to `onStateChange` listeners.
|
|
558
|
+
setup = 'off-user';
|
|
559
|
+
setupDetail = {};
|
|
520
560
|
_enabled = false;
|
|
521
561
|
_current;
|
|
522
562
|
_task;
|
|
@@ -568,7 +608,10 @@ export class SemanticRecallController {
|
|
|
568
608
|
}
|
|
569
609
|
catch { } }); }
|
|
570
610
|
/** A host that could not configure the runtime records why a requested start failed. The controller stays disabled and `lastError` carries the reason until the next `enable`. */
|
|
571
|
-
|
|
611
|
+
setSetup(setup, detail = {}) { const announced = setup === 'ready' && this.setupDetail.announced; this.setup = setup; this.setupDetail = announced ? { ...detail, announced: true } : detail; this.touch(); }
|
|
612
|
+
/** Compatibility for hosts that already record an external start failure. */
|
|
613
|
+
refuse(reason) { this.setSetup('failed', { stage: 'start', reason }); }
|
|
614
|
+
setupNotice() { return semanticSetupNotice(this.setup, this.setupDetail); }
|
|
572
615
|
configure(config) { if (this.enabled || this.worker || this.task)
|
|
573
616
|
throw new Error('Disable before changing semantic configuration'); for (const [key, max] of [['queryWaitMs', 120000], ['prepareWorkMs', 120000], ['maxUnits', MAX_INDEX_UNITS], ['maxSourceCharacters', 2000000]]) {
|
|
574
617
|
const value = config[key];
|
|
@@ -592,9 +635,11 @@ export class SemanticRecallController {
|
|
|
592
635
|
}
|
|
593
636
|
enable() { if (this.closing || this.joiningSearches || this.cleanupError)
|
|
594
637
|
throw new Error('Semantic cleanup is unresolved; retry close before enable'); if (!this.config)
|
|
595
|
-
throw new Error('Optional local semantic runtime is not configured'); this.enabled = true; this.error = null; this.healReason = null; this.healAttempts = 0; this.cancelHeal(); if (this.provider && this.active)
|
|
638
|
+
throw new Error('Optional local semantic runtime is not configured'); this.setSetup('ready'); this.enabled = true; this.error = null; this.healReason = null; this.healAttempts = 0; this.cancelHeal(); if (this.provider && this.active)
|
|
596
639
|
this.schedule(this.provider); }
|
|
597
|
-
status() { return { enabled: this.enabled, configured: Boolean(this.config), cleanupError: this.cleanupError, state: (!this.enabled ? 'disabled' : this.healReason !== null || (this.error !== null && !CHURN.test(this.error)) ? 'unavailable' : this.task || this.timer ? 'preparing' : this.current?.complete ? 'ready' : 'waiting'), snapshotId: this.current?.id ?? null, throughEntryId: this.current?.throughEntryId ?? null, eligibleSources: this.current?.sources.length ?? 0, totalUnits: this.current?.units.length ?? 0, coveredUnits: this.current?.indexed.size ?? 0, indexingComplete: Boolean(this.current?.complete), lastError: this.error ?? this.healReason, window: this.current ? windowCoverage(this.current) : null, recovery: this.healReason === null ? null : { attempts: this.healAttempts, integrity: this.healIntegrity, retryAt: this.healRetryAt === null ? null : new Date(this.healRetryAt).toISOString() }, operations: { ...this.operations }, limits: { keyword: 16000, broad: 20000 } }; }
|
|
640
|
+
status() { return { setup: this.setup, setupMessage: semanticSetupMessage(this.setup, this.setupDetail), enabled: this.enabled, configured: Boolean(this.config), cleanupError: this.cleanupError, state: (!this.enabled ? 'disabled' : this.healReason !== null || (this.error !== null && !CHURN.test(this.error)) ? 'unavailable' : this.task || this.timer ? 'preparing' : this.current?.complete ? 'ready' : 'waiting'), snapshotId: this.current?.id ?? null, throughEntryId: this.current?.throughEntryId ?? null, eligibleSources: this.current?.sources.length ?? 0, totalUnits: this.current?.units.length ?? 0, coveredUnits: this.current?.indexed.size ?? 0, indexingComplete: Boolean(this.current?.complete), lastError: this.error ?? this.healReason ?? semanticSetupMessage(this.setup, this.setupDetail), window: this.current ? windowCoverage(this.current) : null, recovery: this.healReason === null ? null : { attempts: this.healAttempts, integrity: this.healIntegrity, retryAt: this.healRetryAt === null ? null : new Date(this.healRetryAt).toISOString() }, operations: { ...this.operations }, limits: { keyword: 16000, broad: 20000 } }; }
|
|
641
|
+
/** The existing model-facing coverage contract: setup metadata belongs to operator status, not the request. */
|
|
642
|
+
requestCoverage() { const { setup, setupMessage, ...coverage } = this.status(); return coverage; }
|
|
598
643
|
schedule(provider) { this.provider = provider; if (!this.enabled || this.timer || !this.active || this.healTimer)
|
|
599
644
|
return; this.timer = setTimeout(() => { this.timer = undefined; if (this.task) {
|
|
600
645
|
this.dirty = true;
|
|
@@ -858,10 +903,10 @@ export class SemanticRecallController {
|
|
|
858
903
|
if (signal?.aborted)
|
|
859
904
|
throw abort();
|
|
860
905
|
if (!this.enabled)
|
|
861
|
-
return { status: 'disabled', notice: 'Optional semantic recall is disabled; ordinary word recall remains available.', coverage: this.
|
|
906
|
+
return { status: 'disabled', notice: 'Optional semantic recall is disabled; ordinary word recall remains available.', coverage: this.requestCoverage(), groups: [] };
|
|
862
907
|
this.demand();
|
|
863
908
|
this.searched = true;
|
|
864
|
-
const deadline = performance.now() + (this.config.queryWaitMs ?? 30000), incomplete = () => ({ status: 'indexing_incomplete', notice: 'No semantic ranking performed: nothing of the requested eligible snapshot is indexed yet.', coverage: this.
|
|
909
|
+
const deadline = performance.now() + (this.config.queryWaitMs ?? 30000), incomplete = () => ({ status: 'indexing_incomplete', notice: 'No semantic ranking performed: nothing of the requested eligible snapshot is indexed yet.', coverage: this.requestCoverage(), groups: [] }), unavailable = () => ({ status: 'unavailable', notice: 'Local semantic preparation unavailable; permitted ordinary recall remains available.', coverage: this.requestCoverage(), groups: [] });
|
|
865
910
|
const queryGeneration = this.generation;
|
|
866
911
|
let bound;
|
|
867
912
|
try {
|
|
@@ -884,7 +929,7 @@ export class SemanticRecallController {
|
|
|
884
929
|
if (!(this.current?.id === bound.id && ((this.current.complete && !this.current.units.length) || (indexedPrefix(this.current) > 0 && this.worker !== undefined && !this.recycle && !this.idling)))) {
|
|
885
930
|
if (page)
|
|
886
931
|
return page;
|
|
887
|
-
return { status: 'indexing_incomplete', notice: 'No semantic ranking performed: nothing is out of view, so the whole history is in your context.', coverage: this.
|
|
932
|
+
return { status: 'indexing_incomplete', notice: 'No semantic ranking performed: nothing is out of view, so the whole history is in your context.', coverage: this.requestCoverage(), groups: [] };
|
|
888
933
|
}
|
|
889
934
|
}
|
|
890
935
|
else {
|
|
@@ -915,7 +960,7 @@ export class SemanticRecallController {
|
|
|
915
960
|
return failure && !CHURN.test(failure) ? unavailable() : incomplete();
|
|
916
961
|
}
|
|
917
962
|
if (!snapshot.units.length)
|
|
918
|
-
return { status: 'no_matches', notice: 'No eligible original source units in this snapshot.', coverage: this.
|
|
963
|
+
return { status: 'no_matches', notice: 'No eligible original source units in this snapshot.', coverage: this.requestCoverage(), method: 'embedding', groups: [] };
|
|
919
964
|
try {
|
|
920
965
|
const worker = await this.runtime(snapshot.project);
|
|
921
966
|
this.recheckGrown(provider, snapshot, bound.id);
|
|
@@ -947,7 +992,7 @@ export class SemanticRecallController {
|
|
|
947
992
|
// What the caller already read only spares it a repeat. A reference that does not match this history spares nothing, and it must not fail the search.
|
|
948
993
|
const usable = prior.filter(reference => { const source = snapshot.sources.find(s => s.entry.id === reference.entryId); return Number.isSafeInteger(reference.start) && Number.isSafeInteger(reference.end) && reference.start <= reference.end && source !== undefined && reference.sourceSha === source.sha && reference.end <= source.sourceText.length; }), ignored = prior.length - usable.length;
|
|
949
994
|
const previous = [...usable, ...(words?.hits ?? []).map((hit) => ({ entryId: hit.entryId, start: hit.read.offset, end: hit.read.offset + hit.read.limit, sourceSha: snapshot.sources.find(s => s.entry.id === hit.entryId)?.sha }))];
|
|
950
|
-
return { status: 'ok', notice: 'Related original evidence, not current truth or proof that every qualification was found. Scores do not establish authority, event time or supersession. Sources preserve recorded time and original order.' + (ignored ? ` ${ignored} of the priorResults did not match this history and were ignored, so their text may appear again.` : '') + ` Semantic ranking covers ${covered.conversation.indexedCharacters} of ${covered.conversation.outOfViewCharacters} characters of earlier conversation and ${covered.toolResults.indexedCharacters} of ${covered.toolResults.outOfViewCharacters} characters of earlier tool output (newest first, back to ${covered.toolResults.oldestCoveredTime ?? covered.oldestCoveredTime ?? 'an unknown time'}); use tuck_recall for the rest.` + (older ? ` This ranking covers the history through entry ${snapshot.throughEntryId}: later entries are still being indexed, so search them by keyword.` : ''), coverage: { ...this.
|
|
995
|
+
return { status: 'ok', notice: 'Related original evidence, not current truth or proof that every qualification was found. Scores do not establish authority, event time or supersession. Sources preserve recorded time and original order.' + (ignored ? ` ${ignored} of the priorResults did not match this history and were ignored, so their text may appear again.` : '') + ` Semantic ranking covers ${covered.conversation.indexedCharacters} of ${covered.conversation.outOfViewCharacters} characters of earlier conversation and ${covered.toolResults.indexedCharacters} of ${covered.toolResults.outOfViewCharacters} characters of earlier tool output (newest first, back to ${covered.toolResults.oldestCoveredTime ?? covered.oldestCoveredTime ?? 'an unknown time'}); use tuck_recall for the rest.` + (older ? ` This ranking covers the history through entry ${snapshot.throughEntryId}: later entries are still being indexed, so search them by keyword.` : ''), coverage: { ...this.requestCoverage(), window: covered }, method: 'embedding', groups: this.assemble(ranked.map(hit => hit.unit), snapshot, previous) };
|
|
951
996
|
}
|
|
952
997
|
catch (error) {
|
|
953
998
|
if (error instanceof Error && error.name === 'SemanticAccessError') {
|
|
@@ -959,7 +1004,7 @@ export class SemanticRecallController {
|
|
|
959
1004
|
throw abort();
|
|
960
1005
|
}
|
|
961
1006
|
this.heal(error);
|
|
962
|
-
return { status: 'unavailable', notice: 'Local semantic search failed; ordinary recall remains available. ' + (error instanceof Error ? error.message : 'failure'), coverage: this.
|
|
1007
|
+
return { status: 'unavailable', notice: 'Local semantic search failed; ordinary recall remains available. ' + (error instanceof Error ? error.message : 'failure'), coverage: this.requestCoverage(), groups: [] };
|
|
963
1008
|
}
|
|
964
1009
|
}
|
|
965
1010
|
assemble(units, snapshot, previous) {
|
|
@@ -1016,7 +1061,8 @@ export class SemanticRecallController {
|
|
|
1016
1061
|
return { source, start, end };
|
|
1017
1062
|
}
|
|
1018
1063
|
evidence(source, start, end) { return { ...source.row, sourceOrder: source.order, sourceSha: source.sha, text: source.sourceText.slice(start, end), textOffset: start, totalCharacters: source.sourceText.length, read: { entryId: source.entry.id, offset: start, limit: end - start }, complete: start === 0 && end === source.sourceText.length }; }
|
|
1019
|
-
async disable(joinSearches = true) { if (
|
|
1064
|
+
async disable(joinSearches = true, userOff = true) { if (userOff)
|
|
1065
|
+
this.setSetup('off-user'); if (joinSearches)
|
|
1020
1066
|
this.joiningSearches++; try {
|
|
1021
1067
|
if (!this.closing) {
|
|
1022
1068
|
const stop = this.stopOwner();
|
|
@@ -1142,7 +1188,7 @@ export class SemanticRecallController {
|
|
|
1142
1188
|
// A source that can no longer be read stops recall; the reason stays readable until the next enable, so this never looks like recall that was never on.
|
|
1143
1189
|
async revoked(error) { await this.disable(false); this.error = error instanceof Error ? error.message : 'Source access denied'; }
|
|
1144
1190
|
async invalidate() { await this.disable(); this.error = 'Caller invalidated semantic source access; explicit enable required'; }
|
|
1145
|
-
async close() { await this.disable(); this.provider = undefined; this.searched = false; } // a closed controller serves a new session, which has not searched; a disable and enable keeps it
|
|
1191
|
+
async close() { await this.disable(true, false); this.provider = undefined; this.searched = false; } // a closed controller serves a new session, which has not searched; a disable and enable keeps it
|
|
1146
1192
|
}
|
|
1147
1193
|
export const createSemanticRecall = (config) => new SemanticRecallController(config);
|
|
1148
1194
|
/** Consolidate only source-bound coordinate overlap; equal text in distinct events is retained.
|
package/dist/src/status.d.ts
CHANGED
package/dist/src/status.js
CHANGED
|
@@ -18,7 +18,7 @@ export function statusText(input) {
|
|
|
18
18
|
const cuts = input.branch.filter(e => e.type === "compaction"), ours = cuts.filter(e => e.details?.compactor === "pi-tuck");
|
|
19
19
|
const last = ours.at(-1), before = last?.tokensBefore, after = last?.details?.tokensAfter;
|
|
20
20
|
const count = (n) => n.toLocaleString("en-US");
|
|
21
|
-
const cutLine = ours.length === 0 ? "Cuts: none yet in this session. The recall tools appear after the first cut."
|
|
21
|
+
const cutLine = ours.length === 0 ? input.pending ? "Cuts: none saved yet in this session." : "Cuts: none yet in this session. The recall tools appear after the first cut."
|
|
22
22
|
: `Cuts: ${ours.length} by pi-tuck in this session` + (typeof before === "number" && typeof after === "number" ? `, the last from ${count(before)} to ${count(after)} tokens.` : ".");
|
|
23
23
|
const native = cuts.length - ours.length;
|
|
24
24
|
const retries = input.branch.filter(entry => entry.type === "custom" && entry.customType === "pi-tuck-capacity" &&
|
|
@@ -31,6 +31,7 @@ export function statusText(input) {
|
|
|
31
31
|
`pi-tuck ${input.version} (the version this session loaded; restart Pi after an update).`,
|
|
32
32
|
cutLine,
|
|
33
33
|
...(retries ? [`Retries: ${retries} after thinking-only length stops in this session.`] : []),
|
|
34
|
+
...(input.pending ? ["A cut is pending: requests already carry it; the first boundary saves it."] : []),
|
|
34
35
|
...(native > 0 ? [`Pi's own compaction cut ${native} time${native === 1 ? "" : "s"} in this session.`] : []),
|
|
35
36
|
input.cutAt === undefined ? "Cut point: adapts by itself." : `Cut point: fixed at cutAt ${input.cutAt} of the input room. Remove cutAt from pi-tuck.json to let it adapt.`,
|
|
36
37
|
searchLine,
|