@oh-my-pi/snapcompact 18.15.0 → 18.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/types/snapcompact.d.ts +24 -5
- package/package.json +8 -7
- package/src/prompts/snapcompact-summary.md +1 -1
- package/src/snapcompact.ts +101 -54
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.17.0] - 2026-11-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `compact()` now keeps image content from pre-16.1.0 archived histories in the dropped-history summary.
|
|
10
|
+
|
|
11
|
+
## [18.16.0] - 2026-11-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `archiveIncludesThinking()`, whether a compaction may replay reasoning from its summary's legend or its archive's text, and `viewSummary()`, which keeps a summary the template did not write in its own words, followed by the template's guide to the view's text ([#17745](https://github.com/can1357/oh-my-pi/pull/17745) by [@will-bogusz](https://github.com/will-bogusz))
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed `viewWithoutFrames` and `viewWithoutThinking` keeping an archive's reasoning for a reader without it when the summary (say, one an extension wrote) declares no `¶think:` scope, and replacing such a summary with the template's ([#17745](https://github.com/can1357/oh-my-pi/pull/17745) by [@will-bogusz](https://github.com/will-bogusz))
|
|
20
|
+
|
|
5
21
|
## [18.15.0] - 2026-11-01
|
|
6
22
|
|
|
7
23
|
### Added
|
|
@@ -661,6 +661,13 @@ export declare function readerIncludesThinking(model: Model): boolean;
|
|
|
661
661
|
* archives rendered with {@link SerializeOptions.includeThinking}, and for
|
|
662
662
|
* summaries written before that legend was gated on it. */
|
|
663
663
|
export declare function summaryIncludesThinking(summary: string): boolean;
|
|
664
|
+
/**
|
|
665
|
+
* Whether a compaction whose summary is `summary` and archive `archive` may
|
|
666
|
+
* replay assistant reasoning: its summary declares `¶think:` scopes, or its
|
|
667
|
+
* kept text carries a `¶think:` section. A summary an extension or custom
|
|
668
|
+
* template wrote need not carry the legend while its archive's source does.
|
|
669
|
+
*/
|
|
670
|
+
export declare function archiveIncludesThinking(summary: string, archive: Archive): boolean;
|
|
664
671
|
/** `text` (a summary or archive source, raw or normalized) without its
|
|
665
672
|
* `¶think:` sections, and with the reasoning scope dropped from any legend. */
|
|
666
673
|
export declare function textWithoutThinking(text: string): string;
|
|
@@ -668,14 +675,24 @@ export declare function textWithoutThinking(text: string): string;
|
|
|
668
675
|
* What a reader that excludes assistant reasoning reads for a compaction
|
|
669
676
|
* whose summary is `summary` and archive `archive`: the archive's
|
|
670
677
|
* {@link Archive.withoutThinking} variant when it carries one, the original
|
|
671
|
-
* when
|
|
678
|
+
* when it carries no reasoning ({@link archiveIncludesThinking}), else a
|
|
679
|
+
* fail-closed text-only form.
|
|
672
680
|
* That form keeps no frames (pixels cannot be scrubbed), takes its text edges
|
|
673
681
|
* from the source after every `¶think:` section is removed (so a section
|
|
674
682
|
* crossing an edge cannot leak), counts the dropped middle as truncated, and
|
|
675
683
|
* re-renders the summary without the reasoning legend; `details` restores its
|
|
676
|
-
* file list.
|
|
684
|
+
* file list. A summary the template did not write keeps its own words
|
|
685
|
+
* ({@link viewSummary}).
|
|
677
686
|
*/
|
|
678
687
|
export declare function viewWithoutThinking(summary: string, archive: Archive, details?: CompactionDetails): ArchiveView;
|
|
688
|
+
/**
|
|
689
|
+
* The summary a reader reads with an archive view: `rendered`, the template's
|
|
690
|
+
* summary for that view, when `summary` came from the template; otherwise
|
|
691
|
+
* `summary`'s own words (say, an extension's), without reasoning when
|
|
692
|
+
* `dropThinking`, followed by `rendered`, so the facts it alone holds survive
|
|
693
|
+
* the swap and the reader still gets the guide to the view's text.
|
|
694
|
+
*/
|
|
695
|
+
export declare function viewSummary(summary: string, rendered: string, dropThinking: boolean): string;
|
|
679
696
|
/** Options for {@link viewWithoutFrames}. */
|
|
680
697
|
export interface ViewWithoutFramesOptions {
|
|
681
698
|
/** `false` for a reader that excludes assistant reasoning ({@link readerIncludesThinking}). */
|
|
@@ -687,13 +704,15 @@ export interface ViewWithoutFramesOptions {
|
|
|
687
704
|
* What a reader without image input reads for a compaction whose summary is
|
|
688
705
|
* `summary` and archive `archive`: the archive's kept source
|
|
689
706
|
* ({@link Archive.text}) as plain text in place of its frames, and the summary
|
|
690
|
-
* re-rendered without the frame legend, its file list kept
|
|
707
|
+
* re-rendered without the frame legend, its file list kept (one the template
|
|
708
|
+
* did not write keeps its own words, {@link viewSummary}). With
|
|
691
709
|
* `includeThinking: false` the text and legend also leave out reasoning, as
|
|
692
710
|
* {@link viewWithoutThinking} does, without rendering frames this reader
|
|
693
711
|
* cannot see; past `maxChars` the middle of the text is dropped. An archive
|
|
694
712
|
* persisted without that source keeps its text edges and counts the imaged
|
|
695
|
-
* middle as dropped
|
|
696
|
-
*
|
|
713
|
+
* middle as dropped; a reader without reasoning also skips the tail's lines
|
|
714
|
+
* before its first scope label, whatever the summary declares. An archive with
|
|
715
|
+
* no frames, no reasoning to drop and no text past `maxChars` is returned as is.
|
|
697
716
|
*/
|
|
698
717
|
export declare function viewWithoutFrames(summary: string, archive: Archive, options?: ViewWithoutFramesOptions): ArchiveView;
|
|
699
718
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.17.0",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -27,18 +27,19 @@
|
|
|
27
27
|
"types": "./dist/types/index.d.ts",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
|
|
30
|
-
"check:types": "
|
|
30
|
+
"check:types": "bun check --no-script -p tsconfig.json",
|
|
31
31
|
"lint": "oxlint .",
|
|
32
32
|
"test": "bun test --parallel",
|
|
33
33
|
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
34
34
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@oh-my-pi/
|
|
38
|
-
"@oh-my-pi/pi-
|
|
39
|
-
"@oh-my-pi/pi-
|
|
40
|
-
"@oh-my-pi/pi-
|
|
41
|
-
"@oh-my-pi/pi-
|
|
37
|
+
"@oh-my-pi/omptype": "18.17.0",
|
|
38
|
+
"@oh-my-pi/pi-ai": "18.17.0",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "18.17.0",
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.17.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.17.0",
|
|
42
|
+
"@oh-my-pi/pi-wire": "18.17.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/bun": "^1.3.14"
|
|
@@ -19,7 +19,7 @@ Reading HISTORY:
|
|
|
19
19
|
{{/if}}{{#if truncatedChars}}- About {{truncatedChars}} characters of older middle history dropped to fit archive budget.
|
|
20
20
|
{{/if}}- If an exact earlier detail matters and a section is unclear, re-derive from workspace (re-read files, re-run commands), rather than guess.
|
|
21
21
|
|
|
22
|
-
{{!-- viewWithoutFrames (snapcompact.ts) reads this FILES section back out of a rendered summary: keep FILES_SECTION_HEAD and HISTORY_SECTION_HEAD in step with these headings. --}}
|
|
22
|
+
{{!-- viewWithoutFrames (snapcompact.ts) reads this FILES section back out of a rendered summary, and viewSummary tells this template's summaries from others by their closing HISTORY heading: keep FILES_SECTION_HEAD and HISTORY_SECTION_HEAD in step with these headings. --}}
|
|
23
23
|
{{#if files}}FILES
|
|
24
24
|
===================
|
|
25
25
|
{{files}}
|
package/src/snapcompact.ts
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
* re-attached to the compaction summary message on every context rebuild.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
+
import { type } from "@oh-my-pi/omptype";
|
|
56
57
|
import type { Api, ImageContent, Message, Model, TextContent } from "@oh-my-pi/pi-ai";
|
|
57
58
|
import { reachesBudgetedHost } from "@oh-my-pi/pi-ai/providers/anthropic-state";
|
|
58
59
|
import { resolveCatalogPolicy } from "@oh-my-pi/pi-catalog/compat/catalog-policy";
|
|
@@ -320,32 +321,25 @@ export const SHAPES = {
|
|
|
320
321
|
legacy: priceShape(SHAPE_VARIANTS["5x8-sent"], { api: "anthropic-messages" }),
|
|
321
322
|
} satisfies Record<string, Shape>;
|
|
322
323
|
|
|
324
|
+
/** In-memory shapes may carry optional keys as `undefined`. */
|
|
325
|
+
const ShapeSchema = type({
|
|
326
|
+
font: "'5x8' | '8x8' | '6x12' | '8x13' | 'silver'",
|
|
327
|
+
cellWidth: "number > 0",
|
|
328
|
+
cellHeight: "number > 0",
|
|
329
|
+
"stretch?": "boolean | undefined",
|
|
330
|
+
variant: "'sent' | 'bw'",
|
|
331
|
+
"stopwordDim?": "boolean | undefined",
|
|
332
|
+
"columns?": "1 | 2 | undefined",
|
|
333
|
+
lineRepeat: "number > 0",
|
|
334
|
+
frameSize: "number > 0",
|
|
335
|
+
frameTokenEstimate: "number > 0",
|
|
336
|
+
"frameTokensVerified?": "boolean | undefined",
|
|
337
|
+
"imageDetail?": "'auto' | 'low' | 'high' | 'original' | undefined",
|
|
338
|
+
});
|
|
339
|
+
|
|
323
340
|
/** Runtime guard for shape overrides loaded from config or preserve data. */
|
|
324
341
|
export function isShape(value: unknown): value is Shape {
|
|
325
|
-
|
|
326
|
-
const shape = value as Record<string, unknown>;
|
|
327
|
-
const font = shape.font;
|
|
328
|
-
const variant = shape.variant;
|
|
329
|
-
const detail = shape.imageDetail;
|
|
330
|
-
return (
|
|
331
|
-
(font === "5x8" || font === "8x8" || font === "6x12" || font === "8x13" || font === "silver") &&
|
|
332
|
-
typeof shape.cellWidth === "number" &&
|
|
333
|
-
shape.cellWidth > 0 &&
|
|
334
|
-
typeof shape.cellHeight === "number" &&
|
|
335
|
-
shape.cellHeight > 0 &&
|
|
336
|
-
(shape.stretch === undefined || typeof shape.stretch === "boolean") &&
|
|
337
|
-
(variant === "sent" || variant === "bw") &&
|
|
338
|
-
(shape.stopwordDim === undefined || typeof shape.stopwordDim === "boolean") &&
|
|
339
|
-
(shape.columns === undefined || shape.columns === 1 || shape.columns === 2) &&
|
|
340
|
-
typeof shape.lineRepeat === "number" &&
|
|
341
|
-
shape.lineRepeat > 0 &&
|
|
342
|
-
typeof shape.frameSize === "number" &&
|
|
343
|
-
shape.frameSize > 0 &&
|
|
344
|
-
typeof shape.frameTokenEstimate === "number" &&
|
|
345
|
-
shape.frameTokenEstimate > 0 &&
|
|
346
|
-
(shape.frameTokensVerified === undefined || typeof shape.frameTokensVerified === "boolean") &&
|
|
347
|
-
(detail === undefined || detail === "auto" || detail === "low" || detail === "high" || detail === "original")
|
|
348
|
-
);
|
|
342
|
+
return ShapeSchema.allows(value);
|
|
349
343
|
}
|
|
350
344
|
|
|
351
345
|
/** Eval-winning variant per wire family (fallback when the model id matches
|
|
@@ -2259,6 +2253,19 @@ export function summaryIncludesThinking(summary: string): boolean {
|
|
|
2259
2253
|
return summary.includes(THINK_LABEL);
|
|
2260
2254
|
}
|
|
2261
2255
|
|
|
2256
|
+
/**
|
|
2257
|
+
* Whether a compaction whose summary is `summary` and archive `archive` may
|
|
2258
|
+
* replay assistant reasoning: its summary declares `¶think:` scopes, or its
|
|
2259
|
+
* kept text carries a `¶think:` section. A summary an extension or custom
|
|
2260
|
+
* template wrote need not carry the legend while its archive's source does.
|
|
2261
|
+
*/
|
|
2262
|
+
export function archiveIncludesThinking(summary: string, archive: Archive): boolean {
|
|
2263
|
+
if (summaryIncludesThinking(summary)) return true;
|
|
2264
|
+
return [archive.text ?? archive.textHead, archive.text === undefined ? archive.textTail : undefined].some(
|
|
2265
|
+
text => text?.includes(THINK_LABEL) === true && stripThinkingSections(text) !== text,
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2262
2269
|
/** The reasoning scope in a scope legend, current and earlier summary wordings
|
|
2263
2270
|
* alike, wherever it sits: a summary, or one archived inside another archive. */
|
|
2264
2271
|
const LEGEND_REASONING_SCOPE =
|
|
@@ -2273,16 +2280,21 @@ export function textWithoutThinking(text: string): string {
|
|
|
2273
2280
|
const PREVIOUS_SUMMARY_HEAD = "[Summary of earlier history]";
|
|
2274
2281
|
|
|
2275
2282
|
/**
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
* the tail's lines before its first scope label continue a
|
|
2279
|
-
* the dropped middle, whose scope is unknown
|
|
2283
|
+
* Length of the opening of a text-less archive's tail that a reader excluding
|
|
2284
|
+
* reasoning leaves out: an archive persisted without {@link Archive.text} keeps
|
|
2285
|
+
* only its edges, and the tail's lines before its first scope label continue a
|
|
2286
|
+
* section cut off in the dropped middle, whose scope is unknown.
|
|
2280
2287
|
*/
|
|
2281
|
-
function
|
|
2282
|
-
if (archive.text) return archive.text;
|
|
2288
|
+
function unscopedTailLength(archive: Archive): number {
|
|
2283
2289
|
const tail = archive.textTail ?? "";
|
|
2284
2290
|
const labelled = tail.search(SECTION_START);
|
|
2285
|
-
return
|
|
2291
|
+
return labelled < 0 ? tail.length : labelled;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/** `archive`'s kept source as a reader that excludes reasoning may unfold it. */
|
|
2295
|
+
function sourceForReaderWithoutThinking(archive: Archive): string {
|
|
2296
|
+
if (archive.text) return archive.text;
|
|
2297
|
+
return [archive.textHead, (archive.textTail ?? "").slice(unscopedTailLength(archive))]
|
|
2286
2298
|
.filter((part): part is string => typeof part === "string" && part.length > 0)
|
|
2287
2299
|
.join(NEWLINE_GLYPH);
|
|
2288
2300
|
}
|
|
@@ -2291,16 +2303,18 @@ function sourceForReaderWithoutThinking(archive: Archive): string {
|
|
|
2291
2303
|
* What a reader that excludes assistant reasoning reads for a compaction
|
|
2292
2304
|
* whose summary is `summary` and archive `archive`: the archive's
|
|
2293
2305
|
* {@link Archive.withoutThinking} variant when it carries one, the original
|
|
2294
|
-
* when
|
|
2306
|
+
* when it carries no reasoning ({@link archiveIncludesThinking}), else a
|
|
2307
|
+
* fail-closed text-only form.
|
|
2295
2308
|
* That form keeps no frames (pixels cannot be scrubbed), takes its text edges
|
|
2296
2309
|
* from the source after every `¶think:` section is removed (so a section
|
|
2297
2310
|
* crossing an edge cannot leak), counts the dropped middle as truncated, and
|
|
2298
2311
|
* re-renders the summary without the reasoning legend; `details` restores its
|
|
2299
|
-
* file list.
|
|
2312
|
+
* file list. A summary the template did not write keeps its own words
|
|
2313
|
+
* ({@link viewSummary}).
|
|
2300
2314
|
*/
|
|
2301
2315
|
export function viewWithoutThinking(summary: string, archive: Archive, details?: CompactionDetails): ArchiveView {
|
|
2302
2316
|
if (archive.withoutThinking) return archive.withoutThinking;
|
|
2303
|
-
if (!
|
|
2317
|
+
if (!archiveIncludesThinking(summary, archive)) return { summary, archive };
|
|
2304
2318
|
const source = sourceForReaderWithoutThinking(archive);
|
|
2305
2319
|
const text = textWithoutThinking(source);
|
|
2306
2320
|
const headChars = archive.textHead?.length ?? 0;
|
|
@@ -2311,7 +2325,8 @@ export function viewWithoutThinking(summary: string, archive: Archive, details?:
|
|
|
2311
2325
|
const truncatedChars = archive.truncatedChars + (fits ? 0 : text.length - headChars - tailChars);
|
|
2312
2326
|
const files = details ? formatFileList(details.readFiles, details.modifiedFiles) : "";
|
|
2313
2327
|
return {
|
|
2314
|
-
summary:
|
|
2328
|
+
summary: viewSummary(
|
|
2329
|
+
summary,
|
|
2315
2330
|
textHead.length === 0 && files.length === 0
|
|
2316
2331
|
? "No prior history."
|
|
2317
2332
|
: prompt.render(snapcompactSummaryPrompt, {
|
|
@@ -2321,6 +2336,8 @@ export function viewWithoutThinking(summary: string, archive: Archive, details?:
|
|
|
2321
2336
|
files: files.length > 0 ? files : undefined,
|
|
2322
2337
|
includeThinking: false,
|
|
2323
2338
|
}),
|
|
2339
|
+
true,
|
|
2340
|
+
),
|
|
2324
2341
|
archive: {
|
|
2325
2342
|
frames: [],
|
|
2326
2343
|
totalChars: textHead.length + textTail.length,
|
|
@@ -2335,6 +2352,19 @@ export function viewWithoutThinking(summary: string, archive: Archive, details?:
|
|
|
2335
2352
|
const FILES_SECTION_HEAD = "FILES\n===================\n";
|
|
2336
2353
|
const HISTORY_SECTION_HEAD = "HISTORY\n===================";
|
|
2337
2354
|
|
|
2355
|
+
/**
|
|
2356
|
+
* The summary a reader reads with an archive view: `rendered`, the template's
|
|
2357
|
+
* summary for that view, when `summary` came from the template; otherwise
|
|
2358
|
+
* `summary`'s own words (say, an extension's), without reasoning when
|
|
2359
|
+
* `dropThinking`, followed by `rendered`, so the facts it alone holds survive
|
|
2360
|
+
* the swap and the reader still gets the guide to the view's text.
|
|
2361
|
+
*/
|
|
2362
|
+
export function viewSummary(summary: string, rendered: string, dropThinking: boolean): string {
|
|
2363
|
+
if (summary.trimEnd().endsWith(HISTORY_SECTION_HEAD)) return rendered;
|
|
2364
|
+
const own = (dropThinking ? textWithoutThinking(summary) : summary).trim();
|
|
2365
|
+
return own.length > 0 ? `${own}\n\n${rendered}` : rendered;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2338
2368
|
/** The FILES section of a snapcompact summary as it was rendered, or `undefined` when it lists none. */
|
|
2339
2369
|
function summaryFileSection(summary: string): string | undefined {
|
|
2340
2370
|
const start = summary.indexOf(FILES_SECTION_HEAD);
|
|
@@ -2381,22 +2411,31 @@ function keepEnds(archive: Archive, maxChars: number): Archive {
|
|
|
2381
2411
|
* What a reader without image input reads for a compaction whose summary is
|
|
2382
2412
|
* `summary` and archive `archive`: the archive's kept source
|
|
2383
2413
|
* ({@link Archive.text}) as plain text in place of its frames, and the summary
|
|
2384
|
-
* re-rendered without the frame legend, its file list kept
|
|
2414
|
+
* re-rendered without the frame legend, its file list kept (one the template
|
|
2415
|
+
* did not write keeps its own words, {@link viewSummary}). With
|
|
2385
2416
|
* `includeThinking: false` the text and legend also leave out reasoning, as
|
|
2386
2417
|
* {@link viewWithoutThinking} does, without rendering frames this reader
|
|
2387
2418
|
* cannot see; past `maxChars` the middle of the text is dropped. An archive
|
|
2388
2419
|
* persisted without that source keeps its text edges and counts the imaged
|
|
2389
|
-
* middle as dropped
|
|
2390
|
-
*
|
|
2420
|
+
* middle as dropped; a reader without reasoning also skips the tail's lines
|
|
2421
|
+
* before its first scope label, whatever the summary declares. An archive with
|
|
2422
|
+
* no frames, no reasoning to drop and no text past `maxChars` is returned as is.
|
|
2391
2423
|
*/
|
|
2392
2424
|
export function viewWithoutFrames(
|
|
2393
2425
|
summary: string,
|
|
2394
2426
|
archive: Archive,
|
|
2395
2427
|
options: ViewWithoutFramesOptions = {},
|
|
2396
2428
|
): ArchiveView {
|
|
2397
|
-
const
|
|
2429
|
+
const excludeThinking = options.includeThinking === false;
|
|
2430
|
+
const dropThinking = excludeThinking && archiveIncludesThinking(summary, archive);
|
|
2431
|
+
// Without the whole source the tail may open inside a section cut off in the
|
|
2432
|
+
// dropped middle, whose scope is unknown (it may be reasoning even when no
|
|
2433
|
+
// edge shows a label); a reader without reasoning skips those lines (see
|
|
2434
|
+
// sourceForReaderWithoutThinking).
|
|
2435
|
+
const tail = archive.text ? "" : (archive.textTail ?? "");
|
|
2436
|
+
const tailStart = excludeThinking && tail ? tail.search(SECTION_START) : 0;
|
|
2398
2437
|
const fits = options.maxChars === undefined || archive.totalChars <= options.maxChars;
|
|
2399
|
-
if (archive.frames.length === 0 && !dropThinking && fits) return { summary, archive };
|
|
2438
|
+
if (archive.frames.length === 0 && !dropThinking && fits && tailStart === 0) return { summary, archive };
|
|
2400
2439
|
const scrub = (text: string): string => (dropThinking ? textWithoutThinking(text) : text);
|
|
2401
2440
|
let textArchive: Archive;
|
|
2402
2441
|
if (archive.text) {
|
|
@@ -2409,12 +2448,7 @@ export function viewWithoutFrames(
|
|
|
2409
2448
|
textHead: text,
|
|
2410
2449
|
};
|
|
2411
2450
|
} else {
|
|
2412
|
-
|
|
2413
|
-
// the dropped middle, whose scope is unknown; a reader without reasoning
|
|
2414
|
-
// skips those lines (see sourceForReaderWithoutThinking).
|
|
2415
|
-
const tail = archive.textTail ?? "";
|
|
2416
|
-
const labelled = dropThinking ? tail.search(SECTION_START) : 0;
|
|
2417
|
-
const keptTail = labelled < 0 ? "" : tail.slice(labelled);
|
|
2451
|
+
const keptTail = tailStart < 0 ? "" : tail.slice(tailStart);
|
|
2418
2452
|
const textHead = scrub(archive.textHead ?? "");
|
|
2419
2453
|
const textTail = scrub(keptTail);
|
|
2420
2454
|
const framedChars = archive.frames.reduce((sum, frame) => sum + frame.chars, 0);
|
|
@@ -2429,13 +2463,17 @@ export function viewWithoutFrames(
|
|
|
2429
2463
|
if (options.maxChars !== undefined) textArchive = keepEnds(textArchive, Math.max(0, Math.floor(options.maxChars)));
|
|
2430
2464
|
const source = textArchive.text ?? textArchive.textHead ?? "";
|
|
2431
2465
|
return {
|
|
2432
|
-
summary:
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2466
|
+
summary: viewSummary(
|
|
2467
|
+
summary,
|
|
2468
|
+
prompt.render(snapcompactSummaryPrompt, {
|
|
2469
|
+
frameCount: 0,
|
|
2470
|
+
truncatedChars: textArchive.truncatedChars,
|
|
2471
|
+
includedPreviousSummary: source.startsWith(PREVIOUS_SUMMARY_HEAD),
|
|
2472
|
+
files: summaryFileSection(summary),
|
|
2473
|
+
includeThinking: summaryIncludesThinking(summary) && !dropThinking,
|
|
2474
|
+
}),
|
|
2475
|
+
dropThinking,
|
|
2476
|
+
),
|
|
2439
2477
|
archive: textArchive,
|
|
2440
2478
|
};
|
|
2441
2479
|
}
|
|
@@ -2502,7 +2540,16 @@ export async function compact<TMessage = Message>(
|
|
|
2502
2540
|
archiveText = archiveText.length > 0 ? `${head} [Recent conversation] ${archiveText}` : head;
|
|
2503
2541
|
}
|
|
2504
2542
|
|
|
2505
|
-
|
|
2543
|
+
// An archive persisted without `text` (written before the text-first layout)
|
|
2544
|
+
// holds its middle only as frames, which cannot be re-rendered: the new
|
|
2545
|
+
// archive drops them, and for a reader without reasoning its tail's unscoped
|
|
2546
|
+
// opening too, so count both as dropped history instead of dropping silently.
|
|
2547
|
+
let truncatedChars =
|
|
2548
|
+
(previousArchive?.truncatedChars ?? 0) +
|
|
2549
|
+
(previousArchive && previousArchive.text === undefined
|
|
2550
|
+
? previousArchive.frames.reduce((sum, frame) => sum + frame.chars, 0) +
|
|
2551
|
+
(excludeThinking ? unscopedTailLength(previousArchive) : 0)
|
|
2552
|
+
: 0);
|
|
2506
2553
|
|
|
2507
2554
|
// Re-compacting a snapcompacted history unfolds the prior archive's source
|
|
2508
2555
|
// text and treats it as one coherent transcript: the previous kept source
|