@diffexai/diffex 0.3.0 → 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 (96) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.js +39 -0
  4. package/dist/core/agent-session-services.d.ts +1 -0
  5. package/dist/core/agent-session-services.js +1 -0
  6. package/dist/core/agent-session.d.ts +4 -5
  7. package/dist/core/agent-session.js +11 -62
  8. package/dist/core/compaction/branch-summarization.js +3 -2
  9. package/dist/core/compaction/compaction.js +9 -7
  10. package/dist/core/compaction/utils.d.ts +3 -0
  11. package/dist/core/compaction/utils.js +21 -0
  12. package/dist/core/current-plan-state.d.ts +3 -2
  13. package/dist/core/current-plan-state.js +23 -6
  14. package/dist/core/plan-snapshot.d.ts +3 -0
  15. package/dist/core/plan-snapshot.js +23 -0
  16. package/dist/core/sdk.d.ts +3 -0
  17. package/dist/core/sdk.js +1 -0
  18. package/dist/core/session-manager.js +21 -1
  19. package/dist/core/subagents/services.d.ts +2 -0
  20. package/dist/core/subagents/services.js +2 -0
  21. package/dist/core/tools/bash.js +47 -12
  22. package/dist/core/tools/update-plan.d.ts +27 -6
  23. package/dist/core/tools/update-plan.js +122 -18
  24. package/dist/index.d.ts +1 -0
  25. package/dist/main.js +4 -0
  26. package/dist/modes/interactive/components/footer.js +16 -5
  27. package/dist/modes/interactive/components/tool-group.d.ts +1 -0
  28. package/dist/modes/interactive/components/tool-group.js +26 -7
  29. package/dist/modes/print-mode.js +16 -13
  30. package/dist/node_modules/@diffexai/diffex-agent-core/dist/agent-loop.js +21 -3
  31. package/dist/node_modules/@diffexai/diffex-agent-core/dist/types.d.ts +17 -10
  32. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
  33. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +3 -3
  34. package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
  35. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
  36. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/amazon-bedrock.json +1 -1
  37. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/anthropic.json +1 -1
  38. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/azure-openai-responses.json +1 -1
  39. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cerebras.json +1 -1
  40. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  41. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  42. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/fireworks.json +1 -1
  43. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/github-copilot.json +1 -1
  44. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/google-vertex.json +1 -1
  45. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/google.json +1 -1
  46. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/huggingface.json +1 -1
  47. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/minimax-cn.json +1 -1
  48. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/minimax.json +1 -1
  49. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/mistral.json +1 -1
  50. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/moonshotai-cn.json +1 -1
  51. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/moonshotai.json +1 -1
  52. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/nvidia.json +1 -1
  53. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openai.json +1 -1
  54. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode-go.json +1 -1
  55. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
  56. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
  57. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/together.json +1 -1
  58. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  59. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xai.json +1 -1
  60. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  61. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  62. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  63. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/xiaomi.json +1 -1
  64. package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
  65. package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +30 -30
  66. package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
  67. package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
  68. package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
  69. package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
  70. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
  71. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
  72. package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
  73. package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
  74. package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
  75. package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
  76. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
  77. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
  78. package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
  79. package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
  80. package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
  81. package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
  82. package/dist/utils/child-process.d.ts +5 -1
  83. package/dist/utils/child-process.js +9 -1
  84. package/dist/utils/owned-shell-processes.d.ts +23 -2
  85. package/dist/utils/owned-shell-processes.js +149 -39
  86. package/dist/utils/shell-process-identity.d.ts +11 -0
  87. package/dist/utils/shell-process-identity.js +61 -0
  88. package/dist/utils/shell.d.ts +1 -1
  89. package/dist/utils/shell.js +5 -5
  90. package/distribution-components.json +11 -11
  91. package/distribution-files.json +101 -85
  92. package/npm-shrinkwrap.json +2 -2
  93. package/package.json +1 -1
  94. package/release/distribution-manifest.json +4 -4
  95. package/release/install-package-lock.json +5 -5
  96. package/release/install-package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
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
+
31
+ ## [0.3.1] - 2026-09-20
32
+
33
+ ### New Features
34
+
35
+ - Added opt-in background services that survive CLI and subagent exit, with explicit environment ownership.
36
+ See [Background process lifetime](docs/background-processes.md) ([#242](https://github.com/diffexai/diffex/pull/242)).
37
+
38
+ ### Added
39
+
40
+ - Added CLI and SDK options for environment-owned background processes on Linux and macOS, with durable ownership records and inherited policy for child sessions.
41
+ See [Background process lifetime](docs/background-processes.md) ([#242](https://github.com/diffexai/diffex/pull/242)).
42
+
43
+ ### Fixed
44
+
45
+ - Fixed successful background services being terminated after ownership transfers to the environment, including on later failures, interruption, session replacement, and disposal ([#242](https://github.com/diffexai/diffex/pull/242)).
46
+ - Fixed print-mode shutdown to await shared runtime disposal and handle SIGINT cleanup ([#242](https://github.com/diffexai/diffex/pull/242)).
47
+
3
48
  ## [0.3.0] - 2026-09-20
4
49
 
5
50
  ### New Features
@@ -6,6 +6,8 @@ import type { ExtensionFlag } from "../core/extensions/types.ts";
6
6
  import type { TuiMode } from "../core/settings-manager.ts";
7
7
  export type Mode = "text" | "json" | "rpc";
8
8
  export interface Args {
9
+ backgroundProcessLifetime?: "session" | "environment";
10
+ backgroundProcessOwnershipDir?: string;
9
11
  provider?: string;
10
12
  model?: string;
11
13
  apiKey?: string;
package/dist/cli/args.js CHANGED
@@ -28,6 +28,29 @@ export function parseArgs(args) {
28
28
  result.mode = mode;
29
29
  }
30
30
  }
31
+ else if (arg === "--background-process-lifetime" || arg.startsWith("--background-process-lifetime=")) {
32
+ const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[i + 1];
33
+ if (!arg.includes("=") && value !== undefined && !value.startsWith("-"))
34
+ i++;
35
+ if (value === "session" || value === "environment")
36
+ result.backgroundProcessLifetime = value;
37
+ else
38
+ result.diagnostics.push({
39
+ type: "error",
40
+ message: "--background-process-lifetime requires session or environment",
41
+ });
42
+ }
43
+ else if (arg === "--background-process-ownership-dir" ||
44
+ arg.startsWith("--background-process-ownership-dir=")) {
45
+ const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[i + 1];
46
+ if (value && !value.startsWith("-")) {
47
+ result.backgroundProcessOwnershipDir = value;
48
+ if (!arg.includes("="))
49
+ i++;
50
+ }
51
+ else
52
+ result.diagnostics.push({ type: "error", message: "--background-process-ownership-dir requires a path" });
53
+ }
31
54
  else if (arg === "--continue" || arg === "-c") {
32
55
  result.continue = true;
33
56
  }
@@ -213,6 +236,18 @@ export function parseArgs(args) {
213
236
  result.messages.push(arg);
214
237
  }
215
238
  }
239
+ if (result.backgroundProcessLifetime === "environment" && !result.backgroundProcessOwnershipDir) {
240
+ result.diagnostics.push({
241
+ type: "error",
242
+ message: "Environment lifetime requires --background-process-ownership-dir",
243
+ });
244
+ }
245
+ if (result.backgroundProcessOwnershipDir && result.backgroundProcessLifetime !== "environment") {
246
+ result.diagnostics.push({
247
+ type: "error",
248
+ message: "--background-process-ownership-dir requires environment lifetime",
249
+ });
250
+ }
216
251
  return result;
217
252
  }
218
253
  export function printHelp(extensionFlags) {
@@ -248,6 +283,10 @@ ${chalk.bold("Options:")}
248
283
  --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)
249
284
  --mode <mode> Output mode: text (default), json, or rpc
250
285
  --print, -p Non-interactive mode: process prompt and exit
286
+ --background-process-lifetime <session|environment>
287
+ Background shell ownership (default: session)
288
+ --background-process-ownership-dir <path>
289
+ Supervisor handoff records (required for environment lifetime)
251
290
  --continue, -c Continue previous session
252
291
  --resume, -r Select a session to resume
253
292
  --session <path|id> Use specific session file or partial UUID
@@ -46,6 +46,7 @@ export interface CreateAgentSessionServicesOptions {
46
46
  * have been resolved against those services.
47
47
  */
48
48
  export interface CreateAgentSessionFromServicesOptions {
49
+ backgroundProcessPolicy?: CreateAgentSessionOptions["backgroundProcessPolicy"];
49
50
  services: AgentSessionServices;
50
51
  sessionManager: SessionManager;
51
52
  sessionStartEvent?: SessionStartEvent;
@@ -137,6 +137,7 @@ export async function createAgentSessionFromServices(options) {
137
137
  export async function createInternalAgentSessionFromServices(options, internalCustomTools = []) {
138
138
  registerSubagentRuntimeDiagnosticSink(options.sessionManager, options.services.diagnostics);
139
139
  return createInternalAgentSession({
140
+ backgroundProcessPolicy: options.backgroundProcessPolicy,
140
141
  cwd: options.services.cwd,
141
142
  agentDir: options.services.agentDir,
142
143
  modelRuntime: options.services.modelRuntime,
@@ -15,6 +15,7 @@
15
15
  import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type TelemetryContext, type ThinkingLevel } from "@diffexai/diffex-agent-core";
16
16
  import type { ImageContent, Model, TextContent } from "@diffexai/diffex-ai/compat";
17
17
  import type { HarnessStateStore, HarnessVersion, HarnessVersionBundle, HarnessVersionList, HarnessWorkspaceIdentity } from "@diffexai/diffex-harness-state";
18
+ import { type BackgroundProcessPolicy } from "../utils/owned-shell-processes.ts";
18
19
  import type { AgentSessionLifecycleObserver } from "./agent-session-lifecycle.ts";
19
20
  import { type BashResult } from "./bash-executor.ts";
20
21
  import { CHANGE_COLLABORATION_MODE, type CollaborationMode, type CollaborationModeTransitionResult, GET_COLLABORATION_MODE, IS_COLLABORATION_MODE_BUSY, SUBSCRIBE_PROMPT_ADMISSION } from "./collaboration-mode.ts";
@@ -135,6 +136,8 @@ export type AgentSessionEvent = Exclude<AgentEvent, {
135
136
  /** Listener function for agent session events */
136
137
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
137
138
  export interface AgentSessionConfig {
139
+ /** Lifetime of background groups launched by successful local shell commands. */
140
+ backgroundProcessPolicy?: BackgroundProcessPolicy;
138
141
  agent: Agent;
139
142
  sessionManager: SessionManager;
140
143
  settingsManager: SettingsManager;
@@ -250,7 +253,6 @@ export declare class AgentSession {
250
253
  private _followUpMessages;
251
254
  /** Messages queued to be included with the next user prompt as context ("asides"). */
252
255
  private _pendingNextTurnMessages;
253
- private _pendingCurrentPlanRestoration;
254
256
  private readonly _skillReferenceSnapshots;
255
257
  private readonly _skillEnablementOverrides;
256
258
  private _activeSkillTrace;
@@ -334,10 +336,6 @@ export declare class AgentSession {
334
336
  */
335
337
  private _installAgentToolHooks;
336
338
  private _installAgentNextTurnRefresh;
337
- private _installAgentContextRestoration;
338
- private _armInitialCurrentPlanRestoration;
339
- private _armCurrentPlanRestorationIfActive;
340
- private _consumePendingCurrentPlanRestoration;
341
339
  /** Emit an event to all listeners */
342
340
  private _emit;
343
341
  private _emitQueueUpdate;
@@ -565,6 +563,7 @@ export declare class AgentSession {
565
563
  private _abort;
566
564
  /** Stop and verify all shell processes owned by this session, including background jobs. */
567
565
  terminateOwnedShellProcesses(): Promise<void>;
566
+ get backgroundProcessPolicy(): BackgroundProcessPolicy;
568
567
  get ownedShellProcessCount(): number;
569
568
  waitForIdle(): Promise<void>;
570
569
  private _emitModelSelect;
@@ -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;
@@ -162,7 +159,7 @@ export class AgentSession {
162
159
  _bashAbortControllers = new Set();
163
160
  _pendingBashMessages = [];
164
161
  _ownedOperations = new Set();
165
- _shellProcesses = new OwnedShellProcesses();
162
+ _shellProcesses;
166
163
  _bashOperations = new Set();
167
164
  // Extension system
168
165
  _extensionRunner;
@@ -220,6 +217,7 @@ export class AgentSession {
220
217
  _agentRunAborted = false;
221
218
  _disposePromise;
222
219
  constructor(config) {
220
+ this._shellProcesses = new OwnedShellProcesses(config.backgroundProcessPolicy);
223
221
  this.agent = config.agent;
224
222
  this.sessionManager = config.sessionManager;
225
223
  this.settingsManager = config.settingsManager;
@@ -244,12 +242,10 @@ export class AgentSession {
244
242
  : createEvolvedSkillCatalog(config.harnessVersionBundle, config.workspaceIdentity));
245
243
  this._telemetryContext = config.telemetryContext ?? NOOP_TELEMETRY_CONTEXT;
246
244
  this._lifecycleObserver = config.lifecycleObserver;
247
- this._armInitialCurrentPlanRestoration();
248
245
  // Always subscribe to agent events for internal handling
249
246
  // (session persistence, extensions, auto-compaction, retry logic)
250
247
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
251
248
  this._installAgentToolHooks();
252
- this._installAgentContextRestoration();
253
249
  this._installAgentNextTurnRefresh();
254
250
  this._buildRuntime({
255
251
  activeToolNames: this._initialActiveToolNames,
@@ -433,55 +429,6 @@ export class AgentSession {
433
429
  };
434
430
  };
435
431
  }
436
- _installAgentContextRestoration() {
437
- const previousTransformContext = this.agent.transformContext;
438
- this.agent.transformContext = async (messages, signal) => {
439
- const transformedMessages = previousTransformContext
440
- ? await previousTransformContext(messages, signal)
441
- : messages;
442
- return this._consumePendingCurrentPlanRestoration(transformedMessages);
443
- };
444
- }
445
- _armInitialCurrentPlanRestoration() {
446
- if (this._sessionStartEvent.reason !== "startup" && this._sessionStartEvent.reason !== "resume") {
447
- return;
448
- }
449
- this._armCurrentPlanRestorationIfActive();
450
- }
451
- _armCurrentPlanRestorationIfActive() {
452
- if (resolveCurrentPlanState(this.sessionManager).status === "active") {
453
- this._pendingCurrentPlanRestoration = true;
454
- }
455
- }
456
- _consumePendingCurrentPlanRestoration(messages) {
457
- if (!this._pendingCurrentPlanRestoration) {
458
- return messages;
459
- }
460
- const state = resolveCurrentPlanState(this.sessionManager);
461
- this._pendingCurrentPlanRestoration = false;
462
- if (state.status !== "active") {
463
- return messages;
464
- }
465
- const lines = [
466
- "Restored unfinished update_plan checklist from the current session. Use this hidden context to continue the work.",
467
- ];
468
- if (state.operation.explanation !== undefined) {
469
- lines.push("", "Explanation:", state.operation.explanation);
470
- }
471
- lines.push("", "Checklist:");
472
- for (const [index, item] of state.operation.plan.entries()) {
473
- lines.push(`${index + 1}. [${item.status}] ${item.step}`);
474
- }
475
- lines.push("", CURRENT_PLAN_RESTORATION_REMINDER);
476
- const restoredContext = {
477
- role: "custom",
478
- customType: CURRENT_PLAN_RESTORATION_CUSTOM_TYPE,
479
- content: [{ type: "text", text: lines.join("\n") }],
480
- display: false,
481
- timestamp: Date.now(),
482
- };
483
- return [...messages, restoredContext];
484
- }
485
432
  // =========================================================================
486
433
  // Event Subscription
487
434
  // =========================================================================
@@ -1808,7 +1755,6 @@ export class AgentSession {
1808
1755
  return;
1809
1756
  pendingCompaction.commit();
1810
1757
  pendingCompaction = undefined;
1811
- this._armCurrentPlanRestorationIfActive();
1812
1758
  };
1813
1759
  const rollbackPendingCompaction = () => {
1814
1760
  if (!pendingCompaction)
@@ -2401,6 +2347,9 @@ export class AgentSession {
2401
2347
  async terminateOwnedShellProcesses() {
2402
2348
  await this._shellProcesses.terminateAll();
2403
2349
  }
2350
+ get backgroundProcessPolicy() {
2351
+ return this._shellProcesses.policy;
2352
+ }
2404
2353
  get ownedShellProcessCount() {
2405
2354
  return this._shellProcesses.count;
2406
2355
  }
@@ -2725,7 +2674,8 @@ export class AgentSession {
2725
2674
  this._branchSummaryAbortController?.abort();
2726
2675
  }
2727
2676
  async _recordSuccessfulCompaction(options) {
2728
- 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);
2729
2679
  const sessionContext = this.sessionManager.buildSessionContext();
2730
2680
  this.agent.state.messages = sessionContext.messages;
2731
2681
  const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
@@ -2759,15 +2709,13 @@ export class AgentSession {
2759
2709
  else
2760
2710
  transaction.commit();
2761
2711
  const compactionResult = {
2762
- summary: options.summary,
2712
+ summary,
2763
2713
  firstKeptEntryId: options.firstKeptEntryId,
2764
2714
  tokensBefore: options.tokensBefore,
2765
2715
  estimatedTokensAfter,
2766
2716
  usage: options.usage,
2767
2717
  details: options.details,
2768
2718
  };
2769
- if (!options.deferCommit)
2770
- this._armCurrentPlanRestorationIfActive();
2771
2719
  return compactionResult;
2772
2720
  }
2773
2721
  /**
@@ -3755,7 +3703,8 @@ export class AgentSession {
3755
3703
  let summaryEntry;
3756
3704
  const mutateLeaf = () => {
3757
3705
  if (summaryText) {
3758
- 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);
3759
3708
  summaryEntry = this.sessionManager.getEntry(summaryId);
3760
3709
  if (label)
3761
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
+ }
@@ -1,6 +1,7 @@
1
1
  import { type TelemetryContext, type ThinkingLevel } from "@diffexai/diffex-agent-core";
2
2
  import { type Model } from "@diffexai/diffex-ai/compat";
3
3
  import type { HarnessStateStore, HarnessVersionBundle } from "@diffexai/diffex-harness-state";
4
+ import type { BackgroundProcessPolicy } from "../utils/owned-shell-processes.ts";
4
5
  import { AgentSession } from "./agent-session.ts";
5
6
  import type { AgentSessionLifecycleObserver } from "./agent-session-lifecycle.ts";
6
7
  import type { LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
@@ -15,6 +16,8 @@ export interface SubagentOptions extends SubagentSupervisionOptions {
15
16
  maxConcurrent?: number;
16
17
  }
17
18
  export interface CreateAgentSessionOptions {
19
+ /** Local shell background lifetime. Defaults to session-owned cleanup. */
20
+ backgroundProcessPolicy?: BackgroundProcessPolicy;
18
21
  /** Working directory for project-local discovery. Default: process.cwd() */
19
22
  cwd?: string;
20
23
  /** Global config directory. Default: ~/.diffex/agent */
package/dist/core/sdk.js CHANGED
@@ -436,6 +436,7 @@ export async function createInternalAgentSession(options = {}, internalCustomToo
436
436
  sessionManager.appendThinkingLevelChange(thinkingLevel);
437
437
  }
438
438
  const session = new AgentSession({
439
+ backgroundProcessPolicy: options.backgroundProcessPolicy,
439
440
  agent,
440
441
  sessionManager,
441
442
  settingsManager,