pi-posthorse 0.10.18 → 0.10.20
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/package.json +1 -1
- package/src/guidance.ts +3 -1
- package/src/recovery-inputs.ts +255 -0
- package/src/recovery-records.ts +1 -0
- package/src/recovery.ts +28 -180
package/package.json
CHANGED
package/src/guidance.ts
CHANGED
|
@@ -24,7 +24,9 @@ export function registerGuidance(pi: ExtensionAPI, policy: SnapshotReader): void
|
|
|
24
24
|
if (promptOptions?.forceSystemPrompt !== undefined) {
|
|
25
25
|
// ponytail: Pi applies forced prompts after this hook; keep request-local guidance
|
|
26
26
|
// until the host projects forced text before context_with_system.
|
|
27
|
-
|
|
27
|
+
// After a compaction a second system message (the tool update) can follow the first.
|
|
28
|
+
const conversation = event.messages.findIndex((message) => message.role !== "system");
|
|
29
|
+
event.messages.splice(conversation === -1 ? event.messages.length : conversation, 0, {
|
|
28
30
|
role: "custom",
|
|
29
31
|
customType: "posthorse-guidance",
|
|
30
32
|
content: guidance,
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { AUTO_HANDOFF_PREFIX, isRecord, type EntryLike, type ProjectedEntry } from "./contracts.ts";
|
|
2
|
+
import { contextEdits, contextStart, windowHandoff } from "./history-entries.ts";
|
|
3
|
+
import { toolCalls } from "./message-text.ts";
|
|
4
|
+
import {
|
|
5
|
+
formatRecord,
|
|
6
|
+
MAX_RECOVERY_RECORD_CHARS,
|
|
7
|
+
recoveryRecord,
|
|
8
|
+
type RecoveryRecord,
|
|
9
|
+
} from "./recovery-records.ts";
|
|
10
|
+
|
|
11
|
+
/** One input's room as a share of the whole record: 4,000 of the 20,000 characters a record may have. */
|
|
12
|
+
const RECORD_SHARE = 5;
|
|
13
|
+
|
|
14
|
+
function projectedCurrent(
|
|
15
|
+
entries: readonly EntryLike[],
|
|
16
|
+
projected: readonly ProjectedEntry[],
|
|
17
|
+
current: readonly EntryLike[] = entries.slice(contextStart(entries).start),
|
|
18
|
+
retained: ReadonlySet<string> = new Set(),
|
|
19
|
+
): EntryLike[] {
|
|
20
|
+
const edits = contextEdits(entries);
|
|
21
|
+
const omittedIds = new Set(
|
|
22
|
+
projected
|
|
23
|
+
.filter(({ messages }) => messages.length === 0)
|
|
24
|
+
.map(({ sourceEntry }) => sourceEntry.id),
|
|
25
|
+
);
|
|
26
|
+
return current.flatMap((entry) => {
|
|
27
|
+
const id = entry.id ?? "";
|
|
28
|
+
if (retained.has(id) || omittedIds.has(id)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const edit = edits.get(id);
|
|
32
|
+
if (edit?.replacement === null) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
if (edit?.replacement === undefined) {
|
|
36
|
+
return [entry];
|
|
37
|
+
}
|
|
38
|
+
if (entry.type === "message") {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
...entry,
|
|
42
|
+
id: edit.id,
|
|
43
|
+
message: { ...entry.message, content: edit.replacement.content },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
if (entry.type === "custom_message") {
|
|
48
|
+
return [{ ...entry, id: edit.id, content: edit.replacement.content }];
|
|
49
|
+
}
|
|
50
|
+
return [entry];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function ownerRecords(
|
|
54
|
+
entries: readonly EntryLike[],
|
|
55
|
+
projected: readonly ProjectedEntry[],
|
|
56
|
+
ownerQuestionRegistered: boolean,
|
|
57
|
+
): RecoveryRecord[] {
|
|
58
|
+
const calls = new Map(
|
|
59
|
+
projected.flatMap(({ messages }) =>
|
|
60
|
+
messages.flatMap((message) =>
|
|
61
|
+
message.role === "assistant"
|
|
62
|
+
? toolCalls(message.content).map((call) => [call.id, call] as const)
|
|
63
|
+
: [],
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
return entries.flatMap((entry) => {
|
|
68
|
+
const call = calls.get(entry.message?.toolCallId);
|
|
69
|
+
const record = recoveryRecord(
|
|
70
|
+
entry,
|
|
71
|
+
ownerQuestionRegistered &&
|
|
72
|
+
call?.name === "ask_question" &&
|
|
73
|
+
call.namespace === undefined,
|
|
74
|
+
);
|
|
75
|
+
return record === undefined ? [] : [record];
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function anchors(records: readonly RecoveryRecord[]): {
|
|
79
|
+
fixed: RecoveryRecord[];
|
|
80
|
+
others: RecoveryRecord[];
|
|
81
|
+
} {
|
|
82
|
+
const firstOwner = records.find((record) => record.label === "owner input") ?? records[0];
|
|
83
|
+
const latestOwner = [...records].reverse().find((record) => record.kind === "owner");
|
|
84
|
+
const latestOverall = records.at(-1);
|
|
85
|
+
const selected = new Set([firstOwner, latestOwner, latestOverall]);
|
|
86
|
+
return {
|
|
87
|
+
fixed: records.filter((record) => selected.has(record)),
|
|
88
|
+
others: records.filter((record) => !selected.has(record)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function automaticBoundary(entry: EntryLike | undefined): boolean {
|
|
92
|
+
if (
|
|
93
|
+
entry === undefined ||
|
|
94
|
+
windowHandoff(entry)?.trim().startsWith(AUTO_HANDOFF_PREFIX) !== true
|
|
95
|
+
) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
entry.type === "context_window" ||
|
|
100
|
+
(isRecord(entry.details) &&
|
|
101
|
+
entry.details.posthorse === 1 &&
|
|
102
|
+
(entry.details.reason === "threshold" || entry.details.reason === "overflow"))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
export function automaticChainStart(entries: readonly EntryLike[]): number {
|
|
106
|
+
let prefix = entries;
|
|
107
|
+
let window = contextStart(prefix);
|
|
108
|
+
while (automaticBoundary(window.boundary) && window.boundary !== undefined) {
|
|
109
|
+
prefix = prefix.slice(0, prefix.indexOf(window.boundary));
|
|
110
|
+
window = contextStart(prefix);
|
|
111
|
+
}
|
|
112
|
+
return window.start;
|
|
113
|
+
}
|
|
114
|
+
function windowOwners(
|
|
115
|
+
entries: readonly EntryLike[],
|
|
116
|
+
projected: readonly ProjectedEntry[],
|
|
117
|
+
window: readonly EntryLike[],
|
|
118
|
+
ownerQuestionRegistered: boolean,
|
|
119
|
+
): RecoveryRecord[] {
|
|
120
|
+
const restored = projectedCurrent(entries, projected, window);
|
|
121
|
+
const historical = restored.map((sourceEntry) => ({
|
|
122
|
+
sourceEntry,
|
|
123
|
+
messages: sourceEntry.message === undefined ? [] : [sourceEntry.message],
|
|
124
|
+
}));
|
|
125
|
+
return ownerRecords(restored, historical, ownerQuestionRegistered).filter(
|
|
126
|
+
(record) => record.kind === "owner",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
/** The windows before the current one, newest first, as far back as automatic boundaries reach. */
|
|
130
|
+
function* earlierWindows(entries: readonly EntryLike[]): Generator<{
|
|
131
|
+
readonly closedBy: EntryLike;
|
|
132
|
+
readonly entries: readonly EntryLike[];
|
|
133
|
+
readonly openedBy: EntryLike | undefined;
|
|
134
|
+
}> {
|
|
135
|
+
let prefix = entries;
|
|
136
|
+
let boundary = contextStart(prefix).boundary;
|
|
137
|
+
while (boundary !== undefined && automaticBoundary(boundary)) {
|
|
138
|
+
prefix = prefix.slice(0, prefix.indexOf(boundary));
|
|
139
|
+
const window = contextStart(prefix);
|
|
140
|
+
yield {
|
|
141
|
+
closedBy: boundary,
|
|
142
|
+
entries: prefix.slice(window.start),
|
|
143
|
+
openedBy: window.boundary,
|
|
144
|
+
};
|
|
145
|
+
boundary = window.boundary;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** A window's owner inputs that still fit the room, taken newest first and returned oldest first. */
|
|
149
|
+
function newestThatFit(
|
|
150
|
+
owners: readonly RecoveryRecord[],
|
|
151
|
+
room: number,
|
|
152
|
+
seen: Set<string>,
|
|
153
|
+
): { readonly records: readonly RecoveryRecord[]; readonly used: number } {
|
|
154
|
+
const records: RecoveryRecord[] = [];
|
|
155
|
+
let used = 0;
|
|
156
|
+
for (const owner of owners.toReversed()) {
|
|
157
|
+
if (used >= room) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (seen.has(owner.id)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const record = {
|
|
164
|
+
...owner,
|
|
165
|
+
label: `${owner.label}; carried from an earlier window, possibly already handled; not current progress`,
|
|
166
|
+
};
|
|
167
|
+
seen.add(owner.id);
|
|
168
|
+
records.unshift(record);
|
|
169
|
+
used += formatRecord(record, Number.MAX_SAFE_INTEGER).length;
|
|
170
|
+
}
|
|
171
|
+
return { records, used };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Earlier windows give a record the owner's most recent inputs, newest first, until they fill the
|
|
175
|
+
* room of one input. Owner text the current window already holds counts against that room, so a
|
|
176
|
+
* short follow-up keeps the task before it and a long new request stands alone.
|
|
177
|
+
*/
|
|
178
|
+
function recoveryAncestry(
|
|
179
|
+
entries: readonly EntryLike[],
|
|
180
|
+
projected: readonly ProjectedEntry[],
|
|
181
|
+
ownerQuestionRegistered: boolean,
|
|
182
|
+
options: { readonly current: readonly RecoveryRecord[]; readonly maxChars: number },
|
|
183
|
+
): {
|
|
184
|
+
readonly checkpoint?: EntryLike | undefined;
|
|
185
|
+
readonly carried: readonly RecoveryRecord[];
|
|
186
|
+
readonly skipped: readonly string[];
|
|
187
|
+
} {
|
|
188
|
+
const held = options.current
|
|
189
|
+
.filter((record) => record.kind === "owner")
|
|
190
|
+
.reduce((chars, record) => chars + record.text.length, 0);
|
|
191
|
+
let room =
|
|
192
|
+
Math.min(MAX_RECOVERY_RECORD_CHARS, Math.floor(options.maxChars / RECORD_SHARE)) - held;
|
|
193
|
+
const seen = new Set(options.current.map((record) => record.id));
|
|
194
|
+
const carried: RecoveryRecord[] = [];
|
|
195
|
+
const skipped: EntryLike[] = [];
|
|
196
|
+
let ownerWindowFound = false;
|
|
197
|
+
let reached = contextStart(entries).boundary;
|
|
198
|
+
for (const window of earlierWindows(entries)) {
|
|
199
|
+
if (ownerWindowFound && room <= 0) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
if (!ownerWindowFound) {
|
|
203
|
+
skipped.push(window.closedBy);
|
|
204
|
+
}
|
|
205
|
+
const owners = windowOwners(entries, projected, window.entries, ownerQuestionRegistered);
|
|
206
|
+
ownerWindowFound ||= owners.length > 0;
|
|
207
|
+
const fit = newestThatFit(owners, room, seen);
|
|
208
|
+
carried.unshift(...fit.records);
|
|
209
|
+
room -= fit.used;
|
|
210
|
+
reached = window.openedBy;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
checkpoint: automaticBoundary(reached) ? undefined : reached,
|
|
214
|
+
carried,
|
|
215
|
+
skipped: skipped.map((boundary) => boundary.id?.slice(0, 120) ?? "unknown"),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** The inputs a record quotes in full (oldest first), the ones it only lists, and where its walk back ended. */
|
|
219
|
+
export function recoveryInputs(
|
|
220
|
+
entries: readonly EntryLike[],
|
|
221
|
+
projected: readonly ProjectedEntry[],
|
|
222
|
+
options: {
|
|
223
|
+
readonly ownerQuestionRegistered: boolean;
|
|
224
|
+
readonly retained: ReadonlySet<string>;
|
|
225
|
+
readonly maxChars: number;
|
|
226
|
+
},
|
|
227
|
+
): {
|
|
228
|
+
readonly fixed: readonly RecoveryRecord[];
|
|
229
|
+
readonly others: readonly RecoveryRecord[];
|
|
230
|
+
readonly checkpoint?: EntryLike | undefined;
|
|
231
|
+
readonly skipped: readonly string[];
|
|
232
|
+
} {
|
|
233
|
+
const { ownerQuestionRegistered, retained, maxChars } = options;
|
|
234
|
+
const { carried, checkpoint, skipped } = recoveryAncestry(
|
|
235
|
+
entries,
|
|
236
|
+
projected,
|
|
237
|
+
ownerQuestionRegistered,
|
|
238
|
+
{
|
|
239
|
+
current: ownerRecords(
|
|
240
|
+
projectedCurrent(entries, projected),
|
|
241
|
+
projected,
|
|
242
|
+
ownerQuestionRegistered,
|
|
243
|
+
),
|
|
244
|
+
maxChars,
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
const { fixed, others } = anchors(
|
|
248
|
+
ownerRecords(
|
|
249
|
+
projectedCurrent(entries, projected, undefined, retained),
|
|
250
|
+
projected,
|
|
251
|
+
ownerQuestionRegistered,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
return { fixed: [...carried, ...fixed], others, checkpoint, skipped };
|
|
255
|
+
}
|
package/src/recovery-records.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isReminderType, type EntryLike } from "./contracts.ts";
|
|
2
2
|
import { boundedBlock, imageSummary, imagesOf, textOf } from "./message-text.ts";
|
|
3
3
|
|
|
4
|
+
export const MAX_RECOVERY_RECORD_CHARS = 4_000;
|
|
4
5
|
export type RecoveryRecord = {
|
|
5
6
|
readonly id: string;
|
|
6
7
|
readonly timestamp: string;
|
package/src/recovery.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AUTO_HANDOFF_PREFIX,
|
|
1
|
+
import { AUTO_HANDOFF_PREFIX, type EntryLike, type ProjectedEntry } from "./contracts.ts";
|
|
2
2
|
import { contextEdits, contextStart, windowHandoff } from "./history-entries.ts";
|
|
3
|
-
import { boundedBlock, excerpt
|
|
3
|
+
import { boundedBlock, excerpt } from "./message-text.ts";
|
|
4
|
+
import { automaticChainStart, recoveryInputs } from "./recovery-inputs.ts";
|
|
4
5
|
import {
|
|
5
6
|
formatRecord,
|
|
6
7
|
indexLine,
|
|
7
|
-
|
|
8
|
+
MAX_RECOVERY_RECORD_CHARS,
|
|
8
9
|
type RecoveryRecord,
|
|
9
10
|
} from "./recovery-records.ts";
|
|
10
11
|
import type { NotesRecovery } from "./note-evidence.ts";
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
type RecoveryEntry,
|
|
17
18
|
} from "./recovery-results.ts";
|
|
18
19
|
|
|
19
|
-
const MAX_RECOVERY_RECORD_CHARS = 4_000;
|
|
20
20
|
const MAX_RECOVERY_RESULT_CHARS = 1_500;
|
|
21
21
|
const HANDOFF_OVERHEAD_RESERVE = 1_000;
|
|
22
22
|
const MAX_NOTES_SECTION_CHARS = 1_200;
|
|
@@ -69,142 +69,6 @@ function priorCheckpoint(entry: EntryLike | undefined, limit: number): string |
|
|
|
69
69
|
const header = `[older checkpoint; possibly stale | context-window entry ${id}]`;
|
|
70
70
|
return boundedBlock(header, handoff, limit);
|
|
71
71
|
}
|
|
72
|
-
function projectedCurrent(
|
|
73
|
-
entries: readonly EntryLike[],
|
|
74
|
-
projected: readonly ProjectedEntry[],
|
|
75
|
-
current: readonly EntryLike[] = entries.slice(contextStart(entries).start),
|
|
76
|
-
retained: ReadonlySet<string> = new Set(),
|
|
77
|
-
): EntryLike[] {
|
|
78
|
-
const edits = contextEdits(entries);
|
|
79
|
-
const omittedIds = new Set(
|
|
80
|
-
projected
|
|
81
|
-
.filter(({ messages }) => messages.length === 0)
|
|
82
|
-
.map(({ sourceEntry }) => sourceEntry.id),
|
|
83
|
-
);
|
|
84
|
-
return current.flatMap((entry) => {
|
|
85
|
-
const id = entry.id ?? "";
|
|
86
|
-
if (retained.has(id) || omittedIds.has(id)) {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
const edit = edits.get(id);
|
|
90
|
-
if (edit?.replacement === null) {
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
if (edit?.replacement === undefined) {
|
|
94
|
-
return [entry];
|
|
95
|
-
}
|
|
96
|
-
if (entry.type === "message") {
|
|
97
|
-
return [
|
|
98
|
-
{
|
|
99
|
-
...entry,
|
|
100
|
-
id: edit.id,
|
|
101
|
-
message: { ...entry.message, content: edit.replacement.content },
|
|
102
|
-
},
|
|
103
|
-
];
|
|
104
|
-
}
|
|
105
|
-
if (entry.type === "custom_message") {
|
|
106
|
-
return [{ ...entry, id: edit.id, content: edit.replacement.content }];
|
|
107
|
-
}
|
|
108
|
-
return [entry];
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
function ownerRecords(
|
|
112
|
-
entries: readonly EntryLike[],
|
|
113
|
-
projected: readonly ProjectedEntry[],
|
|
114
|
-
ownerQuestionRegistered: boolean,
|
|
115
|
-
): RecoveryRecord[] {
|
|
116
|
-
const calls = new Map(
|
|
117
|
-
projected.flatMap(({ messages }) =>
|
|
118
|
-
messages.flatMap((message) =>
|
|
119
|
-
message.role === "assistant"
|
|
120
|
-
? toolCalls(message.content).map((call) => [call.id, call] as const)
|
|
121
|
-
: [],
|
|
122
|
-
),
|
|
123
|
-
),
|
|
124
|
-
);
|
|
125
|
-
return entries.flatMap((entry) => {
|
|
126
|
-
const call = calls.get(entry.message?.toolCallId);
|
|
127
|
-
const record = recoveryRecord(
|
|
128
|
-
entry,
|
|
129
|
-
ownerQuestionRegistered &&
|
|
130
|
-
call?.name === "ask_question" &&
|
|
131
|
-
call.namespace === undefined,
|
|
132
|
-
);
|
|
133
|
-
return record === undefined ? [] : [record];
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
function anchors(records: readonly RecoveryRecord[]): {
|
|
137
|
-
fixed: RecoveryRecord[];
|
|
138
|
-
others: RecoveryRecord[];
|
|
139
|
-
} {
|
|
140
|
-
const firstOwner = records.find((record) => record.label === "owner input") ?? records[0];
|
|
141
|
-
const latestOwner = [...records].reverse().find((record) => record.kind === "owner");
|
|
142
|
-
const latestOverall = records.at(-1);
|
|
143
|
-
const selected = new Set([firstOwner, latestOwner, latestOverall]);
|
|
144
|
-
return {
|
|
145
|
-
fixed: records.filter((record) => selected.has(record)),
|
|
146
|
-
others: records.filter((record) => !selected.has(record)),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function automaticBoundary(entry: EntryLike | undefined): boolean {
|
|
150
|
-
if (
|
|
151
|
-
entry === undefined ||
|
|
152
|
-
windowHandoff(entry)?.trim().startsWith(AUTO_HANDOFF_PREFIX) !== true
|
|
153
|
-
) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return (
|
|
157
|
-
entry.type === "context_window" ||
|
|
158
|
-
(isRecord(entry.details) &&
|
|
159
|
-
entry.details.posthorse === 1 &&
|
|
160
|
-
(entry.details.reason === "threshold" || entry.details.reason === "overflow"))
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
function automaticChainStart(entries: readonly EntryLike[]): number {
|
|
164
|
-
let prefix = entries;
|
|
165
|
-
let window = contextStart(prefix);
|
|
166
|
-
while (automaticBoundary(window.boundary) && window.boundary !== undefined) {
|
|
167
|
-
prefix = prefix.slice(0, prefix.indexOf(window.boundary));
|
|
168
|
-
window = contextStart(prefix);
|
|
169
|
-
}
|
|
170
|
-
return window.start;
|
|
171
|
-
}
|
|
172
|
-
function recoveryAncestry(
|
|
173
|
-
entries: readonly EntryLike[],
|
|
174
|
-
projected: readonly ProjectedEntry[],
|
|
175
|
-
ownerQuestionRegistered: boolean,
|
|
176
|
-
retained: ReadonlySet<string>,
|
|
177
|
-
): {
|
|
178
|
-
readonly checkpoint?: EntryLike | undefined;
|
|
179
|
-
readonly carried: readonly RecoveryRecord[];
|
|
180
|
-
readonly skipped: readonly string[];
|
|
181
|
-
} {
|
|
182
|
-
let prefix = entries;
|
|
183
|
-
let boundary = contextStart(prefix).boundary;
|
|
184
|
-
const skipped: string[] = [];
|
|
185
|
-
while (automaticBoundary(boundary) && boundary !== undefined) {
|
|
186
|
-
skipped.push(boundary.id?.slice(0, 120) ?? "unknown");
|
|
187
|
-
prefix = prefix.slice(0, prefix.indexOf(boundary));
|
|
188
|
-
const window = contextStart(prefix);
|
|
189
|
-
const restored = projectedCurrent(entries, projected, prefix.slice(window.start));
|
|
190
|
-
const historical = restored.map((sourceEntry) => ({
|
|
191
|
-
sourceEntry,
|
|
192
|
-
messages: sourceEntry.message === undefined ? [] : [sourceEntry.message],
|
|
193
|
-
}));
|
|
194
|
-
const owners = ownerRecords(restored, historical, ownerQuestionRegistered).filter(
|
|
195
|
-
(record) => record.kind === "owner",
|
|
196
|
-
);
|
|
197
|
-
if (owners.length > 0) {
|
|
198
|
-
return {
|
|
199
|
-
checkpoint: automaticBoundary(window.boundary) ? undefined : window.boundary,
|
|
200
|
-
carried: anchors(owners.filter((record) => !retained.has(record.id))).fixed,
|
|
201
|
-
skipped,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
boundary = window.boundary;
|
|
205
|
-
}
|
|
206
|
-
return { checkpoint: boundary, carried: [], skipped };
|
|
207
|
-
}
|
|
208
72
|
function skippedRecords(ids: readonly string[], limit: number): string | undefined {
|
|
209
73
|
const remaining = ids.values();
|
|
210
74
|
const first = remaining.next();
|
|
@@ -346,34 +210,20 @@ function renderIndex(records: readonly RecoveryRecord[], available: number): str
|
|
|
346
210
|
...listed,
|
|
347
211
|
].join("\n");
|
|
348
212
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const records = ownerRecords(
|
|
362
|
-
projectedCurrent(entries, projected, undefined, retained),
|
|
363
|
-
projected,
|
|
364
|
-
ownerQuestionRegistered,
|
|
365
|
-
);
|
|
366
|
-
const { fixed, others } = anchors(records);
|
|
367
|
-
if (!allRecords.some((record) => record.kind === "owner")) {
|
|
368
|
-
fixed.unshift(
|
|
369
|
-
...carried.map((record) =>
|
|
370
|
-
Object.assign({}, record, {
|
|
371
|
-
label: `${record.label}; carried from an earlier window, possibly already handled; not current progress`,
|
|
372
|
-
}),
|
|
373
|
-
),
|
|
374
|
-
);
|
|
213
|
+
/** The per-block limit at which the blocks fit the budget together; short blocks leave room to long ones. */
|
|
214
|
+
function sharedLimit(lengths: readonly number[], budget: number): number {
|
|
215
|
+
let remaining = budget;
|
|
216
|
+
const ascending = lengths
|
|
217
|
+
.map((length) => Math.min(length, MAX_RECOVERY_RECORD_CHARS))
|
|
218
|
+
.toSorted((a, b) => a - b);
|
|
219
|
+
for (const [index, length] of ascending.entries()) {
|
|
220
|
+
const share = Math.floor(remaining / (ascending.length - index));
|
|
221
|
+
if (length > share) {
|
|
222
|
+
return share;
|
|
223
|
+
}
|
|
224
|
+
remaining -= length;
|
|
375
225
|
}
|
|
376
|
-
return
|
|
226
|
+
return MAX_RECOVERY_RECORD_CHARS;
|
|
377
227
|
}
|
|
378
228
|
function rolloverPreambleWithNotes(
|
|
379
229
|
entries: readonly EntryLike[],
|
|
@@ -417,13 +267,12 @@ export function buildAutoHandoff(
|
|
|
417
267
|
},
|
|
418
268
|
): string {
|
|
419
269
|
const { ownerQuestionRegistered, retainedEntryIds: retained = new Set<string>() } = options;
|
|
420
|
-
const history =
|
|
421
|
-
const priorWindow = history.checkpoint;
|
|
422
|
-
const { fixed, others } = selectedInputs(entries, projected, {
|
|
270
|
+
const { fixed, others, ...history } = recoveryInputs(entries, projected, {
|
|
423
271
|
ownerQuestionRegistered,
|
|
424
272
|
retained,
|
|
425
|
-
|
|
273
|
+
maxChars,
|
|
426
274
|
});
|
|
275
|
+
const priorWindow = history.checkpoint;
|
|
427
276
|
const emptyInputHeader =
|
|
428
277
|
retained.size > 0
|
|
429
278
|
? "No discarded owner or visible coordination inputs were selected for this recovery record."
|
|
@@ -453,19 +302,18 @@ export function buildAutoHandoff(
|
|
|
453
302
|
header,
|
|
454
303
|
Math.max(0, maxChars - joined(minimum).length - HANDOFF_OVERHEAD_RESERVE),
|
|
455
304
|
);
|
|
456
|
-
const handoff = windowHandoff(priorWindow)?.trim();
|
|
457
|
-
const count =
|
|
458
|
-
fixed.length +
|
|
459
|
-
(handoff === undefined || handoff === "" ? 0 : 1) +
|
|
460
|
-
(history.skipped.length > 0 ? 1 : 0);
|
|
461
305
|
const fixedBudget = Math.max(
|
|
462
306
|
0,
|
|
463
307
|
maxChars - joined([preamble, inputHeader, plan.bare]).length - HANDOFF_OVERHEAD_RESERVE,
|
|
464
308
|
);
|
|
465
|
-
const fixedLimit =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
309
|
+
const fixedLimit = sharedLimit(
|
|
310
|
+
[
|
|
311
|
+
...fixed.map((record) => formatRecord(record, Number.MAX_SAFE_INTEGER)),
|
|
312
|
+
priorCheckpoint(priorWindow, Number.MAX_SAFE_INTEGER),
|
|
313
|
+
skippedRecords(history.skipped, Number.MAX_SAFE_INTEGER),
|
|
314
|
+
].flatMap((block) => (block === undefined ? [] : [block.length])),
|
|
315
|
+
fixedBudget,
|
|
316
|
+
);
|
|
469
317
|
const prior = joined([
|
|
470
318
|
priorCheckpoint(priorWindow, fixedLimit),
|
|
471
319
|
skippedRecords(history.skipped, fixedLimit),
|