🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@archships/dim-plugin-auto-compact

Package Overview
Dependencies
Maintainers
3
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@archships/dim-plugin-auto-compact - npm Package Compare versions

Comparing version
0.0.14
to
0.0.15
+6
-3
dist/index.d.ts

@@ -1,2 +0,2 @@

import { ModelRef } from "@archships/dim-agent-sdk";
import { ModelRef, SessionCompactorController } from "@archships/dim-agent-sdk";
import { DimPlugin } from "@archships/dim-plugin-api";

@@ -11,3 +11,2 @@

retainMessages?: number;
maxStateEntries?: number;
compaction?: {

@@ -19,5 +18,9 @@ auto?: boolean;

}
declare function createAutoCompactPlugin(options?: AutoCompactPluginOptions): DimPlugin;
declare function createAutoCompactPlugin(): DimPlugin<'auto-compact', SessionCompactorController>;
declare function createAutoCompactPlugin(options: AutoCompactPluginOptions): DimPlugin<'auto-compact', SessionCompactorController>;
declare function createAutoCompactPlugin<TId extends string>(options: AutoCompactPluginOptions & {
id: TId;
}): DimPlugin<TId, SessionCompactorController>;
//#endregion
export { AutoCompactPluginOptions, createAutoCompactPlugin };
//# sourceMappingURL=index.d.ts.map

@@ -5,3 +5,2 @@ //#region src/index.ts

const DEFAULT_RETAIN_MESSAGES = 4;
const DEFAULT_MAX_STATE_ENTRIES = 5;
const DEFAULT_THRESHOLD_HEADROOM = 1024;

@@ -16,4 +15,2 @@ const DEFAULT_RECENT_TOOL_OUTPUTS_TO_PRESERVE = 2;

const CONTINUATION_SUMMARY_TAG_FRAGMENT_PATTERN = /<\/?continuation_summary>?/g;
const SUMMARY_PROMPT_VERSION = "continuation-summary-v2";
const COMPACTION_INJECTION_METADATA_KEY = "_dimCompactionInjection";
const DEFAULT_SUMMARY_PROMPT = `You have been working on the task described above but have not yet completed it. Write a continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary. Your summary should be structured, concise, and actionable. Include:

@@ -50,7 +47,7 @@

function createAutoCompactPlugin(options = {}) {
const normalized = normalizeOptions(options);
const resolved = resolveOptions(options);
return {
manifest: {
id: "auto-compact",
version: "0.1.0",
id: options.id ?? "auto-compact",
version: "0.2.0",
apiVersion: 1,

@@ -64,15 +61,14 @@ permissions: { model: true },

getConfig: async () => ({
summaryModel: normalized.summaryModel ? {
provider: normalized.summaryModel.provider,
modelId: normalized.summaryModel.modelId
summaryModel: resolved.summaryModel ? {
provider: resolved.summaryModel.provider,
modelId: resolved.summaryModel.modelId
} : null,
maxSummaryTokens: normalized.maxSummaryTokens,
summaryInputMaxTokens: normalized.summaryInputMaxTokens,
summaryPrompt: normalized.summaryPrompt,
retainMessages: normalized.retainMessages,
maxStateEntries: normalized.maxStateEntries,
maxSummaryTokens: resolved.maxSummaryTokens,
summaryInputMaxTokens: resolved.summaryInputMaxTokens,
summaryPrompt: resolved.summaryPrompt,
retainMessages: resolved.retainMessages,
compaction: {
auto: normalized.compaction.auto,
prune: normalized.compaction.prune,
reserved: normalized.compaction.reserved
auto: resolved.compaction.auto,
prune: resolved.compaction.prune,
reserved: resolved.compaction.reserved
}

@@ -82,211 +78,8 @@ }),

},
hooks: [{
descriptor: { name: "context.compact.before" },
middleware: [async ({ payload, context }) => {
if (!context.sessionId) return;
if (payload.trigger === "threshold" && !normalized.compaction.auto) return;
const baseAttempt = createAttemptDiagnostics(payload, Date.now());
const previous = readPluginState(await context.services.pluginState.get(context.sessionId));
await persistPluginState(context.sessionId, context, {
...previous,
lastAttempt: baseAttempt
});
const summaryModel = normalized.summaryModel ?? context.status?.model;
const usageKind = payload.trigger === "manual" ? "manual_compaction" : "auto_compaction";
if (!summaryModel) {
await persistPluginState(context.sessionId, context, {
...previous,
lastAttempt: finalizeAttemptDiagnostics(baseAttempt, {
status: "skipped",
reasonCode: "missing_summary_model"
})
});
return;
}
const environmentText = buildAutoCompactEnvironmentMessage(context.status);
const plan = planCompaction(payload.messages, payload.cursor, normalized, Math.max(0, payload.contextWindow - payload.plannedOutput), payload.thresholdTokens, environmentText);
if (!plan) {
const stickyFailure = previous.lastAttempt?.status === "failed" && previous.lastAttempt.reasonCode === "post_summary_over_budget";
await persistPluginState(context.sessionId, context, {
...previous,
lastAttempt: stickyFailure ? {
...previous.lastAttempt,
trigger: baseAttempt.trigger,
estimatedInputTokens: baseAttempt.estimatedInputTokens,
thresholdTokens: baseAttempt.thresholdTokens,
maxInputTokens: baseAttempt.maxInputTokens,
cursor: baseAttempt.cursor,
startedAt: baseAttempt.startedAt,
completedAt: Date.now(),
status: "failed",
reasonCode: "post_summary_over_budget",
reasonMessage: previous.lastAttempt?.reasonMessage ?? "Compaction summary was applied but the projected request is still over budget"
} : finalizeAttemptDiagnostics(baseAttempt, {
status: "skipped",
reasonCode: "nothing_to_compact"
})
});
return;
}
try {
const summaryResult = await generateSummary({
contextStatus: context.status,
summaryModel,
maxSummaryTokens: normalized.maxSummaryTokens,
summaryInputMaxTokens: normalized.summaryInputMaxTokens,
summaryPrompt: normalized.summaryPrompt,
existingSummary: payload.systemSegments[0],
systemSegments: payload.systemSegments,
compactedMessages: plan.compactedMessages,
pruneToolOutputs: normalized.compaction.prune,
sessionId: context.sessionId,
usageKind,
model: context.services.model,
logger: context.services.logger
});
if (summaryResult.status === "empty_body_preserve_previous") {
await persistPluginState(context.sessionId, context, {
strategy: "handoff-v1",
summaryModel,
entries: previous.entries,
lastAttempt: finalizeAttemptDiagnostics(baseAttempt, {
status: "skipped",
reasonCode: "empty_summary_preserved_previous",
reasonMessage: "Auto compact summary normalized to an empty body and preserved the previous continuation summary",
compactedMessageCount: plan.compactedMessages.length,
retainedMessageCount: plan.retainedMessages.length
})
});
return;
}
if (summaryResult.status === "invalid_summary_contract") throw createCodedError("invalid_summary_contract", "Auto compact summary body cannot be empty");
const plannedAttempt = finalizeAttemptDiagnostics(baseAttempt, {
status: "planned",
compactedMessageCount: plan.compactedMessages.length,
retainedMessageCount: plan.retainedMessages.length,
summaryNormalization: summaryResult.summaryNormalization
});
await persistPluginState(context.sessionId, context, {
strategy: "handoff-v1",
summaryModel,
entries: previous.entries,
lastAttempt: plannedAttempt
});
let compactionState;
try {
compactionState = await context.services.compaction.apply({
sessionId: context.sessionId,
cursor: plan.keepStart,
systemSegments: [summaryResult.summary],
summary: summaryResult.summary,
reason: payload.trigger,
metadata: {
pluginId: "auto-compact",
promptVersion: SUMMARY_PROMPT_VERSION,
prune: normalized.compaction.prune,
reserved: normalized.compaction.reserved,
[COMPACTION_INJECTION_METADATA_KEY]: {
mode: "auto-user-context-before-anchor",
environmentText
},
compactedMessageCount: plan.compactedMessages.length,
retainedMessageCount: plan.retainedMessages.length,
summaryInputTokens: summaryResult.summaryInputTokens,
summaryOutputTokensEstimate: summaryResult.summaryOutputTokensEstimate,
summaryChars: summaryResult.summaryChars,
summaryCallCount: summaryResult.summaryCallCount,
summaryChunkCount: summaryResult.summaryChunkCount,
summaryNormalization: summaryResult.summaryNormalization,
prunedToolOutputCount: summaryResult.prunedToolOutputCount,
preservedToolOutputCount: summaryResult.preservedToolOutputCount
}
});
} catch (error) {
if (isCompactionOwnerError(error)) {
await persistPluginState(context.sessionId, context, {
strategy: "handoff-v1",
summaryModel,
entries: previous.entries,
lastAttempt: finalizeAttemptDiagnostics(plannedAttempt, {
status: "failed",
reasonCode: "compaction_owner_required",
reasonMessage: "Auto compact plugin requires compaction.ownerPluginId to be \"auto-compact\""
})
});
const nextError = /* @__PURE__ */ new Error("Auto compact plugin requires compaction.ownerPluginId to be \"auto-compact\"");
nextError.code = "compaction_owner_required";
throw nextError;
}
await persistPluginState(context.sessionId, context, {
strategy: "handoff-v1",
summaryModel,
entries: previous.entries,
lastAttempt: finalizeAttemptDiagnostics(plannedAttempt, {
status: "failed",
reasonCode: "compaction_apply_failed",
reasonMessage: readErrorMessage(error)
})
});
return;
}
const latestCheckpoint = compactionState.checkpoints.at(-1);
const nextEntry = {
checkpointId: latestCheckpoint?.id ?? `compact-${Date.now()}`,
createdAt: latestCheckpoint?.createdAt ?? Date.now()
};
const persistedState = readPluginState(await context.services.pluginState.get(context.sessionId));
await persistPluginState(context.sessionId, context, {
strategy: "handoff-v1",
summaryModel,
entries: [...persistedState.entries, nextEntry].slice(-normalized.maxStateEntries),
lastAttempt: persistedState.lastAttempt ? finalizeAttemptDiagnostics(persistedState.lastAttempt, { checkpointId: latestCheckpoint?.id }) : finalizeAttemptDiagnostics(plannedAttempt, { checkpointId: latestCheckpoint?.id })
});
} catch (error) {
if (isCompactionOwnerError(error)) throw error;
await persistPluginState(context.sessionId, context, {
...previous,
lastAttempt: finalizeAttemptDiagnostics(baseAttempt, {
status: "failed",
reasonCode: readErrorCode(error) ?? "summary_generation_failed",
reasonMessage: readErrorMessage(error),
compactedMessageCount: plan.compactedMessages.length,
retainedMessageCount: plan.retainedMessages.length
})
});
return;
}
}]
}, {
descriptor: {
name: "notify.message",
when: { notificationType: "context.compacted" }
},
observers: [async ({ payload, context }) => {
if (!context.sessionId) return;
const notificationStats = readCompactedNotificationStats(payload.notification.metadata);
if (!notificationStats || notificationStats.pluginId !== "auto-compact") return;
const previous = readPluginState(await context.services.pluginState.get(context.sessionId));
const lastAttempt = previous.lastAttempt;
if (!lastAttempt || lastAttempt.status !== "planned" && lastAttempt.status !== "compacted") return;
if (lastAttempt.checkpointId && notificationStats.checkpointId && lastAttempt.checkpointId !== notificationStats.checkpointId) return;
const postSummaryFitsBudget = notificationStats.postSummaryFitsBudget;
await persistPluginState(context.sessionId, context, {
...previous,
lastAttempt: finalizeAttemptDiagnostics(lastAttempt, {
status: postSummaryFitsBudget === false ? "failed" : "compacted",
...postSummaryFitsBudget === false ? {
reasonCode: "post_summary_over_budget",
reasonMessage: "Compaction summary was applied but the projected request is still over budget"
} : {},
...notificationStats.checkpointId ? { checkpointId: notificationStats.checkpointId } : {},
...notificationStats.summaryNormalization ? { summaryNormalization: notificationStats.summaryNormalization } : {},
...typeof notificationStats.compactedMessageCount === "number" ? { compactedMessageCount: notificationStats.compactedMessageCount } : {},
...typeof notificationStats.retainedMessageCount === "number" ? { retainedMessageCount: notificationStats.retainedMessageCount } : {},
...typeof notificationStats.estimatedInputTokensAfter === "number" ? { estimatedInputTokensAfter: notificationStats.estimatedInputTokensAfter } : {},
...typeof notificationStats.estimatedSavedTokens === "number" ? { estimatedSavedTokens: notificationStats.estimatedSavedTokens } : {},
...typeof notificationStats.postSummaryFitsBudget === "boolean" ? { postSummaryFitsBudget: notificationStats.postSummaryFitsBudget } : {}
})
});
}]
}]
createSessionController: (controllerContext) => createAutoCompactController({
options: resolved,
logger: context.services.logger,
model: context.services.model,
context: controllerContext
})
};

@@ -296,10 +89,98 @@ }

}
function normalizeOptions(options) {
function createAutoCompactController(input) {
return {
readPlanningOptions() {
return {
auto: input.options.compaction.auto,
retainMessages: input.options.retainMessages,
reservedTokens: input.options.compaction.reserved,
summaryTokenReserve: input.options.maxSummaryTokens
};
},
async compact(request) {
const sessionStatus = input.context.getStatus();
const summaryModel = input.options.summaryModel ?? sessionStatus.model;
if (!summaryModel) {
const result = {
status: "skipped",
reasonCode: "missing_summary_model",
reasonMessage: "Auto compact summary model is unavailable",
diagnostics: {}
};
await persistPluginState(input.context, {
strategy: "controller-v2",
...input.options.summaryModel ? { summaryModel: input.options.summaryModel } : {},
lastResult: createPersistedResult(request, result)
});
return result;
}
try {
const summary = await generateSummary({
contextStatus: sessionStatus,
summaryModel,
maxSummaryTokens: input.options.maxSummaryTokens,
summaryInputMaxTokens: input.options.summaryInputMaxTokens,
summaryPrompt: input.options.summaryPrompt,
existingSystemSegments: request.plan.existingSystemSegments,
compactedMessages: request.plan.compactedMessages,
pruneToolOutputs: input.options.compaction.prune,
sessionId: input.context.sessionId,
usageKind: request.plan.trigger === "manual" ? "manual_compaction" : "auto_compaction",
model: input.model,
logger: input.logger
});
const result = summary.status === "normalized" ? {
status: "compacted",
summary: summary.summary,
diagnostics: {
summaryNormalization: summary.summaryNormalization,
summaryInputTokens: summary.summaryInputTokens,
summaryOutputTokensEstimate: summary.summaryOutputTokensEstimate,
summaryChars: summary.summaryChars,
summaryCallCount: summary.summaryCallCount,
summaryChunkCount: summary.summaryChunkCount,
prunedToolOutputCount: summary.prunedToolOutputCount,
preservedToolOutputCount: summary.preservedToolOutputCount
}
} : summary.status === "empty_body_preserve_previous" ? {
status: "skipped",
reasonCode: "empty_summary_preserved_previous",
reasonMessage: "Summary generation produced an empty body and preserved the existing continuation summary",
diagnostics: {}
} : {
status: "failed",
reasonCode: "invalid_summary_contract",
reasonMessage: "Auto compact summary body cannot be empty",
diagnostics: {}
};
await persistPluginState(input.context, {
strategy: "controller-v2",
summaryModel,
lastResult: createPersistedResult(request, result)
});
return result;
} catch (error) {
const result = {
status: "failed",
reasonCode: readErrorCode(error) ?? "summary_generation_failed",
reasonMessage: readErrorMessage(error),
diagnostics: {}
};
await persistPluginState(input.context, {
strategy: "controller-v2",
summaryModel,
lastResult: createPersistedResult(request, result)
});
return result;
}
}
};
}
function resolveOptions(options) {
return {
summaryModel: options.summaryModel,
maxSummaryTokens: normalizePositiveInteger(options.maxSummaryTokens, DEFAULT_MAX_SUMMARY_TOKENS),
summaryInputMaxTokens: Math.max(DEFAULT_SUMMARY_INPUT_SAFETY_MARGIN + 256, normalizePositiveInteger(options.summaryInputMaxTokens, DEFAULT_SUMMARY_INPUT_MAX_TOKENS)),
maxSummaryTokens: positiveIntegerOr(options.maxSummaryTokens, DEFAULT_MAX_SUMMARY_TOKENS),
summaryInputMaxTokens: Math.max(DEFAULT_SUMMARY_INPUT_SAFETY_MARGIN + 256, positiveIntegerOr(options.summaryInputMaxTokens, DEFAULT_SUMMARY_INPUT_MAX_TOKENS)),
summaryPrompt: options.summaryPrompt?.trim() || DEFAULT_SUMMARY_PROMPT,
retainMessages: Math.max(1, normalizePositiveInteger(options.retainMessages, DEFAULT_RETAIN_MESSAGES)),
maxStateEntries: Math.max(1, normalizePositiveInteger(options.maxStateEntries, DEFAULT_MAX_STATE_ENTRIES)),
retainMessages: Math.max(1, positiveIntegerOr(options.retainMessages, DEFAULT_RETAIN_MESSAGES)),
compaction: {

@@ -312,41 +193,4 @@ auto: options.compaction?.auto ?? true,

}
function normalizePositiveInteger(value, fallback) {
if (!Number.isFinite(value) || value === void 0 || value <= 0) return fallback;
return Math.floor(value);
}
function planCompaction(messages, cursor, options, maxInputTokens, thresholdTokens, environmentText) {
const leadingSystemCount = countLeadingSystemMessages(messages);
const effectiveCursor = Math.max(cursor, leadingSystemCount);
const visibleMessages = messages.slice(effectiveCursor);
if (visibleMessages.length <= 1) return null;
const realUserIndices = findRealUserKeepStartCandidates(messages, effectiveCursor);
if (realUserIndices.length === 0) return null;
const effectiveThreshold = thresholdTokens > 0 ? thresholdTokens : maxInputTokens;
const thresholdHeadroom = effectiveThreshold > 1 ? Math.min(options.compaction.reserved, effectiveThreshold - 1) : 0;
const targetTokens = effectiveThreshold > 0 ? Math.max(1, effectiveThreshold - thresholdHeadroom) : 1;
const preferredKeepCount = Math.min(Math.max(options.retainMessages, 1), visibleMessages.length - 1);
let candidateIndex = selectInitialKeepStartCandidate(realUserIndices, messages.length - preferredKeepCount);
while (candidateIndex < realUserIndices.length) {
const keepStart = realUserIndices[candidateIndex];
const compactedMessages = messages.slice(effectiveCursor, keepStart);
const retainedMessages = messages.slice(keepStart);
if (compactedMessages.length > 0 && estimateProjectedTokens(retainedMessages, options.maxSummaryTokens, environmentText) <= targetTokens) return {
keepStart,
compactedMessages,
retainedMessages
};
candidateIndex += 1;
}
const keepStart = realUserIndices.at(-1);
if (keepStart === void 0) return null;
const compactedMessages = messages.slice(effectiveCursor, keepStart);
if (compactedMessages.length === 0) return null;
return {
keepStart,
compactedMessages,
retainedMessages: messages.slice(keepStart)
};
}
async function generateSummary(input) {
const renderedInput = buildSummaryInput(input.systemSegments, input.compactedMessages, input.pruneToolOutputs, input.contextStatus);
const renderedInput = buildSummaryInput(input.existingSystemSegments, input.compactedMessages, input.pruneToolOutputs, input.contextStatus);
const summaryInputTokens = estimateSectionsTokens([...renderedInput.prefixSections, ...renderedInput.messageSections]);

@@ -371,3 +215,3 @@ const summaryInputBudget = Math.max(256, input.summaryInputMaxTokens - DEFAULT_SUMMARY_INPUT_SAFETY_MARGIN);

} catch (error) {
if (readErrorCode(error) === "empty_summary_body") return input.existingSummary?.trim() ? { status: "empty_body_preserve_previous" } : { status: "invalid_summary_contract" };
if (readErrorCode(error) === "empty_summary_body") return input.existingSystemSegments.join("\n\n").trim() ? { status: "empty_body_preserve_previous" } : { status: "invalid_summary_contract" };
throw error;

@@ -388,3 +232,3 @@ }

}
function buildSummaryInput(systemSegments, compactedMessages, pruneToolOutputs, status) {
function buildSummaryInput(existingSystemSegments, compactedMessages, pruneToolOutputs, status) {
const preservedToolOutputIds = buildPreservedToolOutputIds(compactedMessages, pruneToolOutputs);

@@ -401,3 +245,3 @@ let prunedToolOutputCount = 0;

].join("\n") : "- unavailable"].join("\n")),
createRenderedSummarySection("existing-summary", ["Existing compaction summary:", systemSegments.length > 0 ? systemSegments.join("\n\n") : "(none)"].join("\n")),
createRenderedSummarySection("existing-summary", ["Existing compaction summary:", existingSystemSegments.length > 0 ? existingSystemSegments.join("\n\n") : "(none)"].join("\n")),
createRenderedSummarySection("messages-heading", "Messages to compact:")

@@ -418,3 +262,3 @@ ],

if (estimateTextTokens(combinedText) <= input.summaryInputBudget) {
const normalizedSummary = await streamSummaryText({
const canonical = await streamSummaryText({
sessionId: input.sessionId,

@@ -431,4 +275,4 @@ summaryModel: input.summaryModel,

return {
summary: normalizedSummary.summary,
summaryNormalization: normalizedSummary.normalization,
summary: canonical.summary,
summaryNormalization: canonical.normalization,
summaryCallCount: 1,

@@ -445,3 +289,3 @@ summaryChunkCount: 0

for (const [index, chunk] of chunks.entries()) {
const normalizedSummary = await streamSummaryText({
const canonical = await streamSummaryText({
sessionId: input.sessionId,

@@ -460,4 +304,4 @@ summaryModel: input.summaryModel,

summaryCallCount += 1;
summaryNormalization = combineSummaryNormalizationModes(summaryNormalization, normalizedSummary.normalization);
chunkSummarySections.push(createRenderedSummarySection(`chunk-summary-${index + 1}`, `Chunk summary ${index + 1} of ${chunks.length}\n${normalizedSummary.summary}`));
summaryNormalization = mergeSummaryNormalizationModes(summaryNormalization, canonical.normalization);
chunkSummarySections.push(createRenderedSummarySection(`chunk-summary-${index + 1}`, `Chunk summary ${index + 1} of ${chunks.length}\n${canonical.summary}`));
}

@@ -472,3 +316,3 @@ const merged = await summarizeRenderedSections({

summary: merged.summary,
summaryNormalization: combineSummaryNormalizationModes(summaryNormalization, merged.summaryNormalization),
summaryNormalization: mergeSummaryNormalizationModes(summaryNormalization, merged.summaryNormalization),
summaryCallCount: summaryCallCount + merged.summaryCallCount,

@@ -500,5 +344,5 @@ summaryChunkCount: chunks.length + merged.summaryChunkCount

if (!text.trim()) throw createCodedError("empty_summary_body", "Auto compact summary body cannot be empty");
const normalizedSummary = normalizeContinuationSummary(text);
if (normalizedSummary.status === "empty_body") throw createCodedError("empty_summary_body", "Auto compact summary body cannot be empty");
if (normalizedSummary.normalization !== "plain_text") input.logger.emit({
const canonical = canonicalizeContinuationSummary(text);
if (canonical.status === "empty_body") throw createCodedError("empty_summary_body", "Auto compact summary body cannot be empty");
if (canonical.normalization !== "plain_text") input.logger.emit({
level: "warn",

@@ -510,3 +354,3 @@ source: "auto-compact",

summaryKind: input.summaryKind,
summaryNormalization: normalizedSummary.normalization,
summaryNormalization: canonical.normalization,
...typeof input.chunkIndex === "number" ? { chunkIndex: input.chunkIndex } : {},

@@ -516,3 +360,3 @@ ...typeof input.chunkCount === "number" ? { chunkCount: input.chunkCount } : {}

});
return normalizedSummary;
return canonical;
}

@@ -602,62 +446,5 @@ function renderMessage(message, pruneToolOutputs, preservedToolOutputIds) {

}
function estimateProjectedTokens(messages, maxSummaryTokens, environmentText) {
return Math.max(1, estimateMessagesTokens(messages) + maxSummaryTokens + (environmentText ? estimateTextTokens(environmentText) + 8 : 0) + 32);
}
function estimateMessagesTokens(messages) {
return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
}
function estimateMessageTokens(message) {
const metadataChars = message.metadata ? JSON.stringify(message.metadata).length : 0;
const roleChars = message.role.length;
const contentChars = renderMessageText(message).length;
const structuredChars = message.role === "tool" && message.structuredContent ? JSON.stringify(message.structuredContent).length : 0;
const thinkingChars = "thinking" in message && typeof message.thinking === "string" ? message.thinking.length : 0;
return Math.max(1, Math.ceil((roleChars + metadataChars + contentChars + structuredChars + thinkingChars) / 4));
}
function estimateTextTokens(text) {
return Math.max(1, Math.ceil(text.length / 4));
}
function buildAutoCompactEnvironmentMessage(status) {
const model = status ? `${status.model.provider}/${status.model.modelId}` : "(unknown)";
const cwd = status?.cwd ?? "(unknown)";
const messageCount = status?.messageCount ?? 0;
const compactionCursor = status?.compaction.cursor ?? 0;
return [
"[auto compact context]",
`cwd: ${cwd}`,
`model: ${model}`,
`messages: ${messageCount}`,
`cursor: ${compactionCursor}`,
"note: The next user message is the retained anchor query; older history is summarized below."
].join("\n");
}
function countLeadingSystemMessages(messages) {
let count = 0;
for (const message of messages) {
if (message.role !== "system") break;
count += 1;
}
return count;
}
function findRealUserKeepStartCandidates(messages, effectiveCursor) {
const candidates = [];
for (let index = effectiveCursor + 1; index < messages.length; index += 1) if (isRealUserMessage(messages[index])) candidates.push(index);
return candidates;
}
function selectInitialKeepStartCandidate(candidates, preferredStart) {
const preferredIndex = candidates.findIndex((candidate) => candidate >= preferredStart);
return preferredIndex >= 0 ? preferredIndex : Math.max(0, candidates.length - 1);
}
function isRealUserMessage(message) {
if (!message || message.role !== "user") return false;
return !isSyntheticUserWrapper(message.metadata);
}
function isSyntheticUserWrapper(metadata) {
if (!isRecord(metadata)) return false;
if (isRecord(metadata._subagentResult)) return true;
if (isRecord(metadata._dimToolContext)) return true;
const overflowCompacted = metadata._dimOverflowCompacted;
if (!isRecord(overflowCompacted)) return false;
return overflowCompacted.kind === "subagent_parent_commit";
}
function createSyntheticMessage(role, text) {

@@ -674,24 +461,3 @@ return {

}
function createAttemptDiagnostics(payload, startedAt) {
return {
trigger: payload.trigger,
status: "planned",
estimatedInputTokens: payload.estimatedInputTokens,
thresholdTokens: payload.thresholdTokens,
maxInputTokens: Math.max(0, payload.contextWindow - payload.plannedOutput),
cursor: payload.cursor,
startedAt
};
}
function finalizeAttemptDiagnostics(attempt, input) {
return {
...attempt,
...input,
...input.status && input.status !== "planned" ? { completedAt: input.completedAt ?? Date.now() } : input.completedAt !== void 0 ? { completedAt: input.completedAt } : {}
};
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeContinuationSummary(summary) {
function canonicalizeContinuationSummary(summary) {
const trimmed = summary.trim();

@@ -723,3 +489,3 @@ if (!trimmed) return { status: "empty_body" };

}
function combineSummaryNormalizationModes(left, right) {
function mergeSummaryNormalizationModes(left, right) {
const rank = {

@@ -732,10 +498,17 @@ plain_text: 0,

}
function createCodedError(code, message) {
const error = new Error(message);
error.code = code;
return error;
async function persistPluginState(context, state) {
await context.pluginState.replace(context.sessionId, {
version: 2,
data: serializePluginState(state),
updatedAt: Date.now()
});
}
async function persistPluginState(sessionId, context, state) {
const data = {
strategy: "handoff-v1",
function readPluginState(entry) {
if (!entry || entry.version !== 2) return { strategy: "controller-v2" };
if (!isAutoCompactPluginStateData(entry.data)) return { strategy: "controller-v2" };
return structuredClone(entry.data);
}
function serializePluginState(state) {
return {
strategy: state.strategy,
...state.summaryModel ? { summaryModel: {

@@ -745,123 +518,84 @@ provider: state.summaryModel.provider,

} } : {},
entries: state.entries.map((entry) => ({
checkpointId: entry.checkpointId,
createdAt: entry.createdAt
})),
...state.lastAttempt ? { lastAttempt: serializeAttemptDiagnostics(state.lastAttempt) } : {}
...state.lastResult ? { lastResult: {
trigger: state.lastResult.trigger,
status: state.lastResult.status,
compactedMessageCount: state.lastResult.compactedMessageCount,
retainedMessageCount: state.lastResult.retainedMessageCount,
updatedAt: state.lastResult.updatedAt,
...state.lastResult.reasonCode ? { reasonCode: state.lastResult.reasonCode } : {},
...state.lastResult.reasonMessage ? { reasonMessage: state.lastResult.reasonMessage } : {},
...state.lastResult.summaryNormalization ? { summaryNormalization: state.lastResult.summaryNormalization } : {},
...typeof state.lastResult.summaryInputTokens === "number" ? { summaryInputTokens: state.lastResult.summaryInputTokens } : {},
...typeof state.lastResult.summaryOutputTokensEstimate === "number" ? { summaryOutputTokensEstimate: state.lastResult.summaryOutputTokensEstimate } : {},
...typeof state.lastResult.summaryChars === "number" ? { summaryChars: state.lastResult.summaryChars } : {},
...typeof state.lastResult.summaryCallCount === "number" ? { summaryCallCount: state.lastResult.summaryCallCount } : {},
...typeof state.lastResult.summaryChunkCount === "number" ? { summaryChunkCount: state.lastResult.summaryChunkCount } : {},
...typeof state.lastResult.prunedToolOutputCount === "number" ? { prunedToolOutputCount: state.lastResult.prunedToolOutputCount } : {},
...typeof state.lastResult.preservedToolOutputCount === "number" ? { preservedToolOutputCount: state.lastResult.preservedToolOutputCount } : {}
} } : {}
};
await context.services.pluginState.replace(sessionId, {
version: 1,
data,
updatedAt: Date.now()
});
}
function readPluginState(entry) {
if (!entry || typeof entry.data !== "object" || entry.data === null) return {
strategy: "handoff-v1",
entries: []
};
const strategy = entry.data.strategy === "handoff-v1" ? "handoff-v1" : "handoff-v1";
const summaryModel = readModelRef(entry.data.summaryModel);
const entries = Array.isArray(entry.data.entries) ? entry.data.entries.flatMap((value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
const candidate = value;
const checkpointId = typeof candidate.checkpointId === "string" ? candidate.checkpointId : void 0;
const createdAt = typeof candidate.createdAt === "number" ? candidate.createdAt : void 0;
if (!checkpointId || !createdAt) return [];
return [{
checkpointId,
createdAt
}];
}) : [];
const lastAttempt = readAttemptDiagnostics(entry.data.lastAttempt);
function createPersistedResult(request, result) {
return {
strategy,
summaryModel,
entries,
...lastAttempt ? { lastAttempt } : {}
trigger: request.plan.trigger,
status: result.status,
compactedMessageCount: request.plan.compactedMessages.length,
retainedMessageCount: request.plan.retainedMessages.length,
updatedAt: Date.now(),
...result.reasonCode ? { reasonCode: result.reasonCode } : {},
...result.reasonMessage ? { reasonMessage: result.reasonMessage } : {},
...result.diagnostics.summaryNormalization ? { summaryNormalization: result.diagnostics.summaryNormalization } : {},
...typeof result.diagnostics.summaryInputTokens === "number" ? { summaryInputTokens: result.diagnostics.summaryInputTokens } : {},
...typeof result.diagnostics.summaryOutputTokensEstimate === "number" ? { summaryOutputTokensEstimate: result.diagnostics.summaryOutputTokensEstimate } : {},
...typeof result.diagnostics.summaryChars === "number" ? { summaryChars: result.diagnostics.summaryChars } : {},
...typeof result.diagnostics.summaryCallCount === "number" ? { summaryCallCount: result.diagnostics.summaryCallCount } : {},
...typeof result.diagnostics.summaryChunkCount === "number" ? { summaryChunkCount: result.diagnostics.summaryChunkCount } : {},
...typeof result.diagnostics.prunedToolOutputCount === "number" ? { prunedToolOutputCount: result.diagnostics.prunedToolOutputCount } : {},
...typeof result.diagnostics.preservedToolOutputCount === "number" ? { preservedToolOutputCount: result.diagnostics.preservedToolOutputCount } : {}
};
}
function readModelRef(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
const candidate = value;
if (typeof candidate.provider !== "string" || typeof candidate.modelId !== "string") return void 0;
return {
provider: candidate.provider,
modelId: candidate.modelId
};
function positiveIntegerOr(value, fallback) {
if (!Number.isFinite(value) || value === void 0 || value <= 0) return fallback;
return Math.floor(value);
}
function readSummaryNormalizationMode(value) {
if (value === "plain_text" || value === "wrapped_block" || value === "extracted_block") return value;
if (value === "strict") return "wrapped_block";
if (value === "wrapped_plain_text") return "plain_text";
function isAutoCompactPluginStateData(value) {
if (!isRecord(value)) return false;
if (value.strategy !== "controller-v2") return false;
if (value.summaryModel !== void 0 && !isModelRef(value.summaryModel)) return false;
if (value.lastResult !== void 0 && !isAutoCompactPersistedResult(value.lastResult)) return false;
return true;
}
function readAttemptDiagnostics(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
const candidate = value;
const trigger = candidate.trigger === "manual" || candidate.trigger === "threshold" ? candidate.trigger : void 0;
const status = candidate.status === "planned" || candidate.status === "skipped" || candidate.status === "compacted" || candidate.status === "failed" ? candidate.status : void 0;
const estimatedInputTokens = typeof candidate.estimatedInputTokens === "number" ? candidate.estimatedInputTokens : void 0;
const thresholdTokens = typeof candidate.thresholdTokens === "number" ? candidate.thresholdTokens : void 0;
const maxInputTokens = typeof candidate.maxInputTokens === "number" ? candidate.maxInputTokens : void 0;
const cursor = typeof candidate.cursor === "number" ? candidate.cursor : void 0;
const startedAt = typeof candidate.startedAt === "number" ? candidate.startedAt : void 0;
if (trigger === void 0 || status === void 0 || estimatedInputTokens === void 0 || thresholdTokens === void 0 || maxInputTokens === void 0 || cursor === void 0 || startedAt === void 0) return void 0;
return {
trigger,
status,
estimatedInputTokens,
thresholdTokens,
maxInputTokens,
cursor,
startedAt,
...typeof candidate.reasonCode === "string" ? { reasonCode: candidate.reasonCode } : {},
...typeof candidate.reasonMessage === "string" ? { reasonMessage: candidate.reasonMessage } : {},
...readSummaryNormalizationMode(candidate.summaryNormalization) ? { summaryNormalization: readSummaryNormalizationMode(candidate.summaryNormalization) } : {},
...typeof candidate.completedAt === "number" ? { completedAt: candidate.completedAt } : {},
...typeof candidate.checkpointId === "string" ? { checkpointId: candidate.checkpointId } : {},
...typeof candidate.compactedMessageCount === "number" ? { compactedMessageCount: candidate.compactedMessageCount } : {},
...typeof candidate.retainedMessageCount === "number" ? { retainedMessageCount: candidate.retainedMessageCount } : {},
...typeof candidate.estimatedInputTokensAfter === "number" ? { estimatedInputTokensAfter: candidate.estimatedInputTokensAfter } : {},
...typeof candidate.estimatedSavedTokens === "number" ? { estimatedSavedTokens: candidate.estimatedSavedTokens } : {},
...typeof candidate.postSummaryFitsBudget === "boolean" ? { postSummaryFitsBudget: candidate.postSummaryFitsBudget } : {}
};
function isAutoCompactPersistedResult(value) {
if (!isRecord(value)) return false;
if (value.trigger !== "manual" && value.trigger !== "threshold") return false;
if (value.status !== "compacted" && value.status !== "skipped" && value.status !== "failed") return false;
if (typeof value.compactedMessageCount !== "number") return false;
if (typeof value.retainedMessageCount !== "number") return false;
if (typeof value.updatedAt !== "number") return false;
if (value.reasonCode !== void 0 && typeof value.reasonCode !== "string") return false;
if (value.reasonMessage !== void 0 && typeof value.reasonMessage !== "string") return false;
if (value.summaryNormalization !== void 0 && value.summaryNormalization !== "plain_text" && value.summaryNormalization !== "wrapped_block" && value.summaryNormalization !== "extracted_block") return false;
if (value.summaryInputTokens !== void 0 && typeof value.summaryInputTokens !== "number") return false;
if (value.summaryOutputTokensEstimate !== void 0 && typeof value.summaryOutputTokensEstimate !== "number") return false;
if (value.summaryChars !== void 0 && typeof value.summaryChars !== "number") return false;
if (value.summaryCallCount !== void 0 && typeof value.summaryCallCount !== "number") return false;
if (value.summaryChunkCount !== void 0 && typeof value.summaryChunkCount !== "number") return false;
if (value.prunedToolOutputCount !== void 0 && typeof value.prunedToolOutputCount !== "number") return false;
if (value.preservedToolOutputCount !== void 0 && typeof value.preservedToolOutputCount !== "number") return false;
return true;
}
function serializeAttemptDiagnostics(attempt) {
return {
trigger: attempt.trigger,
status: attempt.status,
estimatedInputTokens: attempt.estimatedInputTokens,
thresholdTokens: attempt.thresholdTokens,
maxInputTokens: attempt.maxInputTokens,
cursor: attempt.cursor,
startedAt: attempt.startedAt,
...typeof attempt.reasonCode === "string" ? { reasonCode: attempt.reasonCode } : {},
...typeof attempt.reasonMessage === "string" ? { reasonMessage: attempt.reasonMessage } : {},
...attempt.summaryNormalization ? { summaryNormalization: attempt.summaryNormalization } : {},
...typeof attempt.completedAt === "number" ? { completedAt: attempt.completedAt } : {},
...typeof attempt.checkpointId === "string" ? { checkpointId: attempt.checkpointId } : {},
...typeof attempt.compactedMessageCount === "number" ? { compactedMessageCount: attempt.compactedMessageCount } : {},
...typeof attempt.retainedMessageCount === "number" ? { retainedMessageCount: attempt.retainedMessageCount } : {},
...typeof attempt.estimatedInputTokensAfter === "number" ? { estimatedInputTokensAfter: attempt.estimatedInputTokensAfter } : {},
...typeof attempt.estimatedSavedTokens === "number" ? { estimatedSavedTokens: attempt.estimatedSavedTokens } : {},
...typeof attempt.postSummaryFitsBudget === "boolean" ? { postSummaryFitsBudget: attempt.postSummaryFitsBudget } : {}
};
function isModelRef(value) {
return isRecord(value) && typeof value.provider === "string" && typeof value.modelId === "string";
}
function readCompactedNotificationStats(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const candidate = value;
return {
...typeof candidate.checkpointId === "string" ? { checkpointId: candidate.checkpointId } : {},
...typeof candidate.pluginId === "string" ? { pluginId: candidate.pluginId } : {},
...readSummaryNormalizationMode(candidate.summaryNormalization) ? { summaryNormalization: readSummaryNormalizationMode(candidate.summaryNormalization) } : {},
...typeof candidate.compactedMessageCount === "number" ? { compactedMessageCount: candidate.compactedMessageCount } : {},
...typeof candidate.retainedMessageCount === "number" ? { retainedMessageCount: candidate.retainedMessageCount } : {},
...typeof candidate.estimatedInputTokensAfter === "number" ? { estimatedInputTokensAfter: candidate.estimatedInputTokensAfter } : {},
...typeof candidate.estimatedSavedTokens === "number" ? { estimatedSavedTokens: candidate.estimatedSavedTokens } : {},
...typeof candidate.postSummaryFitsBudget === "boolean" ? { postSummaryFitsBudget: candidate.postSummaryFitsBudget } : {}
};
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function createCodedError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function readErrorCode(error) {
if (typeof error !== "object" || error === null) return void 0;
return "code" in error && typeof error.code === "string" ? error.code : void 0;
if (!isRecord(error)) return void 0;
return typeof error.code === "string" ? error.code : void 0;
}

@@ -871,5 +605,2 @@ function readErrorMessage(error) {

}
function isCompactionOwnerError(error) {
return typeof error === "object" && error !== null && "code" in error && error.code === "compaction_owner_required";
}
//#endregion

@@ -876,0 +607,0 @@ export { createAutoCompactPlugin };

{
"name": "@archships/dim-plugin-auto-compact",
"version": "0.0.14",
"version": "0.0.15",
"description": "Official auto compaction plugin for dim-agent-sdk.",

@@ -27,4 +27,4 @@ "homepage": "https://dimcode.dev/",

"devDependencies": {
"@archships/dim-plugin-api": "0.0.20",
"@archships/dim-agent-sdk": "0.0.53"
"@archships/dim-agent-sdk": "0.0.55",
"@archships/dim-plugin-api": "0.0.21"
},

@@ -31,0 +31,0 @@ "scripts": {

@@ -19,3 +19,3 @@ # @archships/dim-plugin-auto-compact

- publish `ModelCapabilities.contextWindow` on the model adapter so the gate can derive `threshold = contextWindow − plannedOutput − contextWindow × safetyRatio`
- configure `compaction.ownerPluginId: 'auto-compact'`
- configure `compaction.compactorPluginId: 'auto-compact'`
- grant model permission to the plugin runtime

@@ -44,3 +44,3 @@

safetyRatio: 0.2,
ownerPluginId: 'auto-compact',
compactorPluginId: 'auto-compact',
},

@@ -47,0 +47,0 @@ })

Sorry, the diff of this file is too big to display