@diffexai/diffex 0.3.1 → 0.3.2

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/core/agent-session.d.ts +0 -5
  3. package/dist/core/agent-session.js +6 -61
  4. package/dist/core/compaction/branch-summarization.js +3 -2
  5. package/dist/core/compaction/compaction.js +9 -7
  6. package/dist/core/compaction/utils.d.ts +3 -0
  7. package/dist/core/compaction/utils.js +21 -0
  8. package/dist/core/current-plan-state.d.ts +3 -2
  9. package/dist/core/current-plan-state.js +23 -6
  10. package/dist/core/plan-snapshot.d.ts +3 -0
  11. package/dist/core/plan-snapshot.js +23 -0
  12. package/dist/core/session-manager.js +21 -1
  13. package/dist/core/tools/update-plan.d.ts +27 -6
  14. package/dist/core/tools/update-plan.js +122 -18
  15. package/dist/modes/interactive/components/footer.js +16 -5
  16. package/dist/modes/interactive/components/tool-group.d.ts +1 -0
  17. package/dist/modes/interactive/components/tool-group.js +26 -7
  18. package/dist/node_modules/@diffexai/diffex-agent-core/dist/agent-loop.js +21 -3
  19. package/dist/node_modules/@diffexai/diffex-agent-core/dist/types.d.ts +17 -10
  20. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
  21. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +3 -3
  22. package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
  23. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
  24. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/amazon-bedrock.json +1 -1
  25. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/anthropic.json +1 -1
  26. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/azure-openai-responses.json +1 -1
  27. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cerebras.json +1 -1
  28. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  29. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  30. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/fireworks.json +1 -1
  31. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/github-copilot.json +1 -1
  32. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/google-vertex.json +1 -1
  33. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/google.json +1 -1
  34. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/huggingface.json +1 -1
  35. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/minimax-cn.json +1 -1
  36. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/minimax.json +1 -1
  37. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/mistral.json +1 -1
  38. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/moonshotai-cn.json +1 -1
  39. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/moonshotai.json +1 -1
  40. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/nvidia.json +1 -1
  41. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openai.json +1 -1
  42. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode-go.json +1 -1
  43. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
  44. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
  45. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/together.json +1 -1
  46. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  47. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xai.json +1 -1
  48. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  49. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  50. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  51. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi.json +1 -1
  52. package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
  53. package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +30 -30
  54. package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
  55. package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
  56. package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
  57. package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
  58. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
  59. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
  60. package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
  61. package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
  62. package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
  63. package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
  64. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
  65. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
  66. package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
  67. package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
  68. package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
  69. package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
  70. package/distribution-components.json +11 -11
  71. package/distribution-files.json +75 -67
  72. package/npm-shrinkwrap.json +2 -2
  73. package/package.json +1 -1
  74. package/release/distribution-manifest.json +4 -4
  75. package/release/install-package-lock.json +5 -5
  76. package/release/install-package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-09-29
4
+
5
+ ### New Features
6
+
7
+ - Added persistent plan context with stable step IDs and optional success criteria, attached to every model request and restored across session resume, compaction, and tree navigation.
8
+ See [Plan Mode](docs/usage.md#plan-mode) ([#249](https://github.com/diffexai/diffex/pull/249)).
9
+ - Added parallel tool execution phases that overlap independent calls around sequential tool barriers.
10
+ See [Extensions](docs/extensions.md) ([#250](https://github.com/diffexai/diffex/pull/250)).
11
+ - Added a cache hit rate to the interactive footer alongside context usage ([#249](https://github.com/diffexai/diffex/pull/249)).
12
+
13
+ ### Added
14
+
15
+ - Added persistent plan step identity with stable IDs and optional success criteria, attached as hidden current-task context to every model request and restored across session resume, compaction, and tree navigation.
16
+ See [Plan Mode](docs/usage.md#plan-mode) and [SDK tools](docs/sdk.md#tools) ([#249](https://github.com/diffexai/diffex/pull/249)).
17
+ - Added a cache hit rate to the interactive footer alongside context usage ([#249](https://github.com/diffexai/diffex/pull/249)).
18
+ - Added parallel tool execution phases that overlap consecutive parallel-capable calls around individual sequential barriers, such as `update_plan`.
19
+ See [Extensions](docs/extensions.md#tool_execution_start--tool_execution_update--tool_execution_end) ([#250](https://github.com/diffexai/diffex/pull/250)).
20
+
21
+ ### Changed
22
+
23
+ - Excluded plan tool history and saved plan snapshots from compaction and branch summaries; the latest attached snapshot supersedes retained plan context ([#251](https://github.com/diffexai/diffex/pull/251)).
24
+
25
+ ### Fixed
26
+
27
+ - Fixed plan cache-prefix preservation so ordinary requests reuse the existing prompt prefix ([#249](https://github.com/diffexai/diffex/pull/249)).
28
+ - Fixed plan restoration on tree navigation to use the destination branch's authoritative plan snapshot ([#249](https://github.com/diffexai/diffex/pull/249)).
29
+ - Fixed settled tool output being remeasured on every keystroke, keeping typing responsive as session history grows ([#249](https://github.com/diffexai/diffex/pull/249)).
30
+
3
31
  ## [0.3.1] - 2026-09-20
4
32
 
5
33
  ### New Features
@@ -253,7 +253,6 @@ export declare class AgentSession {
253
253
  private _followUpMessages;
254
254
  /** Messages queued to be included with the next user prompt as context ("asides"). */
255
255
  private _pendingNextTurnMessages;
256
- private _pendingCurrentPlanRestoration;
257
256
  private readonly _skillReferenceSnapshots;
258
257
  private readonly _skillEnablementOverrides;
259
258
  private _activeSkillTrace;
@@ -337,10 +336,6 @@ export declare class AgentSession {
337
336
  */
338
337
  private _installAgentToolHooks;
339
338
  private _installAgentNextTurnRefresh;
340
- private _installAgentContextRestoration;
341
- private _armInitialCurrentPlanRestoration;
342
- private _armCurrentPlanRestorationIfActive;
343
- private _consumePendingCurrentPlanRestoration;
344
339
  /** Emit an event to all listeners */
345
340
  private _emit;
346
341
  private _emitQueueUpdate;
@@ -26,7 +26,7 @@ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth
26
26
  import { executeBashWithOperations } from "./bash-executor.js";
27
27
  import { CHANGE_COLLABORATION_MODE, COLLABORATION_MODE_CUSTOM_TYPE, GET_COLLABORATION_MODE, IS_COLLABORATION_MODE_BUSY, PLAN_MODE_SYSTEM_PROMPT, PLAN_MODE_TOOL_NAMES, planHandoffSettlementGuards, SUBSCRIBE_PROMPT_ADMISSION, } from "./collaboration-mode.js";
28
28
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
29
- import { createSessionUpdatePlanToolDefinition, resolveCurrentPlanState } from "./current-plan-state.js";
29
+ import { createSessionUpdatePlanToolDefinition, formatCurrentPlanSnapshot } from "./current-plan-state.js";
30
30
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
31
31
  import { exportSessionToHtml } from "./export-html/index.js";
32
32
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
@@ -109,8 +109,6 @@ function estimateMessagesTokens(messages) {
109
109
  // ============================================================================
110
110
  /** Standard thinking levels */
111
111
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
112
- const CURRENT_PLAN_RESTORATION_CUSTOM_TYPE = "diffex.current-plan-restoration";
113
- const CURRENT_PLAN_RESTORATION_REMINDER = "Reminder: restored context is not proof of completion. Verify current state before claiming work is done.";
114
112
  const MAX_PENDING_SUBAGENT_ESCALATION_CHARACTERS = 8_000;
115
113
  const INCOMPATIBLE_SUBAGENT_PARENT_HOOKS = [
116
114
  "resources_discover",
@@ -145,7 +143,6 @@ export class AgentSession {
145
143
  _followUpMessages = [];
146
144
  /** Messages queued to be included with the next user prompt as context ("asides"). */
147
145
  _pendingNextTurnMessages = [];
148
- _pendingCurrentPlanRestoration = false;
149
146
  _skillReferenceSnapshots = new Map();
150
147
  _skillEnablementOverrides = new Map();
151
148
  _activeSkillTrace;
@@ -245,12 +242,10 @@ export class AgentSession {
245
242
  : createEvolvedSkillCatalog(config.harnessVersionBundle, config.workspaceIdentity));
246
243
  this._telemetryContext = config.telemetryContext ?? NOOP_TELEMETRY_CONTEXT;
247
244
  this._lifecycleObserver = config.lifecycleObserver;
248
- this._armInitialCurrentPlanRestoration();
249
245
  // Always subscribe to agent events for internal handling
250
246
  // (session persistence, extensions, auto-compaction, retry logic)
251
247
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
252
248
  this._installAgentToolHooks();
253
- this._installAgentContextRestoration();
254
249
  this._installAgentNextTurnRefresh();
255
250
  this._buildRuntime({
256
251
  activeToolNames: this._initialActiveToolNames,
@@ -434,55 +429,6 @@ export class AgentSession {
434
429
  };
435
430
  };
436
431
  }
437
- _installAgentContextRestoration() {
438
- const previousTransformContext = this.agent.transformContext;
439
- this.agent.transformContext = async (messages, signal) => {
440
- const transformedMessages = previousTransformContext
441
- ? await previousTransformContext(messages, signal)
442
- : messages;
443
- return this._consumePendingCurrentPlanRestoration(transformedMessages);
444
- };
445
- }
446
- _armInitialCurrentPlanRestoration() {
447
- if (this._sessionStartEvent.reason !== "startup" && this._sessionStartEvent.reason !== "resume") {
448
- return;
449
- }
450
- this._armCurrentPlanRestorationIfActive();
451
- }
452
- _armCurrentPlanRestorationIfActive() {
453
- if (resolveCurrentPlanState(this.sessionManager).status === "active") {
454
- this._pendingCurrentPlanRestoration = true;
455
- }
456
- }
457
- _consumePendingCurrentPlanRestoration(messages) {
458
- if (!this._pendingCurrentPlanRestoration) {
459
- return messages;
460
- }
461
- const state = resolveCurrentPlanState(this.sessionManager);
462
- this._pendingCurrentPlanRestoration = false;
463
- if (state.status !== "active") {
464
- return messages;
465
- }
466
- const lines = [
467
- "Restored unfinished update_plan checklist from the current session. Use this hidden context to continue the work.",
468
- ];
469
- if (state.operation.explanation !== undefined) {
470
- lines.push("", "Explanation:", state.operation.explanation);
471
- }
472
- lines.push("", "Checklist:");
473
- for (const [index, item] of state.operation.plan.entries()) {
474
- lines.push(`${index + 1}. [${item.status}] ${item.step}`);
475
- }
476
- lines.push("", CURRENT_PLAN_RESTORATION_REMINDER);
477
- const restoredContext = {
478
- role: "custom",
479
- customType: CURRENT_PLAN_RESTORATION_CUSTOM_TYPE,
480
- content: [{ type: "text", text: lines.join("\n") }],
481
- display: false,
482
- timestamp: Date.now(),
483
- };
484
- return [...messages, restoredContext];
485
- }
486
432
  // =========================================================================
487
433
  // Event Subscription
488
434
  // =========================================================================
@@ -1809,7 +1755,6 @@ export class AgentSession {
1809
1755
  return;
1810
1756
  pendingCompaction.commit();
1811
1757
  pendingCompaction = undefined;
1812
- this._armCurrentPlanRestorationIfActive();
1813
1758
  };
1814
1759
  const rollbackPendingCompaction = () => {
1815
1760
  if (!pendingCompaction)
@@ -2729,7 +2674,8 @@ export class AgentSession {
2729
2674
  this._branchSummaryAbortController?.abort();
2730
2675
  }
2731
2676
  async _recordSuccessfulCompaction(options) {
2732
- const transaction = appendCompactionTransaction(this.sessionManager, options.summary, options.firstKeptEntryId, options.tokensBefore, options.details, options.fromExtension, options.usage);
2677
+ const summary = `${options.summary}\n\n${formatCurrentPlanSnapshot(this.sessionManager)}`;
2678
+ const transaction = appendCompactionTransaction(this.sessionManager, summary, options.firstKeptEntryId, options.tokensBefore, options.details, options.fromExtension, options.usage);
2733
2679
  const sessionContext = this.sessionManager.buildSessionContext();
2734
2680
  this.agent.state.messages = sessionContext.messages;
2735
2681
  const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
@@ -2763,15 +2709,13 @@ export class AgentSession {
2763
2709
  else
2764
2710
  transaction.commit();
2765
2711
  const compactionResult = {
2766
- summary: options.summary,
2712
+ summary,
2767
2713
  firstKeptEntryId: options.firstKeptEntryId,
2768
2714
  tokensBefore: options.tokensBefore,
2769
2715
  estimatedTokensAfter,
2770
2716
  usage: options.usage,
2771
2717
  details: options.details,
2772
2718
  };
2773
- if (!options.deferCommit)
2774
- this._armCurrentPlanRestorationIfActive();
2775
2719
  return compactionResult;
2776
2720
  }
2777
2721
  /**
@@ -3759,7 +3703,8 @@ export class AgentSession {
3759
3703
  let summaryEntry;
3760
3704
  const mutateLeaf = () => {
3761
3705
  if (summaryText) {
3762
- const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension, summaryUsage);
3706
+ const summary = `${summaryText}\n\nThe plan below is authoritative for the selected conversation branch. Plan references in the exploration summary above are historical.\n${formatCurrentPlanSnapshot(this.sessionManager, newLeafId)}`;
3707
+ const summaryId = this.sessionManager.branchWithSummary(newLeafId, summary, summaryDetails, fromExtension, summaryUsage);
3763
3708
  summaryEntry = this.sessionManager.getEntry(summaryId);
3764
3709
  if (label)
3765
3710
  this.sessionManager.appendLabelChange(summaryId, label);
@@ -7,7 +7,7 @@
7
7
  import { contentText } from "@diffexai/diffex-ai";
8
8
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
9
9
  import { completeSummarization, estimateTokens } from "./compaction.js";
10
- import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
10
+ import { computeFileLists, createFileOps, excludePlanItems, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
11
11
  // ============================================================================
12
12
  // Entry Collection
13
13
  // ============================================================================
@@ -120,7 +120,8 @@ export function prepareBranchEntries(entries, tokenBudget = 0) {
120
120
  // Second pass: walk from newest to oldest, adding messages until token budget
121
121
  for (let i = entries.length - 1; i >= 0; i--) {
122
122
  const entry = entries[i];
123
- const message = getMessageFromEntry(entry);
123
+ const rawMessage = getMessageFromEntry(entry);
124
+ const message = rawMessage ? excludePlanItems([rawMessage])[0] : undefined;
124
125
  if (!message)
125
126
  continue;
126
127
  // Extract file ops from assistant messages (tool calls)
@@ -8,8 +8,9 @@ import { NOOP_TELEMETRY_CONTEXT, startAiSpan, } from "@diffexai/diffex-agent-cor
8
8
  import { contentText, retryAssistantCall, uuidv7 } from "@diffexai/diffex-ai";
9
9
  import { completeSimple } from "@diffexai/diffex-ai/compat";
10
10
  import { convertToLlm } from "../messages.js";
11
+ import { stripCurrentPlanSnapshot } from "../plan-snapshot.js";
11
12
  import { buildSessionContext, sessionEntryToContextMessages, } from "../session-manager.js";
12
- import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
13
+ import { computeFileLists, createFileOps, excludePlanItems, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
13
14
  /**
14
15
  * Extract file operations from messages and previous compaction entries.
15
16
  */
@@ -494,13 +495,14 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
494
495
  export async function generateSummaryWithUsage(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks, telemetryContext) {
495
496
  const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
496
497
  // Use update prompt if we have a previous summary, otherwise initial prompt
498
+ previousSummary = previousSummary === undefined ? undefined : stripCurrentPlanSnapshot(previousSummary);
497
499
  let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
498
500
  if (customInstructions) {
499
501
  basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
500
502
  }
501
503
  // Serialize conversation to text so model doesn't try to continue it
502
504
  // Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
503
- const llmMessages = convertToLlm(currentMessages);
505
+ const llmMessages = convertToLlm(excludePlanItems(currentMessages));
504
506
  const conversationText = serializeConversation(llmMessages);
505
507
  // Build the prompt with conversation wrapped in tags
506
508
  let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
@@ -538,7 +540,7 @@ export function prepareCompaction(pathEntries, settings) {
538
540
  let boundaryStart = 0;
539
541
  if (prevCompactionIndex >= 0) {
540
542
  const prevCompaction = pathEntries[prevCompactionIndex];
541
- previousSummary = prevCompaction.summary;
543
+ previousSummary = stripCurrentPlanSnapshot(prevCompaction.summary);
542
544
  const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
543
545
  boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
544
546
  }
@@ -581,8 +583,8 @@ export function prepareCompaction(pathEntries, settings) {
581
583
  }
582
584
  return {
583
585
  firstKeptEntryId,
584
- messagesToSummarize,
585
- turnPrefixMessages,
586
+ messagesToSummarize: excludePlanItems(messagesToSummarize),
587
+ turnPrefixMessages: excludePlanItems(turnPrefixMessages),
586
588
  isSplitTurn: cutPoint.isSplitTurn,
587
589
  tokensBefore,
588
590
  previousSummary,
@@ -620,7 +622,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
620
622
  let summary;
621
623
  let summaryUsage;
622
624
  if (isSplitTurn && turnPrefixMessages.length > 0) {
623
- let historyText = "No prior history.";
625
+ let historyText = previousSummary ?? "No prior history.";
624
626
  let historyUsage;
625
627
  if (messagesToSummarize.length > 0) {
626
628
  const historyResult = await generateSummaryWithUsage(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, env, retry, callbacks, telemetryContext);
@@ -657,7 +659,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
657
659
  */
658
660
  async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, env, signal, thinkingLevel, streamFn, retry, callbacks, telemetryContext) {
659
661
  const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
660
- const llmMessages = convertToLlm(messages);
662
+ const llmMessages = convertToLlm(excludePlanItems(messages));
661
663
  const conversationText = serializeConversation(llmMessages);
662
664
  const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
663
665
  const summarizationMessages = [
@@ -25,6 +25,8 @@ export declare function computeFileLists(fileOps: FileOperations): {
25
25
  * Format file operations as XML tags for summary.
26
26
  */
27
27
  export declare function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string;
28
+ /** Exclude plan data from a context projection without changing persisted messages. */
29
+ export declare function excludePlanItems(messages: AgentMessage[]): AgentMessage[];
28
30
  /**
29
31
  * Serialize LLM messages to text for summarization.
30
32
  * This prevents the model from treating it as a conversation to continue.
@@ -32,6 +34,7 @@ export declare function formatFileOperations(readFiles: string[], modifiedFiles:
32
34
  *
33
35
  * Tool results are truncated to keep the summarization request within
34
36
  * reasonable token budgets. Full content is not needed for summarization.
37
+ * Plan calls and results are omitted because the latest plan is attached separately.
35
38
  */
36
39
  export declare function serializeConversation(messages: Message[]): string;
37
40
  export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
@@ -2,6 +2,7 @@
2
2
  * Shared utilities for compaction and branch summarization.
3
3
  */
4
4
  import { contentText } from "@diffexai/diffex-ai";
5
+ import { stripCurrentPlanSnapshot } from "../plan-snapshot.js";
5
6
  export function createFileOps() {
6
7
  return {
7
8
  read: new Set(),
@@ -71,6 +72,21 @@ export function formatFileOperations(readFiles, modifiedFiles) {
71
72
  // ============================================================================
72
73
  // Message Serialization
73
74
  // ============================================================================
75
+ /** Exclude plan data from a context projection without changing persisted messages. */
76
+ export function excludePlanItems(messages) {
77
+ return messages.flatMap((message) => {
78
+ if (message.role === "toolResult" && message.toolName === "update_plan")
79
+ return [];
80
+ if (message.role === "assistant") {
81
+ const content = message.content.filter((block) => block.type !== "toolCall" || block.name !== "update_plan");
82
+ return content.length > 0 ? [{ ...message, content }] : [];
83
+ }
84
+ if (message.role === "compactionSummary" || message.role === "branchSummary") {
85
+ return [{ ...message, summary: stripCurrentPlanSnapshot(message.summary) }];
86
+ }
87
+ return [message];
88
+ });
89
+ }
74
90
  /** Maximum characters for a tool result in serialized summaries. */
75
91
  const TOOL_RESULT_MAX_CHARS = 2000;
76
92
  /**
@@ -90,6 +106,7 @@ function truncateForSummary(text, maxChars) {
90
106
  *
91
107
  * Tool results are truncated to keep the summarization request within
92
108
  * reasonable token budgets. Full content is not needed for summarization.
109
+ * Plan calls and results are omitted because the latest plan is attached separately.
93
110
  */
94
111
  export function serializeConversation(messages) {
95
112
  const parts = [];
@@ -107,6 +124,8 @@ export function serializeConversation(messages) {
107
124
  thinkingParts.push(block.thinking);
108
125
  }
109
126
  else if (block.type === "toolCall") {
127
+ if (block.name === "update_plan")
128
+ continue;
110
129
  const args = block.arguments;
111
130
  const argsStr = Object.entries(args)
112
131
  .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
@@ -125,6 +144,8 @@ export function serializeConversation(messages) {
125
144
  }
126
145
  }
127
146
  else if (msg.role === "toolResult") {
147
+ if (msg.toolName === "update_plan")
148
+ continue;
128
149
  const content = contentText(msg.content, "");
129
150
  if (content) {
130
151
  parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
@@ -15,5 +15,6 @@ export type CurrentPlanState = {
15
15
  operation: UpdatePlanToolDetails;
16
16
  };
17
17
  export declare function appendCurrentPlanRecord(sessionManager: Pick<SessionManager, "appendCustomEntry">, operation: UpdatePlanToolDetails): string;
18
- export declare function resolveCurrentPlanState(sessionManager: Pick<SessionManager, "getBranch">): CurrentPlanState;
19
- export declare function createSessionUpdatePlanToolDefinition(sessionManager: Pick<SessionManager, "appendCustomEntry">): ReturnType<typeof createUpdatePlanToolDefinition>;
18
+ export declare function resolveCurrentPlanState(sessionManager: Pick<SessionManager, "getBranch">, fromId?: string | null): CurrentPlanState;
19
+ export declare function formatCurrentPlanSnapshot(sessionManager: Pick<SessionManager, "getBranch">, fromId?: string | null): string;
20
+ export declare function createSessionUpdatePlanToolDefinition(sessionManager: Pick<SessionManager, "appendCustomEntry" | "getBranch">): ReturnType<typeof createUpdatePlanToolDefinition>;
@@ -1,10 +1,11 @@
1
- import { createUpdatePlanToolDefinition, parseUpdatePlanOperation, } from "./tools/update-plan.js";
1
+ import { CURRENT_PLAN_SNAPSHOT_PREFIX } from "./plan-snapshot.js";
2
+ import { createUpdatePlanToolDefinition, resolveUpdatePlanOperation, } from "./tools/update-plan.js";
2
3
  export const CURRENT_PLAN_CUSTOM_ENTRY_TYPE = "diffex.current-plan";
3
4
  export function appendCurrentPlanRecord(sessionManager, operation) {
4
- return sessionManager.appendCustomEntry(CURRENT_PLAN_CUSTOM_ENTRY_TYPE, parseUpdatePlanOperation(operation));
5
+ return sessionManager.appendCustomEntry(CURRENT_PLAN_CUSTOM_ENTRY_TYPE, resolveUpdatePlanOperation(operation));
5
6
  }
6
- export function resolveCurrentPlanState(sessionManager) {
7
- const branch = sessionManager.getBranch();
7
+ export function resolveCurrentPlanState(sessionManager, fromId) {
8
+ const branch = fromId === null ? [] : sessionManager.getBranch(fromId);
8
9
  for (let index = branch.length - 1; index >= 0; index--) {
9
10
  const entry = branch[index];
10
11
  if (entry.type !== "custom" || entry.customType !== CURRENT_PLAN_CUSTOM_ENTRY_TYPE) {
@@ -12,7 +13,7 @@ export function resolveCurrentPlanState(sessionManager) {
12
13
  }
13
14
  let operation;
14
15
  try {
15
- operation = parseUpdatePlanOperation(entry.data);
16
+ operation = resolveUpdatePlanOperation(entry.data);
16
17
  }
17
18
  catch {
18
19
  return { status: "corrupt" };
@@ -24,8 +25,24 @@ export function resolveCurrentPlanState(sessionManager) {
24
25
  }
25
26
  return { status: "absent" };
26
27
  }
28
+ export function formatCurrentPlanSnapshot(sessionManager, fromId) {
29
+ const state = resolveCurrentPlanState(sessionManager, fromId);
30
+ const plan = state.status === "active"
31
+ ? state.operation.plan
32
+ : state.status === "inactive" && "plan" in state.operation
33
+ ? state.operation.plan
34
+ : [];
35
+ return `${CURRENT_PLAN_SNAPSHOT_PREFIX}${JSON.stringify({ plan }, null, 2)}`;
36
+ }
27
37
  export function createSessionUpdatePlanToolDefinition(sessionManager) {
28
- const definition = createUpdatePlanToolDefinition();
38
+ const definition = createUpdatePlanToolDefinition(undefined, () => {
39
+ const state = resolveCurrentPlanState(sessionManager);
40
+ if (state.status === "active")
41
+ return state.operation.plan;
42
+ if (state.status === "inactive" && "plan" in state.operation)
43
+ return state.operation.plan;
44
+ return undefined;
45
+ });
29
46
  const execute = definition.execute;
30
47
  return {
31
48
  ...definition,
@@ -0,0 +1,3 @@
1
+ export declare const CURRENT_PLAN_SNAPSHOT_PREFIX = "Current task context (latest plan):\n";
2
+ /** Remove only the structured plan snapshot appended to a summary. */
3
+ export declare function stripCurrentPlanSnapshot(summary: string): string;
@@ -0,0 +1,23 @@
1
+ export const CURRENT_PLAN_SNAPSHOT_PREFIX = "Current task context (latest plan):\n";
2
+ // Branch navigation attaches this authority paragraph with the snapshot.
3
+ const BRANCH_PLAN_AUTHORITY = "The plan below is authoritative for the selected conversation branch. Plan references in the exploration summary above are historical.\n";
4
+ /** Remove only the structured plan snapshot appended to a summary. */
5
+ export function stripCurrentPlanSnapshot(summary) {
6
+ const index = summary.lastIndexOf(CURRENT_PLAN_SNAPSHOT_PREFIX);
7
+ const authorityStart = index - BRANCH_PLAN_AUTHORITY.length;
8
+ const hasBranchAuthority = authorityStart >= 0 && summary.slice(authorityStart, index) === BRANCH_PLAN_AUTHORITY;
9
+ const attachmentStart = hasBranchAuthority ? authorityStart : index;
10
+ if (index < 0 || (attachmentStart > 0 && summary.slice(attachmentStart - 2, attachmentStart) !== "\n\n")) {
11
+ return summary;
12
+ }
13
+ try {
14
+ const snapshot = JSON.parse(summary.slice(index + CURRENT_PLAN_SNAPSHOT_PREFIX.length));
15
+ if (typeof snapshot !== "object" || snapshot === null || !("plan" in snapshot) || !Array.isArray(snapshot.plan)) {
16
+ return summary;
17
+ }
18
+ }
19
+ catch {
20
+ return summary;
21
+ }
22
+ return summary.slice(0, attachmentStart === 0 ? 0 : attachmentStart - 2);
23
+ }
@@ -7,7 +7,9 @@ import { createInterface } from "readline";
7
7
  import { StringDecoder } from "string_decoder";
8
8
  import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
9
9
  import { normalizePath, resolvePath } from "../utils/paths.js";
10
+ import { excludePlanItems } from "./compaction/utils.js";
10
11
  import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "./messages.js";
12
+ import { stripCurrentPlanSnapshot } from "./plan-snapshot.js";
11
13
  export const CURRENT_SESSION_VERSION = 3;
12
14
  function createSessionId() {
13
15
  return uuidv7();
@@ -237,7 +239,25 @@ export function buildContextEntries(entries, leafId, byId) {
237
239
  export function buildSessionContext(entries, leafId, byId) {
238
240
  const path = buildSessionPath(entries, leafId, byId);
239
241
  const { thinkingLevel, model } = getSessionContextSettings(path);
240
- const messages = buildContextEntries(entries, leafId, byId).flatMap(sessionEntryToContextMessages);
242
+ const contextEntries = buildContextEntries(entries, leafId, byId);
243
+ const contextEntryIds = new Set(contextEntries.map((entry) => entry.id));
244
+ // Context order puts the latest compaction before its retained tail. Use path
245
+ // order to find the snapshot that supersedes earlier plan calls and summaries.
246
+ let snapshotIndex = -1;
247
+ for (let i = path.length - 1; i >= 0; i--) {
248
+ const entry = path[i];
249
+ if (contextEntryIds.has(entry.id) &&
250
+ (entry.type === "compaction" || entry.type === "branch_summary") &&
251
+ stripCurrentPlanSnapshot(entry.summary) !== entry.summary) {
252
+ snapshotIndex = i;
253
+ break;
254
+ }
255
+ }
256
+ const supersededEntryIds = new Set(path.slice(0, Math.max(0, snapshotIndex)).map((entry) => entry.id));
257
+ const messages = contextEntries.flatMap((entry) => {
258
+ const projected = sessionEntryToContextMessages(entry);
259
+ return supersededEntryIds.has(entry.id) ? excludePlanItems(projected) : projected;
260
+ });
241
261
  return { messages, thinkingLevel, model };
242
262
  }
243
263
  /**
@@ -4,30 +4,51 @@ import type { ToolDefinition } from "../extensions/types.ts";
4
4
  declare const UPDATE_PLAN_STATUSES: readonly ["pending", "in_progress", "completed"];
5
5
  export type UpdatePlanStatus = (typeof UPDATE_PLAN_STATUSES)[number];
6
6
  export interface UpdatePlanItem {
7
+ id: string;
7
8
  step: string;
8
9
  status: UpdatePlanStatus;
10
+ success_criteria?: string[];
9
11
  }
10
12
  export type UpdatePlanToolInput = {
11
13
  plan: UpdatePlanItem[];
12
14
  explanation?: string;
15
+ } | {
16
+ update: {
17
+ id: string;
18
+ step?: string;
19
+ status?: UpdatePlanStatus;
20
+ success_criteria?: string[];
21
+ };
22
+ explanation?: string;
13
23
  } | {
14
24
  clear: true;
15
25
  explanation?: string;
16
26
  };
17
- export type UpdatePlanToolDetails = UpdatePlanToolInput;
27
+ export type UpdatePlanToolDetails = Exclude<UpdatePlanToolInput, {
28
+ update: unknown;
29
+ }>;
18
30
  declare const updatePlanSchema: Type.TObject<{
19
31
  plan: Type.TOptional<Type.TArray<Type.TObject<{
32
+ id: Type.TString;
20
33
  step: Type.TString;
21
34
  status: Type.TUnsafe<"completed" | "in_progress" | "pending">;
35
+ success_criteria: Type.TOptional<Type.TArray<Type.TString>>;
22
36
  }>>>;
37
+ update: Type.TOptional<Type.TObject<{
38
+ id: Type.TString;
39
+ step: Type.TOptional<Type.TString>;
40
+ status: Type.TOptional<Type.TUnsafe<"completed" | "in_progress" | "pending">>;
41
+ success_criteria: Type.TOptional<Type.TArray<Type.TString>>;
42
+ }>>;
23
43
  clear: Type.TOptional<Type.TLiteral<true>>;
24
44
  explanation: Type.TOptional<Type.TString>;
25
45
  }>;
26
46
  export declare const updatePlanToolSystemPromptContribution: {
27
- readonly snippet: "track progress with an ordered task checklist";
28
- readonly guidelines: readonly ["Use update_plan for complex, ambiguous, dependent, long-running, or multi-part work, when the user requests a plan, or when checkpoints would improve collaboration; skip it for simple or immediately executable work.", "Write short, ordered, outcome-focused steps with observable evidence where useful; do not narrate commands or pad the checklist with obvious actions.", "Include final verification when correctness requires it.", "Update the checklist at meaningful milestones and mark completed work before moving on.", "Explain material replanning, omit explanations for routine progress, and use clear: true only when abandoning the checklist.", "Publishing the checklist communicates progress but does not execute, schedule, or prove completion of its steps."];
47
+ readonly snippet: "persist an ordered plan with step IDs and optional success criteria";
48
+ readonly guidelines: readonly ["Use update_plan for complex, ambiguous, dependent, long-running, or multi-part work, when the user requests a plan, or when checkpoints would improve collaboration; skip it for simple or immediately executable work.", "Write short, ordered, outcome-focused steps. Give each step a stable ID and add optional success_criteria for subtasks needing validation.", "Include final verification when correctness requires it.", "Update by ID at meaningful milestones; retain IDs when rewording, reordering, or changing status.", "Explain material replanning, omit explanations for routine progress, and use clear: true only when abandoning the checklist.", "Publishing the checklist communicates progress but does not execute, schedule, or prove completion of its steps."];
29
49
  };
30
- export declare function parseUpdatePlanOperation(input: unknown): UpdatePlanToolDetails;
31
- export declare function createUpdatePlanToolDefinition(): ToolDefinition<typeof updatePlanSchema, UpdatePlanToolDetails>;
32
- export declare function createUpdatePlanTool(): AgentTool<typeof updatePlanSchema>;
50
+ export declare function parseUpdatePlanOperation(input: unknown): UpdatePlanToolInput;
51
+ export declare function resolveUpdatePlanOperation(input: unknown, currentPlan?: readonly UpdatePlanItem[]): UpdatePlanToolDetails;
52
+ export declare function createUpdatePlanToolDefinition(_cwd?: string, getCurrentPlan?: () => readonly UpdatePlanItem[] | undefined): ToolDefinition<typeof updatePlanSchema, UpdatePlanToolDetails>;
53
+ export declare function createUpdatePlanTool(getCurrentPlan?: () => readonly UpdatePlanItem[] | undefined): AgentTool<typeof updatePlanSchema>;
33
54
  export {};