@lll9p/pi-better-compaction 0.2.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/LICENSE +21 -0
- package/README.md +183 -0
- package/index.ts +1 -0
- package/package.json +59 -0
- package/src/compact-client.ts +428 -0
- package/src/config.ts +157 -0
- package/src/debug.ts +165 -0
- package/src/details-store.ts +151 -0
- package/src/extension-runtime.ts +499 -0
- package/src/native-fallback.ts +149 -0
- package/src/payload-rewrite.ts +548 -0
- package/src/request-context-cache.ts +84 -0
- package/src/runtime.ts +250 -0
- package/src/serializer.ts +555 -0
- package/src/supported-environment.ts +16 -0
- package/src/types.ts +296 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_EXTENSION_CONFIG,
|
|
7
|
+
EXTENSION_ID,
|
|
8
|
+
RESPONSES_COMPACT_CAPABLE_APIS,
|
|
9
|
+
THINKING_LEVELS,
|
|
10
|
+
type ExtensionConfig,
|
|
11
|
+
type LoadedExtensionConfig,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", EXTENSION_ID);
|
|
15
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isFile(filePath: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
return fs.statSync(filePath).isFile();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readJsonObject(filePath: string, warnings: string[]): Record<string, unknown> | undefined {
|
|
30
|
+
if (!isFile(filePath)) return undefined;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
34
|
+
if (isRecord(parsed)) return parsed;
|
|
35
|
+
warnings.push(`Ignoring ${filePath}: expected a JSON object at the top level.`);
|
|
36
|
+
return undefined;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
warnings.push(`Ignoring ${filePath}: ${message}`);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveConfiguredPath(rawPath: string, baseDir: string): string {
|
|
45
|
+
if (rawPath.startsWith("~/")) {
|
|
46
|
+
return path.join(os.homedir(), rawPath.slice(2));
|
|
47
|
+
}
|
|
48
|
+
if (path.isAbsolute(rawPath)) {
|
|
49
|
+
return path.resolve(rawPath);
|
|
50
|
+
}
|
|
51
|
+
return path.resolve(baseDir, rawPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toBoolean(value: unknown, fieldPath: string, warnings: string[]): boolean | undefined {
|
|
55
|
+
if (value === undefined) return undefined;
|
|
56
|
+
if (typeof value === "boolean") return value;
|
|
57
|
+
warnings.push(`Ignoring ${fieldPath}: expected a boolean.`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
|
|
62
|
+
if (value === undefined) return undefined;
|
|
63
|
+
// Explicit null clears a spec, matching the documented "unset = current model" behavior.
|
|
64
|
+
if (value === null) return null;
|
|
65
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
66
|
+
return value.trim();
|
|
67
|
+
}
|
|
68
|
+
warnings.push(`Ignoring ${fieldPath}: expected "provider/model-id" or null.`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toThinkingLevel(value: unknown, fieldPath: string, warnings: string[]): ThinkingLevel | undefined {
|
|
73
|
+
if (value === undefined) return undefined;
|
|
74
|
+
if (typeof value === "string" && (THINKING_LEVELS as readonly string[]).includes(value)) {
|
|
75
|
+
return value as ThinkingLevel;
|
|
76
|
+
}
|
|
77
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of ${THINKING_LEVELS.join(", ")}.`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toResponsesCompactApis(value: unknown, fieldPath: string, warnings: string[]): string[] | undefined {
|
|
82
|
+
if (value === undefined) return undefined;
|
|
83
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
84
|
+
warnings.push(`Ignoring ${fieldPath}: expected a string array.`);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const capable = new Set<string>(RESPONSES_COMPACT_CAPABLE_APIS);
|
|
89
|
+
const accepted: string[] = [];
|
|
90
|
+
for (const item of new Set(value.map((entry) => entry.trim()).filter(Boolean))) {
|
|
91
|
+
if (capable.has(item)) {
|
|
92
|
+
accepted.push(item);
|
|
93
|
+
} else {
|
|
94
|
+
warnings.push(
|
|
95
|
+
`Ignoring ${fieldPath} entry "${item}": only ${[...RESPONSES_COMPACT_CAPABLE_APIS].join(", ")} support the compact endpoint.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return accepted;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load extension config. Single source: `~/.pi/agent/extensions/pi-better-compaction/config.json`
|
|
105
|
+
* merged over code defaults. A missing file silently yields the defaults.
|
|
106
|
+
*/
|
|
107
|
+
export function loadExtensionConfig(configPath: string = CONFIG_PATH): LoadedExtensionConfig {
|
|
108
|
+
const warnings: string[] = [];
|
|
109
|
+
const resolved: ExtensionConfig = {
|
|
110
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
111
|
+
responsesCompactApis: [...DEFAULT_EXTENSION_CONFIG.responsesCompactApis],
|
|
112
|
+
};
|
|
113
|
+
let source: string | undefined;
|
|
114
|
+
|
|
115
|
+
const raw = readJsonObject(configPath, warnings);
|
|
116
|
+
if (raw) {
|
|
117
|
+
source = configPath;
|
|
118
|
+
|
|
119
|
+
resolved.enabled = toBoolean(raw.enabled, "enabled", warnings) ?? resolved.enabled;
|
|
120
|
+
resolved.notifyOnLoad = toBoolean(raw.notifyOnLoad, "notifyOnLoad", warnings) ?? resolved.notifyOnLoad;
|
|
121
|
+
resolved.debug = toBoolean(raw.debug, "debug", warnings) ?? resolved.debug;
|
|
122
|
+
resolved.logProviderPayloads =
|
|
123
|
+
toBoolean(raw.logProviderPayloads, "logProviderPayloads", warnings) ?? resolved.logProviderPayloads;
|
|
124
|
+
resolved.logCompactResponses =
|
|
125
|
+
toBoolean(raw.logCompactResponses, "logCompactResponses", warnings) ?? resolved.logCompactResponses;
|
|
126
|
+
resolved.redactSensitiveData =
|
|
127
|
+
toBoolean(raw.redactSensitiveData, "redactSensitiveData", warnings) ?? resolved.redactSensitiveData;
|
|
128
|
+
|
|
129
|
+
const modelSpec = toModelSpec(raw.compactionModel, "compactionModel", warnings);
|
|
130
|
+
if (modelSpec !== undefined) {
|
|
131
|
+
resolved.compactionModel = modelSpec === null ? undefined : modelSpec;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
resolved.compactionThinkingLevel =
|
|
135
|
+
toThinkingLevel(raw.compactionThinkingLevel, "compactionThinkingLevel", warnings) ??
|
|
136
|
+
resolved.compactionThinkingLevel;
|
|
137
|
+
|
|
138
|
+
const apis = toResponsesCompactApis(raw.responsesCompactApis, "responsesCompactApis", warnings);
|
|
139
|
+
if (apis !== undefined) {
|
|
140
|
+
resolved.responsesCompactApis = apis;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof raw.artifactRoot === "string" && raw.artifactRoot.trim().length > 0) {
|
|
144
|
+
resolved.artifactRoot = raw.artifactRoot.trim();
|
|
145
|
+
} else if (raw.artifactRoot !== undefined) {
|
|
146
|
+
warnings.push("Ignoring artifactRoot: expected a non-empty string.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
resolved.artifactRoot = resolveConfiguredPath(resolved.artifactRoot, path.dirname(configPath));
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
config: resolved,
|
|
154
|
+
source,
|
|
155
|
+
warnings,
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
EXTENSION_ID,
|
|
7
|
+
REDACTED_VALUE,
|
|
8
|
+
type ArtifactContext,
|
|
9
|
+
type ArtifactPaths,
|
|
10
|
+
type DebugArtifactEnvelope,
|
|
11
|
+
type DebugArtifactKind,
|
|
12
|
+
type ExtensionConfig,
|
|
13
|
+
type RedactOptions,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
const SENSITIVE_KEY_RE = /(authorization|api[-_]?key|token|secret|password|cookie|set-cookie|signature|credential|oauth|auth)/i;
|
|
17
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi;
|
|
18
|
+
const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9\-_]+\b/g;
|
|
19
|
+
const HEADER_TOKEN_RE = /\b(x-api-key|api-key|authorization)\b\s*[:=]\s*[^\s,;]+/gi;
|
|
20
|
+
|
|
21
|
+
function ensureDir(dirPath: string) {
|
|
22
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toSessionInfo(context: ArtifactContext) {
|
|
26
|
+
const maybeExtensionContext = context as Pick<ExtensionContext, "cwd" | "sessionManager">;
|
|
27
|
+
const sessionManager = maybeExtensionContext.sessionManager;
|
|
28
|
+
if (sessionManager) {
|
|
29
|
+
return {
|
|
30
|
+
cwd: context.cwd,
|
|
31
|
+
sessionId: sessionManager.getSessionId(),
|
|
32
|
+
sessionFile: sessionManager.getSessionFile(),
|
|
33
|
+
sessionDir: sessionManager.getSessionDir(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sanitizePathSegment(value: string | undefined, fallback: string): string {
|
|
40
|
+
if (!value) return fallback;
|
|
41
|
+
const normalized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
42
|
+
return normalized.length > 0 ? normalized : fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function redactInlineSecrets(value: string, placeholder: string): string {
|
|
46
|
+
return value
|
|
47
|
+
.replace(BEARER_RE, `Bearer ${placeholder}`)
|
|
48
|
+
.replace(OPENAI_KEY_RE, placeholder)
|
|
49
|
+
.replace(HEADER_TOKEN_RE, (_match, key: string) => `${key}: ${placeholder}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldRedactKey(key: string): boolean {
|
|
53
|
+
return SENSITIVE_KEY_RE.test(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function redactValue(value: unknown, options: RedactOptions = {}): unknown {
|
|
57
|
+
const placeholder = options.placeholder ?? REDACTED_VALUE;
|
|
58
|
+
const seen = new WeakSet<object>();
|
|
59
|
+
|
|
60
|
+
const visit = (input: unknown): unknown => {
|
|
61
|
+
if (typeof input === "string") {
|
|
62
|
+
return redactInlineSecrets(input, placeholder);
|
|
63
|
+
}
|
|
64
|
+
if (!input || typeof input !== "object") {
|
|
65
|
+
return input;
|
|
66
|
+
}
|
|
67
|
+
if (seen.has(input)) {
|
|
68
|
+
return "[Circular]";
|
|
69
|
+
}
|
|
70
|
+
seen.add(input);
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(input)) {
|
|
73
|
+
return input.map((item) => visit(item));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result: Record<string, unknown> = {};
|
|
77
|
+
for (const [key, item] of Object.entries(input)) {
|
|
78
|
+
result[key] = shouldRedactKey(key) ? placeholder : visit(item);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return visit(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveArtifactPaths(settings: ExtensionConfig, context: ArtifactContext): ArtifactPaths {
|
|
87
|
+
const sessionInfo = toSessionInfo(context);
|
|
88
|
+
const rootDir = settings.artifactRoot.startsWith("~/")
|
|
89
|
+
? path.join(os.homedir(), settings.artifactRoot.slice(2))
|
|
90
|
+
: path.resolve(settings.artifactRoot);
|
|
91
|
+
const sessionDir = path.join(rootDir, "sessions", sanitizePathSegment(sessionInfo.sessionId, "no-session"));
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
rootDir,
|
|
95
|
+
sessionDir,
|
|
96
|
+
providerRequestsDir: path.join(sessionDir, "provider-requests"),
|
|
97
|
+
compactResponsesDir: path.join(sessionDir, "compact-responses"),
|
|
98
|
+
compactionDir: path.join(sessionDir, "compaction-events"),
|
|
99
|
+
lifecycleDir: path.join(sessionDir, "lifecycle"),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function selectArtifactDirectory(paths: ArtifactPaths, kind: DebugArtifactKind): string {
|
|
104
|
+
switch (kind) {
|
|
105
|
+
case "provider-request":
|
|
106
|
+
return paths.providerRequestsDir;
|
|
107
|
+
case "compact-response":
|
|
108
|
+
return paths.compactResponsesDir;
|
|
109
|
+
case "compaction-event":
|
|
110
|
+
return paths.compactionDir;
|
|
111
|
+
case "lifecycle":
|
|
112
|
+
default:
|
|
113
|
+
return paths.lifecycleDir;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function shouldWriteArtifact(kind: DebugArtifactKind, settings: ExtensionConfig): boolean {
|
|
118
|
+
switch (kind) {
|
|
119
|
+
case "provider-request":
|
|
120
|
+
return settings.logProviderPayloads;
|
|
121
|
+
case "compact-response":
|
|
122
|
+
return settings.logCompactResponses;
|
|
123
|
+
case "compaction-event":
|
|
124
|
+
case "lifecycle":
|
|
125
|
+
return settings.debug;
|
|
126
|
+
default:
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function writeDebugArtifact(
|
|
132
|
+
kind: DebugArtifactKind,
|
|
133
|
+
data: unknown,
|
|
134
|
+
settings: ExtensionConfig,
|
|
135
|
+
context: ArtifactContext,
|
|
136
|
+
): string | undefined {
|
|
137
|
+
if (!shouldWriteArtifact(kind, settings)) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sessionInfo = toSessionInfo(context);
|
|
142
|
+
const paths = resolveArtifactPaths(settings, context);
|
|
143
|
+
const targetDir = selectArtifactDirectory(paths, kind);
|
|
144
|
+
ensureDir(targetDir);
|
|
145
|
+
|
|
146
|
+
const timestamp = new Date().toISOString();
|
|
147
|
+
const fileName = `${timestamp.replace(/[.:]/g, "-")}-${kind}.json`;
|
|
148
|
+
const filePath = path.join(targetDir, fileName);
|
|
149
|
+
const envelope: DebugArtifactEnvelope = {
|
|
150
|
+
extension: EXTENSION_ID,
|
|
151
|
+
kind,
|
|
152
|
+
timestamp,
|
|
153
|
+
cwd: sessionInfo.cwd,
|
|
154
|
+
sessionId: sessionInfo.sessionId,
|
|
155
|
+
sessionFile: sessionInfo.sessionFile,
|
|
156
|
+
sessionDir: sessionInfo.sessionDir,
|
|
157
|
+
redaction: {
|
|
158
|
+
enabled: settings.redactSensitiveData,
|
|
159
|
+
},
|
|
160
|
+
data: settings.redactSensitiveData ? redactValue(data) : data,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
|
|
164
|
+
return filePath;
|
|
165
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { CompactionEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
isNativeCompactionDetails,
|
|
4
|
+
isNativeCompactionEntry,
|
|
5
|
+
type NativeCompactionDetails,
|
|
6
|
+
type NativeCompactionEntry,
|
|
7
|
+
type NativeCompactionIdentity,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export type NativeCompactionEntryMatch = Partial<NativeCompactionIdentity>;
|
|
11
|
+
|
|
12
|
+
export type LatestNativeCompactionResolutionFailureReason =
|
|
13
|
+
| "no-compaction"
|
|
14
|
+
| "latest-compaction-not-native"
|
|
15
|
+
| "latest-native-compaction-mismatch";
|
|
16
|
+
|
|
17
|
+
export type LatestNativeCompactionResolution =
|
|
18
|
+
| {
|
|
19
|
+
ok: true;
|
|
20
|
+
entry: NativeCompactionEntry;
|
|
21
|
+
index: number;
|
|
22
|
+
latestCompactionIndex: number;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
ok: false;
|
|
26
|
+
reason: LatestNativeCompactionResolutionFailureReason;
|
|
27
|
+
latestCompactionIndex?: number;
|
|
28
|
+
latestCompaction?: CompactionEntry;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function entryMatches(entry: NativeCompactionEntry, match: NativeCompactionEntryMatch): boolean {
|
|
32
|
+
const details = entry.details;
|
|
33
|
+
if (!details) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
(match.provider === undefined || details.provider === match.provider) &&
|
|
39
|
+
(match.api === undefined || details.api === match.api) &&
|
|
40
|
+
(match.model === undefined || details.model === match.model) &&
|
|
41
|
+
(match.baseUrl === undefined || details.baseUrl === match.baseUrl)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getNativeCompactionDetails(
|
|
46
|
+
entry: CompactionEntry | SessionEntry | undefined,
|
|
47
|
+
): NativeCompactionDetails | undefined {
|
|
48
|
+
if (!entry || entry.type !== "compaction") {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return isNativeCompactionDetails(entry.details) ? entry.details : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isPersistedNativeCompactionEntry(
|
|
56
|
+
entry: CompactionEntry | SessionEntry | undefined,
|
|
57
|
+
): entry is NativeCompactionEntry {
|
|
58
|
+
return isNativeCompactionEntry(entry);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function findLatestCompactionEntryIndex(entries: readonly SessionEntry[]): number | undefined {
|
|
62
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
63
|
+
if (entries[index]?.type === "compaction") {
|
|
64
|
+
return index;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function findLatestCompactionEntry(entries: readonly SessionEntry[]): CompactionEntry | undefined {
|
|
72
|
+
const index = findLatestCompactionEntryIndex(entries);
|
|
73
|
+
return index === undefined ? undefined : (entries[index] as CompactionEntry);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function findLatestNativeCompactionEntryIndex(
|
|
77
|
+
entries: readonly SessionEntry[],
|
|
78
|
+
match: NativeCompactionEntryMatch = {},
|
|
79
|
+
): number | undefined {
|
|
80
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
81
|
+
const entry = entries[index];
|
|
82
|
+
if (!isPersistedNativeCompactionEntry(entry)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!entryMatches(entry, match)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return index;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function findLatestNativeCompactionEntry(
|
|
97
|
+
entries: readonly SessionEntry[],
|
|
98
|
+
match: NativeCompactionEntryMatch = {},
|
|
99
|
+
): NativeCompactionEntry | undefined {
|
|
100
|
+
const index = findLatestNativeCompactionEntryIndex(entries, match);
|
|
101
|
+
return index === undefined ? undefined : (entries[index] as NativeCompactionEntry);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function findLatestNativeCompactionDetails(
|
|
105
|
+
entries: readonly SessionEntry[],
|
|
106
|
+
match: NativeCompactionEntryMatch = {},
|
|
107
|
+
): NativeCompactionDetails | undefined {
|
|
108
|
+
return findLatestNativeCompactionEntry(entries, match)?.details;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveLatestNativeCompactionEntry(
|
|
112
|
+
entries: readonly SessionEntry[],
|
|
113
|
+
match: NativeCompactionEntryMatch = {},
|
|
114
|
+
): LatestNativeCompactionResolution {
|
|
115
|
+
const latestCompactionIndex = findLatestCompactionEntryIndex(entries);
|
|
116
|
+
if (latestCompactionIndex === undefined) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: "no-compaction",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const latestCompaction = entries[latestCompactionIndex];
|
|
124
|
+
if (!latestCompaction || latestCompaction.type !== "compaction" || !isPersistedNativeCompactionEntry(latestCompaction)) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: "latest-compaction-not-native",
|
|
128
|
+
latestCompactionIndex,
|
|
129
|
+
latestCompaction:
|
|
130
|
+
latestCompaction && latestCompaction.type === "compaction"
|
|
131
|
+
? (latestCompaction as CompactionEntry)
|
|
132
|
+
: undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!entryMatches(latestCompaction, match)) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reason: "latest-native-compaction-mismatch",
|
|
140
|
+
latestCompactionIndex,
|
|
141
|
+
latestCompaction,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
entry: latestCompaction,
|
|
148
|
+
index: latestCompactionIndex,
|
|
149
|
+
latestCompactionIndex,
|
|
150
|
+
};
|
|
151
|
+
}
|