pi-posthorse 0.10.21 → 0.10.22
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 +2 -2
- package/package.json +1 -1
- package/src/provider-summary.ts +53 -0
- package/src/recovery-notes.ts +43 -15
- package/src/recovery.ts +15 -1
- package/src/retention.ts +63 -8
- package/src/rollover.ts +20 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Posthorse
|
|
2
2
|
|
|
3
|
-
Posthorse starts fresh context windows for long [Pi](https://github.com/earendil-works/pi) tasks without an automatic model-generated summary. Stock Pi already preserves the session journal and retains a recent tail during compaction; Posthorse adds explicit notes and history retrieval, with a handoff or bounded recovery record instead of the automatic summary.
|
|
3
|
+
Posthorse starts fresh context windows for long [Pi](https://github.com/earendil-works/pi) tasks without an automatic model-generated summary of the work. Stock Pi already preserves the session journal and retains a recent tail during compaction; Posthorse adds explicit notes and history retrieval, with a handoff or bounded recovery record instead of the automatic summary.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ Give Pi your task. For example:
|
|
|
25
25
|
|
|
26
26
|
Posthorse can send a routine task-note reminder before rollover. The reminder tells the model to save current task state if needed, then immediately continue the prescribed work. The context window is not a task deadline. The reminder does not require `new_context` or wake an agent after its final answer. Large turns can reach rollover before the reminder. See [Reminder timing](docs/reminder-timing.md) for the trigger and its relationship to Pi's reserve and retention settings.
|
|
27
27
|
|
|
28
|
-
Automatic rollover retains a safe recent-message tail using Pi's `compaction.keepRecentTokens`, alongside a bounded recovery record for discarded inputs and tool evidence.
|
|
28
|
+
Automatic rollover retains a safe recent-message tail using Pi's `compaction.keepRecentTokens`, alongside a bounded recovery record for discarded inputs and tool evidence. No model summarizes the work. The model must restore notes and check live state before it continues. On [fitchmultz/pi](https://github.com/fitchmultz/pi), a Claude model also writes one line that points to the task note, so that Claude keeps [the reasoning of the retained replies](https://github.com/fitchmultz/pi-posthorse/blob/main/docs/context-recovery.md#claude-reasoning-in-the-retained-tail). Optional `new_context` deliberately retains no prior messages; manual `/compact` uses Pi's normal summarization.
|
|
29
29
|
|
|
30
30
|
Posthorse does not schedule rollover when a provider cache expires. An idle session below the
|
|
31
31
|
rollover threshold keeps its context; use an explicit `new_context` when a deliberate clean reset
|
package/package.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as host from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A one-line summary block written by the provider at an automatic rollover.
|
|
5
|
+
*
|
|
6
|
+
* A host that implements `providerSummary` on a compaction asks the session's model for the block
|
|
7
|
+
* and sends it ahead of the retained tail, which keeps the reasoning of the tail's replies valid.
|
|
8
|
+
* The block carries no task state; the recovery record after the tail and the task note do.
|
|
9
|
+
*/
|
|
10
|
+
export type ProviderSummaryRequest = { readonly instructions: string };
|
|
11
|
+
type SupportProbe = (model: unknown) => unknown;
|
|
12
|
+
function isProbe(value: unknown): value is SupportProbe {
|
|
13
|
+
return typeof value === "function";
|
|
14
|
+
}
|
|
15
|
+
/** Exported by a host that can make the block; it knows which models return one. */
|
|
16
|
+
const hostSupport: unknown = Reflect.get(host, "supportsProviderSummary");
|
|
17
|
+
export function hostMakesProviderSummary(model: unknown): boolean {
|
|
18
|
+
return isProbe(hostSupport) && model !== undefined && hostSupport(model) === true;
|
|
19
|
+
}
|
|
20
|
+
/** Measured size of a one-line block is 59 to 77 tokens; Pi's estimate does not count it. */
|
|
21
|
+
export const PROVIDER_SUMMARY_TOKENS = 100;
|
|
22
|
+
/**
|
|
23
|
+
* The model answers a request for a one-line summary. It refuses a request to reply with a fixed
|
|
24
|
+
* line or with the path only, and one that tells the summary to speak about "the recent messages",
|
|
25
|
+
* so the wording asks for a summary that points to the note, or to the record when there is none.
|
|
26
|
+
*/
|
|
27
|
+
export function providerSummaryInstructions(notePath: string | undefined): string {
|
|
28
|
+
const pointer =
|
|
29
|
+
notePath === undefined
|
|
30
|
+
? "state that the details are in the recovery record below"
|
|
31
|
+
: `point to the task note \`${notePath}\` and say to read that note and proceed from there`;
|
|
32
|
+
return `Write a summary of one line, at most 20 words: name the task in a few words, then ${pointer}. Do not mention progress, a pending step or a next step. Do not call any tools; respond with the summary text only.`;
|
|
33
|
+
}
|
|
34
|
+
type MessageLike = { readonly role: string };
|
|
35
|
+
/**
|
|
36
|
+
* How many leading messages Pi sends exactly as the provider last saw them. With a block, those
|
|
37
|
+
* are the leading system message and the retained tail up to the record; changing, removing or
|
|
38
|
+
* reordering one makes the provider drop the reasoning of every reply after it. A tail that
|
|
39
|
+
* reaches back across an earlier rollover holds that rollover's record too, so the boundary is
|
|
40
|
+
* the last record.
|
|
41
|
+
*/
|
|
42
|
+
export function messagesHeldByProviderSummary(messages: readonly MessageLike[]): number {
|
|
43
|
+
const [first] = messages;
|
|
44
|
+
if (
|
|
45
|
+
first?.role !== "system" ||
|
|
46
|
+
!("providerSummary" in first) ||
|
|
47
|
+
first.providerSummary === undefined
|
|
48
|
+
) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const record = messages.findLastIndex((message) => message.role === "compactionSummary");
|
|
52
|
+
return record < 0 ? messages.length : record;
|
|
53
|
+
}
|
package/src/recovery-notes.ts
CHANGED
|
@@ -172,34 +172,62 @@ function trackedNotes(
|
|
|
172
172
|
}
|
|
173
173
|
return [...tracked.values()].sort((a, b) => b.order - a.order);
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
type NoteSources = {
|
|
176
|
+
readonly retained: ReadonlySet<string>;
|
|
177
|
+
readonly notes?: NotesRecovery | undefined;
|
|
178
|
+
};
|
|
179
|
+
function sessionNotes(
|
|
176
180
|
chainEntries: readonly EntryLike[],
|
|
177
181
|
allEntries: readonly EntryLike[],
|
|
178
182
|
projected: readonly ProjectedEntry[],
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
readonly span: RetainedSpan | undefined;
|
|
182
|
-
readonly maxChars: number;
|
|
183
|
-
readonly notes?: NotesRecovery | undefined;
|
|
184
|
-
},
|
|
185
|
-
): string | undefined {
|
|
186
|
-
const { retained, span, maxChars } = options;
|
|
183
|
+
sources: NoteSources,
|
|
184
|
+
): { readonly all: readonly TrackedNote[]; readonly notesDirectory: string | undefined } {
|
|
187
185
|
const collected = sessionNoteTouches({
|
|
188
186
|
branch: chainEntries,
|
|
189
|
-
allEntries:
|
|
187
|
+
allEntries: sources.notes?.entries ?? allEntries,
|
|
190
188
|
projected,
|
|
191
|
-
retained,
|
|
189
|
+
retained: sources.retained,
|
|
192
190
|
});
|
|
193
|
-
const touches = scopeRecoveryTouches(collected.touches,
|
|
191
|
+
const touches = scopeRecoveryTouches(collected.touches, sources.notes);
|
|
194
192
|
const { defaultDir } = collected;
|
|
195
|
-
|
|
193
|
+
return {
|
|
194
|
+
all: trackedNotes(touches, defaultDir),
|
|
195
|
+
notesDirectory:
|
|
196
|
+
sources.notes === undefined ? defaultDir : scopeDirectory(sources.notes.binding),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The file path of the note that the record names first, when this session wrote one. The path is
|
|
201
|
+
* absolute whenever the notes directory is known, so the `read` tool opens it from any directory.
|
|
202
|
+
*/
|
|
203
|
+
export function primaryNotePath(
|
|
204
|
+
chainEntries: readonly EntryLike[],
|
|
205
|
+
allEntries: readonly EntryLike[],
|
|
206
|
+
projected: readonly ProjectedEntry[],
|
|
207
|
+
sources: NoteSources,
|
|
208
|
+
): string | undefined {
|
|
209
|
+
const { all, notesDirectory } = sessionNotes(chainEntries, allEntries, projected, sources);
|
|
210
|
+
const primary = all.find((note) => note.kind === "written");
|
|
211
|
+
return primary === undefined
|
|
212
|
+
? undefined
|
|
213
|
+
: (pathElsewhere(primary, notesDirectory) ?? primary.absPath ?? primary.notePath);
|
|
214
|
+
}
|
|
215
|
+
export function renderSessionNotes(
|
|
216
|
+
chainEntries: readonly EntryLike[],
|
|
217
|
+
allEntries: readonly EntryLike[],
|
|
218
|
+
projected: readonly ProjectedEntry[],
|
|
219
|
+
options: NoteSources & {
|
|
220
|
+
readonly span: RetainedSpan | undefined;
|
|
221
|
+
readonly maxChars: number;
|
|
222
|
+
},
|
|
223
|
+
): string | undefined {
|
|
224
|
+
const { span, maxChars } = options;
|
|
225
|
+
const { all, notesDirectory } = sessionNotes(chainEntries, allEntries, projected, options);
|
|
196
226
|
if (all.length === 0) {
|
|
197
227
|
return;
|
|
198
228
|
}
|
|
199
229
|
const written = all.filter((note) => note.kind === "written").slice(0, MAX_WRITTEN_NOTES);
|
|
200
230
|
const readOnly = all.filter((note) => note.kind === "read").slice(0, MAX_READ_NOTES);
|
|
201
|
-
const notesDirectory =
|
|
202
|
-
options.notes === undefined ? defaultDir : scopeDirectory(options.notes.binding);
|
|
203
231
|
const lines = [
|
|
204
232
|
...written.map((note) => formatWrittenNoteLine(note, span, notesDirectory)),
|
|
205
233
|
...readOnly.map((note) => formatReadNoteLine(note, span, notesDirectory)),
|
package/src/recovery.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type RecoveryRecord,
|
|
10
10
|
} from "./recovery-records.ts";
|
|
11
11
|
import type { NotesRecovery } from "./note-evidence.ts";
|
|
12
|
-
import { renderSessionNotes, type RetainedSpan } from "./recovery-notes.ts";
|
|
12
|
+
import { primaryNotePath, renderSessionNotes, type RetainedSpan } from "./recovery-notes.ts";
|
|
13
13
|
import {
|
|
14
14
|
recoverableToolResults,
|
|
15
15
|
type RecoveryBatch,
|
|
@@ -255,6 +255,20 @@ function rolloverPreambleWithNotes(
|
|
|
255
255
|
);
|
|
256
256
|
return joined([basePreamble, notesSection]);
|
|
257
257
|
}
|
|
258
|
+
/** The note the automatic record tells the agent to read, for text that names it outside the record. */
|
|
259
|
+
export function recoveryNotePath(
|
|
260
|
+
entries: readonly EntryLike[],
|
|
261
|
+
projected: readonly ProjectedEntry[],
|
|
262
|
+
options: {
|
|
263
|
+
readonly retainedEntryIds: ReadonlySet<string>;
|
|
264
|
+
readonly notes?: NotesRecovery | undefined;
|
|
265
|
+
},
|
|
266
|
+
): string | undefined {
|
|
267
|
+
return primaryNotePath(entries.slice(automaticChainStart(entries)), entries, projected, {
|
|
268
|
+
retained: options.retainedEntryIds,
|
|
269
|
+
notes: options.notes,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
258
272
|
/** Raw owner anchors and projected tool evidence have distinct provenance and share one bounded payload. */
|
|
259
273
|
export function buildAutoHandoff(
|
|
260
274
|
entries: readonly EntryLike[],
|
package/src/retention.ts
CHANGED
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
type PolicyContext,
|
|
16
16
|
} from "./contracts.ts";
|
|
17
17
|
import type { NotesRecovery } from "./note-evidence.ts";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
PROVIDER_SUMMARY_TOKENS,
|
|
20
|
+
providerSummaryInstructions,
|
|
21
|
+
type ProviderSummaryRequest,
|
|
22
|
+
} from "./provider-summary.ts";
|
|
23
|
+
import { buildAutoHandoff, recoveryNotePath } from "./recovery.ts";
|
|
19
24
|
|
|
20
25
|
type RecoveryCapacity = {
|
|
21
26
|
readonly host: PolicyContext;
|
|
@@ -23,6 +28,8 @@ type RecoveryCapacity = {
|
|
|
23
28
|
readonly pendingMessages?: readonly MessageLike[];
|
|
24
29
|
readonly firstKeptEntryId?: string | undefined;
|
|
25
30
|
readonly notes?: NotesRecovery | undefined;
|
|
31
|
+
/** The host will ask the provider for a summary block, which needs an assistant message first in the tail. */
|
|
32
|
+
readonly providerSummary?: boolean | undefined;
|
|
26
33
|
};
|
|
27
34
|
type RetainedTail = {
|
|
28
35
|
readonly firstKeptEntryId?: string | undefined;
|
|
@@ -53,6 +60,19 @@ function safeStart(entry: ProjectionEntry): boolean {
|
|
|
53
60
|
visibleMessages(entry).some((message) => CUT_ROLES.has(message.role))
|
|
54
61
|
);
|
|
55
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* A provider summary block stands for the request that produced the tail's first message, so
|
|
65
|
+
* that message must be a reply the provider completed.
|
|
66
|
+
*/
|
|
67
|
+
function assistantStart(entry: ProjectionEntry): boolean {
|
|
68
|
+
const [first] = visibleMessages(entry);
|
|
69
|
+
return (
|
|
70
|
+
entry.sourceEntry.type !== "compaction" &&
|
|
71
|
+
first?.role === "assistant" &&
|
|
72
|
+
first.stopReason !== "error" &&
|
|
73
|
+
first.stopReason !== "aborted"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
56
76
|
function nativeMessage(
|
|
57
77
|
message: ProjectionEntry["messages"][number],
|
|
58
78
|
): SessionMessageEntry["message"] {
|
|
@@ -129,6 +149,7 @@ function boundedTail(
|
|
|
129
149
|
entries: readonly ProjectionEntry[],
|
|
130
150
|
start: number,
|
|
131
151
|
maxTokens: number,
|
|
152
|
+
startsTail: (entry: ProjectionEntry) => boolean,
|
|
132
153
|
): RetainedTail {
|
|
133
154
|
const costs = entries.slice(start).map((entry) => ({
|
|
134
155
|
entry,
|
|
@@ -143,7 +164,7 @@ function boundedTail(
|
|
|
143
164
|
let tokens = costs.reduce((sum, { cost }) => sum + cost, 0);
|
|
144
165
|
for (const [offset, { entry, cost }] of costs.entries()) {
|
|
145
166
|
const index = start + offset;
|
|
146
|
-
if (tokens > 0 && tokens <= maxTokens &&
|
|
167
|
+
if (tokens > 0 && tokens <= maxTokens && startsTail(entry)) {
|
|
147
168
|
const tail = entries.slice(index);
|
|
148
169
|
if (pairedTail(tail)) {
|
|
149
170
|
return {
|
|
@@ -178,7 +199,14 @@ function retainedTail(
|
|
|
178
199
|
(entry) => entry.sourceEntry.id === capacity.firstKeptEntryId,
|
|
179
200
|
);
|
|
180
201
|
const start = prepared >= 0 ? prepared : projectedCut(entries, keep);
|
|
181
|
-
return start < 0
|
|
202
|
+
return start < 0
|
|
203
|
+
? { ids: new Set() }
|
|
204
|
+
: boundedTail(
|
|
205
|
+
entries,
|
|
206
|
+
start,
|
|
207
|
+
maxTokens,
|
|
208
|
+
capacity.providerSummary === true ? assistantStart : safeStart,
|
|
209
|
+
);
|
|
182
210
|
}
|
|
183
211
|
/** Recent messages and recovery share half the fresh operational budget, leaving work headroom. */
|
|
184
212
|
export function automaticRecovery(
|
|
@@ -186,7 +214,13 @@ export function automaticRecovery(
|
|
|
186
214
|
projected: readonly ProjectionEntry[],
|
|
187
215
|
capacity: RecoveryCapacity,
|
|
188
216
|
ownerQuestionRegistered: boolean,
|
|
189
|
-
):
|
|
217
|
+
):
|
|
218
|
+
| {
|
|
219
|
+
readonly handoff: string;
|
|
220
|
+
readonly firstKeptEntryId?: string | undefined;
|
|
221
|
+
readonly providerSummary?: ProviderSummaryRequest | undefined;
|
|
222
|
+
}
|
|
223
|
+
| undefined {
|
|
190
224
|
const payload = freshPayloadChars(
|
|
191
225
|
capacity.host,
|
|
192
226
|
capacity.toolTokens,
|
|
@@ -197,13 +231,34 @@ export function automaticRecovery(
|
|
|
197
231
|
if (limit < MIN_PAGE_CHARS) {
|
|
198
232
|
return undefined;
|
|
199
233
|
}
|
|
200
|
-
const
|
|
234
|
+
const asksProvider = capacity.providerSummary === true;
|
|
235
|
+
const retained = retainedTail(
|
|
236
|
+
projected,
|
|
237
|
+
capacity,
|
|
238
|
+
Math.floor((payload - limit) / 4) - (asksProvider ? PROVIDER_SUMMARY_TOKENS : 0),
|
|
239
|
+
);
|
|
201
240
|
const handoff = buildAutoHandoff(entries, projected, limit, {
|
|
202
241
|
ownerQuestionRegistered,
|
|
203
242
|
retainedEntryIds: retained.ids,
|
|
204
243
|
notes: capacity.notes,
|
|
205
244
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
245
|
+
if (handoff.length > limit) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
handoff,
|
|
250
|
+
firstKeptEntryId: retained.firstKeptEntryId,
|
|
251
|
+
// Without a tail there is no reasoning to keep, so the host is not asked for a block.
|
|
252
|
+
providerSummary:
|
|
253
|
+
!asksProvider || retained.firstKeptEntryId === undefined
|
|
254
|
+
? undefined
|
|
255
|
+
: {
|
|
256
|
+
instructions: providerSummaryInstructions(
|
|
257
|
+
recoveryNotePath(entries, projected, {
|
|
258
|
+
retainedEntryIds: retained.ids,
|
|
259
|
+
notes: capacity.notes,
|
|
260
|
+
}),
|
|
261
|
+
),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
209
264
|
}
|
package/src/rollover.ts
CHANGED
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
import { registerContextTools } from "./context-tools.ts";
|
|
20
20
|
import { registerGuidance } from "./guidance.ts";
|
|
21
21
|
import { safeJsonStringify } from "./message-text.ts";
|
|
22
|
+
import {
|
|
23
|
+
hostMakesProviderSummary,
|
|
24
|
+
messagesHeldByProviderSummary,
|
|
25
|
+
type ProviderSummaryRequest,
|
|
26
|
+
} from "./provider-summary.ts";
|
|
22
27
|
import { hasRetainedRolloverMarker } from "./recovery.ts";
|
|
23
28
|
import { automaticRecovery } from "./retention.ts";
|
|
24
29
|
import { ResetRequests } from "./reset-requests.ts";
|
|
@@ -157,8 +162,11 @@ function registerReminderFilter(
|
|
|
157
162
|
): void {
|
|
158
163
|
pi.on("context_with_system", (event, ctx) => {
|
|
159
164
|
const stale = staleReminderPredicate(event.messages, ctx, policy, reminders);
|
|
160
|
-
const
|
|
161
|
-
|
|
165
|
+
const held = messagesHeldByProviderSummary(event.messages);
|
|
166
|
+
const removable = (message: (typeof event.messages)[number], index: number) =>
|
|
167
|
+
index >= held && stale(message);
|
|
168
|
+
const filtered = event.messages.some(removable)
|
|
169
|
+
? event.messages.filter((message, index) => !removable(message, index))
|
|
162
170
|
: event.messages;
|
|
163
171
|
const ordered = chronologicalRetainedTail(filtered, ctx, stale);
|
|
164
172
|
if (ordered !== event.messages) {
|
|
@@ -167,6 +175,10 @@ function registerReminderFilter(
|
|
|
167
175
|
return;
|
|
168
176
|
});
|
|
169
177
|
}
|
|
178
|
+
/** A host without the capability ignores the field; one that has it stores why no block was made. */
|
|
179
|
+
function providerSummaryField(request: ProviderSummaryRequest | undefined) {
|
|
180
|
+
return request === undefined ? {} : { providerSummary: request };
|
|
181
|
+
}
|
|
170
182
|
function automaticDraft(
|
|
171
183
|
pi: ExtensionAPI,
|
|
172
184
|
ctx: ExtensionContext,
|
|
@@ -175,6 +187,7 @@ function automaticDraft(
|
|
|
175
187
|
readonly tokensBefore: number;
|
|
176
188
|
readonly reason: string;
|
|
177
189
|
readonly firstKeptEntryId?: string | undefined;
|
|
190
|
+
readonly providerSummary?: ProviderSummaryRequest | undefined;
|
|
178
191
|
},
|
|
179
192
|
) {
|
|
180
193
|
let firstKeptEntryId = recovery.firstKeptEntryId;
|
|
@@ -192,6 +205,7 @@ function automaticDraft(
|
|
|
192
205
|
firstKeptEntryId,
|
|
193
206
|
tokensBefore: recovery.tokensBefore,
|
|
194
207
|
details: { posthorse: 1, reason: recovery.reason },
|
|
208
|
+
...providerSummaryField(recovery.providerSummary),
|
|
195
209
|
},
|
|
196
210
|
};
|
|
197
211
|
}
|
|
@@ -280,6 +294,7 @@ function thresholdFallback(
|
|
|
280
294
|
toolTokens: toolTokens(),
|
|
281
295
|
pendingMessages: event.context.pendingMessages,
|
|
282
296
|
notes: capacity.notes(ctx),
|
|
297
|
+
providerSummary: hostMakesProviderSummary(ctx.model),
|
|
283
298
|
},
|
|
284
299
|
ownerQuestionRegistered,
|
|
285
300
|
);
|
|
@@ -298,6 +313,7 @@ function thresholdFallback(
|
|
|
298
313
|
summary: recovery.handoff,
|
|
299
314
|
firstKeptEntryId: recovery.firstKeptEntryId ?? null,
|
|
300
315
|
details: { posthorse: 1, reason: "threshold" },
|
|
316
|
+
...providerSummaryField(recovery.providerSummary),
|
|
301
317
|
},
|
|
302
318
|
],
|
|
303
319
|
continue: event.continue,
|
|
@@ -341,6 +357,8 @@ function prepareAutomatic(
|
|
|
341
357
|
toolTokens: capacity.toolTokens(),
|
|
342
358
|
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
343
359
|
notes: capacity.notes(ctx),
|
|
360
|
+
// Pi makes no block at an overflow, so the tail keeps its usual start there.
|
|
361
|
+
providerSummary: event.reason !== "overflow" && hostMakesProviderSummary(ctx.model),
|
|
344
362
|
},
|
|
345
363
|
ownerQuestionRegistered,
|
|
346
364
|
);
|