opencode-acp 1.14.19 → 1.14.20-pr.286.33
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 +19 -7
- package/README.zh-CN.md +19 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +520 -81
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/export.d.ts +57 -0
- package/dist/lib/commands/export.d.ts.map +1 -0
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/quality-gate/index.d.ts +1 -1
- package/dist/lib/compress/quality-gate/index.d.ts.map +1 -1
- package/dist/lib/compress/quality-gate/rejection.d.ts +0 -1
- package/dist/lib/compress/quality-gate/rejection.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +2 -2
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/messages/filter/apply.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/state/index.d.ts +1 -0
- package/dist/lib/state/index.d.ts.map +1 -1
- package/dist/lib/state/model-limits.d.ts +27 -0
- package/dist/lib/state/model-limits.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +2 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +5 -1
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/ui/notification.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2418,7 +2418,9 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
2418
2418
|
nextRef: sessionState.messageIds.nextRef
|
|
2419
2419
|
},
|
|
2420
2420
|
lastCompaction: sessionState.lastCompaction,
|
|
2421
|
-
modelContextLimit: sessionState.modelContextLimit
|
|
2421
|
+
modelContextLimit: sessionState.modelContextLimit,
|
|
2422
|
+
modelProviderID: sessionState.modelProviderID,
|
|
2423
|
+
modelID: sessionState.modelID
|
|
2422
2424
|
};
|
|
2423
2425
|
await writePersistedSessionState(sessionState.sessionId, state, logger);
|
|
2424
2426
|
}
|
|
@@ -2898,14 +2900,14 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
2898
2900
|
block.directMessageIds = [...newlyCompressedMessageIds];
|
|
2899
2901
|
block.directToolIds = [...newlyCompressedToolIds];
|
|
2900
2902
|
block.compressedTokens = compressedTokens;
|
|
2901
|
-
let
|
|
2903
|
+
let effectiveTokens2 = compressedTokens;
|
|
2902
2904
|
for (const consumedBlockId of consumed) {
|
|
2903
2905
|
const cb = messagesState.blocksById.get(consumedBlockId);
|
|
2904
2906
|
if (cb && (cb.tier ?? 1) === targetTierForConsumption) {
|
|
2905
|
-
|
|
2907
|
+
effectiveTokens2 += cb.effectiveCompressedTokens ?? cb.compressedTokens;
|
|
2906
2908
|
}
|
|
2907
2909
|
}
|
|
2908
|
-
block.effectiveCompressedTokens =
|
|
2910
|
+
block.effectiveCompressedTokens = effectiveTokens2;
|
|
2909
2911
|
state.stats.pruneTokenCounter += compressedTokens;
|
|
2910
2912
|
state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
|
|
2911
2913
|
state.stats.pruneTokenCounter = 0;
|
|
@@ -2949,6 +2951,51 @@ function applyPendingCompressionDurations(state) {
|
|
|
2949
2951
|
return updates;
|
|
2950
2952
|
}
|
|
2951
2953
|
|
|
2954
|
+
// lib/state/model-limits.ts
|
|
2955
|
+
function createModelLimitCatalog() {
|
|
2956
|
+
const modelLimits = /* @__PURE__ */ new Map();
|
|
2957
|
+
return {
|
|
2958
|
+
record(providerId, modelId, limit) {
|
|
2959
|
+
if (!providerId || !modelId || typeof limit !== "number" || limit <= 0) return;
|
|
2960
|
+
modelLimits.set(`${providerId}/${modelId}`, limit);
|
|
2961
|
+
},
|
|
2962
|
+
resolve(providerId, modelId) {
|
|
2963
|
+
if (!providerId || !modelId) return void 0;
|
|
2964
|
+
return modelLimits.get(`${providerId}/${modelId}`);
|
|
2965
|
+
},
|
|
2966
|
+
/**
|
|
2967
|
+
* Best-effort one-time seed from the host's provider catalog
|
|
2968
|
+
* (`client.config.providers()` → GET /config/providers). Never throws;
|
|
2969
|
+
* returns the number of model-limit entries recorded.
|
|
2970
|
+
*/
|
|
2971
|
+
async hydrateFromClient(client) {
|
|
2972
|
+
try {
|
|
2973
|
+
const config = client;
|
|
2974
|
+
const result = await config.config?.providers?.();
|
|
2975
|
+
const payload = result;
|
|
2976
|
+
const providers = payload?.data?.providers;
|
|
2977
|
+
if (!Array.isArray(providers)) return 0;
|
|
2978
|
+
let recorded = 0;
|
|
2979
|
+
for (const provider of providers) {
|
|
2980
|
+
const { id, models } = provider ?? {};
|
|
2981
|
+
if (typeof id !== "string" || !models) continue;
|
|
2982
|
+
for (const [modelId, model] of Object.entries(models)) {
|
|
2983
|
+
const limit = model?.limit;
|
|
2984
|
+
const context = limit?.context;
|
|
2985
|
+
if (typeof context === "number" && context > 0) {
|
|
2986
|
+
modelLimits.set(`${id}/${modelId}`, context);
|
|
2987
|
+
recorded++;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
return recorded;
|
|
2992
|
+
} catch {
|
|
2993
|
+
return 0;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2952
2999
|
// lib/compress/search.ts
|
|
2953
3000
|
import { tool } from "@opencode-ai/plugin";
|
|
2954
3001
|
async function fetchSessionMessages(client, sessionId) {
|
|
@@ -4140,6 +4187,26 @@ var SessionStateRegistry = class {
|
|
|
4140
4187
|
startsByCallId: /* @__PURE__ */ new Map(),
|
|
4141
4188
|
pendingByCallId: /* @__PURE__ */ new Map()
|
|
4142
4189
|
};
|
|
4190
|
+
// [FIX #312] Model-limit catalog (full rationale in ./model-limits.ts):
|
|
4191
|
+
// lets the messages hook reconcile state.modelContextLimit against the
|
|
4192
|
+
// model named on the request's user message instead of waiting one turn
|
|
4193
|
+
// for the system hook. Shared implementation — the test registry stub
|
|
4194
|
+
// composes the same factory.
|
|
4195
|
+
catalog = createModelLimitCatalog();
|
|
4196
|
+
recordModelLimit(providerId, modelId, limit) {
|
|
4197
|
+
this.catalog.record(providerId, modelId, limit);
|
|
4198
|
+
}
|
|
4199
|
+
resolveModelLimit(providerId, modelId) {
|
|
4200
|
+
return this.catalog.resolve(providerId, modelId);
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* Best-effort one-time seed from the host's provider catalog
|
|
4204
|
+
* (`client.config.providers()` → GET /config/providers). Never throws;
|
|
4205
|
+
* returns the number of model-limit entries recorded.
|
|
4206
|
+
*/
|
|
4207
|
+
hydrateModelLimitsFromClient(client) {
|
|
4208
|
+
return this.catalog.hydrateFromClient(client);
|
|
4209
|
+
}
|
|
4143
4210
|
get(sessionId) {
|
|
4144
4211
|
return this.states.get(sessionId);
|
|
4145
4212
|
}
|
|
@@ -4228,6 +4295,8 @@ function createSessionState() {
|
|
|
4228
4295
|
lastCompaction: 0,
|
|
4229
4296
|
currentTurn: 0,
|
|
4230
4297
|
modelContextLimit: void 0,
|
|
4298
|
+
modelProviderID: void 0,
|
|
4299
|
+
modelID: void 0,
|
|
4231
4300
|
systemPromptTokens: void 0,
|
|
4232
4301
|
qualityGateRetryPending: false
|
|
4233
4302
|
};
|
|
@@ -4267,6 +4336,8 @@ function resetSessionState(state) {
|
|
|
4267
4336
|
state.lastCompaction = 0;
|
|
4268
4337
|
state.currentTurn = 0;
|
|
4269
4338
|
state.modelContextLimit = void 0;
|
|
4339
|
+
state.modelProviderID = void 0;
|
|
4340
|
+
state.modelID = void 0;
|
|
4270
4341
|
state.systemPromptTokens = void 0;
|
|
4271
4342
|
state.qualityGateRetryPending = false;
|
|
4272
4343
|
}
|
|
@@ -4341,6 +4412,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
4341
4412
|
}
|
|
4342
4413
|
if (typeof persisted.modelContextLimit === "number" && persisted.modelContextLimit > 0) {
|
|
4343
4414
|
state.modelContextLimit = persisted.modelContextLimit;
|
|
4415
|
+
state.modelProviderID = persisted.modelProviderID;
|
|
4416
|
+
state.modelID = persisted.modelID;
|
|
4344
4417
|
}
|
|
4345
4418
|
const applied = applyPendingCompressionDurations(state);
|
|
4346
4419
|
if (applied > 0) {
|
|
@@ -4688,8 +4761,8 @@ ${progressBar}`;
|
|
|
4688
4761
|
let toastMessage = message;
|
|
4689
4762
|
toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
|
|
4690
4763
|
if (config.debug) {
|
|
4691
|
-
|
|
4692
|
-
|
|
4764
|
+
logger.debug(`[ACP Debug] Compress notification:
|
|
4765
|
+
${message}`);
|
|
4693
4766
|
}
|
|
4694
4767
|
await client.tui.showToast({
|
|
4695
4768
|
body: {
|
|
@@ -5672,11 +5745,6 @@ exact values, errors). Then add "acknowledgeRisk": true to the compress tool cal
|
|
|
5672
5745
|
Without acknowledgeRisk: true, the compression will be rejected again.`;
|
|
5673
5746
|
return new Error(message);
|
|
5674
5747
|
}
|
|
5675
|
-
function buildPreemptiveAcknowledgeError() {
|
|
5676
|
-
return new Error(
|
|
5677
|
-
'Parameter "acknowledgeRisk": true was provided, but no quality gate rejection is pending. This parameter is only valid immediately after a compression was rejected by the quality gate. Remove it and try again.'
|
|
5678
|
-
);
|
|
5679
|
-
}
|
|
5680
5748
|
|
|
5681
5749
|
// lib/compress/pipeline.ts
|
|
5682
5750
|
function snapshotCompressionState(state) {
|
|
@@ -6150,13 +6218,13 @@ function createCompressRangeTool(factoryCtx) {
|
|
|
6150
6218
|
}
|
|
6151
6219
|
const acknowledgeRisk = args.acknowledgeRisk === true;
|
|
6152
6220
|
const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
|
|
6153
|
-
|
|
6154
|
-
|
|
6221
|
+
const bypassQuality = acknowledgeRisk && ctx.state.qualityGateRetryPending;
|
|
6222
|
+
const ignoredAcknowledgeRisk = acknowledgeRisk && !ctx.state.qualityGateRetryPending;
|
|
6223
|
+
if (ignoredAcknowledgeRisk) {
|
|
6224
|
+
ctx.logger.warn("compress: acknowledgeRisk ignored \u2014 no quality gate rejection pending");
|
|
6155
6225
|
}
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
} else {
|
|
6159
|
-
ctx.state.qualityGateRetryPending = false;
|
|
6226
|
+
ctx.state.qualityGateRetryPending = false;
|
|
6227
|
+
if (!bypassQuality) {
|
|
6160
6228
|
for (const plan of preparedPlans) {
|
|
6161
6229
|
const result = evaluatePreCommitQuality(
|
|
6162
6230
|
rawMessages,
|
|
@@ -6232,7 +6300,10 @@ function createCompressRangeTool(factoryCtx) {
|
|
|
6232
6300
|
const skippedNote = phantomSkipNotice !== null ? `
|
|
6233
6301
|
\u26A0\uFE0F ${phantomSkipNotice}
|
|
6234
6302
|
` : "";
|
|
6235
|
-
|
|
6303
|
+
const ackNote = ignoredAcknowledgeRisk ? `
|
|
6304
|
+
\u26A0\uFE0F acknowledgeRisk was ignored: no quality gate rejection was pending, so quality checks ran normally. Only pass it when retrying immediately after a quality gate rejection.
|
|
6305
|
+
` : "";
|
|
6306
|
+
return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}${ackNote}
|
|
6236
6307
|
IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
|
|
6237
6308
|
\u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
|
|
6238
6309
|
}
|
|
@@ -7566,16 +7637,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7566
7637
|
{ logger }
|
|
7567
7638
|
);
|
|
7568
7639
|
const hasRecommendations = recommendedRanges.length > 0;
|
|
7569
|
-
if (config.debug && contextRanges.compressible.length > 0) {
|
|
7570
|
-
const compressible = contextRanges.compressible;
|
|
7571
|
-
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7572
|
-
const lines = [
|
|
7573
|
-
`[ACP Debug] Recommendation filter:`,
|
|
7574
|
-
` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
|
|
7575
|
-
` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
|
|
7576
|
-
];
|
|
7577
|
-
logger.debug(lines.join("\n"));
|
|
7578
|
-
}
|
|
7579
7640
|
const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
|
|
7580
7641
|
const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
|
|
7581
7642
|
const nothingToCompress = allProtected || allInProtectedZone;
|
|
@@ -7671,6 +7732,16 @@ ${rules}`;
|
|
|
7671
7732
|
}
|
|
7672
7733
|
}
|
|
7673
7734
|
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
7735
|
+
if (shouldInject && config.debug && contextRanges.compressible.length > 0) {
|
|
7736
|
+
const compressible = contextRanges.compressible;
|
|
7737
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7738
|
+
const lines = [
|
|
7739
|
+
`[ACP Debug] Recommendation filter:`,
|
|
7740
|
+
` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
|
|
7741
|
+
` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`
|
|
7742
|
+
];
|
|
7743
|
+
logger.debug(lines.join("\n"));
|
|
7744
|
+
}
|
|
7674
7745
|
let tipsText = null;
|
|
7675
7746
|
if (shouldInject) {
|
|
7676
7747
|
if (suffixMessage && composition.total > 0) {
|
|
@@ -8125,10 +8196,6 @@ function resolveSingleBlockTarget(messagesState, blockIdArg) {
|
|
|
8125
8196
|
error: `Error: Block ${target.displayId} is nested inside active block ${activeAncestorBlockId}. Decompress block ${activeAncestorBlockId} first.`
|
|
8126
8197
|
};
|
|
8127
8198
|
}
|
|
8128
|
-
return {
|
|
8129
|
-
ok: false,
|
|
8130
|
-
error: `Error: Block ${target.displayId} is not active. It may have already been decompressed.`
|
|
8131
|
-
};
|
|
8132
8199
|
}
|
|
8133
8200
|
return { ok: true, targets: [target] };
|
|
8134
8201
|
}
|
|
@@ -8280,9 +8347,9 @@ function createDecompressTool(factoryCtx) {
|
|
|
8280
8347
|
}
|
|
8281
8348
|
const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
|
|
8282
8349
|
const lines2 = blockMessages.map(extractMessageText2);
|
|
8283
|
-
const { writeFile:
|
|
8284
|
-
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") :
|
|
8285
|
-
await
|
|
8350
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
8351
|
+
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : targets[0]?.blocks[0]?.summary ?? "(no content available)";
|
|
8352
|
+
await writeFile4(targetPath, fileContent, "utf-8");
|
|
8286
8353
|
const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
|
|
8287
8354
|
return `Block(s) ${displayIds2} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${targetPath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
|
|
8288
8355
|
}
|
|
@@ -8738,16 +8805,23 @@ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
|
|
|
8738
8805
|
lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
|
|
8739
8806
|
lines.push("");
|
|
8740
8807
|
const shown = sorted.slice(0, limit);
|
|
8808
|
+
const activeCount = sorted.filter((b) => b.active).length;
|
|
8809
|
+
const inactiveCount = sorted.length - activeCount;
|
|
8810
|
+
if (inactiveCount > 0) {
|
|
8811
|
+
lines.push(`${activeCount} active, ${inactiveCount} inactive/consumed`);
|
|
8812
|
+
lines.push("");
|
|
8813
|
+
}
|
|
8741
8814
|
for (const b of shown) {
|
|
8742
8815
|
const survived = b.survivedCount ?? 0;
|
|
8743
8816
|
const gen = b.generation ?? "young";
|
|
8744
8817
|
const effCount = b.effectiveMessageIds?.length ?? 0;
|
|
8745
8818
|
const consumed = b.includedBlockIds && b.includedBlockIds.length > 0 ? ` nested=[${b.includedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
|
|
8819
|
+
const status = b.active ? "" : " [inactive]";
|
|
8746
8820
|
const topic = b.topic || "(no topic)";
|
|
8747
8821
|
const tier = tierLabel(b);
|
|
8748
8822
|
const effTokens = getEffectiveCompressedTokens(b, blocksById);
|
|
8749
8823
|
lines.push(
|
|
8750
|
-
` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}`
|
|
8824
|
+
` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}${status}`
|
|
8751
8825
|
);
|
|
8752
8826
|
lines.push(` "${topic}"`);
|
|
8753
8827
|
}
|
|
@@ -8768,8 +8842,10 @@ function buildStatusReport(renderCtx, rawMessages, options) {
|
|
|
8768
8842
|
const sort = options?.sort ?? "size";
|
|
8769
8843
|
const limit = options?.limit ?? 30;
|
|
8770
8844
|
const msgState = renderCtx.state.prune.messages;
|
|
8771
|
-
const
|
|
8772
|
-
|
|
8845
|
+
const allBlocks = Array.from(msgState.blocksById.values()).sort(
|
|
8846
|
+
(a, b) => a.blockId - b.blockId
|
|
8847
|
+
);
|
|
8848
|
+
const activeBlocks = allBlocks.filter((b) => b.active);
|
|
8773
8849
|
const lines = [];
|
|
8774
8850
|
if (scope === "compressed") {
|
|
8775
8851
|
lines.push(...renderCompressedDrilldown(allBlocks, sort, limit, msgState.blocksById));
|
|
@@ -8791,7 +8867,7 @@ function buildStatusReport(renderCtx, rawMessages, options) {
|
|
|
8791
8867
|
visibleMsgs,
|
|
8792
8868
|
summaryTokens,
|
|
8793
8869
|
systemTokens,
|
|
8794
|
-
|
|
8870
|
+
activeBlocks,
|
|
8795
8871
|
false,
|
|
8796
8872
|
rawMessages,
|
|
8797
8873
|
renderCtx
|
|
@@ -8949,7 +9025,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
|
8949
9025
|
import { join as join3 } from "path";
|
|
8950
9026
|
import { existsSync as existsSync3 } from "fs";
|
|
8951
9027
|
import { homedir as homedir3 } from "os";
|
|
8952
|
-
var LOG_VERSION = true ? "1.14.
|
|
9028
|
+
var LOG_VERSION = true ? "1.14.20-pr.286.33" : "dev";
|
|
8953
9029
|
var Logger = class {
|
|
8954
9030
|
logDir;
|
|
8955
9031
|
enabled;
|
|
@@ -9004,7 +9080,7 @@ var Logger = class {
|
|
|
9004
9080
|
}
|
|
9005
9081
|
}
|
|
9006
9082
|
async write(level, component, message, data) {
|
|
9007
|
-
if (!this.enabled) return;
|
|
9083
|
+
if (!this.enabled && level !== "ERROR" && level !== "WARN") return;
|
|
9008
9084
|
try {
|
|
9009
9085
|
await this.ensureLogDir();
|
|
9010
9086
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9031,12 +9107,10 @@ var Logger = class {
|
|
|
9031
9107
|
return this.write("DEBUG", component, message, data);
|
|
9032
9108
|
}
|
|
9033
9109
|
warn(message, data) {
|
|
9034
|
-
if (!this.enabled) return;
|
|
9035
9110
|
const component = this.getCallerFile(2);
|
|
9036
9111
|
return this.write("WARN", component, message, data);
|
|
9037
9112
|
}
|
|
9038
9113
|
error(message, data) {
|
|
9039
|
-
if (!this.enabled) return;
|
|
9040
9114
|
const component = this.getCallerFile(2);
|
|
9041
9115
|
return this.write("ERROR", component, message, data);
|
|
9042
9116
|
}
|
|
@@ -10015,6 +10089,308 @@ ${report}`;
|
|
|
10015
10089
|
);
|
|
10016
10090
|
}
|
|
10017
10091
|
|
|
10092
|
+
// lib/commands/export.ts
|
|
10093
|
+
import * as fs2 from "fs/promises";
|
|
10094
|
+
import { existsSync as existsSync5 } from "fs";
|
|
10095
|
+
import { dirname as dirname3, isAbsolute, join as join5, resolve } from "path";
|
|
10096
|
+
var ALL_TIERS = [1, 2, 3];
|
|
10097
|
+
var TIER_NAMES = {
|
|
10098
|
+
1: "Tier 1 \u2014 Capture",
|
|
10099
|
+
2: "Tier 2 \u2014 Distilled",
|
|
10100
|
+
3: "Tier 3 \u2014 Condensed"
|
|
10101
|
+
};
|
|
10102
|
+
function parseExportArgs(rawArgs) {
|
|
10103
|
+
const options = {
|
|
10104
|
+
outputPath: "",
|
|
10105
|
+
// "" sentinel => default path resolved later
|
|
10106
|
+
tiers: /* @__PURE__ */ new Set(),
|
|
10107
|
+
includeMetadata: true,
|
|
10108
|
+
append: false
|
|
10109
|
+
};
|
|
10110
|
+
const tokens = tokenize2(rawArgs);
|
|
10111
|
+
let i = 0;
|
|
10112
|
+
while (i < tokens.length) {
|
|
10113
|
+
const tok = tokens[i];
|
|
10114
|
+
const [flag, inlineValue] = splitFlag(tok);
|
|
10115
|
+
switch (flag) {
|
|
10116
|
+
case "--output":
|
|
10117
|
+
case "-o": {
|
|
10118
|
+
const value = inlineValue ?? tokens[i + 1];
|
|
10119
|
+
if (value === void 0) {
|
|
10120
|
+
throw new Error("--output requires a path (use `-` for stdout)");
|
|
10121
|
+
}
|
|
10122
|
+
options.outputPath = value;
|
|
10123
|
+
i += inlineValue !== void 0 ? 1 : 2;
|
|
10124
|
+
break;
|
|
10125
|
+
}
|
|
10126
|
+
case "--tier":
|
|
10127
|
+
case "-t": {
|
|
10128
|
+
const value = inlineValue ?? tokens[i + 1];
|
|
10129
|
+
if (value === void 0) {
|
|
10130
|
+
throw new Error("--tier requires a value (e.g. t2,t3 or all)");
|
|
10131
|
+
}
|
|
10132
|
+
options.tiers = parseTiers(value);
|
|
10133
|
+
i += inlineValue !== void 0 ? 1 : 2;
|
|
10134
|
+
break;
|
|
10135
|
+
}
|
|
10136
|
+
case "--no-metadata":
|
|
10137
|
+
if (inlineValue !== void 0) {
|
|
10138
|
+
throw new Error("--no-metadata does not take a value");
|
|
10139
|
+
}
|
|
10140
|
+
options.includeMetadata = false;
|
|
10141
|
+
i += 1;
|
|
10142
|
+
break;
|
|
10143
|
+
case "--metadata":
|
|
10144
|
+
if (inlineValue !== void 0) {
|
|
10145
|
+
throw new Error("--metadata does not take a value");
|
|
10146
|
+
}
|
|
10147
|
+
options.includeMetadata = true;
|
|
10148
|
+
i += 1;
|
|
10149
|
+
break;
|
|
10150
|
+
case "--append":
|
|
10151
|
+
if (inlineValue !== void 0) {
|
|
10152
|
+
throw new Error("--append does not take a value");
|
|
10153
|
+
}
|
|
10154
|
+
options.append = true;
|
|
10155
|
+
i += 1;
|
|
10156
|
+
break;
|
|
10157
|
+
case "--stdout":
|
|
10158
|
+
if (inlineValue !== void 0) {
|
|
10159
|
+
throw new Error("--stdout does not take a value");
|
|
10160
|
+
}
|
|
10161
|
+
options.outputPath = "-";
|
|
10162
|
+
i += 1;
|
|
10163
|
+
break;
|
|
10164
|
+
default:
|
|
10165
|
+
throw new Error(
|
|
10166
|
+
`Unknown flag: ${tok}. Supported: --output, --tier, --no-metadata, --append, --stdout.`
|
|
10167
|
+
);
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
return options;
|
|
10171
|
+
}
|
|
10172
|
+
function tokenize2(input) {
|
|
10173
|
+
const tokens = [];
|
|
10174
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
10175
|
+
let m;
|
|
10176
|
+
while ((m = re.exec(input)) !== null) {
|
|
10177
|
+
tokens.push(m[1] ?? m[2] ?? m[3]);
|
|
10178
|
+
}
|
|
10179
|
+
return tokens;
|
|
10180
|
+
}
|
|
10181
|
+
function splitFlag(tok) {
|
|
10182
|
+
const eq = tok.indexOf("=");
|
|
10183
|
+
if (eq === -1) return [tok, void 0];
|
|
10184
|
+
return [tok.slice(0, eq), tok.slice(eq + 1)];
|
|
10185
|
+
}
|
|
10186
|
+
function parseTiers(raw) {
|
|
10187
|
+
const lower = raw.trim().toLowerCase();
|
|
10188
|
+
if (lower === "all") return new Set(ALL_TIERS);
|
|
10189
|
+
const parts = lower.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
10190
|
+
const result = /* @__PURE__ */ new Set();
|
|
10191
|
+
for (const part of parts) {
|
|
10192
|
+
const digits = part.replace(/^t/i, "");
|
|
10193
|
+
const n = Number(digits);
|
|
10194
|
+
if (!Number.isInteger(n) || n < 1 || n > 3) {
|
|
10195
|
+
throw new Error(
|
|
10196
|
+
`Invalid tier "${part}". Expected t1, t2, t3, a combination (t2,t3), or "all".`
|
|
10197
|
+
);
|
|
10198
|
+
}
|
|
10199
|
+
result.add(n);
|
|
10200
|
+
}
|
|
10201
|
+
if (result.size === 0) {
|
|
10202
|
+
throw new Error(`Invalid tier "${raw}". Expected t1, t2, t3, or "all".`);
|
|
10203
|
+
}
|
|
10204
|
+
return result;
|
|
10205
|
+
}
|
|
10206
|
+
function resolveDefaultOutputPath(sessionId, cwd) {
|
|
10207
|
+
const short = (sessionId || "session").slice(0, 8);
|
|
10208
|
+
return join5(cwd, ".opencode", `acp-export-${short}.md`);
|
|
10209
|
+
}
|
|
10210
|
+
function formatTokens2(n) {
|
|
10211
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
10212
|
+
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
10213
|
+
}
|
|
10214
|
+
function effectiveTokens(block) {
|
|
10215
|
+
return block.effectiveCompressedTokens ?? block.compressedTokens ?? 0;
|
|
10216
|
+
}
|
|
10217
|
+
function collectActiveBlocks(state) {
|
|
10218
|
+
const msgState = state.prune.messages;
|
|
10219
|
+
const blocks = [];
|
|
10220
|
+
for (const id of msgState.activeBlockIds) {
|
|
10221
|
+
const block = msgState.blocksById.get(id);
|
|
10222
|
+
if (block && block.active) {
|
|
10223
|
+
blocks.push(block);
|
|
10224
|
+
}
|
|
10225
|
+
}
|
|
10226
|
+
return blocks;
|
|
10227
|
+
}
|
|
10228
|
+
function filterByTier(blocks, tiers) {
|
|
10229
|
+
if (tiers.size === 0) return blocks;
|
|
10230
|
+
return blocks.filter((b) => tiers.has(b.tier ?? 1));
|
|
10231
|
+
}
|
|
10232
|
+
function tokenSpans(blocks) {
|
|
10233
|
+
return {
|
|
10234
|
+
effective: blocks.reduce((s, b) => s + effectiveTokens(b), 0),
|
|
10235
|
+
summary: blocks.reduce((s, b) => s + (b.summaryTokens || 0), 0)
|
|
10236
|
+
};
|
|
10237
|
+
}
|
|
10238
|
+
function tierCounts(blocks) {
|
|
10239
|
+
const counts = {};
|
|
10240
|
+
for (const b of blocks) {
|
|
10241
|
+
const t = b.tier ?? 1;
|
|
10242
|
+
counts[t] = (counts[t] || 0) + 1;
|
|
10243
|
+
}
|
|
10244
|
+
return ALL_TIERS.filter((t) => counts[t]).map((t) => `T${t}: ${counts[t]}`).join(", ");
|
|
10245
|
+
}
|
|
10246
|
+
function renderExportMarkdown(params) {
|
|
10247
|
+
const { sessionId, generatedAt, blocks, tiers, includeMetadata } = params;
|
|
10248
|
+
const out = [];
|
|
10249
|
+
const tierFilterLabel = tiers.size === 0 ? "all" : ALL_TIERS.filter((t) => tiers.has(t)).map((t) => `T${t}`).join(", ");
|
|
10250
|
+
out.push("# ACP Session Export");
|
|
10251
|
+
out.push("");
|
|
10252
|
+
out.push(`- **Session**: \`${(sessionId || "unknown").slice(0, 16)}\``);
|
|
10253
|
+
out.push(`- **Generated**: ${generatedAt.toISOString()}`);
|
|
10254
|
+
out.push(`- **Blocks exported**: ${blocks.length}`);
|
|
10255
|
+
const breakdown = tierCounts(blocks);
|
|
10256
|
+
if (breakdown) {
|
|
10257
|
+
out.push(` - ${breakdown}`);
|
|
10258
|
+
}
|
|
10259
|
+
out.push(`- **Tiers**: ${tierFilterLabel}`);
|
|
10260
|
+
const span = tokenSpans(blocks);
|
|
10261
|
+
if (blocks.length > 0) {
|
|
10262
|
+
out.push(
|
|
10263
|
+
`- **Coverage**: ${formatTokens2(span.effective)} original \u2192 ${formatTokens2(span.summary)} summary`
|
|
10264
|
+
);
|
|
10265
|
+
}
|
|
10266
|
+
out.push("");
|
|
10267
|
+
out.push("---");
|
|
10268
|
+
out.push("");
|
|
10269
|
+
if (blocks.length === 0) {
|
|
10270
|
+
out.push("_No active compression blocks match the selected tiers._");
|
|
10271
|
+
out.push("");
|
|
10272
|
+
out.push(
|
|
10273
|
+
"Tip: run `/acp export --tier all` to include every active tier, or trigger a compression first."
|
|
10274
|
+
);
|
|
10275
|
+
return out.join("\n");
|
|
10276
|
+
}
|
|
10277
|
+
for (const tier of [3, 2, 1]) {
|
|
10278
|
+
const tierBlocks = blocks.filter((b) => (b.tier ?? 1) === tier).sort((a, b) => a.blockId - b.blockId);
|
|
10279
|
+
if (tierBlocks.length === 0) continue;
|
|
10280
|
+
out.push(`## ${TIER_NAMES[tier]}`);
|
|
10281
|
+
out.push("");
|
|
10282
|
+
for (const block of tierBlocks) {
|
|
10283
|
+
const topic = block.topic || block.batchTopic || "(no topic)";
|
|
10284
|
+
out.push(`### b${block.blockId} \u2014 ${topic}`);
|
|
10285
|
+
out.push("");
|
|
10286
|
+
if (includeMetadata) {
|
|
10287
|
+
const mode = block.mode ?? "range";
|
|
10288
|
+
const msgCount = block.effectiveMessageIds?.length ?? 0;
|
|
10289
|
+
const created = new Date(block.createdAt).toISOString();
|
|
10290
|
+
const age = block.survivedCount ?? 0;
|
|
10291
|
+
out.push(`- **Tier**: T${tier} \xB7 **Mode**: ${mode}`);
|
|
10292
|
+
out.push(`- **Messages**: ${msgCount} (effective)`);
|
|
10293
|
+
out.push(
|
|
10294
|
+
`- **Tokens**: ${formatTokens2(effectiveTokens(block))} \u2192 ${formatTokens2(block.summaryTokens || 0)} summary`
|
|
10295
|
+
);
|
|
10296
|
+
out.push(`- **Created**: ${created}`);
|
|
10297
|
+
out.push(`- **Age**: survived ${age} transform${age === 1 ? "" : "s"}`);
|
|
10298
|
+
out.push("");
|
|
10299
|
+
}
|
|
10300
|
+
const summary = (block.summary || "").trim() || "_(empty summary)_";
|
|
10301
|
+
out.push(summary);
|
|
10302
|
+
out.push("");
|
|
10303
|
+
out.push("---");
|
|
10304
|
+
out.push("");
|
|
10305
|
+
}
|
|
10306
|
+
}
|
|
10307
|
+
return out.join("\n");
|
|
10308
|
+
}
|
|
10309
|
+
async function handleExportCommand(ctx, args) {
|
|
10310
|
+
const { client, state, logger, sessionId } = ctx;
|
|
10311
|
+
let options;
|
|
10312
|
+
try {
|
|
10313
|
+
options = parseExportArgs(args);
|
|
10314
|
+
} catch (err) {
|
|
10315
|
+
const msg = err?.message ?? String(err);
|
|
10316
|
+
logger.warn("export: argument parse failed", { error: msg, args });
|
|
10317
|
+
await sendExportNotice(client, sessionId, ctx, `[ACP Export] ${msg}`);
|
|
10318
|
+
return;
|
|
10319
|
+
}
|
|
10320
|
+
const tiers = options.tiers;
|
|
10321
|
+
const allActive = collectActiveBlocks(state);
|
|
10322
|
+
const blocks = filterByTier(allActive, tiers);
|
|
10323
|
+
const generatedAt = /* @__PURE__ */ new Date();
|
|
10324
|
+
const markdown = renderExportMarkdown({
|
|
10325
|
+
sessionId,
|
|
10326
|
+
generatedAt,
|
|
10327
|
+
blocks,
|
|
10328
|
+
tiers,
|
|
10329
|
+
includeMetadata: options.includeMetadata
|
|
10330
|
+
});
|
|
10331
|
+
if (options.outputPath === "-") {
|
|
10332
|
+
await sendExportNotice(client, sessionId, ctx, markdown);
|
|
10333
|
+
return;
|
|
10334
|
+
}
|
|
10335
|
+
const cwd = ctx.workingDirectory || process.cwd();
|
|
10336
|
+
const targetPath = options.outputPath !== "" ? isAbsolute(options.outputPath) ? options.outputPath : resolve(cwd, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd);
|
|
10337
|
+
try {
|
|
10338
|
+
const dir = dirname3(targetPath);
|
|
10339
|
+
if (!existsSync5(dir)) {
|
|
10340
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
10341
|
+
}
|
|
10342
|
+
const flag = options.append ? "a" : "w";
|
|
10343
|
+
if (options.append && existsSync5(targetPath)) {
|
|
10344
|
+
await fs2.appendFile(targetPath, `
|
|
10345
|
+
|
|
10346
|
+
---
|
|
10347
|
+
|
|
10348
|
+
${markdown}`, "utf-8");
|
|
10349
|
+
} else {
|
|
10350
|
+
await fs2.writeFile(targetPath, markdown, { flag, encoding: "utf-8" });
|
|
10351
|
+
}
|
|
10352
|
+
} catch (err) {
|
|
10353
|
+
const msg = err?.message ?? String(err);
|
|
10354
|
+
logger.warn("export: file write failed", { path: targetPath, error: msg });
|
|
10355
|
+
await sendExportNotice(
|
|
10356
|
+
client,
|
|
10357
|
+
sessionId,
|
|
10358
|
+
ctx,
|
|
10359
|
+
`[ACP Export] Failed to write \`${targetPath}\`: ${msg}`
|
|
10360
|
+
);
|
|
10361
|
+
return;
|
|
10362
|
+
}
|
|
10363
|
+
logger.info("export: wrote markdown", {
|
|
10364
|
+
path: targetPath,
|
|
10365
|
+
blocks: blocks.length,
|
|
10366
|
+
tiers: tiers.size ? [...tiers].sort().join(",") : "all"
|
|
10367
|
+
});
|
|
10368
|
+
const summary = formatExportSummary(targetPath, blocks, allActive, generatedAt);
|
|
10369
|
+
await sendExportNotice(client, sessionId, ctx, summary);
|
|
10370
|
+
}
|
|
10371
|
+
function formatExportSummary(path, exported, allActive, generatedAt) {
|
|
10372
|
+
const lines = [];
|
|
10373
|
+
lines.push("[ACP Export]");
|
|
10374
|
+
lines.push(`Wrote ${exported.length} block${exported.length === 1 ? "" : "s"} to:`);
|
|
10375
|
+
lines.push(` ${path}`);
|
|
10376
|
+
if (exported.length === 0) {
|
|
10377
|
+
lines.push("");
|
|
10378
|
+
lines.push(
|
|
10379
|
+
`No matching blocks (of ${allActive.length} active). Try \`/acp export --tier all\`.`
|
|
10380
|
+
);
|
|
10381
|
+
} else {
|
|
10382
|
+
lines.push("");
|
|
10383
|
+
lines.push(`Generated ${generatedAt.toISOString()}.`);
|
|
10384
|
+
lines.push(
|
|
10385
|
+
"Review, edit, and commit as a devlog / AGENTS.md supplement \u2014 export is user-driven, never auto-injected."
|
|
10386
|
+
);
|
|
10387
|
+
}
|
|
10388
|
+
return lines.join("\n");
|
|
10389
|
+
}
|
|
10390
|
+
async function sendExportNotice(client, sessionId, ctx, text) {
|
|
10391
|
+
await sendIgnoredMessage(client, sessionId, text, {}, ctx.logger);
|
|
10392
|
+
}
|
|
10393
|
+
|
|
10018
10394
|
// lib/messages/filter/registry.ts
|
|
10019
10395
|
var registry3 = /* @__PURE__ */ new Map();
|
|
10020
10396
|
function registerMessageFilter(filter) {
|
|
@@ -10047,6 +10423,7 @@ function applyMessageFilters(messages, config, logger, ctx) {
|
|
|
10047
10423
|
}
|
|
10048
10424
|
const result = { partsFiltered: 0, partsDropped: 0, partsModified: 0 };
|
|
10049
10425
|
const total = messages.length;
|
|
10426
|
+
const warnedFilters = /* @__PURE__ */ new Set();
|
|
10050
10427
|
const buildCtx = (text, role, i) => ({
|
|
10051
10428
|
text,
|
|
10052
10429
|
role,
|
|
@@ -10102,11 +10479,14 @@ function applyMessageFilters(messages, config, logger, ctx) {
|
|
|
10102
10479
|
try {
|
|
10103
10480
|
decision = filter.filter(filterCtx);
|
|
10104
10481
|
} catch (err) {
|
|
10105
|
-
|
|
10106
|
-
filter
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10482
|
+
if (!warnedFilters.has(filter.name)) {
|
|
10483
|
+
warnedFilters.add(filter.name);
|
|
10484
|
+
logger.warn("Message filter threw error", {
|
|
10485
|
+
filter: filter.name,
|
|
10486
|
+
error: err instanceof Error ? err.message : String(err),
|
|
10487
|
+
messageIndex: i
|
|
10488
|
+
});
|
|
10489
|
+
}
|
|
10110
10490
|
continue;
|
|
10111
10491
|
}
|
|
10112
10492
|
if (decision.action === "keep") continue;
|
|
@@ -10514,9 +10894,16 @@ function isInternalAgentRequest(messages) {
|
|
|
10514
10894
|
}
|
|
10515
10895
|
function createSystemPromptHandler(registry4, logger, config, prompts) {
|
|
10516
10896
|
return async (input, output) => {
|
|
10897
|
+
registry4.recordModelLimit(
|
|
10898
|
+
input.model?.providerID,
|
|
10899
|
+
input.model?.id,
|
|
10900
|
+
input.model?.limit?.context
|
|
10901
|
+
);
|
|
10517
10902
|
const state = input.sessionID ? registry4.get(input.sessionID) : void 0;
|
|
10518
10903
|
if (state && input.model?.limit?.context) {
|
|
10519
10904
|
state.modelContextLimit = input.model.limit.context;
|
|
10905
|
+
state.modelProviderID = input.model?.providerID;
|
|
10906
|
+
state.modelID = input.model?.id;
|
|
10520
10907
|
}
|
|
10521
10908
|
if (!state || state.isSubAgent && !config.allowSubAgents) {
|
|
10522
10909
|
return;
|
|
@@ -10569,6 +10956,30 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
10569
10956
|
messages,
|
|
10570
10957
|
config
|
|
10571
10958
|
);
|
|
10959
|
+
const requestModel = lastUserMessage.info.model;
|
|
10960
|
+
const requestModelLimit = registry4.resolveModelLimit(
|
|
10961
|
+
requestModel?.providerID,
|
|
10962
|
+
requestModel?.modelID
|
|
10963
|
+
);
|
|
10964
|
+
if (requestModelLimit !== void 0) {
|
|
10965
|
+
state.modelContextLimit = requestModelLimit;
|
|
10966
|
+
state.modelProviderID = requestModel?.providerID;
|
|
10967
|
+
state.modelID = requestModel?.modelID;
|
|
10968
|
+
} else if (
|
|
10969
|
+
// [FIX #312 fallback] Catalog miss: we cannot CORRECT the
|
|
10970
|
+
// limit, but we can tell when it belongs to a DIFFERENT model.
|
|
10971
|
+
// Invalidate instead of letting every percentage threshold
|
|
10972
|
+
// below run against the wrong window (#312's false positive).
|
|
10973
|
+
// States persisted before this identity pair existed carry no
|
|
10974
|
+
// identity and are treated as stale for the same reason.
|
|
10975
|
+
// Consumers already tolerate undefined — fresh sessions run
|
|
10976
|
+
// with it until the first system.transform sets the pair.
|
|
10977
|
+
requestModel?.providerID && requestModel?.modelID && state.modelContextLimit !== void 0 && (state.modelProviderID !== requestModel.providerID || state.modelID !== requestModel.modelID)
|
|
10978
|
+
) {
|
|
10979
|
+
state.modelContextLimit = void 0;
|
|
10980
|
+
state.modelProviderID = requestModel.providerID;
|
|
10981
|
+
state.modelID = requestModel.modelID;
|
|
10982
|
+
}
|
|
10572
10983
|
await updatePerTurnState(state, logger, messages);
|
|
10573
10984
|
}
|
|
10574
10985
|
syncCompressPermissionState(state, config, hostPermissions, output.messages);
|
|
@@ -10614,23 +11025,6 @@ function createChatMessageTransformHandler(client, registry4, logger, config, pr
|
|
|
10614
11025
|
config.debug ? (text) => {
|
|
10615
11026
|
logger.debug(`[ACP Debug] Nudge injected:
|
|
10616
11027
|
${text}`);
|
|
10617
|
-
if (state.sessionId && lastUserMessage) {
|
|
10618
|
-
const userInfo = lastUserMessage.info;
|
|
10619
|
-
sendIgnoredMessage(
|
|
10620
|
-
client,
|
|
10621
|
-
state.sessionId,
|
|
10622
|
-
`[ACP Debug Nudge]
|
|
10623
|
-
${text}`,
|
|
10624
|
-
{
|
|
10625
|
-
providerId: userInfo.model?.providerID,
|
|
10626
|
-
modelId: userInfo.model?.modelID,
|
|
10627
|
-
agent: userInfo.agent,
|
|
10628
|
-
variant: userInfo.variant
|
|
10629
|
-
},
|
|
10630
|
-
logger
|
|
10631
|
-
).catch(() => {
|
|
10632
|
-
});
|
|
10633
|
-
}
|
|
10634
11028
|
client.tui.showToast({
|
|
10635
11029
|
body: {
|
|
10636
11030
|
title: "ACP: Nudge Injected",
|
|
@@ -10652,6 +11046,20 @@ ${text}`,
|
|
|
10652
11046
|
}
|
|
10653
11047
|
};
|
|
10654
11048
|
}
|
|
11049
|
+
function buildHelpText() {
|
|
11050
|
+
return [
|
|
11051
|
+
"[ACP] Available commands:",
|
|
11052
|
+
"",
|
|
11053
|
+
" /acp Show compression status (same as /acp stats)",
|
|
11054
|
+
" /acp context Token usage breakdown (system, user, assistant, tools)",
|
|
11055
|
+
" /acp stats Compression status: blocks, context usage, ranges",
|
|
11056
|
+
" /acp export Export active compression blocks to markdown",
|
|
11057
|
+
" Options: --output <path>, --tier t1,t2,t3, --stdout, --append",
|
|
11058
|
+
" /acp help Show this help",
|
|
11059
|
+
"",
|
|
11060
|
+
"Also accepts /dcp for backward compatibility."
|
|
11061
|
+
].join("\n");
|
|
11062
|
+
}
|
|
10655
11063
|
function createCommandExecuteHandler(client, registry4, logger, config, workingDirectory, hostPermissions) {
|
|
10656
11064
|
return async (input, output) => {
|
|
10657
11065
|
if (!config.commands.enabled) {
|
|
@@ -10669,23 +11077,35 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
|
|
|
10669
11077
|
config
|
|
10670
11078
|
);
|
|
10671
11079
|
syncCompressPermissionState(state, config, hostPermissions, messages);
|
|
10672
|
-
const effectivePermission = compressPermission(state, config);
|
|
10673
|
-
if (effectivePermission === "deny") {
|
|
10674
|
-
return;
|
|
10675
|
-
}
|
|
10676
11080
|
const commandCtx = {
|
|
10677
11081
|
client,
|
|
10678
11082
|
state,
|
|
10679
11083
|
config,
|
|
10680
11084
|
logger,
|
|
10681
11085
|
sessionId: input.sessionID,
|
|
10682
|
-
messages
|
|
11086
|
+
messages,
|
|
11087
|
+
workingDirectory
|
|
10683
11088
|
};
|
|
10684
11089
|
const sub = input.arguments?.trim().toLowerCase();
|
|
10685
|
-
if (sub === "stats" || sub === "status") {
|
|
11090
|
+
if (sub === "stats" || sub === "status" || sub === "") {
|
|
10686
11091
|
await handleStatsCommand(commandCtx);
|
|
10687
11092
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
10688
11093
|
}
|
|
11094
|
+
if (sub === "export" || sub.startsWith("export ")) {
|
|
11095
|
+
const exportArgs = input.arguments?.trim().slice("export".length).trim() || "";
|
|
11096
|
+
await handleExportCommand(commandCtx, exportArgs);
|
|
11097
|
+
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11098
|
+
}
|
|
11099
|
+
if (sub === "help") {
|
|
11100
|
+
await sendIgnoredMessage(
|
|
11101
|
+
client,
|
|
11102
|
+
input.sessionID,
|
|
11103
|
+
buildHelpText(),
|
|
11104
|
+
{},
|
|
11105
|
+
logger
|
|
11106
|
+
);
|
|
11107
|
+
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
11108
|
+
}
|
|
10689
11109
|
await handleContextCommand(commandCtx);
|
|
10690
11110
|
throw new Error("__DCP_CONTEXT_HANDLED__");
|
|
10691
11111
|
}
|
|
@@ -10795,7 +11215,7 @@ function configureClientAuth(client) {
|
|
|
10795
11215
|
|
|
10796
11216
|
// lib/update.ts
|
|
10797
11217
|
import { readFile as readFile2, rm } from "fs/promises";
|
|
10798
|
-
import { basename, dirname as
|
|
11218
|
+
import { basename, dirname as dirname4, join as join6 } from "path";
|
|
10799
11219
|
import { fileURLToPath } from "url";
|
|
10800
11220
|
var PACKAGE_NAME = "opencode-acp";
|
|
10801
11221
|
function startAutoUpdate(ctx, enabled) {
|
|
@@ -10820,7 +11240,7 @@ function startAutoUpdate(ctx, enabled) {
|
|
|
10820
11240
|
async function checkAutoUpdate(signal) {
|
|
10821
11241
|
const packageDir = await findPackageDir(PACKAGE_NAME);
|
|
10822
11242
|
if (!packageDir) return { updated: false };
|
|
10823
|
-
const pkg = await readPackageJson(
|
|
11243
|
+
const pkg = await readPackageJson(join6(packageDir, "package.json"));
|
|
10824
11244
|
if (!pkg?.name || !pkg.version) return { updated: false };
|
|
10825
11245
|
const latest = await fetchLatestVersion(pkg.name, signal);
|
|
10826
11246
|
if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false };
|
|
@@ -10840,21 +11260,21 @@ async function checkAutoUpdate(signal) {
|
|
|
10840
11260
|
return { updated: true, name: pkg.name, current: pkg.version, latest };
|
|
10841
11261
|
}
|
|
10842
11262
|
async function findPackageDir(name) {
|
|
10843
|
-
let dir =
|
|
11263
|
+
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
10844
11264
|
for (; ; ) {
|
|
10845
|
-
const pkg = await readPackageJson(
|
|
11265
|
+
const pkg = await readPackageJson(join6(dir, "package.json"));
|
|
10846
11266
|
if (pkg?.name === name) return dir;
|
|
10847
|
-
const parent =
|
|
11267
|
+
const parent = dirname4(dir);
|
|
10848
11268
|
if (parent === dir) return void 0;
|
|
10849
11269
|
dir = parent;
|
|
10850
11270
|
}
|
|
10851
11271
|
}
|
|
10852
11272
|
async function updateRemoveDir(packageDir, name) {
|
|
10853
|
-
const packageParent =
|
|
10854
|
-
const nodeModulesDir = basename(packageParent).startsWith("@") ?
|
|
11273
|
+
const packageParent = dirname4(packageDir);
|
|
11274
|
+
const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
|
|
10855
11275
|
if (basename(nodeModulesDir) !== "node_modules") return void 0;
|
|
10856
|
-
const wrapperDir =
|
|
10857
|
-
const wrapperPkg = await readPackageJson(
|
|
11276
|
+
const wrapperDir = dirname4(nodeModulesDir);
|
|
11277
|
+
const wrapperPkg = await readPackageJson(join6(wrapperDir, "package.json"));
|
|
10858
11278
|
const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
|
|
10859
11279
|
if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
|
|
10860
11280
|
return wrapperDir;
|
|
@@ -10862,7 +11282,7 @@ async function updateRemoveDir(packageDir, name) {
|
|
|
10862
11282
|
function wrapperSpec(wrapperDir, name) {
|
|
10863
11283
|
if (name.startsWith("@")) {
|
|
10864
11284
|
const [scope, pkg] = name.split("/");
|
|
10865
|
-
if (!scope || !pkg || basename(
|
|
11285
|
+
if (!scope || !pkg || basename(dirname4(wrapperDir)) !== scope) return void 0;
|
|
10866
11286
|
const prefix2 = `${pkg}@`;
|
|
10867
11287
|
const base2 = basename(wrapperDir);
|
|
10868
11288
|
return base2.startsWith(prefix2) ? base2.slice(prefix2.length) : void 0;
|
|
@@ -10954,6 +11374,25 @@ var server = (async (ctx) => {
|
|
|
10954
11374
|
if (isSecureMode()) {
|
|
10955
11375
|
configureClientAuth(ctx.client);
|
|
10956
11376
|
}
|
|
11377
|
+
registry4.hydrateModelLimitsFromClient(ctx.client).then(
|
|
11378
|
+
(recorded) => {
|
|
11379
|
+
if (recorded > 0) {
|
|
11380
|
+
logger.info("Model limit catalog seeded from provider config", {
|
|
11381
|
+
models: recorded
|
|
11382
|
+
});
|
|
11383
|
+
} else {
|
|
11384
|
+
logger.warn(
|
|
11385
|
+
"Model limit catalog seeding recorded no entries \u2014 falling back to per-request refresh (system.transform)"
|
|
11386
|
+
);
|
|
11387
|
+
}
|
|
11388
|
+
},
|
|
11389
|
+
(error) => {
|
|
11390
|
+
logger.warn(
|
|
11391
|
+
"Model limit catalog seeding failed \u2014 falling back to per-request refresh (system.transform)",
|
|
11392
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
11393
|
+
);
|
|
11394
|
+
}
|
|
11395
|
+
);
|
|
10957
11396
|
logger.info("DCP initialized");
|
|
10958
11397
|
startAutoUpdate(ctx, config.autoUpdate);
|
|
10959
11398
|
const compressToolContext = {
|