@diffexai/diffex 0.2.7 → 0.3.1

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 (63) hide show
  1. package/CHANGELOG.md +34 -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 +9 -0
  7. package/dist/core/agent-session.js +62 -8
  8. package/dist/core/compaction/compaction.js +3 -1
  9. package/dist/core/sdk.d.ts +5 -1
  10. package/dist/core/sdk.js +19 -6
  11. package/dist/core/subagents/controller.d.ts +4 -3
  12. package/dist/core/subagents/controller.js +173 -37
  13. package/dist/core/subagents/services.d.ts +2 -0
  14. package/dist/core/subagents/services.js +2 -0
  15. package/dist/core/subagents/types.d.ts +13 -0
  16. package/dist/core/tools/bash.d.ts +4 -0
  17. package/dist/core/tools/bash.js +55 -11
  18. package/dist/core/tools/subagents.d.ts +12 -10
  19. package/dist/core/tools/subagents.js +61 -49
  20. package/dist/index.d.ts +1 -0
  21. package/dist/main.js +4 -0
  22. package/dist/modes/print-mode.js +16 -13
  23. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
  24. package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
  25. package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -1
  26. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
  27. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode-go.json +1 -1
  28. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/opencode.json +1 -1
  29. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
  30. package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  31. package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
  32. package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +6 -6
  33. package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -1
  34. package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
  35. package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
  36. package/dist/node_modules/@diffexai/diffex-client/package.json +1 -1
  37. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
  38. package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
  39. package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -1
  40. package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
  41. package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
  42. package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -1
  43. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
  44. package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
  45. package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -1
  46. package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
  47. package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
  48. package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -1
  49. package/dist/utils/child-process.d.ts +5 -1
  50. package/dist/utils/child-process.js +9 -1
  51. package/dist/utils/owned-shell-processes.d.ts +29 -0
  52. package/dist/utils/owned-shell-processes.js +171 -0
  53. package/dist/utils/shell-process-identity.d.ts +11 -0
  54. package/dist/utils/shell-process-identity.js +61 -0
  55. package/dist/utils/shell.d.ts +1 -1
  56. package/dist/utils/shell.js +5 -5
  57. package/distribution-components.json +11 -11
  58. package/distribution-files.json +68 -52
  59. package/npm-shrinkwrap.json +2 -2
  60. package/package.json +1 -1
  61. package/release/distribution-manifest.json +4 -4
  62. package/release/install-package-lock.json +5 -5
  63. package/release/install-package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2026-09-20
4
+
5
+ ### New Features
6
+
7
+ - Added opt-in background services that survive CLI and subagent exit, with explicit environment ownership.
8
+ See [Background process lifetime](docs/background-processes.md) ([#242](https://github.com/diffexai/diffex/pull/242)).
9
+
10
+ ### Added
11
+
12
+ - Added CLI and SDK options for environment-owned background processes on Linux and macOS, with durable ownership records and inherited policy for child sessions.
13
+ See [Background process lifetime](docs/background-processes.md) ([#242](https://github.com/diffexai/diffex/pull/242)).
14
+
15
+ ### Fixed
16
+
17
+ - 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)).
18
+ - Fixed print-mode shutdown to await shared runtime disposal and handle SIGINT cleanup ([#242](https://github.com/diffexai/diffex/pull/242)).
19
+
20
+ ## [0.3.0] - 2026-09-20
21
+
22
+ ### New Features
23
+
24
+ - Added nonblocking `agent_status` for immediate child-status and result inspection, with automatic coordination for child completion. See [Automatic Sub-agent Coordination](docs/usage.md#automatic-sub-agent-coordination) ([#234](https://github.com/diffexai/diffex/pull/234)).
25
+ - Added automatic subagent supervision with renewable work allowances and parent-deadline bounds. See [SDK tools](docs/sdk.md#tools) ([#233](https://github.com/diffexai/diffex/pull/233)).
26
+
27
+ ### Breaking Changes
28
+
29
+ - Replaced the blocking `wait_for_agents` tool with nonblocking `agent_status`; rely on automatic child-result coordination instead of tool-managed waits ([#234](https://github.com/diffexai/diffex/pull/234)).
30
+
31
+ ### Fixed
32
+
33
+ - Fixed automatic compaction to check complete tool-result batches ([#233](https://github.com/diffexai/diffex/pull/233)).
34
+ - Fixed subagent completion and cancellation, including renewable work-allowance supervision ([#233](https://github.com/diffexai/diffex/pull/233)).
35
+ - Fixed session and subagent cancellation to terminate owned background shell processes ([#233](https://github.com/diffexai/diffex/pull/233)).
36
+
3
37
  ## [0.2.7] - 2026-09-18
4
38
 
5
39
  ### Fixed
@@ -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;
@@ -263,6 +266,8 @@ export declare class AgentSession {
263
266
  private readonly _bashAbortControllers;
264
267
  private _pendingBashMessages;
265
268
  private readonly _ownedOperations;
269
+ private readonly _shellProcesses;
270
+ private readonly _bashOperations;
266
271
  private _extensionRunner;
267
272
  private _turnIndex;
268
273
  private _resourceLoader;
@@ -561,6 +566,10 @@ export declare class AgentSession {
561
566
  */
562
567
  abort(): Promise<void>;
563
568
  private _abort;
569
+ /** Stop and verify all shell processes owned by this session, including background jobs. */
570
+ terminateOwnedShellProcesses(): Promise<void>;
571
+ get backgroundProcessPolicy(): BackgroundProcessPolicy;
572
+ get ownedShellProcessCount(): number;
564
573
  waitForIdle(): Promise<void>;
565
574
  private _emitModelSelect;
566
575
  /**
@@ -18,6 +18,7 @@ import { NOOP_TELEMETRY_CONTEXT, uuidv7, } from "@diffexai/diffex-agent-core";
18
18
  import { contentText } from "@diffexai/diffex-ai";
19
19
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@diffexai/diffex-ai/compat";
20
20
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
21
+ import { OwnedShellProcesses } from "../utils/owned-shell-processes.js";
21
22
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
22
23
  import { sleep } from "../utils/sleep.js";
23
24
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
@@ -161,6 +162,8 @@ export class AgentSession {
161
162
  _bashAbortControllers = new Set();
162
163
  _pendingBashMessages = [];
163
164
  _ownedOperations = new Set();
165
+ _shellProcesses;
166
+ _bashOperations = new Set();
164
167
  // Extension system
165
168
  _extensionRunner;
166
169
  _turnIndex = 0;
@@ -217,6 +220,7 @@ export class AgentSession {
217
220
  _agentRunAborted = false;
218
221
  _disposePromise;
219
222
  constructor(config) {
223
+ this._shellProcesses = new OwnedShellProcesses(config.backgroundProcessPolicy);
220
224
  this.agent = config.agent;
221
225
  this.sessionManager = config.sessionManager;
222
226
  this.settingsManager = config.settingsManager;
@@ -402,10 +406,25 @@ export class AgentSession {
402
406
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
403
407
  const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
404
408
  const previousContext = previousSnapshot?.context ?? turn.context;
409
+ let messages = previousContext.messages;
410
+ // Tool output is part of the next request even though the model's usage
411
+ // only describes the request before that output. Check every complete batch.
412
+ if (turn.toolResults.length > 0 && !signal?.aborted && this.model) {
413
+ const settings = this.settingsManager.getCompactionSettings();
414
+ if (shouldCompact(estimateContextTokens(messages).tokens, this.model.contextWindow, settings)) {
415
+ const beforeCompaction = this.agent.state.messages;
416
+ await this._runAutoCompaction("threshold", true, signal);
417
+ if (this.agent.state.messages !== beforeCompaction) {
418
+ messages = this.agent.state.messages;
419
+ }
420
+ }
421
+ }
422
+ signal?.throwIfAborted();
405
423
  return {
406
424
  ...previousSnapshot,
407
425
  context: {
408
426
  ...previousContext,
427
+ messages,
409
428
  systemPrompt: this._activeRunCollaborationState?.effectiveSystemPrompt ??
410
429
  this._deriveEffectivePrompt(this._systemPromptOverride ?? this._baseSystemPrompt, this._activeRunCollaborationMode ?? this._collaborationMode),
411
430
  tools: (this._activeRunCollaborationState?.tools ?? this.agent.state.tools).slice(),
@@ -836,7 +855,7 @@ export class AgentSession {
836
855
  failures.push(error);
837
856
  }
838
857
  const ownedOperationsIdle = this._waitForOwnedOperations();
839
- const cleanupResults = await Promise.allSettled([childCleanup, rootIdle, ownedOperationsIdle].filter((promise) => promise !== undefined));
858
+ const cleanupResults = await Promise.allSettled([childCleanup, rootIdle, ownedOperationsIdle, this.terminateOwnedShellProcesses()].filter((promise) => promise !== undefined));
840
859
  for (const result of cleanupResults) {
841
860
  if (result.status !== "rejected")
842
861
  continue;
@@ -1651,6 +1670,22 @@ export class AgentSession {
1651
1670
  if (coordination.exitReason === "cancellation" || this._agentRunAborted) {
1652
1671
  this._exitSubagentCoordination("cancellation");
1653
1672
  }
1673
+ else if (!coordination.completionChecked) {
1674
+ coordination.completionChecked = true;
1675
+ const active = this.getSubagents().filter((child) => coordination.names.includes(child.name) && (child.status === "running" || child.status === "queued"));
1676
+ if (active.length > 0) {
1677
+ await this.sendCustomMessage({
1678
+ customType: "subagent_completion_check",
1679
+ content: JSON.stringify({
1680
+ type: "subagent_completion_check",
1681
+ agents: active,
1682
+ instruction: "Review active children before finishing: cancel obsolete work with agent_action interrupt. If needed work remains, end your response to wait automatically. Renew only justified work.",
1683
+ }),
1684
+ display: false,
1685
+ }, { deliverAs: "steer" });
1686
+ return "continue";
1687
+ }
1688
+ }
1654
1689
  const delivery = await this._waitForSubagentCoordination(coordination);
1655
1690
  if (delivery.reason === "cancellation")
1656
1691
  return "settled";
@@ -2354,11 +2389,24 @@ export class AgentSession {
2354
2389
  }
2355
2390
  async _abort() {
2356
2391
  this.abortRetry();
2392
+ this.abortCompaction();
2393
+ this.abortBranchSummary();
2394
+ this.abortBash();
2357
2395
  if (this._isAgentRunActive)
2358
2396
  this._agentRunAborted = true;
2359
2397
  this._exitSubagentCoordination("cancellation");
2360
2398
  this.agent.abort();
2361
- await this.waitForIdle();
2399
+ await Promise.all([this.waitForIdle(), this.terminateOwnedShellProcesses(), ...this._bashOperations]);
2400
+ }
2401
+ /** Stop and verify all shell processes owned by this session, including background jobs. */
2402
+ async terminateOwnedShellProcesses() {
2403
+ await this._shellProcesses.terminateAll();
2404
+ }
2405
+ get backgroundProcessPolicy() {
2406
+ return this._shellProcesses.policy;
2407
+ }
2408
+ get ownedShellProcessCount() {
2409
+ return this._shellProcesses.count;
2362
2410
  }
2363
2411
  async waitForIdle() {
2364
2412
  if (this.isIdle) {
@@ -2847,6 +2895,9 @@ export class AgentSession {
2847
2895
  return false;
2848
2896
  }
2849
2897
  this._autoCompactionAbortController = new AbortController();
2898
+ const compactionSignal = signal
2899
+ ? AbortSignal.any([signal, this._autoCompactionAbortController.signal])
2900
+ : this._autoCompactionAbortController.signal;
2850
2901
  this._emit({ type: "compaction_start", reason });
2851
2902
  started = true;
2852
2903
  let extensionCompaction;
@@ -2859,7 +2910,7 @@ export class AgentSession {
2859
2910
  customInstructions: undefined,
2860
2911
  reason,
2861
2912
  willRetry,
2862
- signal: this._autoCompactionAbortController.signal,
2913
+ signal: compactionSignal,
2863
2914
  }));
2864
2915
  if (extensionResult?.cancel) {
2865
2916
  this._emit({
@@ -2891,14 +2942,14 @@ export class AgentSession {
2891
2942
  }
2892
2943
  else {
2893
2944
  // Generate compaction result
2894
- const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }, telemetryContext), telemetryContext);
2945
+ const compactResult = await compact(preparation, requestModel, apiKey, headers, undefined, compactionSignal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }, telemetryContext), telemetryContext);
2895
2946
  summary = compactResult.summary;
2896
2947
  firstKeptEntryId = compactResult.firstKeptEntryId;
2897
2948
  tokensBefore = compactResult.tokensBefore;
2898
2949
  usage = compactResult.usage;
2899
2950
  details = compactResult.details;
2900
2951
  }
2901
- if (this._autoCompactionAbortController.signal.aborted) {
2952
+ if (compactionSignal.aborted) {
2902
2953
  this._emit({
2903
2954
  type: "compaction_end",
2904
2955
  reason,
@@ -3257,7 +3308,7 @@ export class AgentSession {
3257
3308
  : {
3258
3309
  ...createAllToolDefinitions(this._cwd, {
3259
3310
  read: { autoResizeImages },
3260
- bash: { commandPrefix: shellCommandPrefix, shellPath },
3311
+ bash: { commandPrefix: shellCommandPrefix, shellPath, owner: this._shellProcesses },
3261
3312
  }),
3262
3313
  update_plan: createSessionUpdatePlanToolDefinition(this.sessionManager),
3263
3314
  };
@@ -3458,7 +3509,7 @@ export class AgentSession {
3458
3509
  * @param options.operations Custom BashOperations for remote execution
3459
3510
  */
3460
3511
  executeBash(command, onChunk, options) {
3461
- return this._runOwnedOperation(() => this._traceOperation("bash", () => startCodingAgentSpan(this.agent.telemetryContext, "diffex.coding_agent.terminal", { "diffex.session.id": this.sessionId }, async (span) => {
3512
+ const operation = this._runOwnedOperation(() => this._traceOperation("bash", () => startCodingAgentSpan(this.agent.telemetryContext, "diffex.coding_agent.terminal", { "diffex.session.id": this.sessionId }, async (span) => {
3462
3513
  const result = await this._executeBash(command, onChunk, options);
3463
3514
  const outcome = result.cancelled ? "cancelled" : result.exitCode === 0 ? "completed" : "failed";
3464
3515
  span.setAttributes({
@@ -3469,6 +3520,9 @@ export class AgentSession {
3469
3520
  span.setStatus({ status: "error" });
3470
3521
  return result;
3471
3522
  })));
3523
+ this._bashOperations.add(operation);
3524
+ void operation.then(() => this._bashOperations.delete(operation), () => this._bashOperations.delete(operation));
3525
+ return operation;
3472
3526
  }
3473
3527
  async _executeBash(command, onChunk, options) {
3474
3528
  const abortController = new AbortController();
@@ -3478,7 +3532,7 @@ export class AgentSession {
3478
3532
  const shellPath = this.settingsManager.getShellPath();
3479
3533
  const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
3480
3534
  try {
3481
- const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath }), {
3535
+ const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations({ shellPath, owner: this._shellProcesses }), {
3482
3536
  onChunk: (delta) => {
3483
3537
  onChunk?.(delta);
3484
3538
  this._emit({ type: "bash_execution_update", id: options?.id, delta });
@@ -322,7 +322,9 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
322
322
  accumulatedTokens += messageTokens;
323
323
  // Check if we've exceeded the budget
324
324
  if (accumulatedTokens >= keepRecentTokens) {
325
- // Find the closest valid cut point at or after this entry
325
+ // A completed tool batch can end the transcript. If no cut point follows
326
+ // its results, keep the batch's assistant message and all paired results.
327
+ cutIndex = cutPoints[cutPoints.length - 1];
326
328
  for (let c = 0; c < cutPoints.length; c++) {
327
329
  if (cutPoints[c] >= i) {
328
330
  cutIndex = cutPoints[c];
@@ -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";
@@ -8,12 +9,15 @@ import { ModelRuntime } from "./model-runtime.ts";
8
9
  import type { ResourceLoader } from "./resource-loader.ts";
9
10
  import { SessionManager } from "./session-manager.ts";
10
11
  import { SettingsManager } from "./settings-manager.ts";
12
+ import type { SubagentSupervisionOptions } from "./subagents/types.ts";
11
13
  import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createUpdatePlanTool, createWriteTool, withFileMutationQueue } from "./tools/index.ts";
12
- export interface SubagentOptions {
14
+ export interface SubagentOptions extends SubagentSupervisionOptions {
13
15
  /** Maximum number of child agents that may run concurrently. Default: 4 */
14
16
  maxConcurrent?: number;
15
17
  }
16
18
  export interface CreateAgentSessionOptions {
19
+ /** Local shell background lifetime. Defaults to session-owned cleanup. */
20
+ backgroundProcessPolicy?: BackgroundProcessPolicy;
17
21
  /** Working directory for project-local discovery. Default: process.cwd() */
18
22
  cwd?: string;
19
23
  /** Global config directory. Default: ~/.diffex/agent */
package/dist/core/sdk.js CHANGED
@@ -29,7 +29,7 @@ import { createBashTool, createCodingTools, createEditTool, createFindTool, crea
29
29
  // provider-agnostic and does not import Diffex AI compat itself.
30
30
  setDefaultStreamFn(streamSimple);
31
31
  const DEFAULT_SUBAGENT_CONCURRENCY = 4;
32
- const RESERVED_SUBAGENT_TOOL_NAMES = new Set(["spawn_agent", "agent_action", "wait_for_agents", "escalate_to_parent"]);
32
+ const RESERVED_SUBAGENT_TOOL_NAMES = new Set(["spawn_agent", "agent_action", "agent_status", "escalate_to_parent"]);
33
33
  // Re-exports
34
34
  export * from "./agent-session-runtime.js";
35
35
  export { withFileMutationQueue,
@@ -48,7 +48,7 @@ function normalizeSubagentOptions(value) {
48
48
  const options = value;
49
49
  if (options) {
50
50
  for (const key of Object.keys(options)) {
51
- if (key !== "maxConcurrent") {
51
+ if (!["maxConcurrent", "leaseMs", "supervisionIntervalMs", "parentDeadline"].includes(key)) {
52
52
  throw new Error(`Unknown subagents option: ${key}`);
53
53
  }
54
54
  }
@@ -57,7 +57,19 @@ function normalizeSubagentOptions(value) {
57
57
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
58
58
  throw new RangeError("subagents.maxConcurrent must be a positive integer");
59
59
  }
60
- return { maxConcurrent };
60
+ return {
61
+ maxConcurrent,
62
+ leaseMs: options?.leaseMs ??
63
+ (process.env.DIFFEX_SUBAGENT_LEASE_MS === undefined
64
+ ? undefined
65
+ : Number(process.env.DIFFEX_SUBAGENT_LEASE_MS)),
66
+ supervisionIntervalMs: options?.supervisionIntervalMs ??
67
+ (process.env.DIFFEX_SUBAGENT_SUPERVISION_MS === undefined
68
+ ? undefined
69
+ : Number(process.env.DIFFEX_SUBAGENT_SUPERVISION_MS)),
70
+ parentDeadline: options?.parentDeadline ??
71
+ (process.env.DIFFEX_TASK_DEADLINE_MS === undefined ? undefined : Number(process.env.DIFFEX_TASK_DEADLINE_MS)),
72
+ };
61
73
  }
62
74
  function assertNoReservedSubagentTools(customTools) {
63
75
  for (const tool of customTools ?? []) {
@@ -66,10 +78,10 @@ function assertNoReservedSubagentTools(customTools) {
66
78
  }
67
79
  }
68
80
  }
69
- function createRootSubagentController(maxConcurrent, agentDir, telemetryContext, parentSessionManager) {
81
+ function createRootSubagentController(options, agentDir, telemetryContext, parentSessionManager) {
70
82
  const sessionManagerFactory = new RootLocalEphemeralSubagentSessionManagerFactory();
71
83
  return new SubagentController({
72
- maxConcurrent,
84
+ ...options,
73
85
  telemetryContext,
74
86
  restoreRoster: (controller, parent) => restoreRootSubagents(controller, parent, agentDir, parentSessionManager),
75
87
  captureSession: (parent, childName, origin) => {
@@ -239,7 +251,7 @@ export async function createInternalAgentSession(options = {}, internalCustomToo
239
251
  const sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));
240
252
  const subagentController = subagentOptions === false
241
253
  ? undefined
242
- : createRootSubagentController(subagentOptions.maxConcurrent, agentDir, telemetryContext, sessionManager);
254
+ : createRootSubagentController(subagentOptions, agentDir, telemetryContext, sessionManager);
243
255
  const customTools = [
244
256
  ...(options.customTools ?? []),
245
257
  ...(subagentController?.createToolDefinitions() ?? []),
@@ -424,6 +436,7 @@ export async function createInternalAgentSession(options = {}, internalCustomToo
424
436
  sessionManager.appendThinkingLevelChange(thinkingLevel);
425
437
  }
426
438
  const session = new AgentSession({
439
+ backgroundProcessPolicy: options.backgroundProcessPolicy,
427
440
  agent,
428
441
  sessionManager,
429
442
  settingsManager,
@@ -2,14 +2,14 @@ import { type TelemetryContext } from "@diffexai/diffex-agent-core";
2
2
  import type { AgentSession } from "../agent-session.ts";
3
3
  import type { ToolDefinition } from "../extensions/types.ts";
4
4
  import type { ChildSnapshotV1 } from "./persistence.ts";
5
- import type { CapturedSubagentSession, CaptureSubagentSession, CreateSubagentSession, SubagentActionResult, SubagentBatchResult, SubagentBatchWaitOptions, SubagentControllerSnapshot, SubagentDeliveryBatch, SubagentLifecycleListener, SubagentPermit, SubagentQueryOptions, SubagentQueryResult, SubagentSpawnOrigin, SubagentSummary, SubagentWaitOptions, SubagentWaitResult } from "./types.ts";
5
+ import type { CapturedSubagentSession, CaptureSubagentSession, CreateSubagentSession, SubagentActionResult, SubagentBatchResult, SubagentBatchWaitOptions, SubagentControllerSnapshot, SubagentDeliveryBatch, SubagentLifecycleListener, SubagentPermit, SubagentQueryOptions, SubagentQueryResult, SubagentSpawnOrigin, SubagentSummary, SubagentSupervisionOptions, SubagentWaitOptions, SubagentWaitResult } from "./types.ts";
6
6
  export interface PreparedSubagentWake extends CapturedSubagentSession {
7
7
  readonly warning?: string;
8
8
  }
9
9
  export type PrepareSubagentWake = () => Promise<PreparedSubagentWake>;
10
10
  export declare class SubagentController {
11
11
  #private;
12
- constructor(options: {
12
+ constructor(options: SubagentSupervisionOptions & {
13
13
  maxConcurrent: number;
14
14
  captureSession?: CaptureSubagentSession;
15
15
  telemetryContext?: TelemetryContext;
@@ -23,8 +23,9 @@ export declare class SubagentController {
23
23
  restore(snapshot: ChildSnapshotV1, prepareWake: PrepareSubagentWake, isolationError?: string): void;
24
24
  restoreIsolated(name: string, error: string, prepareWake: PrepareSubagentWake): void;
25
25
  spawn(name: string, task: string, createSession: CreateSubagentSession, origin?: SubagentSpawnOrigin, telemetryContext?: TelemetryContext, captured?: CapturedSubagentSession): void;
26
- message(name: string, message: string, wake?: boolean, coordinated?: boolean): Promise<SubagentActionResult>;
26
+ message(name: string, message: string, wake?: boolean, coordinated?: boolean, delivery?: "steer" | "follow_up"): Promise<SubagentActionResult>;
27
27
  interrupt(name: string): Promise<SubagentActionResult>;
28
+ renew(name: string, reason: string): Promise<SubagentActionResult>;
28
29
  attachSession(name: string, session: AgentSession): void;
29
30
  trackRun(name: string, run: Promise<void>): void;
30
31
  markRunning(name: string): void;