@lexwdex-org/opencode-dcp 3.4.9 → 3.4.10

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 CHANGED
@@ -124,6 +124,12 @@ DCP 使用自己的配置文件,按以下顺序搜索:
124
124
  // reminders are off (compression less likely). At/above this, reminders
125
125
  // are on. Accepts: number or "X%" of model context window.
126
126
  "minContextLimit": "50%",
127
+ // When true (default), DCP injects a soft turn-boundary reminder at every
128
+ // new user turn, after session.idle, and after vcs.branch.updated events —
129
+ // regardless of minContextLimit, and only while below maxContextLimit —
130
+ // asking the model to compress the previous task when the topic changed
131
+ // (throttled by nudgeFrequency).
132
+ "boundaryNudge": true,
127
133
  // Optional per-model override for maxContextLimit by providerID/modelID.
128
134
  // If present, this wins over the global maxContextLimit.
129
135
  // Accepts: number or "X%".
package/dist/index.js CHANGED
@@ -934,6 +934,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
934
934
  "compress.summaryBuffer",
935
935
  "compress.maxContextLimit",
936
936
  "compress.minContextLimit",
937
+ "compress.boundaryNudge",
937
938
  "compress.modelMaxLimits",
938
939
  "compress.modelMinLimits",
939
940
  "compress.nudgeFrequency",
@@ -1142,6 +1143,13 @@ function validateConfigTypes(config) {
1142
1143
  actual: typeof compress.summaryBuffer
1143
1144
  });
1144
1145
  }
1146
+ if (compress.boundaryNudge !== void 0 && typeof compress.boundaryNudge !== "boolean") {
1147
+ errors.push({
1148
+ key: "compress.boundaryNudge",
1149
+ expected: "boolean",
1150
+ actual: typeof compress.boundaryNudge
1151
+ });
1152
+ }
1145
1153
  if (compress.nudgeFrequency !== void 0 && typeof compress.nudgeFrequency !== "number") {
1146
1154
  errors.push({
1147
1155
  key: "compress.nudgeFrequency",
@@ -1430,6 +1438,7 @@ var defaultConfig = {
1430
1438
  summaryBuffer: true,
1431
1439
  maxContextLimit: "85%",
1432
1440
  minContextLimit: "50%",
1441
+ boundaryNudge: true,
1433
1442
  nudgeFrequency: 2,
1434
1443
  iterationNudgeThreshold: 15,
1435
1444
  nudgeForce: "strong",
@@ -1552,6 +1561,7 @@ function mergeCompress(base, override) {
1552
1561
  summaryBuffer: override.summaryBuffer ?? base.summaryBuffer,
1553
1562
  maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,
1554
1563
  minContextLimit: override.minContextLimit ?? base.minContextLimit,
1564
+ boundaryNudge: override.boundaryNudge ?? base.boundaryNudge,
1555
1565
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1556
1566
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1557
1567
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
@@ -3028,6 +3038,18 @@ function resetOnCompaction(state, messages = []) {
3028
3038
  for (const callId of state.subAgentResultCache.keys()) {
3029
3039
  if (!liveToolIds.has(callId)) state.subAgentResultCache.delete(callId);
3030
3040
  }
3041
+ const anchorSets = [
3042
+ state.nudges.contextLimitAnchors,
3043
+ state.nudges.turnNudgeAnchors,
3044
+ state.nudges.iterationNudgeAnchors
3045
+ ];
3046
+ for (const anchorSet of anchorSets) {
3047
+ for (const anchorId of anchorSet) {
3048
+ if (!liveMessageIds.has(anchorId)) {
3049
+ anchorSet.delete(anchorId);
3050
+ }
3051
+ }
3052
+ }
3031
3053
  }
3032
3054
 
3033
3055
  // lib/concurrency.ts
@@ -3096,7 +3118,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
3096
3118
  nudges: {
3097
3119
  contextLimitAnchors: Array.from(sessionState.nudges.contextLimitAnchors),
3098
3120
  turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
3099
- iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors)
3121
+ iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
3122
+ boundaryPending: sessionState.nudges.boundaryPending === true
3100
3123
  },
3101
3124
  stats: { ...sessionState.stats },
3102
3125
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3180,6 +3203,7 @@ async function loadSessionState(sessionId, logger) {
3180
3203
  });
3181
3204
  }
3182
3205
  state.nudges.iterationNudgeAnchors = dedupedIterationAnchors;
3206
+ state.nudges.boundaryPending = state.nudges.boundaryPending === true;
3183
3207
  logger.info("Loaded session state from disk", {
3184
3208
  sessionId
3185
3209
  });
@@ -3314,6 +3338,11 @@ var SessionStateStore = class {
3314
3338
  peek(sessionId) {
3315
3339
  return this.states.get(sessionId);
3316
3340
  }
3341
+ forEach(callback) {
3342
+ for (const [sessionId, state] of this.states) {
3343
+ callback(state, sessionId);
3344
+ }
3345
+ }
3317
3346
  async initialize(sessionId, initializer) {
3318
3347
  const state = this.get(sessionId);
3319
3348
  const previous = this.initializationQueues.get(sessionId) ?? Promise.resolve();
@@ -3478,7 +3507,8 @@ function createSessionState() {
3478
3507
  nudges: {
3479
3508
  contextLimitAnchors: /* @__PURE__ */ new Set(),
3480
3509
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3481
- iterationNudgeAnchors: /* @__PURE__ */ new Set()
3510
+ iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3511
+ boundaryPending: false
3482
3512
  },
3483
3513
  stats: {
3484
3514
  pruneTokenCounter: 0,
@@ -3517,7 +3547,8 @@ function resetSessionState(state) {
3517
3547
  state.nudges = {
3518
3548
  contextLimitAnchors: /* @__PURE__ */ new Set(),
3519
3549
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3520
- iterationNudgeAnchors: /* @__PURE__ */ new Set()
3550
+ iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3551
+ boundaryPending: false
3521
3552
  };
3522
3553
  state.stats = {
3523
3554
  pruneTokenCounter: 0,
@@ -3576,6 +3607,7 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
3576
3607
  state.nudges.iterationNudgeAnchors = new Set(
3577
3608
  persisted.nudges.iterationNudgeAnchors || []
3578
3609
  );
3610
+ state.nudges.boundaryPending = persisted.nudges.boundaryPending === true;
3579
3611
  state.stats = {
3580
3612
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
3581
3613
  totalPruneTokens: persisted.stats?.totalPruneTokens || 0
@@ -6721,10 +6753,28 @@ function buildMessagePriorityGuidance(messages, compressionPriorities, anchorInd
6721
6753
  const priorityLabel = `${priority[0].toUpperCase()}${priority.slice(1)}`;
6722
6754
  return renderMessagePriorityGuidance(priorityLabel, refs);
6723
6755
  }
6724
- function injectAnchoredNudge(message, nudgeText) {
6756
+ function normalizeNudgeSignature(baseNudgeText) {
6757
+ return baseNudgeText.replace(/<\/dcp-system-reminder>\s*$/, "").replace(/^\n+/, "").trim();
6758
+ }
6759
+ function messageContainsNudgeSignature(message, baseNudgeText) {
6760
+ const signature = normalizeNudgeSignature(baseNudgeText);
6761
+ if (!signature) {
6762
+ return false;
6763
+ }
6764
+ for (const part of message.parts) {
6765
+ if (part.type === "text" && typeof part.text === "string" && part.text.includes(signature)) {
6766
+ return true;
6767
+ }
6768
+ }
6769
+ return false;
6770
+ }
6771
+ function injectAnchoredNudge(message, nudgeText, baseNudgeText) {
6725
6772
  if (!nudgeText.trim()) {
6726
6773
  return;
6727
6774
  }
6775
+ if (messageContainsNudgeSignature(message, baseNudgeText)) {
6776
+ return;
6777
+ }
6728
6778
  if (message.info.role === "user") {
6729
6779
  if (appendToLastTextPart(message, nudgeText)) {
6730
6780
  return;
@@ -6785,7 +6835,7 @@ function applyRangeModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText,
6785
6835
  return;
6786
6836
  }
6787
6837
  for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
6788
- injectAnchoredNudge(message, nudgeText);
6838
+ injectAnchoredNudge(message, nudgeText, baseNudgeText);
6789
6839
  }
6790
6840
  }
6791
6841
  function applyMessageModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText, availableMessageIdGuidance, compressionPriorities) {
@@ -6798,7 +6848,7 @@ function applyMessageModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText
6798
6848
  );
6799
6849
  const combinedGuidance = [availableMessageIdGuidance, priorityGuidance].filter((g) => g.trim().length > 0).join("\n\n");
6800
6850
  const nudgeText = appendGuidanceToDcpTag(baseNudgeText, combinedGuidance);
6801
- injectAnchoredNudge(message, nudgeText);
6851
+ injectAnchoredNudge(message, nudgeText, baseNudgeText);
6802
6852
  }
6803
6853
  }
6804
6854
  function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities) {
@@ -6854,11 +6904,44 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
6854
6904
  }
6855
6905
 
6856
6906
  // lib/messages/inject/inject.ts
6907
+ function userMessagesAfterLastAnchor(state, messages) {
6908
+ let latestAnchorIndex = -1;
6909
+ for (let index = messages.length - 1; index >= 0; index--) {
6910
+ if (state.nudges.turnNudgeAnchors.has(messages[index].info.id)) {
6911
+ latestAnchorIndex = index;
6912
+ break;
6913
+ }
6914
+ }
6915
+ let userCount = 0;
6916
+ for (let index = latestAnchorIndex + 1; index < messages.length; index++) {
6917
+ const message = messages[index];
6918
+ if (message.info.role === "user" && !isIgnoredUserMessage(message)) {
6919
+ userCount++;
6920
+ }
6921
+ }
6922
+ return userCount;
6923
+ }
6857
6924
  var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
6925
+ const clearPendingBoundary = () => {
6926
+ if (!state.nudges.boundaryPending) {
6927
+ return;
6928
+ }
6929
+ state.nudges.boundaryPending = false;
6930
+ if (!state.sessionId) {
6931
+ return;
6932
+ }
6933
+ void saveSessionState(state, logger).catch(
6934
+ (error) => logger.warn("Failed to persist boundary nudge cleanup", {
6935
+ error: error instanceof Error ? error.message : String(error)
6936
+ })
6937
+ );
6938
+ };
6858
6939
  if (compressPermission(state, config) === "deny") {
6940
+ clearPendingBoundary();
6859
6941
  return;
6860
6942
  }
6861
6943
  if (state.manualMode) {
6944
+ clearPendingBoundary();
6862
6945
  return;
6863
6946
  }
6864
6947
  const lastMessage = findLastNonIgnoredMessage(messages);
@@ -6884,6 +6967,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6884
6967
  state.nudges.contextLimitAnchors.clear();
6885
6968
  state.nudges.turnNudgeAnchors.clear();
6886
6969
  state.nudges.iterationNudgeAnchors.clear();
6970
+ state.nudges.boundaryPending = false;
6887
6971
  void saveSessionState(state, logger).catch(
6888
6972
  (error) => logger.warn("Failed to persist context-limit nudge", {
6889
6973
  error: error instanceof Error ? error.message : String(error)
@@ -6892,6 +6976,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6892
6976
  return;
6893
6977
  }
6894
6978
  const { providerId, modelId } = getModelInfo(messages);
6979
+ const boundaryNudgeEnabled = config.compress.boundaryNudge !== false;
6895
6980
  let anchorsChanged = false;
6896
6981
  const { overMaxLimit, overMinLimit } = isContextOverLimits(
6897
6982
  config,
@@ -6901,13 +6986,18 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6901
6986
  messages
6902
6987
  );
6903
6988
  if (!overMinLimit) {
6904
- const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
6905
6989
  const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
6906
- if (hadTurnAnchors || hadIterationAnchors) {
6907
- state.nudges.turnNudgeAnchors.clear();
6990
+ if (hadIterationAnchors) {
6908
6991
  state.nudges.iterationNudgeAnchors.clear();
6909
6992
  anchorsChanged = true;
6910
6993
  }
6994
+ if (!boundaryNudgeEnabled) {
6995
+ const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
6996
+ if (hadTurnAnchors) {
6997
+ state.nudges.turnNudgeAnchors.clear();
6998
+ anchorsChanged = true;
6999
+ }
7000
+ }
6911
7001
  }
6912
7002
  if (overMaxLimit) {
6913
7003
  if (lastMessage) {
@@ -6925,7 +7015,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6925
7015
  }
6926
7016
  } else if (overMinLimit) {
6927
7017
  const isLastMessageUser = lastMessage?.message.info.role === "user";
6928
- if (isLastMessageUser && lastAssistantMessage) {
7018
+ if (!boundaryNudgeEnabled && isLastMessageUser && lastAssistantMessage) {
6929
7019
  const previousSize = state.nudges.turnNudgeAnchors.size;
6930
7020
  state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id);
6931
7021
  state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id);
@@ -6957,6 +7047,33 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6957
7047
  }
6958
7048
  }
6959
7049
  }
7050
+ if (boundaryNudgeEnabled && !overMaxLimit) {
7051
+ const lastUserMessage = getLastUserMessage(messages);
7052
+ const isLastMessageUser = lastMessage?.message.info.role === "user";
7053
+ const hasPendingBoundary = state.nudges.boundaryPending;
7054
+ if (lastUserMessage && lastAssistantMessage) {
7055
+ const lastUserMessageIndex = messages.findIndex(
7056
+ (message) => message.info.id === lastUserMessage.info.id
7057
+ );
7058
+ const lastAssistantMessageIndex = messages.findIndex(
7059
+ (message) => message.info.id === lastAssistantMessage.info.id
7060
+ );
7061
+ const alreadyAnchored = state.nudges.turnNudgeAnchors.has(lastUserMessage.info.id) && state.nudges.turnNudgeAnchors.has(lastAssistantMessage.info.id);
7062
+ if (!alreadyAnchored && lastUserMessageIndex >= 0 && lastAssistantMessageIndex >= 0) {
7063
+ const interval = getNudgeFrequency(config);
7064
+ const shouldAnchor = hasPendingBoundary || isLastMessageUser && userMessagesAfterLastAnchor(state, messages) >= interval;
7065
+ if (shouldAnchor) {
7066
+ state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id);
7067
+ state.nudges.turnNudgeAnchors.add(lastUserMessage.info.id);
7068
+ anchorsChanged = true;
7069
+ }
7070
+ }
7071
+ if (hasPendingBoundary) {
7072
+ state.nudges.boundaryPending = false;
7073
+ anchorsChanged = true;
7074
+ }
7075
+ }
7076
+ }
6960
7077
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities);
6961
7078
  if (anchorsChanged) {
6962
7079
  void saveSessionState(state, logger).catch(
@@ -8374,8 +8491,50 @@ function createTextCompleteHandler() {
8374
8491
  output.text = stripHallucinationsFromString(output.text);
8375
8492
  };
8376
8493
  }
8377
- function createEventHandler(target, logger) {
8494
+ function createEventHandler(target, logger, config) {
8495
+ const markBoundaryPending = async (state) => {
8496
+ if (!state) return;
8497
+ if (config.compress.boundaryNudge === false) return;
8498
+ if (state.manualMode) return;
8499
+ if (state.isSubAgent && !config.experimental.allowSubAgents) return;
8500
+ if (compressPermission(state, config) === "deny") return;
8501
+ if (!state.sessionId) return;
8502
+ if (state.nudges.boundaryPending) return;
8503
+ state.nudges.boundaryPending = true;
8504
+ await saveSessionState(state, logger);
8505
+ logger.debug("Marked boundary nudge pending", { sessionId: state.sessionId });
8506
+ };
8378
8507
  return async (input) => {
8508
+ if (input.event.type === "session.idle") {
8509
+ const sessionId = input.event.properties?.sessionID;
8510
+ if (typeof sessionId === "string") {
8511
+ const state2 = target instanceof SessionStateStore ? target.peek(sessionId) : target.sessionId === sessionId ? target : void 0;
8512
+ await markBoundaryPending(state2).catch(
8513
+ (error) => logger.warn("Failed to persist boundary nudge", {
8514
+ error: error instanceof Error ? error.message : String(error)
8515
+ })
8516
+ );
8517
+ }
8518
+ return;
8519
+ }
8520
+ if (input.event.type === "vcs.branch.updated") {
8521
+ if (target instanceof SessionStateStore) {
8522
+ const sessions = [];
8523
+ target.forEach((state2) => sessions.push(state2));
8524
+ await Promise.all(
8525
+ sessions.map(
8526
+ (state2) => markBoundaryPending(state2).catch(
8527
+ (error) => logger.warn("Failed to persist boundary nudge", {
8528
+ error: error instanceof Error ? error.message : String(error)
8529
+ })
8530
+ )
8531
+ )
8532
+ );
8533
+ } else {
8534
+ await markBoundaryPending(target);
8535
+ }
8536
+ return;
8537
+ }
8379
8538
  const eventTime = typeof input.event?.time === "number" && Number.isFinite(input.event.time) ? input.event.time : typeof input.event?.properties?.time === "number" && Number.isFinite(input.event.properties.time) ? input.event.properties.time : void 0;
8380
8539
  if (input.event.type !== "message.part.updated") {
8381
8540
  return;
@@ -8641,7 +8800,7 @@ var server = (async (ctx) => {
8641
8800
  ctx.directory,
8642
8801
  hostPermissions
8643
8802
  ),
8644
- event: createEventHandler(states, logger),
8803
+ event: createEventHandler(states, logger, config),
8645
8804
  tool: {
8646
8805
  ...config.compress.permission !== "deny" && {
8647
8806
  compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext)