opencode-matrixx 2.6.6 → 2.6.7

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 (37) hide show
  1. package/dist/cli.js +51 -4
  2. package/dist/config/schema/background-task.d.ts +37 -0
  3. package/dist/config/schema/dcp.d.ts +46 -0
  4. package/dist/config/schema/matrixx-config.d.ts +32 -0
  5. package/dist/features/background-agent/admission.d.ts +24 -0
  6. package/dist/features/background-agent/concurrency.d.ts +9 -0
  7. package/dist/features/background-agent/constants.d.ts +13 -0
  8. package/dist/features/background-agent/handle-index.d.ts +12 -0
  9. package/dist/features/background-agent/manager.d.ts +59 -4
  10. package/dist/features/background-agent/notification-builder.d.ts +1 -0
  11. package/dist/features/background-agent/reconcile.d.ts +33 -0
  12. package/dist/features/background-agent/revive.d.ts +39 -0
  13. package/dist/features/background-agent/session-output.d.ts +40 -0
  14. package/dist/features/background-agent/task-history.d.ts +20 -0
  15. package/dist/features/background-agent/types.d.ts +21 -1
  16. package/dist/features/background-agent/wake-scheduler.d.ts +47 -0
  17. package/dist/features/background-agent/wallclock.d.ts +37 -0
  18. package/dist/features/bdd/schema.d.ts +5 -5
  19. package/dist/hooks/context-mode-enforcer/constants.d.ts +1 -1
  20. package/dist/index.js +1909 -716
  21. package/dist/matrixx.schema.json +165 -3
  22. package/dist/plugin-handlers/agent-config-handler.d.ts +2 -1
  23. package/dist/shared/context-mode-enforcement.d.ts +35 -0
  24. package/dist/shared/index.d.ts +1 -1
  25. package/dist/shared/revive-outcome.d.ts +15 -0
  26. package/dist/shared/saturated-outcome.d.ts +18 -0
  27. package/dist/shared/wallclock-outcome.d.ts +18 -0
  28. package/dist/tools/background-task/constants.d.ts +1 -0
  29. package/dist/tools/background-task/create-background-revive.d.ts +3 -0
  30. package/dist/tools/background-task/index.d.ts +1 -1
  31. package/dist/tools/background-task/tools.d.ts +1 -0
  32. package/dist/tools/background-task/types.d.ts +9 -0
  33. package/dist/tools/dcp-switch-profile/tools.d.ts +1 -0
  34. package/dist/tools/index.d.ts +1 -1
  35. package/package.json +1 -1
  36. package/dist/shared/context-mode-prompt.d.ts +0 -12
  37. package/dist/shared/context-mode-prompt.test.d.ts +0 -1
package/dist/index.js CHANGED
@@ -4550,14 +4550,14 @@ __export(exports_storage, {
4550
4550
  readState: () => readState,
4551
4551
  writeState: () => writeState
4552
4552
  });
4553
- import { existsSync as existsSync34, mkdirSync as mkdirSync13, readFileSync as readFileSync28, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
4554
- import { dirname as dirname9, join as join43 } from "path";
4553
+ import { existsSync as existsSync35, mkdirSync as mkdirSync13, readFileSync as readFileSync28, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
4554
+ import { dirname as dirname9, join as join44 } from "path";
4555
4555
  function getStateFilePath(directory, customPath) {
4556
- return customPath ? join43(directory, customPath) : join43(directory, DEFAULT_STATE_FILE);
4556
+ return customPath ? join44(directory, customPath) : join44(directory, DEFAULT_STATE_FILE);
4557
4557
  }
4558
4558
  function readState(directory, customPath) {
4559
4559
  const filePath = getStateFilePath(directory, customPath);
4560
- if (!existsSync34(filePath)) {
4560
+ if (!existsSync35(filePath)) {
4561
4561
  return null;
4562
4562
  }
4563
4563
  try {
@@ -4597,7 +4597,7 @@ function writeState(directory, state3, customPath) {
4597
4597
  const filePath = getStateFilePath(directory, customPath);
4598
4598
  try {
4599
4599
  const dir = dirname9(filePath);
4600
- if (!existsSync34(dir)) {
4600
+ if (!existsSync35(dir)) {
4601
4601
  mkdirSync13(dir, { recursive: true });
4602
4602
  }
4603
4603
  const sessionIdLine = state3.session_id ? `session_id: "${state3.session_id}"
@@ -4624,7 +4624,7 @@ ${state3.prompt}
4624
4624
  function clearState(directory, customPath) {
4625
4625
  const filePath = getStateFilePath(directory, customPath);
4626
4626
  try {
4627
- if (existsSync34(filePath)) {
4627
+ if (existsSync35(filePath)) {
4628
4628
  unlinkSync7(filePath);
4629
4629
  }
4630
4630
  return true;
@@ -30087,7 +30087,7 @@ Write final summary to \`.matrixx/slop-scan-<timestamp>.md\`.
30087
30087
  });
30088
30088
 
30089
30089
  // src/hooks/agent-usage-reminder/constants.ts
30090
- import { join as join15 } from "path";
30090
+ import { join as join16 } from "path";
30091
30091
 
30092
30092
  // src/shared/agent-tool-restrictions.ts
30093
30093
  var EXPLORATION_AGENT_DENYLIST = {
@@ -45003,6 +45003,73 @@ async function updateConnectedProvidersCache(client) {
45003
45003
  log("[connected-providers-cache] Error updating cache", { error: String(err) });
45004
45004
  }
45005
45005
  }
45006
+ // src/shared/context-mode-enforcement.ts
45007
+ import { existsSync as existsSync3 } from "fs";
45008
+ import { homedir as homedir2 } from "os";
45009
+ import { join as join5 } from "path";
45010
+ var CONTEXT_MODE_DEFAULT_BLOCKED_TOOLS = ["grep", "glob"];
45011
+ function resolveContextModeEnforcement(contextMode) {
45012
+ return {
45013
+ enabled: contextMode?.enabled ?? true,
45014
+ enforce: contextMode?.enforce ?? false,
45015
+ blockedTools: (contextMode?.blocked_tools ?? [...CONTEXT_MODE_DEFAULT_BLOCKED_TOOLS]).map((t) => t.toLowerCase())
45016
+ };
45017
+ }
45018
+ function hasGrepGlobToolNames(toolNames) {
45019
+ return toolNames.some((n) => n === "grep" || n === "glob");
45020
+ }
45021
+ function resolveGrepGlobUsable(toolNames, contextMode) {
45022
+ if (!hasGrepGlobToolNames(toolNames))
45023
+ return false;
45024
+ const cfg = contextMode !== undefined ? contextMode : _contextModeForPrompts;
45025
+ return !willBlockGrepGlob(cfg);
45026
+ }
45027
+ var _disciplinePathOverride;
45028
+ var _contextModeForPrompts;
45029
+ function setContextModeForPrompts(contextMode) {
45030
+ _contextModeForPrompts = contextMode;
45031
+ }
45032
+ function getContextModeForPrompts() {
45033
+ return _contextModeForPrompts;
45034
+ }
45035
+ function willBlockGrepGlob(contextMode) {
45036
+ const { enforce, blockedTools } = resolveContextModeEnforcement(contextMode);
45037
+ if (!enforce)
45038
+ return false;
45039
+ if (!(blockedTools.includes("grep") || blockedTools.includes("glob")))
45040
+ return false;
45041
+ return hasWorkingSubstitute();
45042
+ }
45043
+ function resolveContextModeDisciplinePath() {
45044
+ if (_disciplinePathOverride !== undefined)
45045
+ return _disciplinePathOverride;
45046
+ try {
45047
+ const resolved = __require.resolve("context-mode/configs/opencode/AGENTS.md");
45048
+ if (existsSync3(resolved))
45049
+ return resolved;
45050
+ } catch {}
45051
+ try {
45052
+ const cacheDir = getOpenCodeCacheDir();
45053
+ const p = join5(cacheDir, "packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
45054
+ if (existsSync3(p))
45055
+ return p;
45056
+ } catch {}
45057
+ try {
45058
+ const base = process.env.XDG_CACHE_HOME ?? join5(homedir2(), ".cache");
45059
+ const p = join5(base, "opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
45060
+ if (existsSync3(p))
45061
+ return p;
45062
+ } catch {}
45063
+ try {
45064
+ const p2 = join5(homedir2(), ".cache/opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
45065
+ if (existsSync3(p2))
45066
+ return p2;
45067
+ } catch {}
45068
+ return null;
45069
+ }
45070
+ function hasWorkingSubstitute() {
45071
+ return resolveContextModeDisciplinePath() !== null;
45072
+ }
45006
45073
  // src/shared/deep-merge.ts
45007
45074
  var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
45008
45075
  var MAX_DEPTH = 50;
@@ -45218,7 +45285,7 @@ import * as os3 from "os";
45218
45285
  import * as path4 from "path";
45219
45286
 
45220
45287
  // src/shared/jsonc-parser.ts
45221
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
45288
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
45222
45289
 
45223
45290
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
45224
45291
  function createScanner(text, ignoreTrivia = false) {
@@ -46092,10 +46159,10 @@ function parseJsoncSafe(content) {
46092
46159
  function detectConfigFile(basePath) {
46093
46160
  const jsoncPath = `${basePath}.jsonc`;
46094
46161
  const jsonPath = `${basePath}.json`;
46095
- if (existsSync3(jsoncPath)) {
46162
+ if (existsSync4(jsoncPath)) {
46096
46163
  return { format: "jsonc", path: jsoncPath };
46097
46164
  }
46098
- if (existsSync3(jsonPath)) {
46165
+ if (existsSync4(jsonPath)) {
46099
46166
  return { format: "json", path: jsonPath };
46100
46167
  }
46101
46168
  return { format: "none", path: jsonPath };
@@ -46190,8 +46257,8 @@ Both matrixx and ${pluginName} listen to session.idle events.
46190
46257
  2. Or set "notification": { "force_enable": true } in matrixx.json`;
46191
46258
  }
46192
46259
  // src/shared/file-reference-resolver.ts
46193
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync } from "fs";
46194
- import { isAbsolute, join as join6 } from "path";
46260
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync } from "fs";
46261
+ import { isAbsolute, join as join7 } from "path";
46195
46262
  var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
46196
46263
  function findFileReferences(text) {
46197
46264
  const matches = [];
@@ -46210,10 +46277,10 @@ function resolveFilePath(filePath, cwd) {
46210
46277
  if (isAbsolute(filePath)) {
46211
46278
  return filePath;
46212
46279
  }
46213
- return join6(cwd, filePath);
46280
+ return join7(cwd, filePath);
46214
46281
  }
46215
46282
  function readFileContent(resolvedPath) {
46216
- if (!existsSync5(resolvedPath)) {
46283
+ if (!existsSync6(resolvedPath)) {
46217
46284
  return `[file not found: ${resolvedPath}]`;
46218
46285
  }
46219
46286
  const stat = statSync(resolvedPath);
@@ -46279,7 +46346,7 @@ init_frontmatter();
46279
46346
  // src/shared/git-worktree/collect-git-diff-stats.ts
46280
46347
  import { execFileSync } from "child_process";
46281
46348
  import { readFileSync as readFileSync5 } from "fs";
46282
- import { join as join7 } from "path";
46349
+ import { join as join8 } from "path";
46283
46350
 
46284
46351
  // src/shared/git-worktree/parse-diff-numstat.ts
46285
46352
  function parseGitDiffNumstat(output, statusMap) {
@@ -46352,7 +46419,7 @@ function collectGitDiffStats(directory) {
46352
46419
  const untrackedNumstat = untrackedOutput ? untrackedOutput.split(`
46353
46420
  `).filter(Boolean).map((filePath) => {
46354
46421
  try {
46355
- const content = readFileSync5(join7(directory, filePath), "utf-8");
46422
+ const content = readFileSync5(join8(directory, filePath), "utf-8");
46356
46423
  const lineCount = content.split(`
46357
46424
  `).length - (content.endsWith(`
46358
46425
  `) ? 1 : 0);
@@ -46440,8 +46507,8 @@ function isAbortError(error51) {
46440
46507
  init_logger();
46441
46508
 
46442
46509
  // src/shared/model-availability.ts
46443
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
46444
- import { join as join8 } from "path";
46510
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
46511
+ import { join as join9 } from "path";
46445
46512
  init_logger();
46446
46513
  function normalizeModelName(name) {
46447
46514
  return name.toLowerCase().replace(/(opus|sonnet|haiku)-(\d+)[.-](\d+)/g, "$1-$2.$3");
@@ -46577,8 +46644,8 @@ async function fetchAvailableModels(client, options) {
46577
46644
  }
46578
46645
  }
46579
46646
  log("[fetchAvailableModels] provider-models cache not found, falling back to models.json");
46580
- const cacheFile = join8(getOpenCodeCacheDir(), "models.json");
46581
- if (!existsSync6(cacheFile)) {
46647
+ const cacheFile = join9(getOpenCodeCacheDir(), "models.json");
46648
+ if (!existsSync7(cacheFile)) {
46582
46649
  log("[fetchAvailableModels] models.json cache file not found, falling back to client");
46583
46650
  } else {
46584
46651
  try {
@@ -46683,8 +46750,8 @@ function isModelCacheAvailable() {
46683
46750
  if (hasProviderModelsCache()) {
46684
46751
  return true;
46685
46752
  }
46686
- const cacheFile = join8(getOpenCodeCacheDir(), "models.json");
46687
- return existsSync6(cacheFile);
46753
+ const cacheFile = join9(getOpenCodeCacheDir(), "models.json");
46754
+ return existsSync7(cacheFile);
46688
46755
  }
46689
46756
  // src/shared/model-resolution-pipeline.ts
46690
46757
  init_logger();
@@ -46973,9 +47040,9 @@ async function promptSyncWithModelSuggestionRetry(client, args) {
46973
47040
  }
46974
47041
  }
46975
47042
  // src/shared/opencode-config-dir.ts
46976
- import { existsSync as existsSync7 } from "fs";
46977
- import { homedir as homedir3 } from "os";
46978
- import { join as join9, resolve } from "path";
47043
+ import { existsSync as existsSync8 } from "fs";
47044
+ import { homedir as homedir4 } from "os";
47045
+ import { join as join10, resolve } from "path";
46979
47046
  var TAURI_APP_IDENTIFIER = "ai.opencode.desktop";
46980
47047
  var TAURI_APP_IDENTIFIER_DEV = "ai.opencode.desktop.dev";
46981
47048
  function isDevBuild(version2) {
@@ -46987,14 +47054,14 @@ function getTauriConfigDir(identifier) {
46987
47054
  const platform = process.platform;
46988
47055
  switch (platform) {
46989
47056
  case "darwin":
46990
- return join9(homedir3(), "Library", "Application Support", identifier);
47057
+ return join10(homedir4(), "Library", "Application Support", identifier);
46991
47058
  case "win32": {
46992
- const appData = process.env.APPDATA || join9(homedir3(), "AppData", "Roaming");
46993
- return join9(appData, identifier);
47059
+ const appData = process.env.APPDATA || join10(homedir4(), "AppData", "Roaming");
47060
+ return join10(appData, identifier);
46994
47061
  }
46995
47062
  default: {
46996
- const xdgConfig = process.env.XDG_CONFIG_HOME || join9(homedir3(), ".config");
46997
- return join9(xdgConfig, identifier);
47063
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join10(homedir4(), ".config");
47064
+ return join10(xdgConfig, identifier);
46998
47065
  }
46999
47066
  }
47000
47067
  }
@@ -47004,21 +47071,21 @@ function getCliConfigDir() {
47004
47071
  return resolve(envConfigDir);
47005
47072
  }
47006
47073
  if (process.platform === "win32") {
47007
- const crossPlatformDir = join9(homedir3(), ".config", "opencode");
47008
- const crossPlatformConfig = join9(crossPlatformDir, "opencode.json");
47009
- if (existsSync7(crossPlatformConfig)) {
47074
+ const crossPlatformDir = join10(homedir4(), ".config", "opencode");
47075
+ const crossPlatformConfig = join10(crossPlatformDir, "opencode.json");
47076
+ if (existsSync8(crossPlatformConfig)) {
47010
47077
  return crossPlatformDir;
47011
47078
  }
47012
- const appData = process.env.APPDATA || join9(homedir3(), "AppData", "Roaming");
47013
- const appdataDir = join9(appData, "opencode");
47014
- const appdataConfig = join9(appdataDir, "opencode.json");
47015
- if (existsSync7(appdataConfig)) {
47079
+ const appData = process.env.APPDATA || join10(homedir4(), "AppData", "Roaming");
47080
+ const appdataDir = join10(appData, "opencode");
47081
+ const appdataConfig = join10(appdataDir, "opencode.json");
47082
+ if (existsSync8(appdataConfig)) {
47016
47083
  return appdataDir;
47017
47084
  }
47018
47085
  return crossPlatformDir;
47019
47086
  }
47020
- const xdgConfig = process.env.XDG_CONFIG_HOME || join9(homedir3(), ".config");
47021
- return join9(xdgConfig, "opencode");
47087
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join10(homedir4(), ".config");
47088
+ return join10(xdgConfig, "opencode");
47022
47089
  }
47023
47090
  function getOpenCodeConfigDir(options) {
47024
47091
  const { binary, version: version2, checkExisting = true } = options;
@@ -47029,9 +47096,9 @@ function getOpenCodeConfigDir(options) {
47029
47096
  const tauriDir = getTauriConfigDir(identifier);
47030
47097
  if (checkExisting) {
47031
47098
  const legacyDir = getCliConfigDir();
47032
- const legacyConfig = join9(legacyDir, "opencode.json");
47033
- const legacyConfigC = join9(legacyDir, "opencode.jsonc");
47034
- if (existsSync7(legacyConfig) || existsSync7(legacyConfigC)) {
47099
+ const legacyConfig = join10(legacyDir, "opencode.json");
47100
+ const legacyConfigC = join10(legacyDir, "opencode.jsonc");
47101
+ if (existsSync8(legacyConfig) || existsSync8(legacyConfigC)) {
47035
47102
  return legacyDir;
47036
47103
  }
47037
47104
  }
@@ -47041,10 +47108,10 @@ function getOpenCodeConfigPaths(options) {
47041
47108
  const configDir = getOpenCodeConfigDir(options);
47042
47109
  return {
47043
47110
  configDir,
47044
- configJson: join9(configDir, "opencode.json"),
47045
- configJsonc: join9(configDir, "opencode.jsonc"),
47046
- packageJson: join9(configDir, "package.json"),
47047
- matrixxConfig: join9(configDir, "matrixx.json")
47111
+ configJson: join10(configDir, "opencode.json"),
47112
+ configJsonc: join10(configDir, "opencode.jsonc"),
47113
+ packageJson: join10(configDir, "package.json"),
47114
+ matrixxConfig: join10(configDir, "matrixx.json")
47048
47115
  };
47049
47116
  }
47050
47117
  // src/shared/opencode-http-api.ts
@@ -47291,12 +47358,12 @@ async function deletePart(client, sessionID, messageID, partID) {
47291
47358
  }
47292
47359
  // src/shared/opencode-message-dir.ts
47293
47360
  init_logger();
47294
- import { existsSync as existsSync9, readdirSync } from "fs";
47295
- import { join as join12 } from "path";
47361
+ import { existsSync as existsSync10, readdirSync } from "fs";
47362
+ import { join as join13 } from "path";
47296
47363
 
47297
47364
  // src/shared/opencode-storage-detection.ts
47298
- import { existsSync as existsSync8 } from "fs";
47299
- import { join as join10 } from "path";
47365
+ import { existsSync as existsSync9 } from "fs";
47366
+ import { join as join11 } from "path";
47300
47367
 
47301
47368
  // src/shared/opencode-version.ts
47302
47369
  import { execSync } from "child_process";
@@ -47361,8 +47428,8 @@ function isSqliteBackend() {
47361
47428
  return false;
47362
47429
  const check2 = () => {
47363
47430
  const versionOk = isOpenCodeVersionAtLeast(OPENCODE_SQLITE_VERSION);
47364
- const dbPath = join10(getDataDir(), "opencode", "opencode.db");
47365
- return versionOk && existsSync8(dbPath);
47431
+ const dbPath = join11(getDataDir(), "opencode", "opencode.db");
47432
+ return versionOk && existsSync9(dbPath);
47366
47433
  };
47367
47434
  if (cachedResult === FALSE_PENDING_RETRY) {
47368
47435
  const result2 = check2();
@@ -47379,11 +47446,11 @@ function isSqliteBackend() {
47379
47446
  }
47380
47447
 
47381
47448
  // src/shared/opencode-storage-paths.ts
47382
- import { join as join11 } from "path";
47449
+ import { join as join12 } from "path";
47383
47450
  var OPENCODE_STORAGE = getOpenCodeStorageDir();
47384
- var MESSAGE_STORAGE = join11(OPENCODE_STORAGE, "message");
47385
- var PART_STORAGE = join11(OPENCODE_STORAGE, "part");
47386
- var SESSION_STORAGE = join11(OPENCODE_STORAGE, "session");
47451
+ var MESSAGE_STORAGE = join12(OPENCODE_STORAGE, "message");
47452
+ var PART_STORAGE = join12(OPENCODE_STORAGE, "part");
47453
+ var SESSION_STORAGE = join12(OPENCODE_STORAGE, "session");
47387
47454
 
47388
47455
  // src/shared/opencode-message-dir.ts
47389
47456
  function getMessageDir(sessionID) {
@@ -47393,16 +47460,16 @@ function getMessageDir(sessionID) {
47393
47460
  return null;
47394
47461
  if (isSqliteBackend())
47395
47462
  return null;
47396
- if (!existsSync9(MESSAGE_STORAGE))
47463
+ if (!existsSync10(MESSAGE_STORAGE))
47397
47464
  return null;
47398
- const directPath = join12(MESSAGE_STORAGE, sessionID);
47399
- if (existsSync9(directPath)) {
47465
+ const directPath = join13(MESSAGE_STORAGE, sessionID);
47466
+ if (existsSync10(directPath)) {
47400
47467
  return directPath;
47401
47468
  }
47402
47469
  try {
47403
47470
  for (const dir of readdirSync(MESSAGE_STORAGE)) {
47404
- const sessionPath = join12(MESSAGE_STORAGE, dir, sessionID);
47405
- if (existsSync9(sessionPath)) {
47471
+ const sessionPath = join13(MESSAGE_STORAGE, dir, sessionID);
47472
+ if (existsSync10(sessionPath)) {
47406
47473
  return sessionPath;
47407
47474
  }
47408
47475
  }
@@ -47519,8 +47586,8 @@ function resetMessageCursor(sessionID) {
47519
47586
  sessionCursors.clear();
47520
47587
  }
47521
47588
  // src/features/hook-message-injector/injector.ts
47522
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
47523
- import { join as join13 } from "path";
47589
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
47590
+ import { join as join14 } from "path";
47524
47591
  init_logger();
47525
47592
  function convertSDKMessageToStoredMessage(msg) {
47526
47593
  const info = msg.info;
@@ -47594,7 +47661,7 @@ function findNearestMessageWithFields(messageDir) {
47594
47661
  const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
47595
47662
  for (const file2 of files) {
47596
47663
  try {
47597
- const content = readFileSync7(join13(messageDir, file2), "utf-8");
47664
+ const content = readFileSync7(join14(messageDir, file2), "utf-8");
47598
47665
  const msg = JSON.parse(content);
47599
47666
  if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
47600
47667
  return msg;
@@ -47603,7 +47670,7 @@ function findNearestMessageWithFields(messageDir) {
47603
47670
  }
47604
47671
  for (const file2 of files) {
47605
47672
  try {
47606
- const content = readFileSync7(join13(messageDir, file2), "utf-8");
47673
+ const content = readFileSync7(join14(messageDir, file2), "utf-8");
47607
47674
  const msg = JSON.parse(content);
47608
47675
  if (msg.agent || msg.model?.providerID && msg.model?.modelID) {
47609
47676
  return msg;
@@ -47623,7 +47690,7 @@ function findFirstMessageWithAgent(messageDir) {
47623
47690
  const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort();
47624
47691
  for (const file2 of files) {
47625
47692
  try {
47626
- const content = readFileSync7(join13(messageDir, file2), "utf-8");
47693
+ const content = readFileSync7(join14(messageDir, file2), "utf-8");
47627
47694
  const msg = JSON.parse(content);
47628
47695
  if (msg.agent) {
47629
47696
  return msg.agent;
@@ -48068,10 +48135,10 @@ async function spawnTmuxPane(sessionId, description, config2, serverUrl, targetP
48068
48135
  return { success: true, paneId };
48069
48136
  }
48070
48137
  // src/shared/transcript-path.ts
48071
- import { join as join14 } from "path";
48072
- var TRANSCRIPT_DIR = join14(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
48138
+ import { join as join15 } from "path";
48139
+ var TRANSCRIPT_DIR = join15(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
48073
48140
  function getTranscriptPath(sessionId) {
48074
- return join14(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
48141
+ return join15(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
48075
48142
  }
48076
48143
  // src/shared/truncate-description.ts
48077
48144
  function truncateDescription(description, maxLength = 120) {
@@ -48092,7 +48159,7 @@ function withTimeout(promise2, timeoutMs, errorMessage) {
48092
48159
  return Promise.race([promise2, timeoutPromise]).finally(() => clearTimeout(timeoutId));
48093
48160
  }
48094
48161
  // src/hooks/agent-usage-reminder/constants.ts
48095
- var AGENT_USAGE_REMINDER_STORAGE = join15(OPENCODE_STORAGE, "agent-usage-reminder");
48162
+ var AGENT_USAGE_REMINDER_STORAGE = join16(OPENCODE_STORAGE, "agent-usage-reminder");
48096
48163
  var TARGET_TOOLS = new Set([
48097
48164
  "grep",
48098
48165
  "safe_grep",
@@ -48138,19 +48205,19 @@ ALWAYS prefer: Multiple parallel task calls > Direct tool calls
48138
48205
 
48139
48206
  // src/hooks/agent-usage-reminder/storage.ts
48140
48207
  import {
48141
- existsSync as existsSync11,
48208
+ existsSync as existsSync12,
48142
48209
  mkdirSync as mkdirSync4,
48143
48210
  readFileSync as readFileSync8,
48144
48211
  unlinkSync as unlinkSync2,
48145
48212
  writeFileSync as writeFileSync3
48146
48213
  } from "fs";
48147
- import { join as join16 } from "path";
48214
+ import { join as join17 } from "path";
48148
48215
  function getStoragePath(sessionID) {
48149
- return join16(AGENT_USAGE_REMINDER_STORAGE, `${sessionID}.json`);
48216
+ return join17(AGENT_USAGE_REMINDER_STORAGE, `${sessionID}.json`);
48150
48217
  }
48151
48218
  function loadAgentUsageState(sessionID) {
48152
48219
  const filePath = getStoragePath(sessionID);
48153
- if (!existsSync11(filePath))
48220
+ if (!existsSync12(filePath))
48154
48221
  return null;
48155
48222
  try {
48156
48223
  const content = readFileSync8(filePath, "utf-8");
@@ -48160,7 +48227,7 @@ function loadAgentUsageState(sessionID) {
48160
48227
  }
48161
48228
  }
48162
48229
  function saveAgentUsageState(state) {
48163
- if (!existsSync11(AGENT_USAGE_REMINDER_STORAGE)) {
48230
+ if (!existsSync12(AGENT_USAGE_REMINDER_STORAGE)) {
48164
48231
  mkdirSync4(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
48165
48232
  }
48166
48233
  const filePath = getStoragePath(state.sessionID);
@@ -48168,7 +48235,7 @@ function saveAgentUsageState(state) {
48168
48235
  }
48169
48236
  function clearAgentUsageState(sessionID) {
48170
48237
  const filePath = getStoragePath(sessionID);
48171
- if (existsSync11(filePath)) {
48238
+ if (existsSync12(filePath)) {
48172
48239
  unlinkSync2(filePath);
48173
48240
  }
48174
48241
  }
@@ -48249,16 +48316,16 @@ var META_TAG_SUFFIX = "-->";
48249
48316
  var MAX_PLAN_FILE_BYTES = 102400;
48250
48317
  // src/features/mission-state/plan-storage.ts
48251
48318
  import {
48252
- existsSync as existsSync12,
48319
+ existsSync as existsSync13,
48253
48320
  mkdirSync as mkdirSync5,
48254
48321
  readFileSync as readFileSync9,
48255
48322
  renameSync,
48256
48323
  writeFileSync as writeFileSync4
48257
48324
  } from "fs";
48258
- import { join as join17 } from "path";
48325
+ import { join as join18 } from "path";
48259
48326
  function ensurePlanDir(directory) {
48260
48327
  try {
48261
- mkdirSync5(join17(directory, PLANS_DIR), { recursive: true });
48328
+ mkdirSync5(join18(directory, PLANS_DIR), { recursive: true });
48262
48329
  return true;
48263
48330
  } catch {
48264
48331
  return false;
@@ -48266,7 +48333,7 @@ function ensurePlanDir(directory) {
48266
48333
  }
48267
48334
  function readPlanFile(planPath) {
48268
48335
  try {
48269
- if (!existsSync12(planPath))
48336
+ if (!existsSync13(planPath))
48270
48337
  return null;
48271
48338
  const content = readFileSync9(planPath, "utf-8");
48272
48339
  if (content.length > MAX_PLAN_FILE_BYTES)
@@ -48325,17 +48392,17 @@ function parseMetadataComment(content) {
48325
48392
  }
48326
48393
  }
48327
48394
  // src/features/mission-state/rehydrate.ts
48328
- import { existsSync as existsSync14 } from "fs";
48395
+ import { existsSync as existsSync15 } from "fs";
48329
48396
 
48330
48397
  // src/features/mission-state/storage.ts
48331
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
48332
- import { basename, dirname, join as join18 } from "path";
48398
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
48399
+ import { basename, dirname, join as join19 } from "path";
48333
48400
  function getMissionFilePath(directory) {
48334
- return join18(directory, MISSION_DIR, MISSION_FILE);
48401
+ return join19(directory, MISSION_DIR, MISSION_FILE);
48335
48402
  }
48336
48403
  function readMissionState(directory) {
48337
48404
  const filePath = getMissionFilePath(directory);
48338
- if (!existsSync13(filePath)) {
48405
+ if (!existsSync14(filePath)) {
48339
48406
  return null;
48340
48407
  }
48341
48408
  try {
@@ -48356,7 +48423,7 @@ function writeMissionState(directory, state) {
48356
48423
  const filePath = getMissionFilePath(directory);
48357
48424
  try {
48358
48425
  const dir = dirname(filePath);
48359
- if (!existsSync13(dir)) {
48426
+ if (!existsSync14(dir)) {
48360
48427
  mkdirSync6(dir, { recursive: true });
48361
48428
  }
48362
48429
  writeFileSync5(filePath, JSON.stringify(state, null, 2), "utf-8");
@@ -48383,7 +48450,7 @@ function appendSessionId(directory, sessionId) {
48383
48450
  function clearMissionState(directory) {
48384
48451
  const filePath = getMissionFilePath(directory);
48385
48452
  try {
48386
- if (existsSync13(filePath)) {
48453
+ if (existsSync14(filePath)) {
48387
48454
  const { unlinkSync: unlinkSync3 } = __require("fs");
48388
48455
  unlinkSync3(filePath);
48389
48456
  }
@@ -48393,13 +48460,13 @@ function clearMissionState(directory) {
48393
48460
  }
48394
48461
  }
48395
48462
  function findOraclePlans(directory) {
48396
- const plansDir = join18(directory, ORACLE_PLANS_DIR);
48397
- if (!existsSync13(plansDir)) {
48463
+ const plansDir = join19(directory, ORACLE_PLANS_DIR);
48464
+ if (!existsSync14(plansDir)) {
48398
48465
  return [];
48399
48466
  }
48400
48467
  try {
48401
48468
  const files = readdirSync3(plansDir);
48402
- return files.filter((f) => f.endsWith(".md")).map((f) => join18(plansDir, f)).sort((a, b) => {
48469
+ return files.filter((f) => f.endsWith(".md")).map((f) => join19(plansDir, f)).sort((a, b) => {
48403
48470
  const aStat = __require("fs").statSync(a);
48404
48471
  const bStat = __require("fs").statSync(b);
48405
48472
  return bStat.mtimeMs - aStat.mtimeMs;
@@ -48409,7 +48476,7 @@ function findOraclePlans(directory) {
48409
48476
  }
48410
48477
  }
48411
48478
  function getPlanProgress(planPath) {
48412
- if (!existsSync13(planPath)) {
48479
+ if (!existsSync14(planPath)) {
48413
48480
  return { total: 0, completed: 0, isComplete: true };
48414
48481
  }
48415
48482
  try {
@@ -48450,7 +48517,7 @@ function buildRehydrationContext(directory) {
48450
48517
  if (!mission?.active_plan)
48451
48518
  return null;
48452
48519
  const planPath = mission.active_plan;
48453
- if (!existsSync14(planPath))
48520
+ if (!existsSync15(planPath))
48454
48521
  return null;
48455
48522
  const content = readPlanFile(planPath);
48456
48523
  if (!content)
@@ -48496,6 +48563,9 @@ function unregisterSubagentSession(sessionID) {
48496
48563
  subagentSessions.delete(sessionID);
48497
48564
  subagentParentMap.delete(sessionID);
48498
48565
  }
48566
+ function getParentSessionID(subagentSessionID) {
48567
+ return subagentParentMap.get(subagentSessionID);
48568
+ }
48499
48569
  function getSubagentSessionIDs(parentSessionID) {
48500
48570
  const ids = [];
48501
48571
  for (const subID of subagentSessions) {
@@ -49340,8 +49410,8 @@ function findSlashCommandPartIndex(parts) {
49340
49410
  return -1;
49341
49411
  }
49342
49412
  // src/hooks/auto-slash-command/executor.ts
49343
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
49344
- import { basename as basename2, dirname as dirname2, join as join19 } from "path";
49413
+ import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
49414
+ import { basename as basename2, dirname as dirname2, join as join20 } from "path";
49345
49415
 
49346
49416
  // src/features/builtin-commands/templates/assembly.ts
49347
49417
  var ASSEMBLY_TEMPLATE = `Toggle assembly tool availability for the current session.
@@ -51743,7 +51813,7 @@ function sanitizeModelField(model, source = "other") {
51743
51813
 
51744
51814
  // src/hooks/auto-slash-command/executor.ts
51745
51815
  function discoverCommandsFromDir(commandsDir, scope) {
51746
- if (!existsSync15(commandsDir)) {
51816
+ if (!existsSync16(commandsDir)) {
51747
51817
  return [];
51748
51818
  }
51749
51819
  const entries = readdirSync4(commandsDir, { withFileTypes: true });
@@ -51751,7 +51821,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
51751
51821
  for (const entry of entries) {
51752
51822
  if (!isMarkdownFile(entry))
51753
51823
  continue;
51754
- const commandPath = join19(commandsDir, entry.name);
51824
+ const commandPath = join20(commandsDir, entry.name);
51755
51825
  const commandName = basename2(entry.name, ".md");
51756
51826
  try {
51757
51827
  const content = readFileSync11(commandPath, "utf-8");
@@ -51817,8 +51887,8 @@ async function discoverPluginCommands(client) {
51817
51887
  }
51818
51888
  async function discoverAllCommands(options) {
51819
51889
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
51820
- const opencodeGlobalDir = join19(configDir, "command");
51821
- const opencodeProjectDir = join19(process.cwd(), ".opencode", "command");
51890
+ const opencodeGlobalDir = join20(configDir, "command");
51891
+ const opencodeProjectDir = join20(process.cwd(), ".opencode", "command");
51822
51892
  const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
51823
51893
  const opencodeProjectCommands = discoverCommandsFromDir(opencodeProjectDir, "opencode-project");
51824
51894
  const builtinCommandsMap = loadBuiltinCommands();
@@ -52799,27 +52869,27 @@ function createCategorySkillReminderHook(_ctx, availableSkills = []) {
52799
52869
  // src/hooks/comment-checker/hook.ts
52800
52870
  import * as fs11 from "fs";
52801
52871
  import { tmpdir as tmpdir4 } from "os";
52802
- import { join as join26 } from "path";
52872
+ import { join as join27 } from "path";
52803
52873
 
52804
52874
  // src/hooks/comment-checker/cli-runner.ts
52805
- import { existsSync as existsSync23 } from "fs";
52875
+ import { existsSync as existsSync24 } from "fs";
52806
52876
 
52807
52877
  // src/hooks/comment-checker/cli.ts
52808
52878
  import * as fs10 from "fs";
52809
- import { existsSync as existsSync22 } from "fs";
52879
+ import { existsSync as existsSync23 } from "fs";
52810
52880
  import { createRequire as createRequire2 } from "module";
52811
52881
  import { tmpdir as tmpdir3 } from "os";
52812
- import { dirname as dirname5, join as join25 } from "path";
52882
+ import { dirname as dirname5, join as join26 } from "path";
52813
52883
  var {spawn: spawn8 } = globalThis.Bun;
52814
52884
 
52815
52885
  // src/hooks/comment-checker/downloader.ts
52816
- import { appendFileSync as appendFileSync2, existsSync as existsSync21 } from "fs";
52886
+ import { appendFileSync as appendFileSync2, existsSync as existsSync22 } from "fs";
52817
52887
  import { createRequire } from "module";
52818
- import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
52819
- import { join as join24 } from "path";
52888
+ import { homedir as homedir7, tmpdir as tmpdir2 } from "os";
52889
+ import { join as join25 } from "path";
52820
52890
  init_logger();
52821
52891
  var DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1";
52822
- var DEBUG_FILE = join24(tmpdir2(), "comment-checker-debug.log");
52892
+ var DEBUG_FILE = join25(tmpdir2(), "comment-checker-debug.log");
52823
52893
  function debugLog(...args) {
52824
52894
  if (DEBUG) {
52825
52895
  const msg = `[${new Date().toISOString()}] [comment-checker:downloader] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
@@ -52838,12 +52908,12 @@ var PLATFORM_MAP = {
52838
52908
  function getCacheDir2() {
52839
52909
  if (process.platform === "win32") {
52840
52910
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
52841
- const base2 = localAppData || join24(homedir6(), "AppData", "Local");
52842
- return join24(base2, "matrixx", "bin");
52911
+ const base2 = localAppData || join25(homedir7(), "AppData", "Local");
52912
+ return join25(base2, "matrixx", "bin");
52843
52913
  }
52844
52914
  const xdgCache = process.env.XDG_CACHE_HOME;
52845
- const base = xdgCache || join24(homedir6(), ".cache");
52846
- return join24(base, "matrixx", "bin");
52915
+ const base = xdgCache || join25(homedir7(), ".cache");
52916
+ return join25(base, "matrixx", "bin");
52847
52917
  }
52848
52918
  function getBinaryName() {
52849
52919
  return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
@@ -52869,8 +52939,8 @@ async function downloadCommentChecker() {
52869
52939
  }
52870
52940
  const cacheDir = getCacheDir2();
52871
52941
  const binaryName = getBinaryName();
52872
- const binaryPath = join24(cacheDir, binaryName);
52873
- if (existsSync21(binaryPath)) {
52942
+ const binaryPath = join25(cacheDir, binaryName);
52943
+ if (existsSync22(binaryPath)) {
52874
52944
  debugLog("Binary already cached at:", binaryPath);
52875
52945
  return binaryPath;
52876
52946
  }
@@ -52882,7 +52952,7 @@ async function downloadCommentChecker() {
52882
52952
  log(`[matrixx] Downloading comment-checker binary...`);
52883
52953
  try {
52884
52954
  ensureCacheDir(cacheDir);
52885
- const archivePath = join24(cacheDir, assetName);
52955
+ const archivePath = join25(cacheDir, assetName);
52886
52956
  await downloadArchive(downloadUrl, archivePath);
52887
52957
  debugLog(`Downloaded archive to: ${archivePath}`);
52888
52958
  if (ext === "tar.gz") {
@@ -52914,7 +52984,7 @@ async function ensureCommentCheckerBinary() {
52914
52984
 
52915
52985
  // src/hooks/comment-checker/cli.ts
52916
52986
  var DEBUG2 = process.env.COMMENT_CHECKER_DEBUG === "1";
52917
- var DEBUG_FILE2 = join25(tmpdir3(), "comment-checker-debug.log");
52987
+ var DEBUG_FILE2 = join26(tmpdir3(), "comment-checker-debug.log");
52918
52988
  function debugLog2(...args) {
52919
52989
  if (DEBUG2) {
52920
52990
  const msg = `[${new Date().toISOString()}] [comment-checker:cli] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
@@ -52940,8 +53010,8 @@ function findCommentCheckerPathSync() {
52940
53010
  const require2 = createRequire2(import.meta.url);
52941
53011
  const cliPkgPath = require2.resolve("@code-yeongyu/comment-checker/package.json");
52942
53012
  const cliDir = dirname5(cliPkgPath);
52943
- const binaryPath = join25(cliDir, "bin", binaryName);
52944
- if (existsSync22(binaryPath)) {
53013
+ const binaryPath = join26(cliDir, "bin", binaryName);
53014
+ if (existsSync23(binaryPath)) {
52945
53015
  debugLog2("found binary in main package:", binaryPath);
52946
53016
  return binaryPath;
52947
53017
  }
@@ -52962,7 +53032,7 @@ async function getCommentCheckerPath() {
52962
53032
  }
52963
53033
  initPromise2 = (async () => {
52964
53034
  const syncPath = findCommentCheckerPathSync();
52965
- if (syncPath && existsSync22(syncPath)) {
53035
+ if (syncPath && existsSync23(syncPath)) {
52966
53036
  resolvedCliPath = syncPath;
52967
53037
  debugLog2("using sync-resolved path:", syncPath);
52968
53038
  return syncPath;
@@ -52998,7 +53068,7 @@ async function runCommentChecker(input, cliPath, customPrompt) {
52998
53068
  debugLog2("comment-checker binary not found");
52999
53069
  return { hasComments: false, message: "" };
53000
53070
  }
53001
- if (!existsSync22(binaryPath)) {
53071
+ if (!existsSync23(binaryPath)) {
53002
53072
  debugLog2("comment-checker binary does not exist:", binaryPath);
53003
53073
  return { hasComments: false, message: "" };
53004
53074
  }
@@ -53157,7 +53227,7 @@ ${result.message}`;
53157
53227
  }
53158
53228
  }
53159
53229
  function isCliPathUsable(cliPath) {
53160
- return Boolean(cliPath && existsSync23(cliPath));
53230
+ return Boolean(cliPath && existsSync24(cliPath));
53161
53231
  }
53162
53232
 
53163
53233
  // src/hooks/comment-checker/pending-calls.ts
@@ -53195,7 +53265,7 @@ function takePendingCall(callID) {
53195
53265
 
53196
53266
  // src/hooks/comment-checker/hook.ts
53197
53267
  var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
53198
- var DEBUG_FILE3 = join26(tmpdir4(), "comment-checker-debug.log");
53268
+ var DEBUG_FILE3 = join27(tmpdir4(), "comment-checker-debug.log");
53199
53269
  function debugLog3(...args) {
53200
53270
  if (DEBUG3) {
53201
53271
  const msg = `[${new Date().toISOString()}] [comment-checker:hook] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
@@ -53469,18 +53539,18 @@ function createCompactionTodoPreserverHook(ctx) {
53469
53539
  // src/hooks/context-mode-enforcer/constants.ts
53470
53540
  var HOOK_NAME3 = "context-mode-enforcer";
53471
53541
  var WARN_MESSAGE_READ = "Use ctx_search / ctx_batch_execute / ctx_execute for analysis \u2014 raw read/grep/glob bypasses FTS5 sandbox and pollutes context. Use Read only with hashline IDs for Edit (non-plan paths; .matrixx/plans/*.md must use plan_read/plan_update).";
53472
- var BLOCK_MESSAGE_GREP_GLOB = "Blocked: raw grep/glob is forbidden when context-mode is enforced. Use ctx_search (indexed) \u2192 grep/glob fallback only when ctx_* unavailable, or ctx_batch_execute / ctx_execute for multi-file analysis.";
53542
+ var BLOCK_MESSAGE_GREP_GLOB = "Blocked: raw grep/glob is forbidden when context-mode is enforced. " + "Use ctx_search for indexed-KB hits, or ctx_batch_execute / ctx_execute running shell `rg` for codebase search. " + "Raw grep/glob bypasses the FTS5 sandbox and pollutes context.";
53473
53543
  var WARN_MESSAGE_BASH_READ = "Use ctx_* / Read tool instead of `cat`/`head`/`tail`/`grep` via bash for analysis. Raw bash file reads bypass sandbox.";
53474
53544
 
53475
53545
  // src/hooks/context-mode-enforcer/hook.ts
53476
53546
  var BASH_FILE_READ_RE = /^\s*(cat|head|tail)\s+/;
53477
53547
  var BASH_GREP_RE = /\bgrep\b/;
53478
53548
  function resolveContextModeConfig(pluginConfig) {
53479
- const cfg = pluginConfig.context_mode;
53549
+ const { enabled, enforce, blockedTools } = resolveContextModeEnforcement(pluginConfig.context_mode);
53480
53550
  return {
53481
- enabled: cfg?.enabled ?? true,
53482
- enforce: cfg?.enforce ?? false,
53483
- blockedTools: new Set((cfg?.blocked_tools ?? ["grep", "glob"]).map((t) => t.toLowerCase()))
53551
+ enabled,
53552
+ enforce,
53553
+ blockedTools: new Set(blockedTools)
53484
53554
  };
53485
53555
  }
53486
53556
  function createContextModeEnforcerHook(pluginConfig) {
@@ -53525,7 +53595,7 @@ Bash command: ${command.slice(0, 200)}`);
53525
53595
  if (isGrepGlob) {
53526
53596
  if (!isBlocked)
53527
53597
  return;
53528
- if (enforce) {
53598
+ if (enforce && hasWorkingSubstitute()) {
53529
53599
  log(`[${HOOK_NAME3}] BLOCKED ${tool}`, { sessionID: input.sessionID });
53530
53600
  throw new Error(BLOCK_MESSAGE_GREP_GLOB);
53531
53601
  }
@@ -53576,13 +53646,13 @@ function incrementEmptyContentAttempt(autoCompactState, sessionID) {
53576
53646
  }
53577
53647
 
53578
53648
  // src/hooks/context-window-limit-recovery/tool-result-store.ts
53579
- import { existsSync as existsSync24, readdirSync as readdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "fs";
53580
- import { join as join27 } from "path";
53649
+ import { existsSync as existsSync25, readdirSync as readdirSync5, readFileSync as readFileSync19, writeFileSync as writeFileSync8 } from "fs";
53650
+ import { join as join28 } from "path";
53581
53651
  init_logger();
53582
53652
  var TRUNCATION_MESSAGE = "[TOOL RESULT TRUNCATED - Context limit exceeded. Original output was too large and has been truncated to recover the session. Please re-run this tool if you need the full output.]";
53583
53653
  function getMessageIds(sessionID) {
53584
53654
  const messageDir = getMessageDir(sessionID);
53585
- if (!messageDir || !existsSync24(messageDir))
53655
+ if (!messageDir || !existsSync25(messageDir))
53586
53656
  return [];
53587
53657
  const ids = [];
53588
53658
  for (const file2 of readdirSync5(messageDir)) {
@@ -53597,14 +53667,14 @@ function findToolResultsBySize(sessionID) {
53597
53667
  const messageIds = getMessageIds(sessionID);
53598
53668
  const results = [];
53599
53669
  for (const messageID of messageIds) {
53600
- const partDir = join27(PART_STORAGE, messageID);
53601
- if (!existsSync24(partDir))
53670
+ const partDir = join28(PART_STORAGE, messageID);
53671
+ if (!existsSync25(partDir))
53602
53672
  continue;
53603
53673
  for (const file2 of readdirSync5(partDir)) {
53604
53674
  if (!file2.endsWith(".json"))
53605
53675
  continue;
53606
53676
  try {
53607
- const partPath = join27(partDir, file2);
53677
+ const partPath = join28(partDir, file2);
53608
53678
  const content = readFileSync19(partPath, "utf-8");
53609
53679
  const part = JSON.parse(content);
53610
53680
  if (part.type === "tool" && part.state?.output && !part.truncated) {
@@ -53817,20 +53887,20 @@ init_logger();
53817
53887
  init_logger();
53818
53888
 
53819
53889
  // src/hooks/session-recovery/storage/messages-reader.ts
53820
- import { existsSync as existsSync25, readdirSync as readdirSync6, readFileSync as readFileSync20 } from "fs";
53821
- import { join as join28 } from "path";
53890
+ import { existsSync as existsSync26, readdirSync as readdirSync6, readFileSync as readFileSync20 } from "fs";
53891
+ import { join as join29 } from "path";
53822
53892
  function readMessages(sessionID) {
53823
53893
  if (isSqliteBackend())
53824
53894
  return [];
53825
53895
  const messageDir = getMessageDir(sessionID);
53826
- if (!messageDir || !existsSync25(messageDir))
53896
+ if (!messageDir || !existsSync26(messageDir))
53827
53897
  return [];
53828
53898
  const messages = [];
53829
53899
  for (const file2 of readdirSync6(messageDir)) {
53830
53900
  if (!file2.endsWith(".json"))
53831
53901
  continue;
53832
53902
  try {
53833
- const content = readFileSync20(join28(messageDir, file2), "utf-8");
53903
+ const content = readFileSync20(join29(messageDir, file2), "utf-8");
53834
53904
  messages.push(JSON.parse(content));
53835
53905
  } catch {}
53836
53906
  }
@@ -53848,20 +53918,20 @@ var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
53848
53918
  var META_TYPES = new Set(["step-start", "step-finish"]);
53849
53919
 
53850
53920
  // src/hooks/session-recovery/storage/parts-reader.ts
53851
- import { existsSync as existsSync26, readdirSync as readdirSync7, readFileSync as readFileSync21 } from "fs";
53852
- import { join as join29 } from "path";
53921
+ import { existsSync as existsSync27, readdirSync as readdirSync7, readFileSync as readFileSync21 } from "fs";
53922
+ import { join as join30 } from "path";
53853
53923
  function readParts(messageID) {
53854
53924
  if (isSqliteBackend())
53855
53925
  return [];
53856
- const partDir = join29(PART_STORAGE, messageID);
53857
- if (!existsSync26(partDir))
53926
+ const partDir = join30(PART_STORAGE, messageID);
53927
+ if (!existsSync27(partDir))
53858
53928
  return [];
53859
53929
  const parts = [];
53860
53930
  for (const file2 of readdirSync7(partDir)) {
53861
53931
  if (!file2.endsWith(".json"))
53862
53932
  continue;
53863
53933
  try {
53864
- const content = readFileSync21(join29(partDir, file2), "utf-8");
53934
+ const content = readFileSync21(join30(partDir, file2), "utf-8");
53865
53935
  parts.push(JSON.parse(content));
53866
53936
  } catch {}
53867
53937
  }
@@ -53925,22 +53995,22 @@ function findEmptyMessageByIndex(sessionID, targetIndex) {
53925
53995
  return null;
53926
53996
  }
53927
53997
  // src/hooks/session-recovery/storage/empty-text.ts
53928
- import { existsSync as existsSync27, readdirSync as readdirSync8, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "fs";
53929
- import { join as join30 } from "path";
53998
+ import { existsSync as existsSync28, readdirSync as readdirSync8, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "fs";
53999
+ import { join as join31 } from "path";
53930
54000
  function replaceEmptyTextParts(messageID, replacementText) {
53931
54001
  if (isSqliteBackend()) {
53932
54002
  log("[session-recovery] Disabled on SQLite backend: replaceEmptyTextParts (use async variant)");
53933
54003
  return false;
53934
54004
  }
53935
- const partDir = join30(PART_STORAGE, messageID);
53936
- if (!existsSync27(partDir))
54005
+ const partDir = join31(PART_STORAGE, messageID);
54006
+ if (!existsSync28(partDir))
53937
54007
  return false;
53938
54008
  let anyReplaced = false;
53939
54009
  for (const file2 of readdirSync8(partDir)) {
53940
54010
  if (!file2.endsWith(".json"))
53941
54011
  continue;
53942
54012
  try {
53943
- const filePath = join30(partDir, file2);
54013
+ const filePath = join31(partDir, file2);
53944
54014
  const content = readFileSync22(filePath, "utf-8");
53945
54015
  const part = JSON.parse(content);
53946
54016
  if (part.type === "text") {
@@ -54022,15 +54092,15 @@ function generatePartId() {
54022
54092
  return `prt_${timestamp}${random}`;
54023
54093
  }
54024
54094
  // src/hooks/session-recovery/storage/text-part-injector.ts
54025
- import { existsSync as existsSync28, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
54026
- import { join as join31 } from "path";
54095
+ import { existsSync as existsSync29, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
54096
+ import { join as join32 } from "path";
54027
54097
  function injectTextPart(sessionID, messageID, text) {
54028
54098
  if (isSqliteBackend()) {
54029
54099
  log("[session-recovery] Disabled on SQLite backend: injectTextPart (use async variant)");
54030
54100
  return false;
54031
54101
  }
54032
- const partDir = join31(PART_STORAGE, messageID);
54033
- if (!existsSync28(partDir)) {
54102
+ const partDir = join32(PART_STORAGE, messageID);
54103
+ if (!existsSync29(partDir)) {
54034
54104
  mkdirSync7(partDir, { recursive: true });
54035
54105
  }
54036
54106
  const partId = generatePartId();
@@ -54043,7 +54113,7 @@ function injectTextPart(sessionID, messageID, text) {
54043
54113
  synthetic: true
54044
54114
  };
54045
54115
  try {
54046
- writeFileSync10(join31(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
54116
+ writeFileSync10(join32(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
54047
54117
  return true;
54048
54118
  } catch {
54049
54119
  return false;
@@ -54082,8 +54152,8 @@ function findMessagesWithThinkingBlocks(sessionID) {
54082
54152
  return result;
54083
54153
  }
54084
54154
  // src/hooks/session-recovery/storage/thinking-prepend.ts
54085
- import { existsSync as existsSync29, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
54086
- import { join as join32 } from "path";
54155
+ import { existsSync as existsSync30, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
54156
+ import { join as join33 } from "path";
54087
54157
  function findLastThinkingContent(sessionID, beforeMessageID) {
54088
54158
  const messages = readMessages(sessionID);
54089
54159
  const currentIndex = messages.findIndex((message) => message.id === beforeMessageID);
@@ -54112,8 +54182,8 @@ function prependThinkingPart(sessionID, messageID) {
54112
54182
  log("[session-recovery] Disabled on SQLite backend: prependThinkingPart (use async variant)");
54113
54183
  return false;
54114
54184
  }
54115
- const partDir = join32(PART_STORAGE, messageID);
54116
- if (!existsSync29(partDir)) {
54185
+ const partDir = join33(PART_STORAGE, messageID);
54186
+ if (!existsSync30(partDir)) {
54117
54187
  mkdirSync8(partDir, { recursive: true });
54118
54188
  }
54119
54189
  const previousThinking = findLastThinkingContent(sessionID, messageID);
@@ -54127,7 +54197,7 @@ function prependThinkingPart(sessionID, messageID) {
54127
54197
  synthetic: true
54128
54198
  };
54129
54199
  try {
54130
- writeFileSync11(join32(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
54200
+ writeFileSync11(join33(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
54131
54201
  return true;
54132
54202
  } catch {
54133
54203
  return false;
@@ -54178,22 +54248,22 @@ async function prependThinkingPartAsync(client, sessionID, messageID) {
54178
54248
  }
54179
54249
  }
54180
54250
  // src/hooks/session-recovery/storage/thinking-strip.ts
54181
- import { existsSync as existsSync30, readdirSync as readdirSync9, readFileSync as readFileSync23, unlinkSync as unlinkSync3 } from "fs";
54182
- import { join as join33 } from "path";
54251
+ import { existsSync as existsSync31, readdirSync as readdirSync9, readFileSync as readFileSync23, unlinkSync as unlinkSync3 } from "fs";
54252
+ import { join as join34 } from "path";
54183
54253
  function stripThinkingParts(messageID) {
54184
54254
  if (isSqliteBackend()) {
54185
54255
  log("[session-recovery] Disabled on SQLite backend: stripThinkingParts (use async variant)");
54186
54256
  return false;
54187
54257
  }
54188
- const partDir = join33(PART_STORAGE, messageID);
54189
- if (!existsSync30(partDir))
54258
+ const partDir = join34(PART_STORAGE, messageID);
54259
+ if (!existsSync31(partDir))
54190
54260
  return false;
54191
54261
  let anyRemoved = false;
54192
54262
  for (const file2 of readdirSync9(partDir)) {
54193
54263
  if (!file2.endsWith(".json"))
54194
54264
  continue;
54195
54265
  try {
54196
- const filePath = join33(partDir, file2);
54266
+ const filePath = join34(partDir, file2);
54197
54267
  const content = readFileSync23(filePath, "utf-8");
54198
54268
  const part = JSON.parse(content);
54199
54269
  if (THINKING_TYPES.has(part.type)) {
@@ -54950,7 +55020,7 @@ var DELEGATE_TASK_ERROR_PATTERNS = [
54950
55020
  {
54951
55021
  pattern: "run_in_background",
54952
55022
  errorType: "missing_run_in_background",
54953
- fixHint: "Add run_in_background=false (for delegation) or run_in_background=true (for parallel exploration)"
55023
+ fixHint: "Add run_in_background=false (sync, awaits result inline; the default) or run_in_background=true (async, returns task_id immediately for parallel independent work such as exploration or fan-out waves)"
54954
55024
  },
54955
55025
  {
54956
55026
  pattern: "load_skills",
@@ -55064,7 +55134,7 @@ function buildRetryGuidance(errorInfo) {
55064
55134
  description="Task description",
55065
55135
  prompt="Detailed prompt...",
55066
55136
  category="blue-pill", // OR subagent_type="trinity"
55067
- run_in_background=false,
55137
+ run_in_background=false, // false is default \u2014 use true for any parallel independent work (returns task_id; async). Sync awaits result inline.
55068
55138
  load_skills=[]
55069
55139
  )
55070
55140
  \`\`\`
@@ -55089,7 +55159,7 @@ ${guidance}`;
55089
55159
  };
55090
55160
  }
55091
55161
  // src/hooks/design-intent-preserver/hook.ts
55092
- import { join as join35 } from "path";
55162
+ import { join as join36 } from "path";
55093
55163
 
55094
55164
  // src/hooks/design-intent-preserver/constants.ts
55095
55165
  var HOOK_NAME4 = "design-intent-preserver";
@@ -55109,7 +55179,7 @@ If design intent must change, record why in .matrixx/drafts/design-changes.md fi
55109
55179
 
55110
55180
  // src/hooks/design-intent-preserver/phase-detector.ts
55111
55181
  import { readdirSync as readdirSync10, readFileSync as readFileSync24, statSync as statSync3 } from "fs";
55112
- import { join as join34 } from "path";
55182
+ import { join as join35 } from "path";
55113
55183
  function extractPromptText2(parts) {
55114
55184
  return parts.filter((p) => p.type === "text" && p.text !== undefined).map((p) => p.text ?? "").join(" ");
55115
55185
  }
@@ -55127,7 +55197,7 @@ function findMostRecentPlanFile(plansDir) {
55127
55197
  return null;
55128
55198
  let mostRecent = null;
55129
55199
  for (const entry of entries) {
55130
- const fullPath = join34(plansDir, entry);
55200
+ const fullPath = join35(plansDir, entry);
55131
55201
  try {
55132
55202
  const stats = statSync3(fullPath);
55133
55203
  if (!stats.isFile())
@@ -55162,7 +55232,7 @@ function createDesignIntentPreserverHook(_ctx) {
55162
55232
  if (!hasPhaseTransition(promptText)) {
55163
55233
  return;
55164
55234
  }
55165
- const plansDir = join35(_ctx.directory, PLANS_DIR2);
55235
+ const plansDir = join36(_ctx.directory, PLANS_DIR2);
55166
55236
  const planPath = findMostRecentPlanFile(plansDir);
55167
55237
  if (!planPath) {
55168
55238
  return;
@@ -55234,11 +55304,11 @@ import { dirname as dirname7 } from "path";
55234
55304
 
55235
55305
  // src/hooks/directory-agents-injector/finder.ts
55236
55306
  import { access } from "fs/promises";
55237
- import { dirname as dirname6, join as join37, resolve as resolve2 } from "path";
55307
+ import { dirname as dirname6, join as join38, resolve as resolve2 } from "path";
55238
55308
 
55239
55309
  // src/hooks/directory-agents-injector/constants.ts
55240
- import { join as join36 } from "path";
55241
- var AGENTS_INJECTOR_STORAGE = join36(OPENCODE_STORAGE, "directory-agents");
55310
+ import { join as join37 } from "path";
55311
+ var AGENTS_INJECTOR_STORAGE = join37(OPENCODE_STORAGE, "directory-agents");
55242
55312
  var AGENTS_FILENAME = "AGENTS.md";
55243
55313
 
55244
55314
  // src/hooks/directory-agents-injector/finder.ts
@@ -55255,7 +55325,7 @@ async function findAgentsMdUp(input) {
55255
55325
  while (true) {
55256
55326
  const isRootDir = current === input.rootDir;
55257
55327
  if (!isRootDir) {
55258
- const agentsPath = join37(current, AGENTS_FILENAME);
55328
+ const agentsPath = join38(current, AGENTS_FILENAME);
55259
55329
  const exists = await access(agentsPath).then(() => true).catch(() => false);
55260
55330
  if (exists) {
55261
55331
  found.push(agentsPath);
@@ -55275,18 +55345,18 @@ async function findAgentsMdUp(input) {
55275
55345
 
55276
55346
  // src/shared/session-injected-paths.ts
55277
55347
  import {
55278
- existsSync as existsSync31,
55348
+ existsSync as existsSync32,
55279
55349
  mkdirSync as mkdirSync9,
55280
55350
  readFileSync as readFileSync25,
55281
55351
  unlinkSync as unlinkSync4,
55282
55352
  writeFileSync as writeFileSync12
55283
55353
  } from "fs";
55284
- import { join as join38 } from "path";
55354
+ import { join as join39 } from "path";
55285
55355
  function createInjectedPathsStorage(storageDir) {
55286
- const getStoragePath2 = (sessionID) => join38(storageDir, `${sessionID}.json`);
55356
+ const getStoragePath2 = (sessionID) => join39(storageDir, `${sessionID}.json`);
55287
55357
  const loadInjectedPaths = (sessionID) => {
55288
55358
  const filePath = getStoragePath2(sessionID);
55289
- if (!existsSync31(filePath))
55359
+ if (!existsSync32(filePath))
55290
55360
  return new Set;
55291
55361
  try {
55292
55362
  const content = readFileSync25(filePath, "utf-8");
@@ -55297,7 +55367,7 @@ function createInjectedPathsStorage(storageDir) {
55297
55367
  }
55298
55368
  };
55299
55369
  const saveInjectedPaths = (sessionID, paths) => {
55300
- if (!existsSync31(storageDir)) {
55370
+ if (!existsSync32(storageDir)) {
55301
55371
  mkdirSync9(storageDir, { recursive: true });
55302
55372
  }
55303
55373
  const data = {
@@ -55309,7 +55379,7 @@ function createInjectedPathsStorage(storageDir) {
55309
55379
  };
55310
55380
  const clearInjectedPaths = (sessionID) => {
55311
55381
  const filePath = getStoragePath2(sessionID);
55312
- if (existsSync31(filePath)) {
55382
+ if (existsSync32(filePath)) {
55313
55383
  unlinkSync4(filePath);
55314
55384
  }
55315
55385
  };
@@ -58822,8 +58892,8 @@ ${originalText}`;
58822
58892
  init_logger();
58823
58893
 
58824
58894
  // src/hooks/interactive-bash-session/constants.ts
58825
- import { join as join41 } from "path";
58826
- var INTERACTIVE_BASH_SESSION_STORAGE = join41(OPENCODE_STORAGE, "interactive-bash-session");
58895
+ import { join as join42 } from "path";
58896
+ var INTERACTIVE_BASH_SESSION_STORAGE = join42(OPENCODE_STORAGE, "interactive-bash-session");
58827
58897
  var MATRIXX_SESSION_PREFIX = "matrixx-";
58828
58898
  function buildSessionReminderMessage(sessions) {
58829
58899
  if (sessions.length === 0)
@@ -58918,19 +58988,19 @@ function findSubcommand(tokens) {
58918
58988
 
58919
58989
  // src/hooks/interactive-bash-session/storage.ts
58920
58990
  import {
58921
- existsSync as existsSync33,
58991
+ existsSync as existsSync34,
58922
58992
  mkdirSync as mkdirSync12,
58923
58993
  readFileSync as readFileSync27,
58924
58994
  unlinkSync as unlinkSync6,
58925
58995
  writeFileSync as writeFileSync14
58926
58996
  } from "fs";
58927
- import { join as join42 } from "path";
58997
+ import { join as join43 } from "path";
58928
58998
  function getStoragePath2(sessionID) {
58929
- return join42(INTERACTIVE_BASH_SESSION_STORAGE, `${sessionID}.json`);
58999
+ return join43(INTERACTIVE_BASH_SESSION_STORAGE, `${sessionID}.json`);
58930
59000
  }
58931
59001
  function loadInteractiveBashSessionState(sessionID) {
58932
59002
  const filePath = getStoragePath2(sessionID);
58933
- if (!existsSync33(filePath))
59003
+ if (!existsSync34(filePath))
58934
59004
  return null;
58935
59005
  try {
58936
59006
  const content = readFileSync27(filePath, "utf-8");
@@ -58945,7 +59015,7 @@ function loadInteractiveBashSessionState(sessionID) {
58945
59015
  }
58946
59016
  }
58947
59017
  function saveInteractiveBashSessionState(state3) {
58948
- if (!existsSync33(INTERACTIVE_BASH_SESSION_STORAGE)) {
59018
+ if (!existsSync34(INTERACTIVE_BASH_SESSION_STORAGE)) {
58949
59019
  mkdirSync12(INTERACTIVE_BASH_SESSION_STORAGE, { recursive: true });
58950
59020
  }
58951
59021
  const filePath = getStoragePath2(state3.sessionID);
@@ -58958,7 +59028,7 @@ function saveInteractiveBashSessionState(state3) {
58958
59028
  }
58959
59029
  function clearInteractiveBashSessionState(sessionID) {
58960
59030
  const filePath = getStoragePath2(sessionID);
58961
- if (existsSync33(filePath)) {
59031
+ if (existsSync34(filePath)) {
58962
59032
  unlinkSync6(filePath);
58963
59033
  }
58964
59034
  }
@@ -59302,7 +59372,7 @@ init_logger();
59302
59372
 
59303
59373
  // src/hooks/matrix-loop/completion-promise-detector.ts
59304
59374
  init_logger();
59305
- import { existsSync as existsSync35, readFileSync as readFileSync29 } from "fs";
59375
+ import { existsSync as existsSync36, readFileSync as readFileSync29 } from "fs";
59306
59376
  function escapeRegex2(str) {
59307
59377
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59308
59378
  }
@@ -59313,7 +59383,7 @@ function detectCompletionInTranscript(transcriptPath, promise2) {
59313
59383
  if (!transcriptPath)
59314
59384
  return false;
59315
59385
  try {
59316
- if (!existsSync35(transcriptPath))
59386
+ if (!existsSync36(transcriptPath))
59317
59387
  return false;
59318
59388
  const content = readFileSync29(transcriptPath, "utf-8");
59319
59389
  const pattern = buildPromisePattern(promise2);
@@ -60096,13 +60166,13 @@ function createOracleMdOnlyHook(ctx) {
60096
60166
  };
60097
60167
  }
60098
60168
  // src/hooks/plan-persister/hook.ts
60099
- import { existsSync as existsSync37, readdirSync as readdirSync12 } from "fs";
60169
+ import { existsSync as existsSync38, readdirSync as readdirSync12 } from "fs";
60100
60170
 
60101
60171
  // src/features/task-storage/storage.ts
60102
60172
  init_logger();
60103
60173
  import { randomUUID } from "crypto";
60104
- import { existsSync as existsSync36, mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync30, renameSync as renameSync3, unlinkSync as unlinkSync8, writeFileSync as writeFileSync16 } from "fs";
60105
- import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute3, join as join44 } from "path";
60174
+ import { existsSync as existsSync37, mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync30, renameSync as renameSync3, unlinkSync as unlinkSync8, writeFileSync as writeFileSync16 } from "fs";
60175
+ import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute3, join as join45 } from "path";
60106
60176
 
60107
60177
  // src/shared/task-system-gating.ts
60108
60178
  var TASK_SYSTEM_DEFAULT = true;
@@ -60126,18 +60196,18 @@ function isTaskSystemEnabled(config2) {
60126
60196
 
60127
60197
  // src/features/task-storage/storage.ts
60128
60198
  function getProjectTaskDir(directory) {
60129
- return join44(directory, ".matrixx", "tasks");
60199
+ return join45(directory, ".matrixx", "tasks");
60130
60200
  }
60131
60201
  function getTaskDir(config2 = {}, directory) {
60132
60202
  const tasksConfig = resolveTasksConfig(config2);
60133
60203
  const storagePath = tasksConfig.storage_path;
60134
60204
  if (storagePath) {
60135
- return isAbsolute3(storagePath) ? storagePath : join44(directory ?? process.cwd(), storagePath);
60205
+ return isAbsolute3(storagePath) ? storagePath : join45(directory ?? process.cwd(), storagePath);
60136
60206
  }
60137
60207
  if (tasksConfig.scope === "global" || !directory) {
60138
60208
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
60139
60209
  const listId = resolveTaskListId(config2);
60140
- return join44(configDir, "tasks", listId);
60210
+ return join45(configDir, "tasks", listId);
60141
60211
  }
60142
60212
  return getProjectTaskDir(directory);
60143
60213
  }
@@ -60154,13 +60224,13 @@ function resolveTaskListId(config2 = {}) {
60154
60224
  return sanitizePathSegment(basename5(process.cwd()));
60155
60225
  }
60156
60226
  function ensureDir2(dirPath) {
60157
- if (!existsSync36(dirPath)) {
60227
+ if (!existsSync37(dirPath)) {
60158
60228
  mkdirSync14(dirPath, { recursive: true });
60159
60229
  }
60160
60230
  }
60161
60231
  function readJsonSafe(filePath, schema) {
60162
60232
  try {
60163
- if (!existsSync36(filePath)) {
60233
+ if (!existsSync37(filePath)) {
60164
60234
  return null;
60165
60235
  }
60166
60236
  const content = readFileSync30(filePath, "utf-8");
@@ -60183,7 +60253,7 @@ function writeJsonAtomic(filePath, data) {
60183
60253
  renameSync3(tempPath, filePath);
60184
60254
  } catch (error51) {
60185
60255
  try {
60186
- if (existsSync36(tempPath)) {
60256
+ if (existsSync37(tempPath)) {
60187
60257
  unlinkSync8(tempPath);
60188
60258
  }
60189
60259
  } catch {}
@@ -60207,8 +60277,8 @@ function generateTaskId() {
60207
60277
  }
60208
60278
  function migrateLegacyTasksIfNeeded(config2, directory) {
60209
60279
  const projectDir = getProjectTaskDir(directory);
60210
- const legacyDir = join44(getOpenCodeConfigDir({ binary: "opencode" }), "tasks", resolveTaskListId(config2));
60211
- if (existsSync36(projectDir)) {
60280
+ const legacyDir = join45(getOpenCodeConfigDir({ binary: "opencode" }), "tasks", resolveTaskListId(config2));
60281
+ if (existsSync37(projectDir)) {
60212
60282
  try {
60213
60283
  const existing = readdirSync11(projectDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
60214
60284
  if (existing.length > 0) {
@@ -60216,7 +60286,7 @@ function migrateLegacyTasksIfNeeded(config2, directory) {
60216
60286
  }
60217
60287
  } catch {}
60218
60288
  }
60219
- if (!existsSync36(legacyDir)) {
60289
+ if (!existsSync37(legacyDir)) {
60220
60290
  return { migrated: 0, legacyDir, projectDir };
60221
60291
  }
60222
60292
  let files = [];
@@ -60231,9 +60301,9 @@ function migrateLegacyTasksIfNeeded(config2, directory) {
60231
60301
  ensureDir2(projectDir);
60232
60302
  let migrated = 0;
60233
60303
  for (const file2 of files) {
60234
- const src = join44(legacyDir, file2);
60235
- const dest = join44(projectDir, file2);
60236
- if (existsSync36(dest))
60304
+ const src = join45(legacyDir, file2);
60305
+ const dest = join45(projectDir, file2);
60306
+ if (existsSync37(dest))
60237
60307
  continue;
60238
60308
  try {
60239
60309
  const content = readFileSync30(src, "utf-8");
@@ -60247,7 +60317,7 @@ function migrateLegacyTasksIfNeeded(config2, directory) {
60247
60317
  return { migrated, legacyDir, projectDir };
60248
60318
  }
60249
60319
  function acquireLock(dirPath) {
60250
- const lockPath = join44(dirPath, ".lock");
60320
+ const lockPath = join45(dirPath, ".lock");
60251
60321
  const lockId = randomUUID();
60252
60322
  const createLock = (timestamp) => {
60253
60323
  writeFileSync16(lockPath, JSON.stringify({ id: lockId, timestamp }), {
@@ -60295,7 +60365,7 @@ function acquireLock(dirPath) {
60295
60365
  acquired: true,
60296
60366
  release: () => {
60297
60367
  try {
60298
- if (!existsSync36(lockPath))
60368
+ if (!existsSync37(lockPath))
60299
60369
  return;
60300
60370
  const lockContent = readFileSync30(lockPath, "utf-8");
60301
60371
  const lockData = JSON.parse(lockContent);
@@ -60437,12 +60507,12 @@ function createPlanPersister(ctx, options) {
60437
60507
  if (!mission.session_ids?.includes(sessionID))
60438
60508
  return;
60439
60509
  const planPath = mission.active_plan;
60440
- if (!existsSync37(planPath))
60510
+ if (!existsSync38(planPath))
60441
60511
  return;
60442
60512
  let todos = [];
60443
60513
  try {
60444
60514
  const taskDir = getTaskDir({}, directory);
60445
- if (existsSync37(taskDir)) {
60515
+ if (existsSync38(taskDir)) {
60446
60516
  const files = readdirSync12(taskDir).filter((f) => f.startsWith("T-") && f.endsWith(".json"));
60447
60517
  const tasks = [];
60448
60518
  for (const f of files) {
@@ -61770,12 +61840,12 @@ function createRtkBashRewriterHook(_ctx, config2) {
61770
61840
  };
61771
61841
  }
61772
61842
  // src/hooks/rules-injector/project-root-finder.ts
61773
- import { existsSync as existsSync38, statSync as statSync5 } from "fs";
61774
- import { dirname as dirname11, join as join46 } from "path";
61843
+ import { existsSync as existsSync39, statSync as statSync5 } from "fs";
61844
+ import { dirname as dirname11, join as join47 } from "path";
61775
61845
 
61776
61846
  // src/hooks/rules-injector/constants.ts
61777
- import { join as join45 } from "path";
61778
- var RULES_INJECTOR_STORAGE = join45(OPENCODE_STORAGE, "rules-injector");
61847
+ import { join as join46 } from "path";
61848
+ var RULES_INJECTOR_STORAGE = join46(OPENCODE_STORAGE, "rules-injector");
61779
61849
  var PROJECT_MARKERS = [
61780
61850
  ".git",
61781
61851
  "pyproject.toml",
@@ -61806,8 +61876,8 @@ function findProjectRoot(startPath) {
61806
61876
  }
61807
61877
  while (true) {
61808
61878
  for (const marker of PROJECT_MARKERS) {
61809
- const markerPath = join46(current, marker);
61810
- if (existsSync38(markerPath)) {
61879
+ const markerPath = join47(current, marker);
61880
+ if (existsSync39(markerPath)) {
61811
61881
  return current;
61812
61882
  }
61813
61883
  }
@@ -61819,12 +61889,12 @@ function findProjectRoot(startPath) {
61819
61889
  }
61820
61890
  }
61821
61891
  // src/hooks/rules-injector/rule-file-finder.ts
61822
- import { existsSync as existsSync40, statSync as statSync6 } from "fs";
61823
- import { dirname as dirname12, join as join48 } from "path";
61892
+ import { existsSync as existsSync41, statSync as statSync6 } from "fs";
61893
+ import { dirname as dirname12, join as join49 } from "path";
61824
61894
 
61825
61895
  // src/hooks/rules-injector/rule-file-scanner.ts
61826
- import { existsSync as existsSync39, readdirSync as readdirSync13, realpathSync as realpathSync2 } from "fs";
61827
- import { join as join47 } from "path";
61896
+ import { existsSync as existsSync40, readdirSync as readdirSync13, realpathSync as realpathSync2 } from "fs";
61897
+ import { join as join48 } from "path";
61828
61898
  function isGitHubInstructionsDir(dir) {
61829
61899
  return dir.includes(".github/instructions") || dir.endsWith(".github/instructions");
61830
61900
  }
@@ -61835,12 +61905,12 @@ function isValidRuleFile(fileName, dir) {
61835
61905
  return RULE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
61836
61906
  }
61837
61907
  function findRuleFilesRecursive(dir, results) {
61838
- if (!existsSync39(dir))
61908
+ if (!existsSync40(dir))
61839
61909
  return;
61840
61910
  try {
61841
61911
  const entries = readdirSync13(dir, { withFileTypes: true });
61842
61912
  for (const entry of entries) {
61843
- const fullPath = join47(dir, entry.name);
61913
+ const fullPath = join48(dir, entry.name);
61844
61914
  if (entry.isDirectory()) {
61845
61915
  findRuleFilesRecursive(fullPath, results);
61846
61916
  } else if (entry.isFile()) {
@@ -61867,7 +61937,7 @@ function findRuleFiles(projectRoot, currentFile) {
61867
61937
  let distance = 0;
61868
61938
  while (true) {
61869
61939
  for (const [parent, subdir] of PROJECT_RULE_SUBDIRS) {
61870
- const ruleDir = join48(currentDir, parent, subdir);
61940
+ const ruleDir = join49(currentDir, parent, subdir);
61871
61941
  const files = [];
61872
61942
  findRuleFilesRecursive(ruleDir, files);
61873
61943
  for (const filePath of files) {
@@ -61893,8 +61963,8 @@ function findRuleFiles(projectRoot, currentFile) {
61893
61963
  }
61894
61964
  if (projectRoot) {
61895
61965
  for (const ruleFile of PROJECT_RULE_FILES) {
61896
- const filePath = join48(projectRoot, ruleFile);
61897
- if (existsSync40(filePath)) {
61966
+ const filePath = join49(projectRoot, ruleFile);
61967
+ if (existsSync41(filePath)) {
61898
61968
  try {
61899
61969
  const stat = statSync6(filePath);
61900
61970
  if (stat.isFile()) {
@@ -61924,19 +61994,19 @@ function findRuleFiles(projectRoot, currentFile) {
61924
61994
  }
61925
61995
  // src/hooks/rules-injector/storage.ts
61926
61996
  import {
61927
- existsSync as existsSync41,
61997
+ existsSync as existsSync42,
61928
61998
  mkdirSync as mkdirSync15,
61929
61999
  readFileSync as readFileSync31,
61930
62000
  unlinkSync as unlinkSync9,
61931
62001
  writeFileSync as writeFileSync17
61932
62002
  } from "fs";
61933
- import { join as join49 } from "path";
62003
+ import { join as join50 } from "path";
61934
62004
  function getStoragePath3(sessionID) {
61935
- return join49(RULES_INJECTOR_STORAGE, `${sessionID}.json`);
62005
+ return join50(RULES_INJECTOR_STORAGE, `${sessionID}.json`);
61936
62006
  }
61937
62007
  function loadInjectedRules(sessionID) {
61938
62008
  const filePath = getStoragePath3(sessionID);
61939
- if (!existsSync41(filePath))
62009
+ if (!existsSync42(filePath))
61940
62010
  return { contentHashes: new Set, realPaths: new Set };
61941
62011
  try {
61942
62012
  const content = readFileSync31(filePath, "utf-8");
@@ -61950,7 +62020,7 @@ function loadInjectedRules(sessionID) {
61950
62020
  }
61951
62021
  }
61952
62022
  function saveInjectedRules(sessionID, data) {
61953
- if (!existsSync41(RULES_INJECTOR_STORAGE)) {
62023
+ if (!existsSync42(RULES_INJECTOR_STORAGE)) {
61954
62024
  mkdirSync15(RULES_INJECTOR_STORAGE, { recursive: true });
61955
62025
  }
61956
62026
  const storageData = {
@@ -61963,7 +62033,7 @@ function saveInjectedRules(sessionID, data) {
61963
62033
  }
61964
62034
  function clearInjectedRules(sessionID) {
61965
62035
  const filePath = getStoragePath3(sessionID);
61966
- if (existsSync41(filePath)) {
62036
+ if (existsSync42(filePath)) {
61967
62037
  unlinkSync9(filePath);
61968
62038
  }
61969
62039
  }
@@ -64921,8 +64991,8 @@ var FAILURE_RESET_WINDOW_MS = 5 * 60 * 1000;
64921
64991
  init_logger();
64922
64992
 
64923
64993
  // src/hooks/task-continuation-enforcer/idle-event.ts
64924
- import { existsSync as existsSync43, readdirSync as readdirSync15 } from "fs";
64925
- import { join as join52 } from "path";
64994
+ import { existsSync as existsSync44, readdirSync as readdirSync15 } from "fs";
64995
+ import { join as join53 } from "path";
64926
64996
  init_logger();
64927
64997
 
64928
64998
  // src/hooks/task-continuation-enforcer/abort-detection.ts
@@ -64945,12 +65015,12 @@ function isLastAssistantMessageAborted(messages) {
64945
65015
  init_logger();
64946
65016
 
64947
65017
  // src/hooks/task-continuation-enforcer/continuation-injection.ts
64948
- import { existsSync as existsSync42, readdirSync as readdirSync14 } from "fs";
64949
- import { join as join51 } from "path";
65018
+ import { existsSync as existsSync43, readdirSync as readdirSync14 } from "fs";
65019
+ import { join as join52 } from "path";
64950
65020
  init_logger();
64951
65021
  // src/hooks/task-continuation-enforcer/staleness.ts
64952
65022
  import { statSync as statSync8 } from "fs";
64953
- import { join as join50 } from "path";
65023
+ import { join as join51 } from "path";
64954
65024
  var DEFAULT_STALE_AFTER_HOURS = 24;
64955
65025
  var HOUR_MS = 60 * 60 * 1000;
64956
65026
  function getStaleAfterMs(config2) {
@@ -64970,7 +65040,7 @@ function isTaskStale(taskPath, staleAfterMs) {
64970
65040
  return ageMs !== null && ageMs > staleAfterMs;
64971
65041
  }
64972
65042
  function filterFreshIncompleteTasks(tasks, taskDir, staleAfterMs) {
64973
- return tasks.filter((t) => !isTaskStale(join50(taskDir, `${t.id}.json`), staleAfterMs));
65043
+ return tasks.filter((t) => !isTaskStale(join51(taskDir, `${t.id}.json`), staleAfterMs));
64974
65044
  }
64975
65045
  function formatTaskAge(ageMs) {
64976
65046
  const hours = Math.floor(ageMs / HOUR_MS);
@@ -65058,7 +65128,7 @@ async function injectContinuation(args) {
65058
65128
  let taskDir = "";
65059
65129
  try {
65060
65130
  taskDir = getTaskDir(config2, ctx.directory);
65061
- if (!existsSync42(taskDir)) {
65131
+ if (!existsSync43(taskDir)) {
65062
65132
  const hadBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).length > 0 : false;
65063
65133
  if (hadBgTasks) {
65064
65134
  log(`[${HOOK_NAME14}] Bootstrap injection: no task dir (hadBgTasks)`, { sessionID, taskDir });
@@ -65141,7 +65211,7 @@ async function injectContinuation(args) {
65141
65211
  const incompleteTasks = getIncompleteTasks(filteredTasks);
65142
65212
  const freshIncompleteTasks = filterFreshIncompleteTasks(incompleteTasks, taskDir, staleAfterMs);
65143
65213
  const taskList = incompleteTasks.map((task) => {
65144
- const ageMs = getTaskAgeMs(join51(taskDir, `${task.id}.json`));
65214
+ const ageMs = getTaskAgeMs(join52(taskDir, `${task.id}.json`));
65145
65215
  const staleSuffix = ageMs !== null && ageMs > staleAfterMs ? ` (stale: ${formatTaskAge(ageMs)})` : "";
65146
65216
  return `- [${task.status}] ${task.subject} (${task.id})${staleSuffix}`;
65147
65217
  }).join(`
@@ -65320,7 +65390,7 @@ async function handleSessionIdle(args) {
65320
65390
  let taskDir = "";
65321
65391
  try {
65322
65392
  taskDir = getTaskDir(config2, ctx.directory);
65323
- if (!existsSync43(taskDir)) {
65393
+ if (!existsSync44(taskDir)) {
65324
65394
  const hadBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).length > 0 : false;
65325
65395
  if (hadBgTasks) {
65326
65396
  log(`[${HOOK_NAME14}] No task dir \u2014 bootstrap (hadBgTasks)`, { sessionID, taskDir });
@@ -65359,7 +65429,7 @@ async function handleSessionIdle(args) {
65359
65429
  const incompleteTasks = getIncompleteTasks(filteredTasks);
65360
65430
  incompleteCount = incompleteTasks.length;
65361
65431
  const staleAfterMs = getStaleAfterMs(config2);
65362
- const staleIncompleteCount = incompleteTasks.filter((t) => isTaskStale(join52(taskDir, `${t.id}.json`), staleAfterMs)).length;
65432
+ const staleIncompleteCount = incompleteTasks.filter((t) => isTaskStale(join53(taskDir, `${t.id}.json`), staleAfterMs)).length;
65363
65433
  const activeIncompleteCount = incompleteCount - staleIncompleteCount;
65364
65434
  if (activeIncompleteCount === 0 && staleIncompleteCount > 0) {
65365
65435
  log(`[${HOOK_NAME14}] Skipped: only stale tasks remain`, {
@@ -65806,8 +65876,8 @@ var TASK_NOTEPAD_FRAGMENT = "task-notepad";
65806
65876
  var TODO_FETCH_ERROR_FRAGMENT = "Failed to fetch todos for task-notepad";
65807
65877
  // src/hooks/task-notepad/hook.ts
65808
65878
  init_logger();
65809
- import { existsSync as existsSync44, mkdirSync as mkdirSync16, readdirSync as readdirSync16, readFileSync as readFileSync33, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
65810
- import { dirname as dirname13, join as join53 } from "path";
65879
+ import { existsSync as existsSync45, mkdirSync as mkdirSync16, readdirSync as readdirSync16, readFileSync as readFileSync33, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
65880
+ import { dirname as dirname13, join as join54 } from "path";
65811
65881
  function createTaskNotepadHook(ctx) {
65812
65882
  return {
65813
65883
  "tool.execute.after": async (input, output) => {
@@ -65858,8 +65928,8 @@ async function fetchTodos(ctx, sessionID) {
65858
65928
  }
65859
65929
  }
65860
65930
  function resolveActivePlan(directory) {
65861
- const plansDir = join53(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
65862
- if (!existsSync44(plansDir))
65931
+ const plansDir = join54(directory, PLAN_DIR_NAME, PLANS_SUBDIR);
65932
+ if (!existsSync45(plansDir))
65863
65933
  return null;
65864
65934
  let entries;
65865
65935
  try {
@@ -65871,7 +65941,7 @@ function resolveActivePlan(directory) {
65871
65941
  for (const entry of entries) {
65872
65942
  if (!entry.endsWith(PLAN_EXTENSION))
65873
65943
  continue;
65874
- const fullPath = join53(plansDir, entry);
65944
+ const fullPath = join54(plansDir, entry);
65875
65945
  try {
65876
65946
  const stats = statSync9(fullPath);
65877
65947
  if (!stats.isFile())
@@ -65890,7 +65960,7 @@ function resolveActivePlan(directory) {
65890
65960
  }
65891
65961
  function ensureNotepad(directory, planName, todo) {
65892
65962
  const notepadPath = notepadPathFor(directory, planName, todo.id);
65893
- if (existsSync44(notepadPath))
65963
+ if (existsSync45(notepadPath))
65894
65964
  return;
65895
65965
  mkdirSync16(dirname13(notepadPath), { recursive: true });
65896
65966
  const content = renderNotepad(todo);
@@ -65899,7 +65969,7 @@ function ensureNotepad(directory, planName, todo) {
65899
65969
  }
65900
65970
  function appendCompletion(directory, planName, todo) {
65901
65971
  const notepadPath = notepadPathFor(directory, planName, todo.id);
65902
- if (!existsSync44(notepadPath))
65972
+ if (!existsSync45(notepadPath))
65903
65973
  return;
65904
65974
  const existing = readFileSync33(notepadPath, "utf-8");
65905
65975
  if (existing.includes("**Status**: completed"))
@@ -65914,7 +65984,7 @@ function appendCompletion(directory, planName, todo) {
65914
65984
  log(`[${HOOK_NAME16}] Appended completion to notepad`, { plan: planName, taskID: todo.id });
65915
65985
  }
65916
65986
  function notepadPathFor(directory, planName, taskID) {
65917
- return join53(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
65987
+ return join54(directory, PLAN_DIR_NAME, NOTEPAD_DIR_NAME, planName, TASKS_SUBDIR, `${taskID}${NOTEPAD_EXTENSION}`);
65918
65988
  }
65919
65989
  function renderNotepad(todo) {
65920
65990
  const priority = todo.priority ?? "medium";
@@ -67589,8 +67659,8 @@ function createWebFetchRedirectGuardHook(_ctx) {
67589
67659
  };
67590
67660
  }
67591
67661
  // src/hooks/write-existing-file-guard/hook.ts
67592
- import { existsSync as existsSync45 } from "fs";
67593
- import { isAbsolute as isAbsolute4, join as join54, normalize, resolve as resolve6, sep } from "path";
67662
+ import { existsSync as existsSync46 } from "fs";
67663
+ import { isAbsolute as isAbsolute4, join as join55, normalize, resolve as resolve6, sep } from "path";
67594
67664
  function createWriteExistingFileGuardHook(ctx) {
67595
67665
  return {
67596
67666
  "tool.execute.before": async (input, output) => {
@@ -67604,8 +67674,8 @@ function createWriteExistingFileGuardHook(ctx) {
67604
67674
  return;
67605
67675
  }
67606
67676
  const resolvedPath = normalize(isAbsolute4(filePath) ? filePath : resolve6(ctx.directory, filePath));
67607
- if (existsSync45(resolvedPath)) {
67608
- const matrixxRoot = join54(ctx.directory, ".matrixx") + sep;
67677
+ if (existsSync46(resolvedPath)) {
67678
+ const matrixxRoot = join55(ctx.directory, ".matrixx") + sep;
67609
67679
  const isMatrixMarkdown = resolvedPath.startsWith(matrixxRoot) && resolvedPath.endsWith(".md");
67610
67680
  const normalizedPath = resolvedPath.replace(/\\/g, "/");
67611
67681
  const isPlansPath = normalizedPath.includes(".matrixx/plans");
@@ -68050,14 +68120,14 @@ function getLanguageId(ext) {
68050
68120
  return EXT_TO_LANG[ext] || "plaintext";
68051
68121
  }
68052
68122
  // src/tools/lsp/server-installation.ts
68053
- import { existsSync as existsSync46 } from "fs";
68054
- import { join as join55 } from "path";
68123
+ import { existsSync as existsSync47 } from "fs";
68124
+ import { join as join56 } from "path";
68055
68125
  function isServerInstalled(command) {
68056
68126
  if (command.length === 0)
68057
68127
  return false;
68058
68128
  const cmd = command[0];
68059
68129
  if (cmd.includes("/") || cmd.includes("\\")) {
68060
- if (existsSync46(cmd))
68130
+ if (existsSync47(cmd))
68061
68131
  return true;
68062
68132
  }
68063
68133
  const isWindows = process.platform === "win32";
@@ -68079,23 +68149,23 @@ function isServerInstalled(command) {
68079
68149
  const paths = pathEnv.split(pathSeparator);
68080
68150
  for (const p of paths) {
68081
68151
  for (const suffix of exts) {
68082
- if (existsSync46(join55(p, cmd + suffix))) {
68152
+ if (existsSync47(join56(p, cmd + suffix))) {
68083
68153
  return true;
68084
68154
  }
68085
68155
  }
68086
68156
  }
68087
68157
  const cwd = process.cwd();
68088
68158
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
68089
- const dataDir = join55(getDataDir(), "opencode");
68159
+ const dataDir = join56(getDataDir(), "opencode");
68090
68160
  const additionalBases = [
68091
- join55(cwd, "node_modules", ".bin"),
68092
- join55(configDir, "bin"),
68093
- join55(configDir, "node_modules", ".bin"),
68094
- join55(dataDir, "bin")
68161
+ join56(cwd, "node_modules", ".bin"),
68162
+ join56(configDir, "bin"),
68163
+ join56(configDir, "node_modules", ".bin"),
68164
+ join56(dataDir, "bin")
68095
68165
  ];
68096
68166
  for (const base of additionalBases) {
68097
68167
  for (const suffix of exts) {
68098
- if (existsSync46(join55(base, cmd + suffix))) {
68168
+ if (existsSync47(join56(base, cmd + suffix))) {
68099
68169
  return true;
68100
68170
  }
68101
68171
  }
@@ -68106,10 +68176,10 @@ function isServerInstalled(command) {
68106
68176
  return false;
68107
68177
  }
68108
68178
  // src/tools/lsp/server-config-loader.ts
68109
- import { existsSync as existsSync47, readFileSync as readFileSync34 } from "fs";
68110
- import { join as join56 } from "path";
68179
+ import { existsSync as existsSync48, readFileSync as readFileSync34 } from "fs";
68180
+ import { join as join57 } from "path";
68111
68181
  function loadJsonFile(path12) {
68112
- if (!existsSync47(path12))
68182
+ if (!existsSync48(path12))
68113
68183
  return null;
68114
68184
  try {
68115
68185
  return parseJsonc(readFileSync34(path12, "utf-8"));
@@ -68121,9 +68191,9 @@ function getConfigPaths3() {
68121
68191
  const cwd = process.cwd();
68122
68192
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
68123
68193
  return {
68124
- project: detectConfigFile(join56(cwd, ".opencode", "matrixx")).path,
68125
- user: detectConfigFile(join56(configDir, "matrixx")).path,
68126
- opencode: detectConfigFile(join56(configDir, "opencode")).path
68194
+ project: detectConfigFile(join57(cwd, ".opencode", "matrixx")).path,
68195
+ user: detectConfigFile(join57(configDir, "matrixx")).path,
68196
+ opencode: detectConfigFile(join57(configDir, "opencode")).path
68127
68197
  };
68128
68198
  }
68129
68199
  function loadAllConfigs() {
@@ -68241,14 +68311,14 @@ import { Readable, Writable } from "stream";
68241
68311
  // src/tools/lsp/lsp-process.ts
68242
68312
  init_logger();
68243
68313
  import { spawn as nodeSpawn } from "child_process";
68244
- import { existsSync as existsSync48, statSync as statSync10 } from "fs";
68314
+ import { existsSync as existsSync49, statSync as statSync10 } from "fs";
68245
68315
  var {spawn: bunSpawn } = globalThis.Bun;
68246
68316
  function shouldUseNodeSpawn() {
68247
68317
  return process.platform === "win32";
68248
68318
  }
68249
68319
  function validateCwd(cwd) {
68250
68320
  try {
68251
- if (!existsSync48(cwd)) {
68321
+ if (!existsSync49(cwd)) {
68252
68322
  return { valid: false, error: `Working directory does not exist: ${cwd}` };
68253
68323
  }
68254
68324
  const stats = statSync10(cwd);
@@ -68971,8 +69041,8 @@ class LSPServerManager {
68971
69041
  }
68972
69042
  var lspManager = LSPServerManager.getInstance();
68973
69043
  // src/tools/lsp/lsp-client-wrapper.ts
68974
- import { existsSync as existsSync49, statSync as statSync11 } from "fs";
68975
- import { dirname as dirname14, extname as extname3, join as join57, resolve as resolve8 } from "path";
69044
+ import { existsSync as existsSync50, statSync as statSync11 } from "fs";
69045
+ import { dirname as dirname14, extname as extname3, join as join58, resolve as resolve8 } from "path";
68976
69046
  import { fileURLToPath as fileURLToPath3 } from "url";
68977
69047
  function findWorkspaceRoot(filePath) {
68978
69048
  let dir = resolve8(filePath);
@@ -68988,7 +69058,7 @@ function findWorkspaceRoot(filePath) {
68988
69058
  let prevDir = "";
68989
69059
  while (dir !== prevDir) {
68990
69060
  for (const marker of markers) {
68991
- if (existsSync49(join57(dir, marker))) {
69061
+ if (existsSync50(join58(dir, marker))) {
68992
69062
  return dir;
68993
69063
  }
68994
69064
  }
@@ -81798,12 +81868,12 @@ var lsp_symbols = tool({
81798
81868
  }
81799
81869
  });
81800
81870
  // src/tools/ast-grep/cli.ts
81801
- import { existsSync as existsSync53 } from "fs";
81871
+ import { existsSync as existsSync54 } from "fs";
81802
81872
  var {spawn: spawn9 } = globalThis.Bun;
81803
81873
 
81804
81874
  // src/tools/ast-grep/cli-binary-path-resolution.ts
81805
81875
  init_logger();
81806
- import { existsSync as existsSync52 } from "fs";
81876
+ import { existsSync as existsSync53 } from "fs";
81807
81877
 
81808
81878
  // src/tools/ast-grep/language-support.ts
81809
81879
  var CLI_LANGUAGES = [
@@ -81838,15 +81908,15 @@ var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
81838
81908
  var DEFAULT_MAX_MATCHES = 500;
81839
81909
 
81840
81910
  // src/tools/ast-grep/sg-cli-path.ts
81841
- import { existsSync as existsSync51, statSync as statSync12 } from "fs";
81911
+ import { existsSync as existsSync52, statSync as statSync12 } from "fs";
81842
81912
  import { createRequire as createRequire4 } from "module";
81843
- import { dirname as dirname15, join as join59 } from "path";
81913
+ import { dirname as dirname15, join as join60 } from "path";
81844
81914
 
81845
81915
  // src/tools/ast-grep/downloader.ts
81846
- import { existsSync as existsSync50 } from "fs";
81916
+ import { existsSync as existsSync51 } from "fs";
81847
81917
  import { createRequire as createRequire3 } from "module";
81848
- import { homedir as homedir8 } from "os";
81849
- import { join as join58 } from "path";
81918
+ import { homedir as homedir9 } from "os";
81919
+ import { join as join59 } from "path";
81850
81920
  init_logger();
81851
81921
  var REPO2 = "ast-grep/ast-grep";
81852
81922
  var DEFAULT_VERSION = "0.40.0";
@@ -81871,12 +81941,12 @@ var PLATFORM_MAP2 = {
81871
81941
  function getCacheDir3() {
81872
81942
  if (process.platform === "win32") {
81873
81943
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
81874
- const base2 = localAppData || join58(homedir8(), "AppData", "Local");
81875
- return join58(base2, "matrixx", "bin");
81944
+ const base2 = localAppData || join59(homedir9(), "AppData", "Local");
81945
+ return join59(base2, "matrixx", "bin");
81876
81946
  }
81877
81947
  const xdgCache = process.env.XDG_CACHE_HOME;
81878
- const base = xdgCache || join58(homedir8(), ".cache");
81879
- return join58(base, "matrixx", "bin");
81948
+ const base = xdgCache || join59(homedir9(), ".cache");
81949
+ return join59(base, "matrixx", "bin");
81880
81950
  }
81881
81951
  function getBinaryName3() {
81882
81952
  return process.platform === "win32" ? "sg.exe" : "sg";
@@ -81893,8 +81963,8 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81893
81963
  }
81894
81964
  const cacheDir = getCacheDir3();
81895
81965
  const binaryName = getBinaryName3();
81896
- const binaryPath = join58(cacheDir, binaryName);
81897
- if (existsSync50(binaryPath)) {
81966
+ const binaryPath = join59(cacheDir, binaryName);
81967
+ if (existsSync51(binaryPath)) {
81898
81968
  return binaryPath;
81899
81969
  }
81900
81970
  const { arch, os: os7 } = platformInfo;
@@ -81902,7 +81972,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
81902
81972
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version3}/${assetName}`;
81903
81973
  log(`[matrixx] Downloading ast-grep binary...`);
81904
81974
  try {
81905
- const archivePath = join58(cacheDir, assetName);
81975
+ const archivePath = join59(cacheDir, assetName);
81906
81976
  ensureCacheDir(cacheDir);
81907
81977
  await downloadArchive(downloadUrl, archivePath);
81908
81978
  await extractZipArchive(archivePath, cacheDir);
@@ -81956,8 +82026,8 @@ function findSgCliPathSync() {
81956
82026
  const require2 = createRequire4(import.meta.url);
81957
82027
  const cliPackageJsonPath = require2.resolve("@ast-grep/cli/package.json");
81958
82028
  const cliDirectory = dirname15(cliPackageJsonPath);
81959
- const sgPath = join59(cliDirectory, binaryName);
81960
- if (existsSync51(sgPath) && isValidBinary(sgPath)) {
82029
+ const sgPath = join60(cliDirectory, binaryName);
82030
+ if (existsSync52(sgPath) && isValidBinary(sgPath)) {
81961
82031
  return sgPath;
81962
82032
  }
81963
82033
  } catch {}
@@ -81968,8 +82038,8 @@ function findSgCliPathSync() {
81968
82038
  const packageJsonPath = require2.resolve(`${platformPackage}/package.json`);
81969
82039
  const packageDirectory = dirname15(packageJsonPath);
81970
82040
  const astGrepBinaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
81971
- const binaryPath = join59(packageDirectory, astGrepBinaryName);
81972
- if (existsSync51(binaryPath) && isValidBinary(binaryPath)) {
82041
+ const binaryPath = join60(packageDirectory, astGrepBinaryName);
82042
+ if (existsSync52(binaryPath) && isValidBinary(binaryPath)) {
81973
82043
  return binaryPath;
81974
82044
  }
81975
82045
  } catch {}
@@ -81977,7 +82047,7 @@ function findSgCliPathSync() {
81977
82047
  if (process.platform === "darwin") {
81978
82048
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
81979
82049
  for (const path12 of homebrewPaths) {
81980
- if (existsSync51(path12) && isValidBinary(path12)) {
82050
+ if (existsSync52(path12) && isValidBinary(path12)) {
81981
82051
  return path12;
81982
82052
  }
81983
82053
  }
@@ -82003,7 +82073,7 @@ function setSgCliPath(path12) {
82003
82073
  var resolvedCliPath3 = null;
82004
82074
  var initPromise3 = null;
82005
82075
  async function getAstGrepPath() {
82006
- if (resolvedCliPath3 !== null && existsSync52(resolvedCliPath3)) {
82076
+ if (resolvedCliPath3 !== null && existsSync53(resolvedCliPath3)) {
82007
82077
  return resolvedCliPath3;
82008
82078
  }
82009
82079
  if (initPromise3) {
@@ -82011,7 +82081,7 @@ async function getAstGrepPath() {
82011
82081
  }
82012
82082
  initPromise3 = (async () => {
82013
82083
  const syncPath = findSgCliPathSync();
82014
- if (syncPath && existsSync52(syncPath)) {
82084
+ if (syncPath && existsSync53(syncPath)) {
82015
82085
  resolvedCliPath3 = syncPath;
82016
82086
  setSgCliPath(syncPath);
82017
82087
  return syncPath;
@@ -82110,7 +82180,7 @@ async function runSg(options) {
82110
82180
  const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
82111
82181
  args.push(...paths);
82112
82182
  let cliPath = getSgCliPath();
82113
- if (!cliPath || !existsSync53(cliPath)) {
82183
+ if (!cliPath || !existsSync54(cliPath)) {
82114
82184
  const downloadedPath = await getAstGrepPath();
82115
82185
  if (downloadedPath) {
82116
82186
  cliPath = downloadedPath;
@@ -84398,18 +84468,18 @@ var {spawn: spawn10 } = globalThis.Bun;
84398
84468
 
84399
84469
  // src/tools/grep/constants.ts
84400
84470
  import { spawnSync as spawnSync2 } from "child_process";
84401
- import { existsSync as existsSync58 } from "fs";
84402
- import { dirname as dirname16, join as join61 } from "path";
84471
+ import { existsSync as existsSync59 } from "fs";
84472
+ import { dirname as dirname16, join as join62 } from "path";
84403
84473
 
84404
84474
  // src/tools/grep/downloader.ts
84405
- import { existsSync as existsSync57, readdirSync as readdirSync18 } from "fs";
84406
- import { join as join60 } from "path";
84475
+ import { existsSync as existsSync58, readdirSync as readdirSync18 } from "fs";
84476
+ import { join as join61 } from "path";
84407
84477
  function findFileRecursive(dir, filename) {
84408
84478
  try {
84409
84479
  const entries = readdirSync18(dir, { withFileTypes: true, recursive: true });
84410
84480
  for (const entry of entries) {
84411
84481
  if (entry.isFile() && entry.name === filename) {
84412
- return join60(entry.parentPath ?? dir, entry.name);
84482
+ return join61(entry.parentPath ?? dir, entry.name);
84413
84483
  }
84414
84484
  }
84415
84485
  } catch {
@@ -84430,11 +84500,11 @@ function getPlatformKey() {
84430
84500
  }
84431
84501
  function getInstallDir() {
84432
84502
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
84433
- return join60(homeDir, ".cache", "matrixx", "bin");
84503
+ return join61(homeDir, ".cache", "matrixx", "bin");
84434
84504
  }
84435
84505
  function getRgPath() {
84436
84506
  const isWindows = process.platform === "win32";
84437
- return join60(getInstallDir(), isWindows ? "rg.exe" : "rg");
84507
+ return join61(getInstallDir(), isWindows ? "rg.exe" : "rg");
84438
84508
  }
84439
84509
  async function extractTarGz2(archivePath, destDir) {
84440
84510
  const platformKey = getPlatformKey();
@@ -84451,7 +84521,7 @@ async function extractZip2(archivePath, destDir) {
84451
84521
  const binaryName = process.platform === "win32" ? "rg.exe" : "rg";
84452
84522
  const foundPath = findFileRecursive(destDir, binaryName);
84453
84523
  if (foundPath) {
84454
- const destPath = join60(destDir, binaryName);
84524
+ const destPath = join61(destDir, binaryName);
84455
84525
  if (foundPath !== destPath) {
84456
84526
  const { renameSync: renameSync4 } = await import("fs");
84457
84527
  renameSync4(foundPath, destPath);
@@ -84466,13 +84536,13 @@ async function downloadAndInstallRipgrep() {
84466
84536
  }
84467
84537
  const installDir = getInstallDir();
84468
84538
  const rgPath = getRgPath();
84469
- if (existsSync57(rgPath)) {
84539
+ if (existsSync58(rgPath)) {
84470
84540
  return rgPath;
84471
84541
  }
84472
84542
  ensureCacheDir(installDir);
84473
84543
  const filename = `ripgrep-${RG_VERSION}-${config4.platform}.${config4.extension}`;
84474
84544
  const url3 = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`;
84475
- const archivePath = join60(installDir, filename);
84545
+ const archivePath = join61(installDir, filename);
84476
84546
  try {
84477
84547
  await downloadArchive(url3, archivePath);
84478
84548
  if (config4.extension === "tar.gz") {
@@ -84481,7 +84551,7 @@ async function downloadAndInstallRipgrep() {
84481
84551
  await extractZip2(archivePath, installDir);
84482
84552
  }
84483
84553
  ensureExecutable(rgPath);
84484
- if (!existsSync57(rgPath)) {
84554
+ if (!existsSync58(rgPath)) {
84485
84555
  throw new Error("ripgrep binary not found after extraction");
84486
84556
  }
84487
84557
  return rgPath;
@@ -84493,7 +84563,7 @@ async function downloadAndInstallRipgrep() {
84493
84563
  }
84494
84564
  function getInstalledRipgrepPath() {
84495
84565
  const rgPath = getRgPath();
84496
- return existsSync57(rgPath) ? rgPath : null;
84566
+ return existsSync58(rgPath) ? rgPath : null;
84497
84567
  }
84498
84568
 
84499
84569
  // src/tools/grep/constants.ts
@@ -84517,14 +84587,14 @@ function getOpenCodeBundledRg() {
84517
84587
  const isWindows = process.platform === "win32";
84518
84588
  const rgName = isWindows ? "rg.exe" : "rg";
84519
84589
  const candidates = [
84520
- join61(getDataDir(), "opencode", "bin", rgName),
84521
- join61(execDir, rgName),
84522
- join61(execDir, "bin", rgName),
84523
- join61(execDir, "..", "bin", rgName),
84524
- join61(execDir, "..", "libexec", rgName)
84590
+ join62(getDataDir(), "opencode", "bin", rgName),
84591
+ join62(execDir, rgName),
84592
+ join62(execDir, "bin", rgName),
84593
+ join62(execDir, "..", "bin", rgName),
84594
+ join62(execDir, "..", "libexec", rgName)
84525
84595
  ];
84526
84596
  for (const candidate of candidates) {
84527
- if (existsSync58(candidate)) {
84597
+ if (existsSync59(candidate)) {
84528
84598
  return candidate;
84529
84599
  }
84530
84600
  }
@@ -85028,17 +85098,17 @@ var HandoffSchema = exports_external.object({
85028
85098
  sections: HandoffSectionSchema
85029
85099
  });
85030
85100
  // src/features/handoff/storage.ts
85031
- import { existsSync as existsSync59, mkdirSync as mkdirSync17, readFileSync as readFileSync39, renameSync as renameSync4, writeFileSync as writeFileSync22 } from "fs";
85032
- import { join as join62 } from "path";
85101
+ import { existsSync as existsSync60, mkdirSync as mkdirSync17, readFileSync as readFileSync39, renameSync as renameSync4, writeFileSync as writeFileSync22 } from "fs";
85102
+ import { join as join63 } from "path";
85033
85103
  function getHandoffFilePath(directory) {
85034
- return join62(directory, HANDOFF_FILE_PATH);
85104
+ return join63(directory, HANDOFF_FILE_PATH);
85035
85105
  }
85036
85106
  function getHandoffConsumedFilePath(directory) {
85037
- return join62(directory, HANDOFF_CONSUMED_FILE_PATH);
85107
+ return join63(directory, HANDOFF_CONSUMED_FILE_PATH);
85038
85108
  }
85039
85109
  function handoffFileExists(directory) {
85040
85110
  try {
85041
- return existsSync59(getHandoffFilePath(directory));
85111
+ return existsSync60(getHandoffFilePath(directory));
85042
85112
  } catch {
85043
85113
  return false;
85044
85114
  }
@@ -85046,7 +85116,7 @@ function handoffFileExists(directory) {
85046
85116
  function readHandoffFile(directory) {
85047
85117
  const filePath = getHandoffFilePath(directory);
85048
85118
  try {
85049
- if (!existsSync59(filePath))
85119
+ if (!existsSync60(filePath))
85050
85120
  return null;
85051
85121
  return readFileSync39(filePath, "utf-8");
85052
85122
  } catch {
@@ -85072,7 +85142,7 @@ function writeHandoffFile(directory, content) {
85072
85142
  }
85073
85143
  function ensureHandoffDir(directory) {
85074
85144
  try {
85075
- mkdirSync17(join62(directory, ".matrixx"), { recursive: true });
85145
+ mkdirSync17(join63(directory, ".matrixx"), { recursive: true });
85076
85146
  return true;
85077
85147
  } catch {
85078
85148
  return false;
@@ -85227,15 +85297,15 @@ ${body}
85227
85297
  }
85228
85298
 
85229
85299
  // src/tools/handoff/list-handler.ts
85230
- import { existsSync as existsSync60, readdirSync as readdirSync19, statSync as statSync14 } from "fs";
85231
- import { join as join63 } from "path";
85300
+ import { existsSync as existsSync61, readdirSync as readdirSync19, statSync as statSync14 } from "fs";
85301
+ import { join as join64 } from "path";
85232
85302
  var MATRIX_DIR = ".matrixx";
85233
85303
  function isHandoffFile(name) {
85234
85304
  return name === "handoff.md" || name.startsWith("handoff.consumed");
85235
85305
  }
85236
85306
  function handleList(directory) {
85237
- const matrixxDir = join63(directory, MATRIX_DIR);
85238
- if (!existsSync60(matrixxDir)) {
85307
+ const matrixxDir = join64(directory, MATRIX_DIR);
85308
+ if (!existsSync61(matrixxDir)) {
85239
85309
  return "No handoffs found in .matrixx/";
85240
85310
  }
85241
85311
  let entries;
@@ -85252,7 +85322,7 @@ function handleList(directory) {
85252
85322
  lines.push(`Handoff files in ${matrixxDir}:`);
85253
85323
  lines.push("");
85254
85324
  for (const name of handoffs) {
85255
- const fullPath = join63(matrixxDir, name);
85325
+ const fullPath = join64(matrixxDir, name);
85256
85326
  let size = "?";
85257
85327
  let mtime = "?";
85258
85328
  try {
@@ -85476,9 +85546,9 @@ The Bash tool can execute these commands directly. Do NOT retry with interactive
85476
85546
  }
85477
85547
  });
85478
85548
  // src/tools/pdf-extract-figures/tools.ts
85479
- import { existsSync as existsSync61, mkdtempSync, writeFileSync as writeFileSync23 } from "fs";
85549
+ import { existsSync as existsSync62, mkdtempSync, writeFileSync as writeFileSync23 } from "fs";
85480
85550
  import { tmpdir as tmpdir5 } from "os";
85481
- import { join as join64 } from "path";
85551
+ import { join as join65 } from "path";
85482
85552
  var PYTHON_SCRIPT = `#!/usr/bin/env python3
85483
85553
  import sys
85484
85554
  import json
@@ -85580,7 +85650,7 @@ function validateArgs(args) {
85580
85650
  if (!args.file_path) {
85581
85651
  return "Error: 'file_path' is required.";
85582
85652
  }
85583
- if (!existsSync61(args.file_path)) {
85653
+ if (!existsSync62(args.file_path)) {
85584
85654
  return `Error: File not found: ${args.file_path}`;
85585
85655
  }
85586
85656
  if (!args.file_path.toLowerCase().endsWith(".pdf")) {
@@ -85629,11 +85699,11 @@ async function runExtraction(args) {
85629
85699
  }
85630
85700
  let tmpDir;
85631
85701
  try {
85632
- tmpDir = mkdtempSync(join64(tmpdir5(), "matrixx-pdf-extract-"));
85702
+ tmpDir = mkdtempSync(join65(tmpdir5(), "matrixx-pdf-extract-"));
85633
85703
  } catch {
85634
85704
  return "Error: Could not create temporary directory.";
85635
85705
  }
85636
- const scriptPath = join64(tmpDir, "extract_pdf_figures.py");
85706
+ const scriptPath = join65(tmpDir, "extract_pdf_figures.py");
85637
85707
  writeFileSync23(scriptPath, PYTHON_SCRIPT);
85638
85708
  const payload = buildArgsPayload(args);
85639
85709
  log("[pdf_extract_figures] Running extraction", { file_path: args.file_path, payload });
@@ -85681,11 +85751,11 @@ function createPdfExtractFiguresTool() {
85681
85751
  return { pdf_extract_figures };
85682
85752
  }
85683
85753
  // src/tools/plan/plan-create.ts
85684
- import { existsSync as existsSync62 } from "fs";
85754
+ import { existsSync as existsSync63 } from "fs";
85685
85755
  import { basename as basename7 } from "path";
85686
85756
 
85687
85757
  // src/tools/plan/types.ts
85688
- import { basename as basename6, isAbsolute as isAbsolute5, join as join65, relative as relative4, resolve as resolve9 } from "path";
85758
+ import { basename as basename6, isAbsolute as isAbsolute5, join as join66, relative as relative4, resolve as resolve9 } from "path";
85689
85759
 
85690
85760
  // src/tools/plan/constants.ts
85691
85761
  var PLANS_DIR3 = ".matrixx/plans";
@@ -85711,8 +85781,8 @@ function validatePlanFilePath(filePath, directory) {
85711
85781
  if (!PLAN_FILENAME_KEBAB_REGEX.test(base)) {
85712
85782
  return { error: `fileName must match kebab-case ^[a-z0-9-]+\\.md$: ${base}` };
85713
85783
  }
85714
- const resolved = isAbsolute5(filePath) ? resolve9(filePath) : resolve9(join65(directory, filePath));
85715
- const plansDir = resolve9(join65(directory, PLANS_DIR3));
85784
+ const resolved = isAbsolute5(filePath) ? resolve9(filePath) : resolve9(join66(directory, filePath));
85785
+ const plansDir = resolve9(join66(directory, PLANS_DIR3));
85716
85786
  const relToPlans = relative4(plansDir, resolved);
85717
85787
  if (relToPlans.startsWith("..") || isAbsolute5(relToPlans)) {
85718
85788
  return { error: `filePath must be inside ${PLANS_DIR3}: ${filePath}` };
@@ -85755,7 +85825,7 @@ Uses atomicWrite (.tmp.pid+rename) + upsertMetadataComment. Warns if file alread
85755
85825
  }
85756
85826
  const resolved = validation.resolved;
85757
85827
  ensurePlanDir(directory);
85758
- if (existsSync62(resolved)) {
85828
+ if (existsSync63(resolved)) {
85759
85829
  return JSON.stringify({
85760
85830
  error: "file_exists",
85761
85831
  message: `Plan file already exists: ${resolved}. Use plan_read + plan_update to modify.`,
@@ -85786,7 +85856,7 @@ Uses atomicWrite (.tmp.pid+rename) + upsertMetadataComment. Warns if file alread
85786
85856
  });
85787
85857
  }
85788
85858
  // src/tools/plan/plan-delete.ts
85789
- import { existsSync as existsSync63, unlinkSync as unlinkSync10 } from "fs";
85859
+ import { existsSync as existsSync64, unlinkSync as unlinkSync10 } from "fs";
85790
85860
  function createPlanDeleteTool(ctx) {
85791
85861
  return tool({
85792
85862
  description: `Delete a plan file under .matrixx/plans/*.md. Scoped unlink inside PLANS_DIR, refuses outside paths.`,
@@ -85802,7 +85872,7 @@ function createPlanDeleteTool(ctx) {
85802
85872
  return JSON.stringify({ error: "invalid_file_path", message: validation.error });
85803
85873
  }
85804
85874
  const resolved = validation.resolved;
85805
- if (!existsSync63(resolved)) {
85875
+ if (!existsSync64(resolved)) {
85806
85876
  return JSON.stringify({ error: "file_not_found", message: `File not found: ${resolved}` });
85807
85877
  }
85808
85878
  unlinkSync10(resolved);
@@ -85815,8 +85885,8 @@ function createPlanDeleteTool(ctx) {
85815
85885
  });
85816
85886
  }
85817
85887
  // src/tools/plan/plan-list.ts
85818
- import { existsSync as existsSync64, readdirSync as readdirSync20, statSync as statSync15 } from "fs";
85819
- import { join as join66 } from "path";
85888
+ import { existsSync as existsSync65, readdirSync as readdirSync20, statSync as statSync15 } from "fs";
85889
+ import { join as join67 } from "path";
85820
85890
  function createPlanListTool(ctx) {
85821
85891
  return tool({
85822
85892
  description: `List plan files under .matrixx/plans/*.md. Filters *.md with kebab-case, sorts by mtime.`,
@@ -85824,13 +85894,13 @@ function createPlanListTool(ctx) {
85824
85894
  execute: async (_args, context) => {
85825
85895
  try {
85826
85896
  const directory = resolveDirectory(context?.directory, ctx?.directory);
85827
- const plansDir = join66(directory, PLANS_DIR3);
85828
- if (!existsSync64(plansDir)) {
85897
+ const plansDir = join67(directory, PLANS_DIR3);
85898
+ if (!existsSync65(plansDir)) {
85829
85899
  return JSON.stringify({ plans: [] });
85830
85900
  }
85831
85901
  const entries = readdirSync20(plansDir).filter((f) => f.endsWith(".md") && PLAN_FILENAME_KEBAB_REGEX.test(f));
85832
85902
  const plans = entries.map((fileName) => {
85833
- const filePath = join66(plansDir, fileName);
85903
+ const filePath = join67(plansDir, fileName);
85834
85904
  try {
85835
85905
  const stat2 = statSync15(filePath);
85836
85906
  return {
@@ -85853,7 +85923,7 @@ function createPlanListTool(ctx) {
85853
85923
  });
85854
85924
  }
85855
85925
  // src/tools/plan/plan-read.ts
85856
- import { existsSync as existsSync65, statSync as statSync16 } from "fs";
85926
+ import { existsSync as existsSync66, statSync as statSync16 } from "fs";
85857
85927
  function createPlanReadTool(ctx) {
85858
85928
  return tool({
85859
85929
  description: `Read a plan file from .matrixx/plans/*.md with hashline-tagged output (1#AB|content) + lsp-style. Enforces scoping and ${MAX_PLAN_FILE_BYTES2} byte cap.`,
@@ -85869,7 +85939,7 @@ function createPlanReadTool(ctx) {
85869
85939
  return JSON.stringify({ error: "invalid_file_path", message: validation.error });
85870
85940
  }
85871
85941
  const resolved = validation.resolved;
85872
- if (!existsSync65(resolved)) {
85942
+ if (!existsSync66(resolved)) {
85873
85943
  return JSON.stringify({ error: "file_not_found", message: `File not found: ${resolved}` });
85874
85944
  }
85875
85945
  try {
@@ -85901,7 +85971,7 @@ function createPlanReadTool(ctx) {
85901
85971
  });
85902
85972
  }
85903
85973
  // src/tools/plan/plan-update.ts
85904
- import { existsSync as existsSync66 } from "fs";
85974
+ import { existsSync as existsSync67 } from "fs";
85905
85975
 
85906
85976
  // src/features/tool-metadata-store/store.ts
85907
85977
  var pendingStore = new Map;
@@ -86964,7 +87034,7 @@ function createPlanUpdateTool(ctx) {
86964
87034
  return JSON.stringify({ error: "invalid_file_path", message: validation.error });
86965
87035
  }
86966
87036
  const resolved = validation.resolved;
86967
- if (!existsSync66(resolved)) {
87037
+ if (!existsSync67(resolved)) {
86968
87038
  return JSON.stringify({ error: "file_not_found", message: `File not found: ${resolved}` });
86969
87039
  }
86970
87040
  const result = await executeHashlineEditTool({ filePath: resolved, edits }, context, ctx);
@@ -86977,9 +87047,9 @@ function createPlanUpdateTool(ctx) {
86977
87047
  });
86978
87048
  }
86979
87049
  // src/tools/preset/tools.ts
86980
- import { existsSync as existsSync67, mkdirSync as mkdirSync18, readFileSync as readFileSync40, writeFileSync as writeFileSync24 } from "fs";
86981
- import { homedir as homedir9 } from "os";
86982
- import { dirname as dirname17, join as join67 } from "path";
87050
+ import { existsSync as existsSync68, mkdirSync as mkdirSync18, readFileSync as readFileSync40, writeFileSync as writeFileSync24 } from "fs";
87051
+ import { homedir as homedir10 } from "os";
87052
+ import { dirname as dirname17, join as join68 } from "path";
86983
87053
 
86984
87054
  // src/features/preset-state/manager.ts
86985
87055
  init_logger();
@@ -86998,14 +87068,14 @@ var ACTIONS = ["list", "show", "set"];
86998
87068
  var SCOPES = ["global", "project"];
86999
87069
  function resolveConfigPath(scope, directory) {
87000
87070
  if (scope === "global") {
87001
- return join67(homedir9(), ".config", "opencode", "matrixx.jsonc");
87071
+ return join68(homedir10(), ".config", "opencode", "matrixx.jsonc");
87002
87072
  }
87003
- return join67(directory ?? process.cwd(), ".opencode", "matrixx.jsonc");
87073
+ return join68(directory ?? process.cwd(), ".opencode", "matrixx.jsonc");
87004
87074
  }
87005
87075
  function persistActivePreset(presetName, scope, directory) {
87006
87076
  const configPath = resolveConfigPath(scope, directory);
87007
87077
  let raw = {};
87008
- if (existsSync67(configPath)) {
87078
+ if (existsSync68(configPath)) {
87009
87079
  const content = readFileSync40(configPath, "utf-8");
87010
87080
  const parsed = parseJsoncSafe(content);
87011
87081
  if (parsed.errors.length > 0) {
@@ -87119,9 +87189,9 @@ function createPresetTool(options) {
87119
87189
  return { preset };
87120
87190
  }
87121
87191
  // src/tools/session-manager/constants.ts
87122
- import { join as join68 } from "path";
87123
- var TODO_DIR = join68(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
87124
- var TRANSCRIPT_DIR2 = join68(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
87192
+ import { join as join69 } from "path";
87193
+ var TODO_DIR = join69(getOpenCodeConfigDir({ binary: "opencode" }), "todos");
87194
+ var TRANSCRIPT_DIR2 = join69(getOpenCodeConfigDir({ binary: "opencode" }), "transcripts");
87125
87195
  var SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.
87126
87196
 
87127
87197
  Returns a list of available session IDs with metadata including message count, date range, and agents used.
@@ -87193,9 +87263,9 @@ Agents Used: build, oracle, librarian
87193
87263
  Has Todos: Yes (12 items, 8 completed)
87194
87264
  Has Transcript: Yes (234 entries)`;
87195
87265
  // src/tools/session-manager/storage.ts
87196
- import { existsSync as existsSync68 } from "fs";
87266
+ import { existsSync as existsSync69 } from "fs";
87197
87267
  import { readdir, readFile as readFile3 } from "fs/promises";
87198
- import { join as join69 } from "path";
87268
+ import { join as join70 } from "path";
87199
87269
  var sdkClient = null;
87200
87270
  function setStorageClient(client2) {
87201
87271
  sdkClient = client2;
@@ -87214,7 +87284,7 @@ async function getMainSessions(options) {
87214
87284
  return [];
87215
87285
  }
87216
87286
  }
87217
- if (!existsSync68(SESSION_STORAGE))
87287
+ if (!existsSync69(SESSION_STORAGE))
87218
87288
  return [];
87219
87289
  const sessions = [];
87220
87290
  try {
@@ -87222,13 +87292,13 @@ async function getMainSessions(options) {
87222
87292
  for (const projectDir of projectDirs) {
87223
87293
  if (!projectDir.isDirectory())
87224
87294
  continue;
87225
- const projectPath = join69(SESSION_STORAGE, projectDir.name);
87295
+ const projectPath = join70(SESSION_STORAGE, projectDir.name);
87226
87296
  const sessionFiles = await readdir(projectPath);
87227
87297
  for (const file3 of sessionFiles) {
87228
87298
  if (!file3.endsWith(".json"))
87229
87299
  continue;
87230
87300
  try {
87231
- const content = await readFile3(join69(projectPath, file3), "utf-8");
87301
+ const content = await readFile3(join70(projectPath, file3), "utf-8");
87232
87302
  const meta3 = JSON.parse(content);
87233
87303
  if (meta3.parentID)
87234
87304
  continue;
@@ -87253,7 +87323,7 @@ async function getAllSessions() {
87253
87323
  return [];
87254
87324
  }
87255
87325
  }
87256
- if (!existsSync68(MESSAGE_STORAGE))
87326
+ if (!existsSync69(MESSAGE_STORAGE))
87257
87327
  return [];
87258
87328
  const sessions = [];
87259
87329
  async function scanDirectory(dir) {
@@ -87261,7 +87331,7 @@ async function getAllSessions() {
87261
87331
  const entries = await readdir(dir, { withFileTypes: true });
87262
87332
  for (const entry of entries) {
87263
87333
  if (entry.isDirectory()) {
87264
- const sessionPath = join69(dir, entry.name);
87334
+ const sessionPath = join70(dir, entry.name);
87265
87335
  const files = await readdir(sessionPath);
87266
87336
  if (files.some((f) => f.endsWith(".json"))) {
87267
87337
  sessions.push(entry.name);
@@ -87322,7 +87392,7 @@ async function readSessionMessages(sessionID) {
87322
87392
  }
87323
87393
  }
87324
87394
  const messageDir = getMessageDir(sessionID);
87325
- if (!messageDir || !existsSync68(messageDir))
87395
+ if (!messageDir || !existsSync69(messageDir))
87326
87396
  return [];
87327
87397
  const messages2 = [];
87328
87398
  try {
@@ -87331,7 +87401,7 @@ async function readSessionMessages(sessionID) {
87331
87401
  if (!file3.endsWith(".json"))
87332
87402
  continue;
87333
87403
  try {
87334
- const content = await readFile3(join69(messageDir, file3), "utf-8");
87404
+ const content = await readFile3(join70(messageDir, file3), "utf-8");
87335
87405
  const meta3 = JSON.parse(content);
87336
87406
  const parts = await readParts2(meta3.id);
87337
87407
  messages2.push({
@@ -87355,8 +87425,8 @@ async function readSessionMessages(sessionID) {
87355
87425
  });
87356
87426
  }
87357
87427
  async function readParts2(messageID) {
87358
- const partDir = join69(PART_STORAGE, messageID);
87359
- if (!existsSync68(partDir))
87428
+ const partDir = join70(PART_STORAGE, messageID);
87429
+ if (!existsSync69(partDir))
87360
87430
  return [];
87361
87431
  const parts = [];
87362
87432
  try {
@@ -87365,7 +87435,7 @@ async function readParts2(messageID) {
87365
87435
  if (!file3.endsWith(".json"))
87366
87436
  continue;
87367
87437
  try {
87368
- const content = await readFile3(join69(partDir, file3), "utf-8");
87438
+ const content = await readFile3(join70(partDir, file3), "utf-8");
87369
87439
  parts.push(JSON.parse(content));
87370
87440
  } catch {}
87371
87441
  }
@@ -87389,14 +87459,14 @@ async function readSessionTodos(sessionID) {
87389
87459
  return [];
87390
87460
  }
87391
87461
  }
87392
- if (!existsSync68(TODO_DIR))
87462
+ if (!existsSync69(TODO_DIR))
87393
87463
  return [];
87394
87464
  try {
87395
87465
  const allFiles = await readdir(TODO_DIR);
87396
87466
  const todoFiles = allFiles.filter((f) => f.includes(sessionID) && f.endsWith(".json"));
87397
87467
  for (const file3 of todoFiles) {
87398
87468
  try {
87399
- const content = await readFile3(join69(TODO_DIR, file3), "utf-8");
87469
+ const content = await readFile3(join70(TODO_DIR, file3), "utf-8");
87400
87470
  const data = JSON.parse(content);
87401
87471
  if (Array.isArray(data)) {
87402
87472
  return data.map((item) => ({
@@ -87414,10 +87484,10 @@ async function readSessionTodos(sessionID) {
87414
87484
  return [];
87415
87485
  }
87416
87486
  async function readSessionTranscript(sessionID) {
87417
- if (!existsSync68(TRANSCRIPT_DIR2))
87487
+ if (!existsSync69(TRANSCRIPT_DIR2))
87418
87488
  return 0;
87419
- const transcriptFile = join69(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
87420
- if (!existsSync68(transcriptFile))
87489
+ const transcriptFile = join70(TRANSCRIPT_DIR2, `${sessionID}.jsonl`);
87490
+ if (!existsSync69(transcriptFile))
87421
87491
  return 0;
87422
87492
  try {
87423
87493
  const content = await readFile3(transcriptFile, "utf-8");
@@ -87927,17 +87997,17 @@ function createSkillTool(options = {}) {
87927
87997
  }
87928
87998
  var skill = createSkillTool();
87929
87999
  // src/tools/slashcommand/command-discovery.ts
87930
- import { existsSync as existsSync69, readdirSync as readdirSync21, readFileSync as readFileSync41 } from "fs";
87931
- import { basename as basename8, join as join70 } from "path";
88000
+ import { existsSync as existsSync70, readdirSync as readdirSync21, readFileSync as readFileSync41 } from "fs";
88001
+ import { basename as basename8, join as join71 } from "path";
87932
88002
  function discoverCommandsFromDir2(commandsDir, scope) {
87933
- if (!existsSync69(commandsDir))
88003
+ if (!existsSync70(commandsDir))
87934
88004
  return [];
87935
88005
  const entries = readdirSync21(commandsDir, { withFileTypes: true });
87936
88006
  const commands2 = [];
87937
88007
  for (const entry of entries) {
87938
88008
  if (!isMarkdownFile(entry))
87939
88009
  continue;
87940
- const commandPath = join70(commandsDir, entry.name);
88010
+ const commandPath = join71(commandsDir, entry.name);
87941
88011
  const commandName = basename8(entry.name, ".md");
87942
88012
  try {
87943
88013
  const content = readFileSync41(commandPath, "utf-8");
@@ -87964,8 +88034,8 @@ function discoverCommandsFromDir2(commandsDir, scope) {
87964
88034
  }
87965
88035
  function discoverCommandsSync(directory) {
87966
88036
  const configDir = getOpenCodeConfigDir({ binary: "opencode" });
87967
- const opencodeGlobalDir = join70(configDir, "command");
87968
- const opencodeProjectDir = join70(directory ?? process.cwd(), ".opencode", "command");
88037
+ const opencodeGlobalDir = join71(configDir, "command");
88038
+ const opencodeProjectDir = join71(directory ?? process.cwd(), ".opencode", "command");
87969
88039
  const opencodeGlobalCommands = discoverCommandsFromDir2(opencodeGlobalDir, "opencode");
87970
88040
  const opencodeProjectCommands = discoverCommandsFromDir2(opencodeProjectDir, "opencode-project");
87971
88041
  const builtinCommandsMap = loadBuiltinCommands();
@@ -88199,6 +88269,14 @@ var slashcommand = createSlashcommandTool();
88199
88269
  // src/tools/background-task/constants.ts
88200
88270
  var BACKGROUND_OUTPUT_DESCRIPTION = `Get output from background task. Use full_session=true to fetch session messages with filters. System notifies on completion, so block=true rarely needed.`;
88201
88271
  var BACKGROUND_CANCEL_DESCRIPTION = `Cancel running background task(s). Use all=true to cancel ALL before final answer.`;
88272
+ var BACKGROUND_REVIVE_DESCRIPTION = `Revive a retained terminal background task with a NEW instruction.
88273
+
88274
+ Call with no arguments (or list=true) to DISCOVER revivable tasks.
88275
+
88276
+ Revivable: cancelled, stopped, interrupt, error, completed \u2014 provided the task still has a session ID.
88277
+ Not revivable: pending/running (still active \u2014 use background_output), statusUncertain (unknown liveness \u2014 pass force=true to acknowledge), and tasks that never produced a session (e.g. queue-saturated).
88278
+
88279
+ Handles are retained for 30 minutes, so revive works after a task was pruned or the plugin restarted.`;
88202
88280
  // src/tools/background-task/create-background-cancel.ts
88203
88281
  function createBackgroundCancel(manager, _client) {
88204
88282
  return tool({
@@ -88367,6 +88445,14 @@ ${truncated}
88367
88445
  statusNote = `
88368
88446
 
88369
88447
  > **Interrupted**: The task was interrupted by a prompt error. The session may contain partial results.`;
88448
+ } else if (task.status === "stopped") {
88449
+ statusNote = `
88450
+
88451
+ > **Stopped**: the session ended without a terminal result \u2014 partial output may exist.`;
88452
+ } else if (task.status === "statusUncertain") {
88453
+ statusNote = `
88454
+
88455
+ > **Uncertain**: liveness could not be determined after restart; re-check with background_output.`;
88370
88456
  }
88371
88457
  const durationLabel = task.status === "pending" ? "Queued for" : "Duration";
88372
88458
  return `# Task Status
@@ -88525,6 +88611,18 @@ function getTimeString(value) {
88525
88611
  return typeof value === "string" ? value : "";
88526
88612
  }
88527
88613
  async function formatTaskResult(task, client2) {
88614
+ if (task.compactedResult) {
88615
+ const duration6 = formatDuration(task.startedAt ?? new Date, task.completedAt);
88616
+ return `Task Result
88617
+
88618
+ Task ID: ${task.id}
88619
+ Description: ${task.description}
88620
+ Duration: ${duration6}
88621
+ Session ID: ${task.sessionID}
88622
+
88623
+ ---
88624
+ ${task.compactedResult}`;
88625
+ }
88528
88626
  if (!task.sessionID) {
88529
88627
  return `Error: Task has no sessionID`;
88530
88628
  }
@@ -88729,7 +88827,7 @@ function createBackgroundOutput(manager, client2) {
88729
88827
  if (resolvedTask.status === "completed") {
88730
88828
  return await formatTaskResult(resolvedTask, client2);
88731
88829
  }
88732
- if (resolvedTask.status === "error" || resolvedTask.status === "cancelled" || resolvedTask.status === "interrupt") {
88830
+ if (resolvedTask.status === "error" || resolvedTask.status === "cancelled" || resolvedTask.status === "interrupt" || resolvedTask.status === "stopped" || resolvedTask.status === "statusUncertain") {
88733
88831
  return formatTaskStatus(resolvedTask);
88734
88832
  }
88735
88833
  const statusOutput = formatTaskStatus(resolvedTask);
@@ -88740,6 +88838,262 @@ function createBackgroundOutput(manager, client2) {
88740
88838
  }
88741
88839
  });
88742
88840
  }
88841
+ // src/tools/background-task/create-background-revive.ts
88842
+ import { existsSync as existsSync72 } from "fs";
88843
+
88844
+ // src/features/background-agent/handle-index.ts
88845
+ import { existsSync as existsSync71, mkdirSync as mkdirSync19, readdirSync as readdirSync22, readFileSync as readFileSync42, renameSync as renameSync5, unlinkSync as unlinkSync11, writeFileSync as writeFileSync25 } from "fs";
88846
+ import { dirname as dirname19, join as join72 } from "path";
88847
+
88848
+ // src/features/background-agent/constants.ts
88849
+ var TASK_TTL_MS = 30 * 60 * 1000;
88850
+ var MIN_STABILITY_TIME_MS = 10 * 1000;
88851
+ var DEFAULT_STALE_TIMEOUT_MS = 180000;
88852
+ var DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 600000;
88853
+ var DEFAULT_MAX_TOOL_CALLS = 4000;
88854
+ var DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20;
88855
+ var DEFAULT_CIRCUIT_BREAKER_ENABLED = true;
88856
+ var MIN_RUNTIME_BEFORE_STALE_MS = 30000;
88857
+ var MIN_IDLE_TIME_MS = 5000;
88858
+ var POLLING_INTERVAL_MS = 3000;
88859
+ var PRUNE_THROTTLE_MS = 30000;
88860
+ var TASK_CLEANUP_DELAY_MS = 10 * 60 * 1000;
88861
+ var DEFAULT_ADMISSION_TIMEOUT_MS = 0;
88862
+ var DEFAULT_WALL_CLOCK_TIMEOUT_MS = 0;
88863
+ var DEFAULT_WALL_CLOCK_ABORT_GRACE_MS = 5000;
88864
+ var DEFAULT_NESTED_ADMISSION_ENABLED = true;
88865
+ var DEFAULT_NESTED_ADMISSION_MODE = "bypass";
88866
+ var DEFAULT_NESTED_MAX_DEPTH = 2;
88867
+ var DEFAULT_WAKE_INTERVAL_MS = 300000;
88868
+ var MIN_WAKE_INTERVAL_MS = 60000;
88869
+ var RECONCILE_CONFIRMATION_GRACE_MS = 5000;
88870
+
88871
+ // src/features/background-agent/handle-index.ts
88872
+ var BG_HANDLE_DIR_NAME = "bg-handles";
88873
+ var BG_HANDLE_FILE_PREFIX = "bg_";
88874
+ var BgHandleSchema = exports_external.object({
88875
+ taskId: exports_external.string(),
88876
+ parentSessionID: exports_external.string(),
88877
+ parentMessageID: exports_external.string(),
88878
+ description: exports_external.string(),
88879
+ agent: exports_external.string(),
88880
+ status: exports_external.enum(["pending", "running", "completed", "error", "cancelled", "interrupt", "stopped", "statusUncertain"]),
88881
+ sessionID: exports_external.string().optional(),
88882
+ terminalReason: exports_external.enum(["queue-saturated", "no-output", "uncertain", "aborted", "stale", "nested-depth-exceeded", "wall-clock-timeout"]).optional(),
88883
+ model: exports_external.object({
88884
+ providerID: exports_external.string(),
88885
+ modelID: exports_external.string(),
88886
+ variant: exports_external.string().optional(),
88887
+ temperature: exports_external.number().optional()
88888
+ }).optional(),
88889
+ category: exports_external.string().optional(),
88890
+ concurrencyGroup: exports_external.string().optional(),
88891
+ queuedAt: exports_external.number().optional(),
88892
+ startedAt: exports_external.number().optional(),
88893
+ completedAt: exports_external.number().optional()
88894
+ }).strict();
88895
+ function getBgHandleDir(directory) {
88896
+ return join72(directory, ".matrixx", BG_HANDLE_DIR_NAME);
88897
+ }
88898
+ function getHandlePath(directory, taskId) {
88899
+ return join72(getBgHandleDir(directory), `${taskId}.json`);
88900
+ }
88901
+ function ensureDir3(dirPath) {
88902
+ if (!existsSync71(dirPath)) {
88903
+ mkdirSync19(dirPath, { recursive: true });
88904
+ }
88905
+ }
88906
+ function toHandle(task) {
88907
+ return {
88908
+ taskId: task.id,
88909
+ parentSessionID: task.parentSessionID,
88910
+ parentMessageID: task.parentMessageID,
88911
+ description: task.description,
88912
+ agent: task.agent,
88913
+ status: task.status,
88914
+ sessionID: task.sessionID,
88915
+ terminalReason: task.terminalReason,
88916
+ model: task.model,
88917
+ category: task.category,
88918
+ concurrencyGroup: task.concurrencyGroup,
88919
+ queuedAt: task.queuedAt?.getTime(),
88920
+ startedAt: task.startedAt?.getTime(),
88921
+ completedAt: task.completedAt?.getTime()
88922
+ };
88923
+ }
88924
+ function writeHandle(directory, task) {
88925
+ const filePath = getHandlePath(directory, task.id);
88926
+ ensureDir3(dirname19(filePath));
88927
+ const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
88928
+ try {
88929
+ writeFileSync25(tempPath, JSON.stringify(toHandle(task), null, 2), "utf-8");
88930
+ renameSync5(tempPath, filePath);
88931
+ } catch (error95) {
88932
+ try {
88933
+ if (existsSync71(tempPath)) {
88934
+ unlinkSync11(tempPath);
88935
+ }
88936
+ } catch {}
88937
+ throw error95;
88938
+ }
88939
+ }
88940
+ function readHandleFile(filePath) {
88941
+ try {
88942
+ if (!existsSync71(filePath)) {
88943
+ return null;
88944
+ }
88945
+ const parsed = JSON.parse(readFileSync42(filePath, "utf-8"));
88946
+ const result = BgHandleSchema.safeParse(parsed);
88947
+ return result.success ? result.data : null;
88948
+ } catch {
88949
+ return null;
88950
+ }
88951
+ }
88952
+ function readHandles(directory) {
88953
+ const dir = getBgHandleDir(directory);
88954
+ if (!existsSync71(dir)) {
88955
+ return [];
88956
+ }
88957
+ const handles = [];
88958
+ for (const name of readdirSync22(dir)) {
88959
+ if (!name.startsWith(BG_HANDLE_FILE_PREFIX) || !name.endsWith(".json")) {
88960
+ continue;
88961
+ }
88962
+ const handle = readHandleFile(join72(dir, name));
88963
+ if (handle) {
88964
+ handles.push(handle);
88965
+ }
88966
+ }
88967
+ return handles;
88968
+ }
88969
+ function sweepStaleHandles(directory, ttlMs = TASK_TTL_MS, now = Date.now()) {
88970
+ const dir = getBgHandleDir(directory);
88971
+ if (!existsSync71(dir)) {
88972
+ return 0;
88973
+ }
88974
+ let removed = 0;
88975
+ for (const name of readdirSync22(dir)) {
88976
+ if (!name.startsWith(BG_HANDLE_FILE_PREFIX) || !name.endsWith(".json")) {
88977
+ continue;
88978
+ }
88979
+ const filePath = join72(dir, name);
88980
+ const handle = readHandleFile(filePath);
88981
+ const timestamp = handle?.completedAt ?? handle?.startedAt ?? handle?.queuedAt;
88982
+ if (handle && timestamp !== undefined && now - timestamp <= ttlMs) {
88983
+ continue;
88984
+ }
88985
+ try {
88986
+ unlinkSync11(filePath);
88987
+ removed++;
88988
+ } catch {}
88989
+ }
88990
+ if (removed > 0) {
88991
+ log(`[bg-handles] Swept ${removed} stale handle file(s) from ${dir}`);
88992
+ }
88993
+ return removed;
88994
+ }
88995
+
88996
+ // src/shared/revive-outcome.ts
88997
+ function formatReviveOutcome(taskId, sessionID, status) {
88998
+ const metadata = JSON.stringify({
88999
+ task_id: taskId,
89000
+ session_id: sessionID,
89001
+ status,
89002
+ reason: "revived"
89003
+ });
89004
+ return `Background task revived.
89005
+
89006
+ Task ID: ${taskId}
89007
+ Session ID: ${sessionID}
89008
+ Status: ${status}
89009
+
89010
+ <task_metadata>${metadata}</task_metadata>`;
89011
+ }
89012
+ function formatUnrevivable(taskId, reason, detail) {
89013
+ const metadata = JSON.stringify({
89014
+ task_id: taskId,
89015
+ status: "unrevived",
89016
+ reason
89017
+ });
89018
+ const detailLine = detail === undefined ? "" : `Detail: ${detail}
89019
+ `;
89020
+ return `Background task NOT revived.
89021
+
89022
+ Task ID: ${taskId}
89023
+ Reason: ${reason}
89024
+ ${detailLine}
89025
+ <task_metadata>${metadata}</task_metadata>`;
89026
+ }
89027
+
89028
+ // src/tools/background-task/create-background-revive.ts
89029
+ function formatListing(entries) {
89030
+ if (entries.length === 0) {
89031
+ return `No revivable background tasks found.
89032
+
89033
+ Handles are retained for 30 minutes after a task reaches a terminal state.`;
89034
+ }
89035
+ const rows = entries.map((entry) => `| \`${entry.taskId}\` | ${entry.description} | ${entry.agent} | ${entry.status} | ${entry.terminalReason ?? "-"} | \`${entry.sessionID}\` |`).join(`
89036
+ `);
89037
+ return `Revivable background tasks (${entries.length}):
89038
+
89039
+ | Task ID | Description | Agent | Status | Reason | Session ID |
89040
+ |---------|-------------|-------|--------|--------|------------|
89041
+ ${rows}
89042
+
89043
+ Revive one with background_revive(taskId="<id>", prompt="<new instruction>").`;
89044
+ }
89045
+ function resolveFailure(identifier, message, directory) {
89046
+ if (message.includes("not found for revive")) {
89047
+ const handleOnDisk = existsSync72(getHandlePath(directory, identifier));
89048
+ return { reason: handleOnDisk ? "unknown-task" : "expired", detail: message };
89049
+ }
89050
+ if (message.includes("still active"))
89051
+ return { reason: "active", detail: message };
89052
+ if (message.includes("unknown liveness"))
89053
+ return { reason: "uncertain", detail: message };
89054
+ if (message.includes("no session to revive") || message.includes("has no sessionID")) {
89055
+ return { reason: "no-session", detail: message };
89056
+ }
89057
+ return { reason: "unknown-task", detail: message };
89058
+ }
89059
+ function createBackgroundRevive(manager, directory) {
89060
+ return tool({
89061
+ description: BACKGROUND_REVIVE_DESCRIPTION,
89062
+ args: {
89063
+ taskId: tool.schema.string().optional().describe("Task ID to revive (provide this or session_id)"),
89064
+ session_id: tool.schema.string().optional().describe("Session ID of the task to revive (alternative to taskId)"),
89065
+ prompt: tool.schema.string().optional().describe("NEW instruction for the revived session (required unless list=true)"),
89066
+ list: tool.schema.boolean().optional().describe("List revivable tasks instead of reviving one (default: false)"),
89067
+ force: tool.schema.boolean().optional().describe("Acknowledge unknown liveness for statusUncertain tasks (default: false)"),
89068
+ parentSessionID: tool.schema.string().optional().describe("Restrict discovery to this parent session")
89069
+ },
89070
+ async execute(args, toolContext) {
89071
+ const isListing = args.list === true || args.taskId === undefined && args.session_id === undefined;
89072
+ if (isListing) {
89073
+ return formatListing(manager.listRevivable(args.parentSessionID ?? toolContext.sessionID));
89074
+ }
89075
+ const identifier = args.taskId ?? args.session_id;
89076
+ if (args.prompt === undefined || args.prompt.trim().length === 0) {
89077
+ return formatUnrevivable(identifier, "unknown-task", "a prompt is required to revive a task");
89078
+ }
89079
+ try {
89080
+ const task = await manager.revive({
89081
+ taskId: args.taskId,
89082
+ sessionId: args.session_id,
89083
+ prompt: args.prompt,
89084
+ parentSessionID: toolContext.sessionID,
89085
+ parentMessageID: toolContext.messageID,
89086
+ force: args.force
89087
+ });
89088
+ return formatReviveOutcome(task.id, task.sessionID ?? "", task.status);
89089
+ } catch (error95) {
89090
+ const message = error95 instanceof Error ? error95.message : String(error95);
89091
+ const { reason, detail } = resolveFailure(identifier, message, directory);
89092
+ return formatUnrevivable(identifier, reason, detail);
89093
+ }
89094
+ }
89095
+ });
89096
+ }
88743
89097
  // src/tools/assembly/constants.ts
88744
89098
  var DEFAULT_VOTERS = 3;
88745
89099
  var DEFAULT_ROUNDS = 2;
@@ -89105,9 +89459,9 @@ function createAssemblyTool(options) {
89105
89459
  });
89106
89460
  }
89107
89461
  // src/tools/dcp-switch-profile/tools.ts
89108
- import { existsSync as existsSync70, writeFileSync as writeFileSync25 } from "fs";
89109
- import { homedir as homedir10 } from "os";
89110
- import { join as join71 } from "path";
89462
+ import { existsSync as existsSync73, writeFileSync as writeFileSync26 } from "fs";
89463
+ import { homedir as homedir11 } from "os";
89464
+ import { join as join73 } from "path";
89111
89465
 
89112
89466
  // src/config/schema/dcp.ts
89113
89467
  var DcpCompressOverrideSchema = exports_external.object({
@@ -89192,12 +89546,37 @@ var BUILTIN_DCP_PROFILES = {
89192
89546
  turnProtection: { enabled: true, turns: 5 },
89193
89547
  experimental: { allowSubAgents: true },
89194
89548
  strategies: { purgeErrors: { turns: 4 } }
89549
+ },
89550
+ brutal: {
89551
+ pruneNotification: "off",
89552
+ compress: {
89553
+ maxContextLimit: "15%",
89554
+ minContextLimit: "10%",
89555
+ nudgeFrequency: 1,
89556
+ nudgeForce: "strong",
89557
+ iterationNudgeThreshold: 1
89558
+ },
89559
+ turnProtection: { enabled: false },
89560
+ experimental: { allowSubAgents: false },
89561
+ strategies: { purgeErrors: { turns: 1 } }
89195
89562
  }
89196
89563
  };
89564
+ var DcpHandoffCompressionSchema = exports_external.object({
89565
+ enabled: exports_external.boolean().default(true),
89566
+ maxMessages: exports_external.number().int().min(1).default(6),
89567
+ keepFirst: exports_external.number().int().min(0).default(2),
89568
+ keepLast: exports_external.number().int().min(0).default(3)
89569
+ });
89197
89570
  var DcpConfigSchema = exports_external.object({
89198
89571
  enabled: exports_external.boolean().default(true),
89199
89572
  profiles: exports_external.record(exports_external.string(), DcpProfileDefinitionSchema).default(BUILTIN_DCP_PROFILES),
89200
89573
  default_profile: exports_external.string().optional(),
89574
+ handoffCompression: DcpHandoffCompressionSchema.default({
89575
+ enabled: true,
89576
+ maxMessages: 6,
89577
+ keepFirst: 2,
89578
+ keepLast: 3
89579
+ }),
89201
89580
  base: exports_external.object({
89202
89581
  autoUpdate: exports_external.boolean().default(false),
89203
89582
  debug: exports_external.boolean().default(false),
@@ -89254,7 +89633,8 @@ var DcpConfigSchema = exports_external.object({
89254
89633
  enabled: false,
89255
89634
  automaticStrategies: true
89256
89635
  }),
89257
- protectedFilePatterns: exports_external.array(exports_external.string()).default([])
89636
+ protectedFilePatterns: exports_external.array(exports_external.string()).default([]),
89637
+ experimental: DcpExperimentalSchema.default({ allowSubAgents: true })
89258
89638
  }).default({
89259
89639
  autoUpdate: false,
89260
89640
  debug: false,
@@ -89275,25 +89655,54 @@ var DcpConfigSchema = exports_external.object({
89275
89655
  },
89276
89656
  commands: { enabled: true, protectedTools: [] },
89277
89657
  manualMode: { enabled: false, automaticStrategies: true },
89278
- protectedFilePatterns: []
89658
+ protectedFilePatterns: [],
89659
+ experimental: { allowSubAgents: true }
89279
89660
  })
89280
89661
  });
89281
89662
 
89282
89663
  // src/tools/dcp-switch-profile/tools.ts
89283
- var DCP_PLUGIN_DIR = join71(homedir10(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
89284
- var DCP_SYMLINK = join71(homedir10(), ".config", "opencode", "dcp.jsonc");
89285
- var VALID_PROFILES = ["economy", "balanced", "performance", "ultimate"];
89664
+ var DCP_PLUGIN_DIR = join73(homedir11(), ".config", "opencode", "node_modules", "@tarquinen", "opencode-dcp");
89665
+ var DCP_SYMLINK = join73(homedir11(), ".config", "opencode", "dcp.jsonc");
89666
+ var VALID_PROFILES = ["economy", "balanced", "performance", "ultimate", "brutal"];
89286
89667
  function checkDcpInstalled() {
89287
- if (!existsSync70(DCP_PLUGIN_DIR)) {
89668
+ if (!existsSync73(DCP_PLUGIN_DIR)) {
89288
89669
  return `DCP is not installed at ${DCP_PLUGIN_DIR}. Install it with: npm install --prefix ~/.config/opencode @tarquinen/opencode-dcp`;
89289
89670
  }
89290
89671
  return null;
89291
89672
  }
89673
+ function deepMergeProfile(builtin, override) {
89674
+ const result = { ...builtin };
89675
+ if (override.pruneNotification !== undefined) {
89676
+ result.pruneNotification = override.pruneNotification;
89677
+ }
89678
+ const subObjects = ["compress", "turnProtection", "experimental"];
89679
+ for (const key of subObjects) {
89680
+ const builtinObj = builtin[key];
89681
+ const overrideObj = override[key];
89682
+ if (builtinObj || overrideObj) {
89683
+ result[key] = { ...builtinObj ?? {}, ...overrideObj ?? {} };
89684
+ }
89685
+ }
89686
+ const builtinStrategies = builtin.strategies;
89687
+ const overrideStrategies = override.strategies;
89688
+ if (builtinStrategies || overrideStrategies) {
89689
+ const merged = { ...builtinStrategies ?? {}, ...overrideStrategies ?? {} };
89690
+ const builtinPurge = builtinStrategies?.purgeErrors;
89691
+ const overridePurge = overrideStrategies?.purgeErrors;
89692
+ if (builtinPurge || overridePurge) {
89693
+ merged.purgeErrors = { ...builtinPurge ?? {}, ...overridePurge ?? {} };
89694
+ }
89695
+ result.strategies = merged;
89696
+ }
89697
+ return result;
89698
+ }
89292
89699
  function buildInlineConfig(profile, options) {
89293
89700
  const dcpConfig = options?.pluginConfig?.dcp;
89294
89701
  const base = dcpConfig?.base ? dcpConfig.base : {};
89295
89702
  const validProfiles = BUILTIN_DCP_PROFILES;
89296
- const profileConfig = dcpConfig?.profiles?.[profile] ?? validProfiles[profile] ?? {};
89703
+ const builtinProfile = validProfiles[profile] ?? {};
89704
+ const userOverride = dcpConfig?.profiles?.[profile];
89705
+ const profileConfig = userOverride ? deepMergeProfile(builtinProfile, userOverride) : builtinProfile;
89297
89706
  const compressBase = base.compress ?? {};
89298
89707
  const strategiesBase = base.strategies ?? {};
89299
89708
  const commandsCfg = base.commands ?? {};
@@ -89303,6 +89712,7 @@ function buildInlineConfig(profile, options) {
89303
89712
  const profileExp = profileConfig.experimental ?? {};
89304
89713
  const profilePurge = profileConfig.strategies?.purgeErrors ?? {};
89305
89714
  const basePurge = strategiesBase.purgeErrors ?? {};
89715
+ const baseExp = base.experimental ?? {};
89306
89716
  return {
89307
89717
  $schema: "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/v3.1.14/dcp.schema.json",
89308
89718
  enabled: true,
@@ -89329,7 +89739,7 @@ function buildInlineConfig(profile, options) {
89329
89739
  turns: profileTurn.turns ?? 2
89330
89740
  },
89331
89741
  experimental: {
89332
- allowSubAgents: profileExp.allowSubAgents ?? true,
89742
+ allowSubAgents: profileExp.allowSubAgents ?? baseExp.allowSubAgents ?? true,
89333
89743
  customPrompts: false
89334
89744
  },
89335
89745
  protectedFilePatterns: base.protectedFilePatterns ?? [],
@@ -89355,14 +89765,16 @@ function buildInlineConfig(profile, options) {
89355
89765
  };
89356
89766
  }
89357
89767
  function switchProfile(profile, options) {
89358
- if (!VALID_PROFILES.includes(profile)) {
89359
- return `Error: Invalid profile "${profile}". Valid profiles: ${VALID_PROFILES.join(", ")}`;
89768
+ const customProfiles = Object.keys(options?.pluginConfig?.dcp?.profiles ?? {});
89769
+ const allValid = [...VALID_PROFILES, ...customProfiles];
89770
+ if (!allValid.includes(profile)) {
89771
+ return `Error: Invalid profile "${profile}". Valid profiles: ${allValid.join(", ")}`;
89360
89772
  }
89361
89773
  const dcpError = checkDcpInstalled();
89362
89774
  if (dcpError)
89363
89775
  return dcpError;
89364
89776
  const inlineConfig = buildInlineConfig(profile, options);
89365
- writeFileSync25(DCP_SYMLINK, `${JSON.stringify(inlineConfig, null, 2)}
89777
+ writeFileSync26(DCP_SYMLINK, `${JSON.stringify(inlineConfig, null, 2)}
89366
89778
  `);
89367
89779
  return `\u2713 Switched to DCP profile: ${profile}
89368
89780
 
@@ -89370,9 +89782,9 @@ Restart OpenCode session for changes to take effect.`;
89370
89782
  }
89371
89783
  function createDcpSwitchProfileTool(options) {
89372
89784
  const dcp_switch_profile = tool({
89373
- description: "Switch the active DCP (Dynamic Context Pruning) profile tier. " + "Reads profile parameters from the Matrixx plugin configuration and writes a full inline DCP config " + "to ~/.config/opencode/dcp.jsonc. No external files needed. " + "Call this with one of: economy, balanced, performance, ultimate.",
89785
+ description: "Switch the active DCP (Dynamic Context Pruning) profile tier. " + "Reads profile parameters from the Matrixx plugin configuration and writes a full inline DCP config " + "to ~/.config/opencode/dcp.jsonc. No external files needed. " + "Built-in profiles: economy, balanced, performance, ultimate. " + "Custom profiles (e.g. brutal) are read from dcp.profiles in opencode.jsonc.",
89374
89786
  args: {
89375
- profile: tool.schema.enum(VALID_PROFILES).describe("Target DCP profile tier: economy (most aggressive), balanced, performance, ultimate (least aggressive)")
89787
+ profile: tool.schema.string().describe("Target DCP profile tier: economy (most aggressive), balanced, performance, ultimate (least aggressive), or a custom profile name from config (e.g., brutal)")
89376
89788
  },
89377
89789
  async execute(args) {
89378
89790
  const profile = args.profile;
@@ -89393,6 +89805,39 @@ var CALL_DELEGATE_AGENT_DESCRIPTION = `Spawn explore/librarian agent. run_in_bac
89393
89805
  Available: {agents}
89394
89806
 
89395
89807
  Pass \`session_id=<id>\` to continue previous agent with full context. Prompts MUST be in English. Use \`background_output\` for async results.`;
89808
+ // src/shared/saturated-outcome.ts
89809
+ var LAUNCH_TERMINAL_STATUSES = [
89810
+ "error",
89811
+ "cancelled",
89812
+ "interrupt",
89813
+ "stopped",
89814
+ "statusUncertain"
89815
+ ];
89816
+ function isLaunchTerminalStatus(status) {
89817
+ return LAUNCH_TERMINAL_STATUSES.includes(status);
89818
+ }
89819
+ function isQueueSaturated(status, terminalReason) {
89820
+ return status === "stopped" && terminalReason === "queue-saturated";
89821
+ }
89822
+ function formatSaturatedOutcome(taskId) {
89823
+ const metadata = JSON.stringify({
89824
+ task_id: taskId,
89825
+ status: "stopped",
89826
+ reason: "queue-saturated"
89827
+ });
89828
+ return `Background task NOT admitted (queue saturated).
89829
+
89830
+ Task ID: ${taskId}
89831
+ Status: stopped
89832
+ Reason: queue-saturated
89833
+
89834
+ <task_metadata>${metadata}</task_metadata>`;
89835
+ }
89836
+ function formatLaunchFailure(taskId, status) {
89837
+ return `Task failed to start (status: ${status}).
89838
+
89839
+ Task ID: ${taskId}`;
89840
+ }
89396
89841
  // src/tools/delegate-agent/background-executor.ts
89397
89842
  async function executeBackground(args, toolContext, manager, client2) {
89398
89843
  try {
@@ -89429,10 +89874,11 @@ async function executeBackground(args, toolContext, manager, client2) {
89429
89874
  Task ID: ${task.id}`;
89430
89875
  }
89431
89876
  const updated = manager.getTask(task.id);
89432
- if (updated?.status === "error" || updated?.status === "cancelled" || updated?.status === "interrupt") {
89433
- return `Task failed to start (status: ${updated.status}).
89434
-
89435
- Task ID: ${task.id}`;
89877
+ if (updated && isLaunchTerminalStatus(updated.status)) {
89878
+ if (isQueueSaturated(updated.status, updated.terminalReason)) {
89879
+ return formatSaturatedOutcome(task.id);
89880
+ }
89881
+ return formatLaunchFailure(task.id, updated.status);
89436
89882
  }
89437
89883
  await new Promise((resolve10) => setTimeout(resolve10, WAIT_FOR_SESSION_INTERVAL_MS));
89438
89884
  sessionId = manager.getTask(task.id)?.sessionID;
@@ -90411,7 +90857,7 @@ ${task.agent ? `subagent: ${task.agent}
90411
90857
  }
90412
90858
  // src/tools/delegate-task/timing.ts
90413
90859
  var POLL_INTERVAL_MS3 = 1000;
90414
- var MIN_STABILITY_TIME_MS = 1e4;
90860
+ var MIN_STABILITY_TIME_MS2 = 1e4;
90415
90861
  var STABILITY_POLLS_REQUIRED = 3;
90416
90862
  var WAIT_FOR_SESSION_INTERVAL_MS = 100;
90417
90863
  var WAIT_FOR_SESSION_TIMEOUT_MS = 30000;
@@ -90420,7 +90866,7 @@ var SESSION_CONTINUATION_STABILITY_MS = 5000;
90420
90866
  function getTimingConfig() {
90421
90867
  return {
90422
90868
  POLL_INTERVAL_MS: POLL_INTERVAL_MS3,
90423
- MIN_STABILITY_TIME_MS,
90869
+ MIN_STABILITY_TIME_MS: MIN_STABILITY_TIME_MS2,
90424
90870
  STABILITY_POLLS_REQUIRED,
90425
90871
  WAIT_FOR_SESSION_INTERVAL_MS,
90426
90872
  WAIT_FOR_SESSION_TIMEOUT_MS,
@@ -90461,6 +90907,12 @@ Task ID: ${task.id}`;
90461
90907
  }
90462
90908
  await new Promise((resolve10) => setTimeout(resolve10, timing.WAIT_FOR_SESSION_INTERVAL_MS));
90463
90909
  const updated = manager.getTask(task.id);
90910
+ if (updated && isLaunchTerminalStatus(updated.status)) {
90911
+ if (isQueueSaturated(updated.status, updated.terminalReason)) {
90912
+ return formatSaturatedOutcome(task.id);
90913
+ }
90914
+ return formatLaunchFailure(task.id, updated.status);
90915
+ }
90464
90916
  sessionId = updated?.sessionID;
90465
90917
  }
90466
90918
  if (!sessionId) {
@@ -92101,7 +92553,7 @@ REQUIRED: You MUST provide EITHER category OR subagent_type (one of them is REQU
92101
92553
  Available categories:
92102
92554
  ${categoryList}
92103
92555
  - subagent_type: Use specific agent directly (e.g., "oracle", "trinity")
92104
- - run_in_background: true=async (returns task_id), false=sync (waits for result). Default: false. Use background=true ONLY for parallel exploration with 5+ independent queries.
92556
+ - run_in_background: true=async (returns task_id), false=sync (waits for result). Default: false. Use true for ANY parallel independent work (exploration, fan-out, multi-agent waves); false awaits the result inline.
92105
92557
  - session_id: Existing Task session to continue (from previous task output). Continues agent with FULL CONTEXT PRESERVED - saves tokens, maintains continuity.
92106
92558
  - command: The command that triggered this task (optional, for slash command tracking).
92107
92559
 
@@ -92598,8 +93050,8 @@ Original error: ${createResult.error}`;
92598
93050
  });
92599
93051
  }
92600
93052
  // src/tools/task/task-cleanup.ts
92601
- import { existsSync as existsSync71, readdirSync as readdirSync22, readFileSync as readFileSync42, unlinkSync as unlinkSync11 } from "fs";
92602
- import { join as join72 } from "path";
93053
+ import { existsSync as existsSync74, readdirSync as readdirSync23, readFileSync as readFileSync43, unlinkSync as unlinkSync12 } from "fs";
93054
+ import { join as join74 } from "path";
92603
93055
  init_logger();
92604
93056
  function parseOlderThan(value) {
92605
93057
  const match = value.trim().match(/^(\d+)(d|h|m)$/);
@@ -92658,17 +93110,17 @@ Returns counts: {deleted, remaining, deletedIds}`,
92658
93110
  }
92659
93111
  const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92660
93112
  const taskDir = getTaskDir(config4, directory);
92661
- if (!existsSync71(taskDir)) {
93113
+ if (!existsSync74(taskDir)) {
92662
93114
  return JSON.stringify({ deleted: 0, remaining: 0, deletedIds: [] });
92663
93115
  }
92664
- const files = readdirSync22(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
93116
+ const files = readdirSync23(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
92665
93117
  const total = files.length;
92666
93118
  const deletedIds = [];
92667
93119
  for (const file3 of files) {
92668
- const filePath = join72(taskDir, file3);
93120
+ const filePath = join74(taskDir, file3);
92669
93121
  let raw;
92670
93122
  try {
92671
- const content = readFileSync42(filePath, "utf-8");
93123
+ const content = readFileSync43(filePath, "utf-8");
92672
93124
  raw = JSON.parse(content);
92673
93125
  } catch {
92674
93126
  continue;
@@ -92683,7 +93135,7 @@ Returns counts: {deleted, remaining, deletedIds}`,
92683
93135
  continue;
92684
93136
  }
92685
93137
  try {
92686
- unlinkSync11(filePath);
93138
+ unlinkSync12(filePath);
92687
93139
  deletedIds.push(id);
92688
93140
  } catch (err) {
92689
93141
  log("[task-cleanup] Failed to delete", { file: file3, error: String(err) });
@@ -92701,8 +93153,8 @@ Returns counts: {deleted, remaining, deletedIds}`,
92701
93153
  });
92702
93154
  }
92703
93155
  // src/tools/task/task-create.ts
92704
- import { readdirSync as readdirSync23, statSync as statSync17 } from "fs";
92705
- import { join as join73 } from "path";
93156
+ import { readdirSync as readdirSync24, statSync as statSync17 } from "fs";
93157
+ import { join as join75 } from "path";
92706
93158
  init_logger();
92707
93159
  function createTaskCreateTool(config4, ctx) {
92708
93160
  return tool({
@@ -92768,7 +93220,7 @@ async function handleCreate2(args, config4, ctx, context) {
92768
93220
  projectRoot: directory
92769
93221
  };
92770
93222
  const validatedTask = TaskObjectSchema.parse(task);
92771
- writeJsonAtomic(join73(taskDir, `${taskId}.json`), validatedTask);
93223
+ writeJsonAtomic(join75(taskDir, `${taskId}.json`), validatedTask);
92772
93224
  return JSON.stringify({
92773
93225
  task: {
92774
93226
  id: validatedTask.id,
@@ -92791,13 +93243,13 @@ async function handleCreate2(args, config4, ctx, context) {
92791
93243
  function findDuplicateTask(taskDir, subject, directory) {
92792
93244
  let files;
92793
93245
  try {
92794
- files = readdirSync23(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
93246
+ files = readdirSync24(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-"));
92795
93247
  } catch (error95) {
92796
93248
  log(`[task-create] Failed to list task dir ${taskDir} during dedup scan`, error95);
92797
93249
  return null;
92798
93250
  }
92799
93251
  for (const file3 of files) {
92800
- const filePath = join73(taskDir, file3);
93252
+ const filePath = join75(taskDir, file3);
92801
93253
  let existing;
92802
93254
  try {
92803
93255
  existing = readJsonSafe(filePath, TaskObjectSchema);
@@ -92828,7 +93280,7 @@ function findDuplicateTask(taskDir, subject, directory) {
92828
93280
  return null;
92829
93281
  }
92830
93282
  // src/tools/task/task-get.ts
92831
- import { join as join74 } from "path";
93283
+ import { join as join76 } from "path";
92832
93284
  function parseTaskId(id) {
92833
93285
  if (!TASK_ID_PATTERN.test(id))
92834
93286
  return null;
@@ -92853,7 +93305,7 @@ Returns null if the task does not exist or the file is invalid.`,
92853
93305
  }
92854
93306
  const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92855
93307
  const taskDir = getTaskDir(config4, directory);
92856
- const taskPath = join74(taskDir, `${taskId}.json`);
93308
+ const taskPath = join76(taskDir, `${taskId}.json`);
92857
93309
  const task = readJsonSafe(taskPath, TaskObjectSchema);
92858
93310
  return JSON.stringify({ task: task ?? null });
92859
93311
  } catch (error95) {
@@ -92866,8 +93318,8 @@ Returns null if the task does not exist or the file is invalid.`,
92866
93318
  });
92867
93319
  }
92868
93320
  // src/tools/task/task-list.ts
92869
- import { existsSync as existsSync72, readdirSync as readdirSync24 } from "fs";
92870
- import { join as join75 } from "path";
93321
+ import { existsSync as existsSync75, readdirSync as readdirSync25 } from "fs";
93322
+ import { join as join77 } from "path";
92871
93323
  function createTaskList(config4, ctx) {
92872
93324
  return tool({
92873
93325
  description: `List all active tasks with summary information.
@@ -92885,16 +93337,16 @@ Optional \`parentID\` filter: when provided, returns only tasks whose parentID m
92885
93337
  const parentID = args.parentID;
92886
93338
  const directory = context?.directory ?? ctx?.directory ?? process.cwd();
92887
93339
  const taskDir = getTaskDir(config4, directory);
92888
- if (!existsSync72(taskDir)) {
93340
+ if (!existsSync75(taskDir)) {
92889
93341
  return JSON.stringify({ tasks: [] });
92890
93342
  }
92891
- const files = readdirSync24(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
93343
+ const files = readdirSync25(taskDir).filter((f) => f.endsWith(".json") && f.startsWith("T-")).map((f) => f.replace(".json", ""));
92892
93344
  if (files.length === 0) {
92893
93345
  return JSON.stringify({ tasks: [] });
92894
93346
  }
92895
93347
  const allTasks = [];
92896
93348
  for (const fileId of files) {
92897
- const task = readJsonSafe(join75(taskDir, `${fileId}.json`), TaskObjectSchema);
93349
+ const task = readJsonSafe(join77(taskDir, `${fileId}.json`), TaskObjectSchema);
92898
93350
  if (task) {
92899
93351
  allTasks.push(task);
92900
93352
  }
@@ -92923,7 +93375,7 @@ Optional \`parentID\` filter: when provided, returns only tasks whose parentID m
92923
93375
  });
92924
93376
  }
92925
93377
  // src/tools/task/task-update.ts
92926
- import { join as join76 } from "path";
93378
+ import { join as join78 } from "path";
92927
93379
  function parseTaskId2(id) {
92928
93380
  if (!TASK_ID_PATTERN.test(id))
92929
93381
  return null;
@@ -92969,7 +93421,7 @@ async function handleUpdate(args, config4, ctx, context) {
92969
93421
  return JSON.stringify({ error: "task_lock_unavailable" });
92970
93422
  }
92971
93423
  try {
92972
- const taskPath = join76(taskDir, `${taskId}.json`);
93424
+ const taskPath = join78(taskDir, `${taskId}.json`);
92973
93425
  const task = readJsonSafe(taskPath, TaskObjectSchema);
92974
93426
  if (!task) {
92975
93427
  return JSON.stringify({ error: "task_not_found" });
@@ -93022,12 +93474,13 @@ async function handleUpdate(args, config4, ctx, context) {
93022
93474
  }
93023
93475
  }
93024
93476
  // src/tools/index.ts
93025
- function createBackgroundTools(manager, client2) {
93477
+ function createBackgroundTools(manager, client2, directory) {
93026
93478
  const outputManager = manager;
93027
93479
  const cancelClient = client2;
93028
93480
  return {
93029
93481
  background_output: createBackgroundOutput(outputManager, client2),
93030
- background_cancel: createBackgroundCancel(manager, cancelClient)
93482
+ background_cancel: createBackgroundCancel(manager, cancelClient),
93483
+ background_revive: createBackgroundRevive(manager, directory)
93031
93484
  };
93032
93485
  }
93033
93486
  var builtinTools = {
@@ -93436,30 +93889,63 @@ class ConcurrencyManager {
93436
93889
  }
93437
93890
  return 5;
93438
93891
  }
93439
- async acquire(model) {
93892
+ tryAcquireSlot(model) {
93440
93893
  const limit = this.getConcurrencyLimit(model);
93441
93894
  if (limit === Infinity) {
93442
- return;
93895
+ return true;
93443
93896
  }
93444
93897
  const current = this.counts.get(model) ?? 0;
93445
93898
  if (current < limit) {
93446
93899
  this.counts.set(model, current + 1);
93900
+ return true;
93901
+ }
93902
+ return false;
93903
+ }
93904
+ async acquire(model) {
93905
+ if (this.tryAcquireSlot(model))
93906
+ return;
93907
+ const outcome = await this.acquireWithDeadline(model, undefined);
93908
+ if (outcome.granted)
93447
93909
  return;
93910
+ if (outcome.reason === "cancelled") {
93911
+ throw new Error(`Concurrency queue cancelled for model: ${model}`);
93448
93912
  }
93449
- return new Promise((resolve10, reject) => {
93913
+ }
93914
+ async acquireWithDeadline(model, timeoutMs) {
93915
+ if (this.tryAcquireSlot(model)) {
93916
+ return { granted: true };
93917
+ }
93918
+ return new Promise((resolve10) => {
93450
93919
  const queue = this.queues.get(model) ?? [];
93920
+ let timer;
93451
93921
  const entry = {
93452
- resolve: () => {
93922
+ settled: false,
93923
+ enqueuedAt: Date.now(),
93924
+ settle: (outcome) => {
93453
93925
  if (entry.settled)
93454
93926
  return;
93455
93927
  entry.settled = true;
93456
- resolve10();
93457
- },
93458
- rawReject: reject,
93459
- settled: false
93928
+ if (timer !== undefined)
93929
+ clearTimeout(timer);
93930
+ resolve10(outcome);
93931
+ }
93460
93932
  };
93461
93933
  queue.push(entry);
93462
93934
  this.queues.set(model, queue);
93935
+ if (timeoutMs === undefined || timeoutMs <= 0)
93936
+ return;
93937
+ timer = setTimeout(() => {
93938
+ if (entry.settled)
93939
+ return;
93940
+ entry.settled = true;
93941
+ const pending = this.queues.get(model);
93942
+ if (pending) {
93943
+ const index = pending.indexOf(entry);
93944
+ if (index !== -1)
93945
+ pending.splice(index, 1);
93946
+ }
93947
+ resolve10({ granted: false, reason: "timeout", waitedMs: Date.now() - entry.enqueuedAt });
93948
+ }, timeoutMs);
93463
93949
  });
93464
93950
  }
93465
93951
  release(model) {
@@ -93473,7 +93959,7 @@ class ConcurrencyManager {
93473
93959
  if (!next)
93474
93960
  continue;
93475
93961
  if (!next.settled) {
93476
- next.resolve();
93962
+ next.settle({ granted: true });
93477
93963
  return;
93478
93964
  }
93479
93965
  }
@@ -93485,11 +93971,9 @@ class ConcurrencyManager {
93485
93971
  cancelWaiters(model) {
93486
93972
  const queue = this.queues.get(model);
93487
93973
  if (queue) {
93974
+ const now = Date.now();
93488
93975
  for (const entry of queue) {
93489
- if (!entry.settled) {
93490
- entry.settled = true;
93491
- entry.rawReject(new Error(`Concurrency queue cancelled for model: ${model}`));
93492
- }
93976
+ entry.settle({ granted: false, reason: "cancelled", waitedMs: now - entry.enqueuedAt });
93493
93977
  }
93494
93978
  this.queues.delete(model);
93495
93979
  }
@@ -93508,144 +93992,55 @@ class ConcurrencyManager {
93508
93992
  return this.queues.get(model)?.length ?? 0;
93509
93993
  }
93510
93994
  }
93511
- // src/features/background-agent/handle-index.ts
93512
- import { existsSync as existsSync73, mkdirSync as mkdirSync19, readdirSync as readdirSync25, readFileSync as readFileSync43, renameSync as renameSync5, unlinkSync as unlinkSync12, writeFileSync as writeFileSync26 } from "fs";
93513
- import { dirname as dirname19, join as join77 } from "path";
93514
-
93515
- // src/features/background-agent/constants.ts
93516
- var TASK_TTL_MS = 30 * 60 * 1000;
93517
- var MIN_STABILITY_TIME_MS2 = 10 * 1000;
93518
- var DEFAULT_STALE_TIMEOUT_MS = 180000;
93519
- var DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 600000;
93520
- var DEFAULT_MAX_TOOL_CALLS = 4000;
93521
- var DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20;
93522
- var DEFAULT_CIRCUIT_BREAKER_ENABLED = true;
93523
- var MIN_RUNTIME_BEFORE_STALE_MS = 30000;
93524
- var MIN_IDLE_TIME_MS = 5000;
93525
- var POLLING_INTERVAL_MS = 3000;
93526
- var PRUNE_THROTTLE_MS = 30000;
93527
- var TASK_CLEANUP_DELAY_MS = 10 * 60 * 1000;
93528
-
93529
- // src/features/background-agent/handle-index.ts
93530
- var BG_HANDLE_DIR_NAME = "bg-handles";
93531
- var BG_HANDLE_FILE_PREFIX = "bg_";
93532
- var BgHandleSchema = exports_external.object({
93533
- taskId: exports_external.string(),
93534
- parentSessionID: exports_external.string(),
93535
- parentMessageID: exports_external.string(),
93536
- description: exports_external.string(),
93537
- agent: exports_external.string(),
93538
- status: exports_external.enum(["pending", "running", "completed", "error", "cancelled", "interrupt"]),
93539
- model: exports_external.object({
93540
- providerID: exports_external.string(),
93541
- modelID: exports_external.string(),
93542
- variant: exports_external.string().optional(),
93543
- temperature: exports_external.number().optional()
93544
- }).optional(),
93545
- category: exports_external.string().optional(),
93546
- concurrencyGroup: exports_external.string().optional(),
93547
- queuedAt: exports_external.number().optional(),
93548
- startedAt: exports_external.number().optional(),
93549
- completedAt: exports_external.number().optional()
93550
- }).strict();
93551
- function getBgHandleDir(directory) {
93552
- return join77(directory, ".matrixx", BG_HANDLE_DIR_NAME);
93553
- }
93554
- function getHandlePath(directory, taskId) {
93555
- return join77(getBgHandleDir(directory), `${taskId}.json`);
93556
- }
93557
- function ensureDir3(dirPath) {
93558
- if (!existsSync73(dirPath)) {
93559
- mkdirSync19(dirPath, { recursive: true });
93560
- }
93561
- }
93562
- function toHandle(task) {
93563
- return {
93564
- taskId: task.id,
93565
- parentSessionID: task.parentSessionID,
93566
- parentMessageID: task.parentMessageID,
93567
- description: task.description,
93568
- agent: task.agent,
93569
- status: task.status,
93570
- model: task.model,
93571
- category: task.category,
93572
- concurrencyGroup: task.concurrencyGroup,
93573
- queuedAt: task.queuedAt?.getTime(),
93574
- startedAt: task.startedAt?.getTime(),
93575
- completedAt: task.completedAt?.getTime()
93576
- };
93577
- }
93578
- function writeHandle(directory, task) {
93579
- const filePath = getHandlePath(directory, task.id);
93580
- ensureDir3(dirname19(filePath));
93581
- const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
93582
- try {
93583
- writeFileSync26(tempPath, JSON.stringify(toHandle(task), null, 2), "utf-8");
93584
- renameSync5(tempPath, filePath);
93585
- } catch (error95) {
93586
- try {
93587
- if (existsSync73(tempPath)) {
93588
- unlinkSync12(tempPath);
93589
- }
93590
- } catch {}
93591
- throw error95;
93592
- }
93593
- }
93594
- function readHandleFile(filePath) {
93595
- try {
93596
- if (!existsSync73(filePath)) {
93597
- return null;
93598
- }
93599
- const parsed = JSON.parse(readFileSync43(filePath, "utf-8"));
93600
- const result = BgHandleSchema.safeParse(parsed);
93601
- return result.success ? result.data : null;
93602
- } catch {
93603
- return null;
93604
- }
93605
- }
93606
- function readHandles(directory) {
93607
- const dir = getBgHandleDir(directory);
93608
- if (!existsSync73(dir)) {
93609
- return [];
93610
- }
93611
- const handles = [];
93612
- for (const name of readdirSync25(dir)) {
93613
- if (!name.startsWith(BG_HANDLE_FILE_PREFIX) || !name.endsWith(".json")) {
93614
- continue;
93615
- }
93616
- const handle = readHandleFile(join77(dir, name));
93617
- if (handle) {
93618
- handles.push(handle);
93619
- }
93620
- }
93621
- return handles;
93622
- }
93623
- function sweepStaleHandles(directory, ttlMs = TASK_TTL_MS, now = Date.now()) {
93624
- const dir = getBgHandleDir(directory);
93625
- if (!existsSync73(dir)) {
93626
- return 0;
93627
- }
93628
- let removed = 0;
93629
- for (const name of readdirSync25(dir)) {
93630
- if (!name.startsWith(BG_HANDLE_FILE_PREFIX) || !name.endsWith(".json")) {
93631
- continue;
93632
- }
93633
- const filePath = join77(dir, name);
93634
- const handle = readHandleFile(filePath);
93635
- const timestamp = handle?.completedAt ?? handle?.startedAt ?? handle?.queuedAt;
93636
- if (handle && timestamp !== undefined && now - timestamp <= ttlMs) {
93637
- continue;
93638
- }
93639
- try {
93640
- unlinkSync12(filePath);
93641
- removed++;
93642
- } catch {}
93643
- }
93644
- if (removed > 0) {
93645
- log(`[bg-handles] Swept ${removed} stale handle file(s) from ${dir}`);
93995
+ // src/shared/wallclock-outcome.ts
93996
+ function formatWallclockTimeout(taskId, elapsedMs, limitMs) {
93997
+ const metadata = JSON.stringify({
93998
+ task_id: taskId,
93999
+ status: "cancelled",
94000
+ reason: "wall-clock-timeout",
94001
+ timedOut: true,
94002
+ elapsedMs,
94003
+ limitMs
94004
+ });
94005
+ return `Background task wall-clock timeout. Task ID ${taskId} exceeded its elapsed-time bound.
94006
+
94007
+ Task ID: ${taskId}
94008
+ Elapsed: ${formatDuration2(elapsedMs)}
94009
+ Limit: ${formatDuration2(limitMs)}
94010
+ Status: cancelled
94011
+ Reason: wall-clock-timeout
94012
+
94013
+ <task_metadata>${metadata}</task_metadata>`;
94014
+ }
94015
+ function formatDuration2(ms) {
94016
+ if (ms < 0)
94017
+ ms = 0;
94018
+ if (ms >= 60000) {
94019
+ const mins = Math.floor(ms / 60000);
94020
+ return `${mins}min`;
94021
+ }
94022
+ return `${Math.floor(ms / 1000)}sec`;
94023
+ }
94024
+
94025
+ // src/features/background-agent/admission.ts
94026
+ function classifyAdmission(parentSessionID, maxDepth) {
94027
+ if (!parentSessionID)
94028
+ return { kind: "root" };
94029
+ const immediateParent = getParentSessionID(parentSessionID);
94030
+ if (!immediateParent)
94031
+ return { kind: "root" };
94032
+ let depth = 1;
94033
+ let current = immediateParent;
94034
+ while (depth < maxDepth) {
94035
+ const parent = getParentSessionID(current);
94036
+ if (!parent)
94037
+ break;
94038
+ depth += 1;
94039
+ current = parent;
93646
94040
  }
93647
- return removed;
94041
+ return { kind: "nested", depth };
93648
94042
  }
94043
+
93649
94044
  // src/features/background-agent/error-helpers.ts
93650
94045
  function getErrorText(error95) {
93651
94046
  if (!error95)
@@ -93748,19 +94143,19 @@ function detectRepetitiveToolUse(window2) {
93748
94143
  }
93749
94144
 
93750
94145
  // src/features/background-agent/message-dir.ts
93751
- import { existsSync as existsSync74, readdirSync as readdirSync26, readFileSync as readFileSync44 } from "fs";
93752
- import { join as join78 } from "path";
94146
+ import { existsSync as existsSync76, readdirSync as readdirSync26, readFileSync as readFileSync44 } from "fs";
94147
+ import { join as join79 } from "path";
93753
94148
  var MESSAGE_DIR_CACHE_CAP = 1000;
93754
94149
  var messageDirCache = new Map;
93755
94150
  function computeMessageDir(sessionID) {
93756
- if (!existsSync74(MESSAGE_STORAGE))
94151
+ if (!existsSync76(MESSAGE_STORAGE))
93757
94152
  return null;
93758
- const directPath = join78(MESSAGE_STORAGE, sessionID);
93759
- if (existsSync74(directPath))
94153
+ const directPath = join79(MESSAGE_STORAGE, sessionID);
94154
+ if (existsSync76(directPath))
93760
94155
  return directPath;
93761
94156
  for (const dir of readdirSync26(MESSAGE_STORAGE)) {
93762
- const sessionPath = join78(MESSAGE_STORAGE, dir, sessionID);
93763
- if (existsSync74(sessionPath))
94157
+ const sessionPath = join79(MESSAGE_STORAGE, dir, sessionID);
94158
+ if (existsSync76(sessionPath))
93764
94159
  return sessionPath;
93765
94160
  }
93766
94161
  return null;
@@ -93798,7 +94193,7 @@ function findNearestMessageExcludingCompaction(messageDir) {
93798
94193
  const parsedByFile = new Map;
93799
94194
  for (const file3 of files) {
93800
94195
  try {
93801
- const content = readFileSync44(join78(messageDir, file3), "utf-8");
94196
+ const content = readFileSync44(join79(messageDir, file3), "utf-8");
93802
94197
  const parsed = JSON.parse(content);
93803
94198
  parsedByFile.set(file3, parsed);
93804
94199
  if (hasFullAgentAndModel(parsed)) {
@@ -93818,6 +94213,19 @@ function findNearestMessageExcludingCompaction(messageDir) {
93818
94213
  }
93819
94214
 
93820
94215
  // src/features/background-agent/notification-builder.ts
94216
+ var STATUS_LABELS = {
94217
+ completed: "COMPLETED",
94218
+ interrupt: "INTERRUPTED",
94219
+ cancelled: "CANCELLED",
94220
+ stopped: "STOPPED",
94221
+ statusUncertain: "STATUS UNCERTAIN",
94222
+ error: "ERROR",
94223
+ pending: "IN PROGRESS",
94224
+ running: "IN PROGRESS"
94225
+ };
94226
+ function statusToLabel(status) {
94227
+ return STATUS_LABELS[status];
94228
+ }
93821
94229
  function buildCompletionNotification(task, allComplete, completedTasks, remainingCount, duration5, errorInfo) {
93822
94230
  if (allComplete) {
93823
94231
  const completedTasksText = completedTasks.map((t) => `- \`${t.id}\`: ${t.description}`).join(`
@@ -93831,7 +94239,7 @@ ${completedTasksText || `- \`${task.id}\`: ${task.description}`}
93831
94239
  Use \`background_output(task_id="<id>")\` to retrieve each result.
93832
94240
  </system-reminder>`;
93833
94241
  }
93834
- const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED";
94242
+ const statusText = statusToLabel(task.status);
93835
94243
  return `<system-reminder>
93836
94244
  [BACKGROUND TASK ${statusText}]
93837
94245
  **ID:** \`${task.id}\`
@@ -93886,6 +94294,164 @@ async function resolveAgentAndModel(client2, task, enableParentSessionNotificati
93886
94294
  return { agent, model };
93887
94295
  }
93888
94296
 
94297
+ // src/features/background-agent/session-output.ts
94298
+ async function fetchSessionMessages2(client2, sessionID) {
94299
+ const response = await client2.session.messages({ path: { id: sessionID } });
94300
+ return normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
94301
+ }
94302
+ function messagesHaveAssistantOutput(messages2, sessionID) {
94303
+ const hasAssistantOrToolMessage = messages2.some((m) => m.info?.role === "assistant" || m.info?.role === "tool");
94304
+ if (!hasAssistantOrToolMessage) {
94305
+ if (sessionID)
94306
+ log("[background-agent] No assistant/tool messages found in session:", sessionID);
94307
+ return false;
94308
+ }
94309
+ const hasContent2 = messages2.some((m) => {
94310
+ if (m.info?.role !== "assistant" && m.info?.role !== "tool")
94311
+ return false;
94312
+ const parts = m.parts ?? [];
94313
+ return parts.some((p) => p.type === "text" && p.text && p.text.trim().length > 0 || p.type === "reasoning" && p.text && p.text.trim().length > 0 || p.type === "tool" || p.type === "tool_result" && p.content && (typeof p.content === "string" ? p.content.trim().length > 0 : p.content.length > 0));
94314
+ });
94315
+ if (!hasContent2 && sessionID) {
94316
+ log("[background-agent] Messages exist but no content found in session:", sessionID);
94317
+ }
94318
+ return hasContent2;
94319
+ }
94320
+ function messagesHaveRecordedError(messages2) {
94321
+ return messages2.some((m) => m.info?.role === "assistant" && m.info.error != null);
94322
+ }
94323
+ async function validateSessionHasOutput(client2, sessionID) {
94324
+ try {
94325
+ const messages2 = await fetchSessionMessages2(client2, sessionID);
94326
+ return messagesHaveAssistantOutput(messages2, sessionID);
94327
+ } catch (error95) {
94328
+ log("[background-agent] Error validating session output:", error95);
94329
+ return true;
94330
+ }
94331
+ }
94332
+
94333
+ // src/features/background-agent/reconcile.ts
94334
+ function uncertain() {
94335
+ return { status: "statusUncertain", terminalReason: "uncertain" };
94336
+ }
94337
+ function responseHasError(response) {
94338
+ if (typeof response !== "object" || response === null)
94339
+ return false;
94340
+ return Boolean(response.error);
94341
+ }
94342
+ function isNonIdle(status) {
94343
+ return status !== undefined && status.type !== "idle";
94344
+ }
94345
+ async function statusesOrThrow(client2) {
94346
+ const response = await client2.session.status();
94347
+ if (responseHasError(response))
94348
+ throw new Error("session.status returned an error response");
94349
+ return normalizeSDKResponse(response, {});
94350
+ }
94351
+ async function hasIncompleteTodos2(client2, sessionID) {
94352
+ try {
94353
+ const response = await client2.session.todo({ path: { id: sessionID } });
94354
+ const todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
94355
+ if (!todos || todos.length === 0)
94356
+ return false;
94357
+ return todos.some((t) => t.status !== "completed" && t.status !== "cancelled");
94358
+ } catch {
94359
+ return false;
94360
+ }
94361
+ }
94362
+ async function reconcileHandle(client2, handle, deps) {
94363
+ const sessionID = handle.sessionID?.trim();
94364
+ if (!sessionID)
94365
+ return uncertain();
94366
+ try {
94367
+ const statuses = await statusesOrThrow(client2);
94368
+ if (isNonIdle(statuses[sessionID]))
94369
+ return { status: "running" };
94370
+ const messages2 = await fetchSessionMessages2(client2, sessionID);
94371
+ if (messagesHaveRecordedError(messages2))
94372
+ return { status: "error" };
94373
+ if (hasPendingQuestionMessage(messages2))
94374
+ return { status: "running" };
94375
+ const hasOutput = deps?.validateOutput ? await deps.validateOutput(sessionID) : messagesHaveAssistantOutput(messages2);
94376
+ if (!hasOutput) {
94377
+ return await confirmStopped(client2, sessionID, deps);
94378
+ }
94379
+ if (await hasIncompleteTodos2(client2, sessionID))
94380
+ return { status: "running" };
94381
+ return { status: "completed" };
94382
+ } catch {
94383
+ return uncertain();
94384
+ }
94385
+ }
94386
+ async function confirmStopped(client2, sessionID, deps) {
94387
+ if (!deps?.sleep)
94388
+ return { status: "stopped", terminalReason: "no-output" };
94389
+ await deps.sleep(RECONCILE_CONFIRMATION_GRACE_MS);
94390
+ const statuses = await statusesOrThrow(client2);
94391
+ if (isNonIdle(statuses[sessionID]))
94392
+ return { status: "running" };
94393
+ const messages2 = await fetchSessionMessages2(client2, sessionID);
94394
+ if (messagesHaveRecordedError(messages2))
94395
+ return { status: "error" };
94396
+ if (messagesHaveAssistantOutput(messages2) || hasPendingQuestionMessage(messages2)) {
94397
+ return { status: "running" };
94398
+ }
94399
+ return { status: "stopped", terminalReason: "no-output" };
94400
+ }
94401
+
94402
+ // src/features/background-agent/revive.ts
94403
+ var REVIVABLE_STATUSES = ["cancelled", "stopped", "interrupt", "error", "completed"];
94404
+ function isRevivableStatus(status) {
94405
+ return REVIVABLE_STATUSES.includes(status);
94406
+ }
94407
+ function classifyRevivable(handle, opts) {
94408
+ if (handle.status === "pending" || handle.status === "running") {
94409
+ return { eligible: false, reason: "active" };
94410
+ }
94411
+ if (handle.sessionID === undefined || handle.sessionID.trim().length === 0) {
94412
+ return { eligible: false, reason: "no-session" };
94413
+ }
94414
+ if (handle.status === "statusUncertain") {
94415
+ if (opts?.force === true) {
94416
+ return { eligible: true };
94417
+ }
94418
+ return { eligible: false, reason: "uncertain" };
94419
+ }
94420
+ if (isRevivableStatus(handle.status)) {
94421
+ return { eligible: true };
94422
+ }
94423
+ return { eligible: false, reason: "unknown-task" };
94424
+ }
94425
+ function findRevivableHandle(handles, opts) {
94426
+ if (opts.taskId !== undefined && opts.taskId !== "") {
94427
+ const byTask = handles.find((handle) => handle.taskId === opts.taskId);
94428
+ if (byTask)
94429
+ return byTask;
94430
+ }
94431
+ if (opts.sessionId !== undefined && opts.sessionId !== "") {
94432
+ return handles.find((handle) => handle.sessionID === opts.sessionId);
94433
+ }
94434
+ return;
94435
+ }
94436
+ function toReviveTask(handle, prompt) {
94437
+ return {
94438
+ id: handle.taskId,
94439
+ parentSessionID: handle.parentSessionID,
94440
+ parentMessageID: handle.parentMessageID,
94441
+ description: handle.description,
94442
+ prompt,
94443
+ agent: handle.agent,
94444
+ status: "pending",
94445
+ sessionID: handle.sessionID,
94446
+ queuedAt: handle.queuedAt !== undefined ? new Date(handle.queuedAt) : undefined,
94447
+ completedAt: undefined,
94448
+ error: undefined,
94449
+ model: handle.model,
94450
+ category: handle.category,
94451
+ concurrencyGroup: handle.concurrencyGroup
94452
+ };
94453
+ }
94454
+
93889
94455
  // src/features/background-agent/task-history.ts
93890
94456
  var MAX_ENTRIES_PER_PARENT = 100;
93891
94457
 
@@ -93943,11 +94509,232 @@ class TaskHistory {
93943
94509
  return lines.join(`
93944
94510
  `);
93945
94511
  }
94512
+ getJobBoardEntries(parentSessionID) {
94513
+ return this.getByParentSession(parentSessionID).map((e) => ({
94514
+ agent: e.agent,
94515
+ ...e.category !== undefined ? { category: e.category } : {},
94516
+ status: e.status,
94517
+ description: e.description.replace(/[\n\r]+/g, " ").trim(),
94518
+ ...e.sessionID !== undefined ? { session: e.sessionID } : {}
94519
+ }));
94520
+ }
94521
+ buildJobBoardSnapshot(parentSessionID, opts) {
94522
+ const entries = this.getJobBoardEntries(parentSessionID);
94523
+ if (entries.length === 0)
94524
+ return null;
94525
+ const strategy = opts?.strategy ?? "latest";
94526
+ const block = formatJobBoardBlock(entries);
94527
+ if (strategy === "latest")
94528
+ return { strategy, block };
94529
+ const maxRetained = clampMaxRetainedSnapshots(opts?.maxRetained);
94530
+ const retained = [...opts?.previous ?? [], block].slice(-maxRetained);
94531
+ return { strategy, block, retained };
94532
+ }
94533
+ }
94534
+ function clampMaxRetainedSnapshots(value) {
94535
+ if (value === undefined || !Number.isFinite(value))
94536
+ return 20;
94537
+ return Math.min(100, Math.max(1, Math.floor(value)));
94538
+ }
94539
+ function formatJobBoardBlock(entries) {
94540
+ const lines = entries.map((row) => {
94541
+ const parts = [
94542
+ `- **${row.agent}**`,
94543
+ row.category ? `[${row.category}]` : null,
94544
+ `(${row.status})`,
94545
+ `: ${row.description}`,
94546
+ row.session ? ` | session: \`${row.session}\`` : null
94547
+ ];
94548
+ return parts.filter(Boolean).join("");
94549
+ });
94550
+ return `**Background task board (${entries.length} task${entries.length === 1 ? "" : "s"}):**
94551
+ ${lines.join(`
94552
+ `)}`;
94553
+ }
94554
+
94555
+ // src/features/background-agent/wake-scheduler.ts
94556
+ var WAKE_REMINDER_TEXT = `<system-reminder>
94557
+ [BACKGROUND WAKE]
94558
+
94559
+ One or more background tasks for this session are still running or have undelivered results.
94560
+ Use background_output with the task ID to retrieve results when ready.
94561
+ Do NOT poll - continue productive work.
94562
+ </system-reminder>`;
94563
+ function resolveWakeIntervalMs(cfg) {
94564
+ const raw = cfg?.intervalMs ?? DEFAULT_WAKE_INTERVAL_MS;
94565
+ return Math.max(MIN_WAKE_INTERVAL_MS, raw);
94566
+ }
94567
+ function shouldWakeForParent(state3) {
94568
+ if (!state3.hasPendingChildren && !state3.hasUndeliveredNotifications)
94569
+ return false;
94570
+ if (state3.hasUnfinishedTodos === false)
94571
+ return false;
94572
+ return true;
94573
+ }
94574
+ function nextWakeDelay(lastWakeAtMs, intervalMs, nowMs = Date.now()) {
94575
+ const interval = Math.max(MIN_WAKE_INTERVAL_MS, intervalMs);
94576
+ return Math.max(0, lastWakeAtMs + interval - nowMs);
94577
+ }
94578
+ var MAX_STAGNANT_WAKES = 2;
94579
+
94580
+ class WakeTracker {
94581
+ stagnantByParent = new Map;
94582
+ recordWake(parentSessionID, progressed) {
94583
+ if (progressed) {
94584
+ this.stagnantByParent.delete(parentSessionID);
94585
+ return true;
94586
+ }
94587
+ const stagnant = (this.stagnantByParent.get(parentSessionID) ?? 0) + 1;
94588
+ this.stagnantByParent.set(parentSessionID, stagnant);
94589
+ return stagnant <= MAX_STAGNANT_WAKES;
94590
+ }
94591
+ resetOnActivity(parentSessionID) {
94592
+ this.stagnantByParent.delete(parentSessionID);
94593
+ }
94594
+ stagnantCount(parentSessionID) {
94595
+ return this.stagnantByParent.get(parentSessionID) ?? 0;
94596
+ }
94597
+ }
94598
+ function defaultScheduleTimer(callback, delayMs) {
94599
+ return setTimeout(callback, delayMs);
94600
+ }
94601
+ function defaultClearTimer(handle) {
94602
+ clearTimeout(handle);
94603
+ }
94604
+
94605
+ class WakeScheduler {
94606
+ timers = new Map;
94607
+ registrations = new Map;
94608
+ snapshots = new Map;
94609
+ tracker = new WakeTracker;
94610
+ scheduleTimer;
94611
+ clearTimer;
94612
+ constructor(deps) {
94613
+ this.scheduleTimer = deps?.scheduleTimer ?? defaultScheduleTimer;
94614
+ this.clearTimer = deps?.clearTimer ?? defaultClearTimer;
94615
+ }
94616
+ armTimer(parentSessionID, callback, delayMs) {
94617
+ const handle = this.scheduleTimer(callback, delayMs);
94618
+ const maybeUnref = handle;
94619
+ if (typeof maybeUnref.unref === "function")
94620
+ maybeUnref.unref();
94621
+ this.timers.set(parentSessionID, handle);
94622
+ }
94623
+ schedule(parentSessionID, registration) {
94624
+ this.clearTimerFor(parentSessionID);
94625
+ this.registrations.set(parentSessionID, registration);
94626
+ this.snapshots.delete(parentSessionID);
94627
+ this.tracker.resetOnActivity(parentSessionID);
94628
+ this.armTimer(parentSessionID, () => void this.tick(parentSessionID), nextWakeDelay(Date.now(), registration.intervalMs));
94629
+ }
94630
+ clear(parentSessionID) {
94631
+ this.clearTimerFor(parentSessionID);
94632
+ this.registrations.delete(parentSessionID);
94633
+ this.snapshots.delete(parentSessionID);
94634
+ this.tracker.resetOnActivity(parentSessionID);
94635
+ }
94636
+ dispose() {
94637
+ for (const handle of this.timers.values())
94638
+ this.clearTimer(handle);
94639
+ this.timers.clear();
94640
+ this.registrations.clear();
94641
+ this.snapshots.clear();
94642
+ }
94643
+ hasScheduledWake(parentSessionID) {
94644
+ return this.timers.has(parentSessionID);
94645
+ }
94646
+ clearTimerFor(parentSessionID) {
94647
+ const handle = this.timers.get(parentSessionID);
94648
+ if (handle !== undefined) {
94649
+ this.clearTimer(handle);
94650
+ this.timers.delete(parentSessionID);
94651
+ }
94652
+ }
94653
+ async tick(parentSessionID) {
94654
+ this.timers.delete(parentSessionID);
94655
+ const registration = this.registrations.get(parentSessionID);
94656
+ if (!registration)
94657
+ return;
94658
+ const state3 = registration.readState();
94659
+ const previous = this.snapshots.get(parentSessionID);
94660
+ const progressed = previous === undefined || previous.hasPendingChildren !== state3.hasPendingChildren || previous.hasUndeliveredNotifications !== state3.hasUndeliveredNotifications || previous.hasUnfinishedTodos !== state3.hasUnfinishedTodos;
94661
+ this.snapshots.set(parentSessionID, state3);
94662
+ if (!shouldWakeForParent(state3)) {
94663
+ this.registrations.delete(parentSessionID);
94664
+ this.snapshots.delete(parentSessionID);
94665
+ return;
94666
+ }
94667
+ const allowed = this.tracker.recordWake(parentSessionID, progressed);
94668
+ if (allowed) {
94669
+ try {
94670
+ await registration.deliverWake();
94671
+ } catch {}
94672
+ }
94673
+ if (allowed && this.registrations.has(parentSessionID)) {
94674
+ this.armTimer(parentSessionID, () => void this.tick(parentSessionID), nextWakeDelay(Date.now(), registration.intervalMs));
94675
+ } else {
94676
+ this.registrations.delete(parentSessionID);
94677
+ this.snapshots.delete(parentSessionID);
94678
+ }
94679
+ }
94680
+ }
94681
+
94682
+ // src/features/background-agent/wallclock.ts
94683
+ var MAX_WALLCLOCK_TIMEOUT_MS = 2147483647;
94684
+ function isWallclockEnabled(timeoutMs) {
94685
+ return timeoutMs !== undefined && timeoutMs !== 0;
94686
+ }
94687
+
94688
+ class WallclockSupervisor {
94689
+ entries = new Map;
94690
+ arm(taskId, startedAtMs, timeoutMs, onFire) {
94691
+ if (!isWallclockEnabled(timeoutMs))
94692
+ return;
94693
+ const limitMs = Math.min(timeoutMs, MAX_WALLCLOCK_TIMEOUT_MS);
94694
+ this.disarm(taskId);
94695
+ const deadlineAt = startedAtMs + limitMs;
94696
+ const delayMs = Math.max(deadlineAt - Date.now(), 0);
94697
+ let timer;
94698
+ const entry = { deadlineAt, fired: false, timer };
94699
+ timer = setTimeout(() => {
94700
+ if (entry.fired)
94701
+ return;
94702
+ entry.fired = true;
94703
+ this.entries.delete(taskId);
94704
+ onFire({ taskId, elapsedMs: Date.now() - startedAtMs, limitMs });
94705
+ }, delayMs);
94706
+ entry.timer = timer;
94707
+ this.entries.set(taskId, entry);
94708
+ }
94709
+ disarm(taskId) {
94710
+ const entry = this.entries.get(taskId);
94711
+ if (entry === undefined)
94712
+ return false;
94713
+ entry.fired = true;
94714
+ if (entry.timer !== undefined)
94715
+ clearTimeout(entry.timer);
94716
+ this.entries.delete(taskId);
94717
+ return true;
94718
+ }
94719
+ disarmAll() {
94720
+ for (const entry of this.entries.values()) {
94721
+ entry.fired = true;
94722
+ if (entry.timer !== undefined)
94723
+ clearTimeout(entry.timer);
94724
+ }
94725
+ this.entries.clear();
94726
+ }
94727
+ size() {
94728
+ return this.entries.size;
94729
+ }
94730
+ getDeadlineAt(taskId) {
94731
+ return this.entries.get(taskId)?.deadlineAt;
94732
+ }
93946
94733
  }
93947
94734
 
93948
94735
  // src/features/background-agent/manager.ts
94736
+ var NESTED_RESERVE_ATTEMPT_TIMEOUT_MS = 100;
93949
94737
  function restoreTaskFromHandle(handle) {
93950
- const interrupted = handle.status === "running" || handle.status === "pending";
93951
94738
  return {
93952
94739
  id: handle.taskId,
93953
94740
  parentSessionID: handle.parentSessionID,
@@ -93955,7 +94742,9 @@ function restoreTaskFromHandle(handle) {
93955
94742
  description: handle.description,
93956
94743
  prompt: "",
93957
94744
  agent: handle.agent,
93958
- status: interrupted ? "interrupt" : handle.status,
94745
+ status: handle.status,
94746
+ sessionID: handle.sessionID,
94747
+ terminalReason: handle.terminalReason,
93959
94748
  queuedAt: handle.queuedAt !== undefined ? new Date(handle.queuedAt) : undefined,
93960
94749
  startedAt: handle.startedAt !== undefined ? new Date(handle.startedAt) : undefined,
93961
94750
  completedAt: handle.completedAt !== undefined ? new Date(handle.completedAt) : undefined,
@@ -93987,9 +94776,15 @@ class BackgroundManager {
93987
94776
  processingKeys = new Set;
93988
94777
  completionTimers = new Map;
93989
94778
  idleDeferralTimers = new Map;
94779
+ wakeScheduler = new WakeScheduler;
93990
94780
  notificationQueueByParent = new Map;
93991
94781
  enableParentSessionNotifications;
93992
94782
  cachedCircuitBreakerSettings;
94783
+ handoffCompression;
94784
+ nestedActive = new Map;
94785
+ nestedBypassTaskIds = new Set;
94786
+ wallclock = new WallclockSupervisor;
94787
+ wallclockGraceTimers = new Map;
93993
94788
  taskHistory = new TaskHistory;
93994
94789
  constructor(ctx, config4, options) {
93995
94790
  this.tasks = new Map;
@@ -94003,6 +94798,7 @@ class BackgroundManager {
94003
94798
  this.onSubagentSessionCreated = options?.onSubagentSessionCreated;
94004
94799
  this.onShutdown = options?.onShutdown;
94005
94800
  this.enableParentSessionNotifications = options?.enableParentSessionNotifications ?? true;
94801
+ this.handoffCompression = options?.handoffCompression;
94006
94802
  this.registerProcessCleanup();
94007
94803
  }
94008
94804
  persistHandle(task) {
@@ -94012,6 +94808,66 @@ class BackgroundManager {
94012
94808
  log("[background-agent] Failed to persist background handle:", { taskId: task.id, error: error95 });
94013
94809
  }
94014
94810
  }
94811
+ armWallclock(task) {
94812
+ const timeoutMs = this.config?.wallClockTimeoutMs ?? DEFAULT_WALL_CLOCK_TIMEOUT_MS;
94813
+ if (timeoutMs === 0 || timeoutMs === undefined || !task.startedAt)
94814
+ return;
94815
+ const startedAtMs = task.startedAt.getTime();
94816
+ this.wallclock.arm(task.id, startedAtMs, timeoutMs, () => {
94817
+ this.onWallclockFire(task.id);
94818
+ });
94819
+ }
94820
+ disarmWallclock(taskId) {
94821
+ this.wallclock.disarm(taskId);
94822
+ const grace = this.wallclockGraceTimers.get(taskId);
94823
+ if (grace !== undefined) {
94824
+ clearTimeout(grace);
94825
+ this.wallclockGraceTimers.delete(taskId);
94826
+ }
94827
+ }
94828
+ async onWallclockFire(taskId) {
94829
+ const task = this.tasks.get(taskId);
94830
+ if (task?.status !== "running")
94831
+ return;
94832
+ if (this.wallclockGraceTimers.has(taskId))
94833
+ return;
94834
+ if (task.sessionID) {
94835
+ this.abortSessionQuietly(task.sessionID, "wall-clock-timeout");
94836
+ }
94837
+ const graceMs = this.config?.wallClockAbortGraceMs ?? DEFAULT_WALL_CLOCK_ABORT_GRACE_MS;
94838
+ const startedAtMs = task.startedAt?.getTime() ?? Date.now();
94839
+ const limitMs = this.config?.wallClockTimeoutMs ?? DEFAULT_WALL_CLOCK_TIMEOUT_MS;
94840
+ const timer = setTimeout(() => {
94841
+ this.wallclockGraceTimers.delete(taskId);
94842
+ this.completeWallclockTimeout(taskId, startedAtMs, limitMs);
94843
+ }, graceMs);
94844
+ this.wallclockGraceTimers.set(taskId, timer);
94845
+ }
94846
+ async completeWallclockTimeout(taskId, startedAtMs, limitMs) {
94847
+ const task = this.tasks.get(taskId);
94848
+ if (task?.status !== "running")
94849
+ return;
94850
+ const elapsedMs = Date.now() - startedAtMs;
94851
+ const elapsedMin = Math.round(elapsedMs / 60000);
94852
+ const limitMin = Math.round(limitMs / 60000);
94853
+ task.status = "cancelled";
94854
+ task.terminalReason = "wall-clock-timeout";
94855
+ task.error = `Wall-clock timeout (elapsed ${elapsedMin}min, limit ${limitMin}min)
94856
+
94857
+ ${formatWallclockTimeout(task.id, elapsedMs, limitMs)}`;
94858
+ task.completedAt = new Date;
94859
+ this.persistHandle(task);
94860
+ if (task.concurrencyKey) {
94861
+ this.concurrencyManager.release(task.concurrencyKey);
94862
+ task.concurrencyKey = undefined;
94863
+ }
94864
+ this.decrementNestedActive(task);
94865
+ try {
94866
+ await this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task));
94867
+ } catch (err) {
94868
+ log("[background-agent] Error in notifyParentSession for wall-clock task:", { taskId: task.id, error: err });
94869
+ }
94870
+ }
94015
94871
  async restoreHandles() {
94016
94872
  const swept = sweepStaleHandles(this.directory);
94017
94873
  const handles = readHandles(this.directory);
@@ -94020,13 +94876,44 @@ class BackgroundManager {
94020
94876
  if (this.tasks.has(handle.taskId)) {
94021
94877
  continue;
94022
94878
  }
94023
- this.tasks.set(handle.taskId, restoreTaskFromHandle(handle));
94879
+ const task = restoreTaskFromHandle(handle);
94880
+ this.tasks.set(handle.taskId, task);
94024
94881
  restored++;
94882
+ if (handle.status === "running" || handle.status === "pending") {
94883
+ await this.reconcileRestoredTask(task, handle);
94884
+ }
94025
94885
  }
94026
94886
  if (restored > 0 || swept > 0) {
94027
94887
  log("[background-agent] Restored background handles:", { restored, swept });
94028
94888
  }
94029
94889
  }
94890
+ async reconcileRestoredTask(task, handle) {
94891
+ let outcome;
94892
+ try {
94893
+ outcome = await reconcileHandle(this.client, handle);
94894
+ } catch (error95) {
94895
+ log("[background-agent] Handle reconciliation failed:", { taskId: handle.taskId, error: error95 });
94896
+ outcome = { status: "statusUncertain", terminalReason: "uncertain" };
94897
+ }
94898
+ if (outcome.status === "running") {
94899
+ task.status = "running";
94900
+ if (task.sessionID) {
94901
+ registerSubagentSession(task.sessionID, task.parentSessionID);
94902
+ }
94903
+ task.concurrencyKey = undefined;
94904
+ const pending = this.pendingByParent.get(task.parentSessionID) ?? new Set;
94905
+ pending.add(task.id);
94906
+ this.pendingByParent.set(task.parentSessionID, pending);
94907
+ this.startPolling();
94908
+ this.armWallclock(task);
94909
+ return;
94910
+ }
94911
+ task.status = outcome.status;
94912
+ task.terminalReason = outcome.terminalReason;
94913
+ task.completedAt = new Date;
94914
+ this.persistHandle(task);
94915
+ this.disarmWallclock(task.id);
94916
+ }
94030
94917
  async launch(input) {
94031
94918
  log("[background-agent] launch() called with:", {
94032
94919
  agent: input.agent,
@@ -94088,7 +94975,48 @@ class BackgroundManager {
94088
94975
  const queue = this.queuesByKey.get(key);
94089
94976
  while (queue && queue.length > 0) {
94090
94977
  const item = queue[0];
94091
- await this.concurrencyManager.acquire(key);
94978
+ const nestedEnabled = this.config?.nestedAdmission?.enabled ?? DEFAULT_NESTED_ADMISSION_ENABLED;
94979
+ const maxDepth = this.config?.nestedAdmission?.maxDepth ?? DEFAULT_NESTED_MAX_DEPTH;
94980
+ const admission = classifyAdmission(item.input.parentSessionID, maxDepth);
94981
+ if (admission.kind === "nested" && nestedEnabled) {
94982
+ const result = await this.admitNested(key, item, maxDepth);
94983
+ if (result.kind === "skip") {
94984
+ queue.shift();
94985
+ continue;
94986
+ }
94987
+ acquired = result.slotHeld;
94988
+ try {
94989
+ await this.startTask(item);
94990
+ acquired = false;
94991
+ } catch (error95) {
94992
+ log("[background-agent] Error starting nested task:", error95);
94993
+ if (!item.task.concurrencyKey && acquired) {
94994
+ this.concurrencyManager.release(key);
94995
+ acquired = false;
94996
+ }
94997
+ this.decrementNestedActive(item.task);
94998
+ }
94999
+ if (!result.slotHeld) {
95000
+ item.task.concurrencyKey = undefined;
95001
+ }
95002
+ queue.shift();
95003
+ continue;
95004
+ }
95005
+ const admissionTimeoutMs = this.config?.admissionTimeoutMs ?? DEFAULT_ADMISSION_TIMEOUT_MS;
95006
+ const outcome = await this.concurrencyManager.acquireWithDeadline(key, admissionTimeoutMs);
95007
+ if (!outcome.granted) {
95008
+ if (outcome.reason === "cancelled") {
95009
+ item.task.status = "cancelled";
95010
+ item.task.completedAt = new Date;
95011
+ this.persistHandle(item.task);
95012
+ queue.shift();
95013
+ continue;
95014
+ }
95015
+ const limit = this.concurrencyManager.getConcurrencyLimit(key);
95016
+ await this.terminateQueuedTask(item.task, "queue-saturated", `Queue admission timed out after ${outcome.waitedMs}ms (key "${key}", limit ${limit})`);
95017
+ queue.shift();
95018
+ continue;
95019
+ }
94092
95020
  acquired = true;
94093
95021
  if (item.task.status === "cancelled" || item.task.status === "error") {
94094
95022
  this.concurrencyManager.release(key);
@@ -94115,6 +95043,76 @@ class BackgroundManager {
94115
95043
  this.processingKeys.delete(key);
94116
95044
  }
94117
95045
  }
95046
+ async admitNested(key, item, maxDepth) {
95047
+ const mode = this.config?.nestedAdmission?.mode ?? DEFAULT_NESTED_ADMISSION_MODE;
95048
+ if (mode === "reserve") {
95049
+ const outcome = await this.concurrencyManager.acquireWithDeadline(key, NESTED_RESERVE_ATTEMPT_TIMEOUT_MS);
95050
+ if (outcome.granted) {
95051
+ return { kind: "admitted", slotHeld: true };
95052
+ }
95053
+ if (outcome.reason === "cancelled") {
95054
+ item.task.status = "cancelled";
95055
+ item.task.completedAt = new Date;
95056
+ this.persistHandle(item.task);
95057
+ return { kind: "skip" };
95058
+ }
95059
+ }
95060
+ const active = this.nestedActive.get(key) ?? 0;
95061
+ if (active >= maxDepth) {
95062
+ await this.terminateQueuedTask(item.task, "nested-depth-exceeded", `Nested admission depth exceeded (maxDepth ${maxDepth}, key "${key}")`);
95063
+ return { kind: "skip" };
95064
+ }
95065
+ this.nestedActive.set(key, active + 1);
95066
+ this.nestedBypassTaskIds.add(item.task.id);
95067
+ return { kind: "admitted", slotHeld: false };
95068
+ }
95069
+ async terminateQueuedTask(task, terminalReason, errorMessage) {
95070
+ task.status = "stopped";
95071
+ task.terminalReason = terminalReason;
95072
+ task.completedAt = new Date;
95073
+ task.error = errorMessage;
95074
+ this.persistHandle(task);
95075
+ this.disarmWallclock(task.id);
95076
+ this.taskHistory.record(task.parentSessionID, {
95077
+ id: task.id,
95078
+ sessionID: task.sessionID,
95079
+ agent: task.agent,
95080
+ description: task.description,
95081
+ status: "stopped",
95082
+ category: task.category,
95083
+ startedAt: task.startedAt,
95084
+ completedAt: task.completedAt
95085
+ });
95086
+ const existingTimer = this.completionTimers.get(task.id);
95087
+ if (existingTimer) {
95088
+ clearTimeout(existingTimer);
95089
+ this.completionTimers.delete(task.id);
95090
+ }
95091
+ const idleTimer = this.idleDeferralTimers.get(task.id);
95092
+ if (idleTimer) {
95093
+ clearTimeout(idleTimer);
95094
+ this.idleDeferralTimers.delete(task.id);
95095
+ }
95096
+ this.cleanupPendingByParent(task);
95097
+ this.markForNotification(task);
95098
+ try {
95099
+ await this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task));
95100
+ } catch (err) {
95101
+ log("[background-agent] Error in notifyParentSession for stopped task:", { taskId: task.id, error: err });
95102
+ }
95103
+ }
95104
+ decrementNestedActive(task) {
95105
+ if (!this.nestedBypassTaskIds.has(task.id))
95106
+ return;
95107
+ this.nestedBypassTaskIds.delete(task.id);
95108
+ const key = task.concurrencyGroup ?? task.concurrencyKey;
95109
+ if (!key)
95110
+ return;
95111
+ const current = this.nestedActive.get(key) ?? 0;
95112
+ if (current > 0) {
95113
+ this.nestedActive.set(key, current - 1);
95114
+ }
95115
+ }
94118
95116
  async startTask(item) {
94119
95117
  const { task, input } = item;
94120
95118
  log("[background-agent] Starting task:", {
@@ -94179,6 +95177,7 @@ class BackgroundManager {
94179
95177
  task.concurrencyKey = concurrencyKey;
94180
95178
  task.concurrencyGroup = concurrencyKey;
94181
95179
  this.persistHandle(task);
95180
+ this.armWallclock(task);
94182
95181
  this.taskHistory.record(input.parentSessionID, { id: task.id, sessionID, agent: input.agent, description: input.description, status: "running", category: input.category, startedAt: task.startedAt });
94183
95182
  this.startPolling();
94184
95183
  log("[background-agent] Launching task:", { taskId: task.id, sessionID, agent: input.agent });
@@ -94231,6 +95230,7 @@ class BackgroundManager {
94231
95230
  }
94232
95231
  existingTask.completedAt = new Date;
94233
95232
  this.persistHandle(existingTask);
95233
+ this.disarmWallclock(existingTask.id);
94234
95234
  if (existingTask.concurrencyKey) {
94235
95235
  this.concurrencyManager.release(existingTask.concurrencyKey);
94236
95236
  existingTask.concurrencyKey = undefined;
@@ -94381,6 +95381,7 @@ class BackgroundManager {
94381
95381
  }
94382
95382
  existingTask.startedAt = new Date;
94383
95383
  this.persistHandle(existingTask);
95384
+ this.armWallclock(existingTask);
94384
95385
  existingTask.progress = {
94385
95386
  toolCalls: existingTask.progress?.toolCalls ?? 0,
94386
95387
  lastUpdate: new Date
@@ -94437,6 +95438,7 @@ class BackgroundManager {
94437
95438
  existingTask.error = errorMessage;
94438
95439
  existingTask.completedAt = new Date;
94439
95440
  this.persistHandle(existingTask);
95441
+ this.disarmWallclock(existingTask.id);
94440
95442
  if (existingTask.concurrencyKey) {
94441
95443
  this.concurrencyManager.release(existingTask.concurrencyKey);
94442
95444
  existingTask.concurrencyKey = undefined;
@@ -94452,6 +95454,158 @@ class BackgroundManager {
94452
95454
  });
94453
95455
  return existingTask;
94454
95456
  }
95457
+ async revive(input) {
95458
+ const id = input.taskId ?? input.sessionId ?? "";
95459
+ let task = input.taskId !== undefined ? this.tasks.get(input.taskId) : undefined;
95460
+ if (!task && input.sessionId !== undefined)
95461
+ task = this.findBySession(input.sessionId);
95462
+ if (task?.status === "running") {
95463
+ log("[background-agent] Revive skipped - task already running:", { taskId: task.id, sessionID: task.sessionID });
95464
+ return task;
95465
+ }
95466
+ const guidance = { active: `Task ${id} is still active; use background_output to inspect it.`, uncertain: `Task ${id} has unknown liveness; re-run with force: true to acknowledge this.`, "no-session": `Task ${id} has no session to revive (it never produced one, e.g. it was queue-saturated).`, "unknown-task": `Task ${id} is not revivable (unrecognised state).`, expired: `Task ${id} is not revivable (unrecognised state).` };
95467
+ if (!task) {
95468
+ const handle = findRevivableHandle(readHandles(this.directory), { taskId: input.taskId, sessionId: input.sessionId });
95469
+ if (!handle)
95470
+ throw new Error(`Task not found for revive: ${id}. It may have expired (handles are retained 30 minutes) or never existed.`);
95471
+ const handleEligibility = classifyRevivable(handle, { force: input.force });
95472
+ if (!handleEligibility.eligible)
95473
+ throw new Error(guidance[handleEligibility.reason]);
95474
+ task = toReviveTask(handle, input.prompt);
95475
+ } else {
95476
+ task.prompt = input.prompt;
95477
+ const taskEligibility = classifyRevivable(task, { force: input.force });
95478
+ if (!taskEligibility.eligible)
95479
+ throw new Error(guidance[taskEligibility.reason]);
95480
+ }
95481
+ if (!task.sessionID)
95482
+ throw new Error(`Task has no sessionID: ${task.id}`);
95483
+ const sessionID = task.sessionID;
95484
+ const completionTimer = this.completionTimers.get(task.id);
95485
+ if (completionTimer) {
95486
+ clearTimeout(completionTimer);
95487
+ this.completionTimers.delete(task.id);
95488
+ }
95489
+ task.status = "pending";
95490
+ task.parentSessionID = input.parentSessionID;
95491
+ task.parentMessageID = input.parentMessageID;
95492
+ if (input.parentModel !== undefined)
95493
+ task.parentModel = input.parentModel;
95494
+ if (input.parentAgent !== undefined)
95495
+ task.parentAgent = input.parentAgent;
95496
+ if (input.parentTools !== undefined)
95497
+ task.parentTools = input.parentTools;
95498
+ this.tasks.set(task.id, task);
95499
+ this.taskHistory.record(task.parentSessionID, { id: task.id, sessionID, agent: task.agent, description: task.description, status: "pending", startedAt: task.startedAt });
95500
+ const pending = this.pendingByParent.get(task.parentSessionID) ?? new Set;
95501
+ pending.add(task.id);
95502
+ this.pendingByParent.set(task.parentSessionID, pending);
95503
+ const key = task.concurrencyGroup ?? task.agent;
95504
+ const maxDepth = this.config?.nestedAdmission?.maxDepth ?? DEFAULT_NESTED_MAX_DEPTH;
95505
+ const admissionTimeoutMs = this.config?.admissionTimeoutMs ?? DEFAULT_ADMISSION_TIMEOUT_MS;
95506
+ const nestedEnabled = this.config?.nestedAdmission?.enabled ?? DEFAULT_NESTED_ADMISSION_ENABLED;
95507
+ const nestedMode = this.config?.nestedAdmission?.mode ?? DEFAULT_NESTED_ADMISSION_MODE;
95508
+ const admission = classifyAdmission(input.parentSessionID, maxDepth);
95509
+ log("[background-agent] Reviving task:", { taskId: task.id, admission: admission.kind, key });
95510
+ let slotHeld = true;
95511
+ if (admission.kind === "nested" && nestedEnabled) {
95512
+ let bypass = true;
95513
+ if (nestedMode === "reserve") {
95514
+ const reserved = await this.concurrencyManager.acquireWithDeadline(key, NESTED_RESERVE_ATTEMPT_TIMEOUT_MS);
95515
+ if (reserved.granted) {
95516
+ bypass = false;
95517
+ } else if (reserved.reason === "cancelled") {
95518
+ task.status = "cancelled";
95519
+ this.persistHandle(task);
95520
+ throw new Error(`Revive admission cancelled for task ${task.id}.`);
95521
+ }
95522
+ }
95523
+ if (bypass) {
95524
+ const active = this.nestedActive.get(key) ?? 0;
95525
+ if (active >= maxDepth) {
95526
+ const depthMessage = `Nested admission depth exceeded (maxDepth ${maxDepth}, key "${key}")`;
95527
+ await this.terminateQueuedTask(task, "nested-depth-exceeded", depthMessage);
95528
+ throw new Error(depthMessage);
95529
+ }
95530
+ this.nestedActive.set(key, active + 1);
95531
+ this.nestedBypassTaskIds.add(task.id);
95532
+ slotHeld = false;
95533
+ }
95534
+ } else {
95535
+ const outcome = await this.concurrencyManager.acquireWithDeadline(key, admissionTimeoutMs);
95536
+ if (!outcome.granted) {
95537
+ if (outcome.reason === "cancelled") {
95538
+ task.status = "cancelled";
95539
+ this.persistHandle(task);
95540
+ throw new Error(`Revive admission cancelled for task ${task.id}.`);
95541
+ }
95542
+ const errorMessage = `Queue admission timed out after ${outcome.waitedMs}ms (key "${key}", limit ${this.concurrencyManager.getConcurrencyLimit(key)})`;
95543
+ await this.terminateQueuedTask(task, "queue-saturated", errorMessage);
95544
+ throw new Error(errorMessage);
95545
+ }
95546
+ }
95547
+ task.concurrencyGroup = key;
95548
+ task.concurrencyKey = slotHeld ? key : undefined;
95549
+ task.status = "running";
95550
+ task.startedAt = new Date;
95551
+ task.completedAt = undefined;
95552
+ task.error = undefined;
95553
+ task.progress = { toolCalls: task.progress?.toolCalls ?? 0, lastUpdate: new Date };
95554
+ this.persistHandle(task);
95555
+ this.armWallclock(task);
95556
+ registerSubagentSession(sessionID, input.parentSessionID);
95557
+ this.startPolling();
95558
+ const toastManager = getTaskToastManager();
95559
+ if (toastManager)
95560
+ toastManager.addTask({ id: task.id, description: task.description, agent: task.agent, isBackground: true });
95561
+ const tools = { task: false, delegate_agent: true, question: false, ...getAgentToolRestrictions(task.agent) };
95562
+ setSessionTools(sessionID, tools);
95563
+ const reviveModel = task.model ? { providerID: task.model.providerID, modelID: task.model.modelID } : undefined;
95564
+ const reviveVariant = task.model?.variant;
95565
+ this.client.session.promptAsync({
95566
+ path: { id: sessionID },
95567
+ body: { agent: task.agent, ...reviveModel ? { model: reviveModel } : {}, ...reviveVariant ? { variant: reviveVariant } : {}, tools, parts: [{ type: "text", text: input.prompt }] }
95568
+ }).catch((error95) => {
95569
+ log("[background-agent] revive prompt error:", error95);
95570
+ task.status = "interrupt";
95571
+ task.error = error95 instanceof Error ? error95.message : String(error95);
95572
+ task.completedAt = new Date;
95573
+ this.persistHandle(task);
95574
+ this.disarmWallclock(task.id);
95575
+ this.decrementNestedActive(task);
95576
+ if (task.concurrencyKey) {
95577
+ this.concurrencyManager.release(task.concurrencyKey);
95578
+ task.concurrencyKey = undefined;
95579
+ }
95580
+ this.abortSessionQuietly(sessionID, "revive-error");
95581
+ this.markForNotification(task);
95582
+ this.cleanupPendingByParent(task);
95583
+ this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task)).catch((err) => {
95584
+ log("[background-agent] Failed to notify on revive error:", err);
95585
+ });
95586
+ });
95587
+ return task;
95588
+ }
95589
+ listRevivable(parentSessionID) {
95590
+ const seen = new Set;
95591
+ const rows = [];
95592
+ const consider = (id, parent, description, agent, status, sessionID, terminalReason, ts) => {
95593
+ if (seen.has(id) || sessionID === undefined)
95594
+ return;
95595
+ if (parentSessionID !== undefined && parent !== parentSessionID)
95596
+ return;
95597
+ if (!classifyRevivable({ status, sessionID }, {}).eligible)
95598
+ return;
95599
+ seen.add(id);
95600
+ rows.push({ taskId: id, description, agent, status, sessionID, terminalReason, ts });
95601
+ };
95602
+ for (const task of this.tasks.values())
95603
+ consider(task.id, task.parentSessionID, task.description, task.agent, task.status, task.sessionID, task.terminalReason, task.completedAt?.getTime() ?? task.startedAt?.getTime() ?? task.queuedAt?.getTime() ?? 0);
95604
+ for (const handle of readHandles(this.directory))
95605
+ consider(handle.taskId, handle.parentSessionID, handle.description, handle.agent, handle.status, handle.sessionID, handle.terminalReason, handle.completedAt ?? handle.startedAt ?? handle.queuedAt ?? 0);
95606
+ rows.sort((a, b) => b.ts - a.ts);
95607
+ return rows.map((row) => ({ taskId: row.taskId, description: row.description, agent: row.agent, status: row.status, sessionID: row.sessionID, terminalReason: row.terminalReason }));
95608
+ }
94455
95609
  async checkSessionTodos(sessionID) {
94456
95610
  try {
94457
95611
  const response = await this.client.session.todo({
@@ -94576,12 +95730,12 @@ class BackgroundManager {
94576
95730
  log("[background-agent] Session.idle but no valid output yet, waiting:", task.id);
94577
95731
  return;
94578
95732
  }
94579
- const hasIncompleteTodos2 = await this.checkSessionTodos(sessionID);
95733
+ const hasIncompleteTodos3 = await this.checkSessionTodos(sessionID);
94580
95734
  if (task.status !== "running") {
94581
95735
  log("[background-agent] Task status changed during todo check, skipping:", { taskId: task.id, status: task.status });
94582
95736
  return;
94583
95737
  }
94584
- if (hasIncompleteTodos2) {
95738
+ if (hasIncompleteTodos3) {
94585
95739
  log("[background-agent] Task has incomplete todos, waiting for todo-continuation:", task.id);
94586
95740
  return;
94587
95741
  }
@@ -94602,11 +95756,13 @@ class BackgroundManager {
94602
95756
  task.error = errorMessage ?? "Session error";
94603
95757
  task.completedAt = new Date;
94604
95758
  this.persistHandle(task);
95759
+ this.disarmWallclock(task.id);
94605
95760
  this.taskHistory.record(task.parentSessionID, { id: task.id, sessionID: task.sessionID, agent: task.agent, description: task.description, status: "error", category: task.category, startedAt: task.startedAt, completedAt: task.completedAt });
94606
95761
  if (task.concurrencyKey) {
94607
95762
  this.concurrencyManager.release(task.concurrencyKey);
94608
95763
  task.concurrencyKey = undefined;
94609
95764
  }
95765
+ this.decrementNestedActive(task);
94610
95766
  const completionTimer = this.completionTimers.get(task.id);
94611
95767
  if (completionTimer) {
94612
95768
  clearTimeout(completionTimer);
@@ -94620,6 +95776,7 @@ class BackgroundManager {
94620
95776
  this.cleanupPendingByParent(task);
94621
95777
  this.tasks.delete(task.id);
94622
95778
  this.clearNotificationsForTask(task.id);
95779
+ this.refreshParentWake(task.parentSessionID);
94623
95780
  const toastManager = getTaskToastManager();
94624
95781
  if (toastManager) {
94625
95782
  toastManager.removeTask(task.id);
@@ -94691,31 +95848,7 @@ class BackgroundManager {
94691
95848
  this.notifications.delete(sessionID);
94692
95849
  }
94693
95850
  async validateSessionHasOutput(sessionID) {
94694
- try {
94695
- const response = await this.client.session.messages({
94696
- path: { id: sessionID }
94697
- });
94698
- const messages2 = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
94699
- const hasAssistantOrToolMessage = messages2.some((m) => m.info?.role === "assistant" || m.info?.role === "tool");
94700
- if (!hasAssistantOrToolMessage) {
94701
- log("[background-agent] No assistant/tool messages found in session:", sessionID);
94702
- return false;
94703
- }
94704
- const hasContent2 = messages2.some((m) => {
94705
- if (m.info?.role !== "assistant" && m.info?.role !== "tool")
94706
- return false;
94707
- const parts = m.parts ?? [];
94708
- return parts.some((p) => p.type === "text" && p.text && p.text.trim().length > 0 || p.type === "reasoning" && p.text && p.text.trim().length > 0 || p.type === "tool" || p.type === "tool_result" && p.content && (typeof p.content === "string" ? p.content.trim().length > 0 : p.content.length > 0));
94709
- });
94710
- if (!hasContent2) {
94711
- log("[background-agent] Messages exist but no content found in session:", sessionID);
94712
- return false;
94713
- }
94714
- return true;
94715
- } catch (error95) {
94716
- log("[background-agent] Error validating session output:", error95);
94717
- return true;
94718
- }
95851
+ return validateSessionHasOutput(this.client, sessionID);
94719
95852
  }
94720
95853
  clearNotificationsForTask(taskId) {
94721
95854
  for (const [sessionID, tasks] of this.notifications.entries()) {
@@ -94738,6 +95871,27 @@ class BackgroundManager {
94738
95871
  }
94739
95872
  }
94740
95873
  }
95874
+ refreshParentWake(parentSessionID) {
95875
+ if (this.config?.wakeScheduler?.enabled === false)
95876
+ return;
95877
+ const readState2 = () => ({
95878
+ hasPendingChildren: (this.pendingByParent.get(parentSessionID)?.size ?? 0) > 0,
95879
+ hasUndeliveredNotifications: (this.notifications.get(parentSessionID)?.length ?? 0) > 0
95880
+ });
95881
+ if (!shouldWakeForParent(readState2())) {
95882
+ this.wakeScheduler.clear(parentSessionID);
95883
+ return;
95884
+ }
95885
+ this.wakeScheduler.schedule(parentSessionID, {
95886
+ intervalMs: resolveWakeIntervalMs(this.config?.wakeScheduler),
95887
+ readState: readState2,
95888
+ deliverWake: async () => {
95889
+ await this.enqueueNotificationForParent(parentSessionID, async () => {
95890
+ await this.client.session.promptAsync({ path: { id: parentSessionID }, body: { noReply: true, parts: [{ type: "text", text: WAKE_REMINDER_TEXT }] } });
95891
+ });
95892
+ }
95893
+ });
95894
+ }
94741
95895
  abortSessionQuietly(sessionID, context) {
94742
95896
  this.client.session.abort({ path: { id: sessionID } }).catch((err) => {
94743
95897
  log(`[background-agent] Session abort failed${context ? ` (${context})` : ""}:`, {
@@ -94771,6 +95925,7 @@ class BackgroundManager {
94771
95925
  task.status = "cancelled";
94772
95926
  task.completedAt = new Date;
94773
95927
  this.persistHandle(task);
95928
+ this.disarmWallclock(task.id);
94774
95929
  if (reason) {
94775
95930
  task.error = reason;
94776
95931
  }
@@ -94779,6 +95934,7 @@ class BackgroundManager {
94779
95934
  this.concurrencyManager.release(task.concurrencyKey);
94780
95935
  task.concurrencyKey = undefined;
94781
95936
  }
95937
+ this.decrementNestedActive(task);
94782
95938
  const existingTimer = this.completionTimers.get(task.id);
94783
95939
  if (existingTimer) {
94784
95940
  clearTimeout(existingTimer);
@@ -94805,6 +95961,7 @@ class BackgroundManager {
94805
95961
  try {
94806
95962
  await this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task));
94807
95963
  log(`[background-agent] Task cancelled via ${source}:`, task.id);
95964
+ this.refreshParentWake(task.parentSessionID);
94808
95965
  } catch (err) {
94809
95966
  log("[background-agent] Error in notifyParentSession for cancelled task:", { taskId: task.id, error: err });
94810
95967
  }
@@ -94893,12 +96050,48 @@ class BackgroundManager {
94893
96050
  }
94894
96051
  task.status = "completed";
94895
96052
  task.completedAt = new Date;
96053
+ const hc = this.handoffCompression;
96054
+ if (hc && hc.enabled !== false && task.sessionID) {
96055
+ try {
96056
+ const raw = await this.client.session.messages({ path: { id: task.sessionID } });
96057
+ let msgs = [];
96058
+ if (Array.isArray(raw)) {
96059
+ msgs = raw;
96060
+ } else if (raw && typeof raw === "object" && "data" in raw && Array.isArray(raw.data)) {
96061
+ msgs = raw.data;
96062
+ }
96063
+ const assistantMsgs = msgs.filter((m) => m.info?.role === "assistant");
96064
+ const maxMsgs = hc?.maxMessages ?? 6;
96065
+ const keepFirst = hc?.keepFirst ?? 2;
96066
+ const keepLast = hc?.keepLast ?? 3;
96067
+ if (assistantMsgs.length > maxMsgs) {
96068
+ const first = assistantMsgs.slice(0, keepFirst);
96069
+ const last = assistantMsgs.slice(-keepLast);
96070
+ const extractText = (msgs2) => msgs2.map((m) => m.parts?.filter((p) => p.type === "text" || p.type === "reasoning").map((p) => p.text).filter(Boolean).join(`
96071
+ `) ?? "").filter(Boolean).join(`
96072
+
96073
+ `);
96074
+ const firstText = extractText(first);
96075
+ const lastText = extractText(last);
96076
+ const truncated = assistantMsgs.length - keepFirst - keepLast;
96077
+ task.compactedResult = `${firstText}
96078
+
96079
+ [... ${truncated} messages truncated]
96080
+
96081
+ ${lastText}`;
96082
+ }
96083
+ } catch (err) {
96084
+ log("[background-agent] Handoff compression failed:", { taskId: task.id, error: err });
96085
+ }
96086
+ }
94896
96087
  this.persistHandle(task);
96088
+ this.disarmWallclock(task.id);
94897
96089
  this.taskHistory.record(task.parentSessionID, { id: task.id, sessionID: task.sessionID, agent: task.agent, description: task.description, status: "completed", category: task.category, startedAt: task.startedAt, completedAt: task.completedAt });
94898
96090
  if (task.concurrencyKey) {
94899
96091
  this.concurrencyManager.release(task.concurrencyKey);
94900
96092
  task.concurrencyKey = undefined;
94901
96093
  }
96094
+ this.decrementNestedActive(task);
94902
96095
  this.markForNotification(task);
94903
96096
  this.cleanupPendingByParent(task);
94904
96097
  const idleTimer = this.idleDeferralTimers.get(task.id);
@@ -94912,6 +96105,7 @@ class BackgroundManager {
94912
96105
  try {
94913
96106
  await this.enqueueNotificationForParent(task.parentSessionID, () => this.notifyParentSession(task));
94914
96107
  log(`[background-agent] Task completed via ${source}:`, task.id);
96108
+ this.refreshParentWake(task.parentSessionID);
94915
96109
  } catch (err) {
94916
96110
  log("[background-agent] Error in notifyParentSession:", { taskId: task.id, error: err });
94917
96111
  }
@@ -95027,10 +96221,12 @@ class BackgroundManager {
95027
96221
  task.error = errorMessage;
95028
96222
  task.completedAt = new Date;
95029
96223
  this.persistHandle(task);
96224
+ this.disarmWallclock(task.id);
95030
96225
  if (task.concurrencyKey) {
95031
96226
  this.concurrencyManager.release(task.concurrencyKey);
95032
96227
  task.concurrencyKey = undefined;
95033
96228
  }
96229
+ this.decrementNestedActive(task);
95034
96230
  this.cleanupPendingByParent(task);
95035
96231
  if (wasPending) {
95036
96232
  const key = task.model ? `${task.model.providerID}/${task.model.modelID}` : task.agent;
@@ -95073,6 +96269,8 @@ class BackgroundManager {
95073
96269
  this.notifications.set(sessionID, validNotifications);
95074
96270
  }
95075
96271
  }
96272
+ this.nestedActive.clear();
96273
+ this.nestedBypassTaskIds.clear();
95076
96274
  }
95077
96275
  async isSessionAwaitingUser(sessionID) {
95078
96276
  try {
@@ -95112,10 +96310,12 @@ class BackgroundManager {
95112
96310
  task.error = `Stale timeout (no activity for ${staleMinutes2}min since start)`;
95113
96311
  task.completedAt = new Date;
95114
96312
  this.persistHandle(task);
96313
+ this.disarmWallclock(task.id);
95115
96314
  if (task.concurrencyKey) {
95116
96315
  this.concurrencyManager.release(task.concurrencyKey);
95117
96316
  task.concurrencyKey = undefined;
95118
96317
  }
96318
+ this.decrementNestedActive(task);
95119
96319
  this.abortSessionQuietly(sessionID, "stale-no-progress");
95120
96320
  log(`[background-agent] Task ${task.id} interrupted: no progress since start`);
95121
96321
  try {
@@ -95141,10 +96341,12 @@ class BackgroundManager {
95141
96341
  task.error = `Stale timeout (no activity for ${staleMinutes}min)`;
95142
96342
  task.completedAt = new Date;
95143
96343
  this.persistHandle(task);
96344
+ this.disarmWallclock(task.id);
95144
96345
  if (task.concurrencyKey) {
95145
96346
  this.concurrencyManager.release(task.concurrencyKey);
95146
96347
  task.concurrencyKey = undefined;
95147
96348
  }
96349
+ this.decrementNestedActive(task);
95148
96350
  this.abortSessionQuietly(sessionID, "stale-timeout");
95149
96351
  log(`[background-agent] Task ${task.id} interrupted: stale timeout`);
95150
96352
  try {
@@ -95182,8 +96384,8 @@ class BackgroundManager {
95182
96384
  }
95183
96385
  if (task.status !== "running")
95184
96386
  continue;
95185
- const hasIncompleteTodos2 = await this.checkSessionTodos(sessionID);
95186
- if (hasIncompleteTodos2) {
96387
+ const hasIncompleteTodos3 = await this.checkSessionTodos(sessionID);
96388
+ if (hasIncompleteTodos3) {
95187
96389
  log("[background-agent] Task has incomplete todos via polling, waiting:", task.id);
95188
96390
  continue;
95189
96391
  }
@@ -95239,6 +96441,12 @@ class BackgroundManager {
95239
96441
  clearTimeout(timer);
95240
96442
  }
95241
96443
  this.idleDeferralTimers.clear();
96444
+ this.wakeScheduler.dispose();
96445
+ this.wallclock.disarmAll();
96446
+ for (const timer of this.wallclockGraceTimers.values()) {
96447
+ clearTimeout(timer);
96448
+ }
96449
+ this.wallclockGraceTimers.clear();
95242
96450
  this.concurrencyManager.clear();
95243
96451
  this.tasks.clear();
95244
96452
  this.notifications.clear();
@@ -95246,6 +96454,8 @@ class BackgroundManager {
95246
96454
  this.notificationQueueByParent.clear();
95247
96455
  this.queuesByKey.clear();
95248
96456
  this.processingKeys.clear();
96457
+ this.nestedActive.clear();
96458
+ this.nestedBypassTaskIds.clear();
95249
96459
  this.unregisterProcessCleanup();
95250
96460
  log("[background-agent] Shutdown complete");
95251
96461
  }
@@ -96032,9 +97242,7 @@ class TmuxSessionManager {
96032
97242
  var SESSION_TIMEOUT_MS3 = 10 * 60 * 1000;
96033
97243
  var MIN_STABILITY_TIME_MS4 = 10 * 1000;
96034
97244
  // src/agents/dynamic-agent-prompt-builder.ts
96035
- import { existsSync as existsSync75, readFileSync as readFileSync45 } from "fs";
96036
- import { homedir as homedir11 } from "os";
96037
- import { join as join79 } from "path";
97245
+ import { readFileSync as readFileSync45 } from "fs";
96038
97246
  init_logger();
96039
97247
  function categorizeTools(toolNames) {
96040
97248
  return toolNames.map((name) => {
@@ -96351,16 +97559,13 @@ function buildAntiPatternsSection() {
96351
97559
  ${patterns2.join(`
96352
97560
  `)}`;
96353
97561
  }
96354
- var cachedRuntimeFull = null;
96355
- var cachedRuntimeCompact = null;
97562
+ var cachedRuntimeFull = {};
97563
+ var cachedRuntimeCompact = {};
96356
97564
  var cachedFallbackFull = {};
96357
97565
  var cachedFallbackCompact = {};
96358
- function hasGrepGlobToolNames(toolNames) {
96359
- return toolNames.some((n) => n === "grep" || n === "glob");
96360
- }
96361
97566
  function fallbackFullDiscipline(hasGrepGlob) {
96362
97567
  const analysis = hasGrepGlob ? "| Analysis / Processing | Use ctx_* tools \u2014 NEVER raw read/bash/grep/glob for analysis |" : "| Analysis / Processing | Use ctx_* tools \u2014 NEVER raw read/bash for analysis |";
96363
- const search = hasGrepGlob ? "| Search | ctx_search FIRST -> grep/glob fallback |" : "| Search | ctx_search FIRST (indexed KB) -> LSP/ast_grep fallback |";
97568
+ const search = hasGrepGlob ? "| Search | ctx_search FIRST -> grep/glob fallback |" : "| Search | ctx_search FIRST (indexed KB) -> ctx_batch_execute / ctx_execute (rg) or LSP/ast_grep fallback |";
96364
97569
  return `### Context Discipline (ALWAYS)
96365
97570
 
96366
97571
  | Scenario | Tool |
@@ -96377,7 +97582,7 @@ ${search}
96377
97582
  }
96378
97583
  function fallbackCompactDiscipline(hasGrepGlob) {
96379
97584
  const analysis = hasGrepGlob ? "| Analysis / Aggregation / Counting | ctx_batch_execute / ctx_execute(_file) \u2014 NEVER raw read/grep for analysis |" : "| Analysis / Aggregation / Counting | ctx_batch_execute / ctx_execute(_file) \u2014 NEVER raw read for analysis |";
96380
- const search = hasGrepGlob ? "| Search | ctx_search FIRST (indexed KB) \u2192 grep/glob fallback (raw FS) |" : "| Search | ctx_search FIRST (indexed KB) \u2192 LSP/ast_grep fallback |";
97585
+ const search = hasGrepGlob ? "| Search | ctx_search FIRST (indexed KB) \u2192 grep/glob fallback (raw FS) |" : "| Search | ctx_search FIRST (indexed KB) \u2192 ctx_batch_execute / ctx_execute (rg) or LSP/ast_grep fallback |";
96381
97586
  const note = hasGrepGlob ? "Edits need prior read for LINE#ID \u2014 read\u2192edit chain exempt (non-plan paths; .matrixx/plans/*.md must use plan_read/plan_update). MUST use ctx_* when available \u2014 raw grep/read is forbidden for analysis." : "Edits need prior read for LINE#ID \u2014 read\u2192edit chain exempt (non-plan paths; .matrixx/plans/*.md must use plan_read/plan_update). MUST use ctx_* when available \u2014 raw read for analysis is forbidden.";
96382
97587
  return `### Context Discipline (when ctx_* available)
96383
97588
 
@@ -96391,69 +97596,54 @@ ${search}
96391
97596
  ${note}`;
96392
97597
  }
96393
97598
  function exploreCtxPart(hasGrepGlob) {
96394
- return hasGrepGlob ? "MUST use ctx_search for indexed hits \u2192 grep/glob fallback ONLY when ctx_* unavailable; use ctx_batch_execute / ctx_execute for multi-file analysis; use ctx_fetch_and_index for docs/web \u2192 ctx_search. Raw grep/read/glob for analysis is forbidden when ctx_* is available." : "MUST use ctx_search for indexed hits \u2192 LSP/ast_grep fallback ONLY when ctx_* unavailable; use ctx_batch_execute / ctx_execute for multi-file analysis; use ctx_fetch_and_index for docs/web \u2192 ctx_search. Raw read for analysis is forbidden when ctx_* is available.";
96395
- }
96396
- function resolveContextModeDisciplinePath() {
96397
- try {
96398
- const resolved = __require.resolve("context-mode/configs/opencode/AGENTS.md");
96399
- if (existsSync75(resolved))
96400
- return resolved;
96401
- } catch {}
96402
- try {
96403
- const cacheDir = getOpenCodeCacheDir();
96404
- const p = join79(cacheDir, "packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96405
- if (existsSync75(p))
96406
- return p;
96407
- } catch {}
96408
- try {
96409
- const base = process.env.XDG_CACHE_HOME ?? join79(homedir11(), ".cache");
96410
- const p = join79(base, "opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96411
- if (existsSync75(p))
96412
- return p;
96413
- } catch {}
96414
- try {
96415
- const p2 = join79(homedir11(), ".cache/opencode/packages/context-mode@latest/node_modules/context-mode/configs/opencode/AGENTS.md");
96416
- if (existsSync75(p2))
96417
- return p2;
96418
- } catch {}
96419
- return null;
97599
+ return hasGrepGlob ? "MUST use ctx_search for indexed hits \u2192 grep/glob fallback ONLY when ctx_* unavailable; use ctx_batch_execute / ctx_execute for multi-file analysis; use ctx_fetch_and_index for docs/web \u2192 ctx_search. Raw grep/read/glob for analysis is forbidden when ctx_* is available." : "MUST use ctx_search for indexed hits \u2192 ctx_batch_execute / ctx_execute (rg) or LSP/ast_grep fallback ONLY when ctx_* unavailable; use ctx_batch_execute / ctx_execute for multi-file analysis; use ctx_fetch_and_index for docs/web \u2192 ctx_search. Raw read for analysis is forbidden when ctx_* is available.";
96420
97600
  }
96421
97601
  function stripDisciplineHeader(content) {
96422
97602
  return content.trim();
96423
97603
  }
96424
- function loadContextModeDiscipline(hasGrepGlob = true) {
96425
- if (cachedRuntimeFull)
96426
- return cachedRuntimeFull;
96427
- const fallbackKey = hasGrepGlob ? "1" : "0";
96428
- if (cachedFallbackFull[fallbackKey])
96429
- return cachedFallbackFull[fallbackKey];
97604
+ function readDisciplineFile() {
97605
+ const p = resolveContextModeDisciplinePath();
97606
+ if (!p)
97607
+ throw new Error("discipline file not found");
97608
+ const raw = readFileSync45(p, "utf8");
97609
+ let version4 = "1.0.169";
96430
97610
  try {
96431
- const p = resolveContextModeDisciplinePath();
96432
- if (!p)
96433
- throw new Error("discipline file not found");
96434
- const raw = readFileSync45(p, "utf8");
96435
- let version4 = "1.0.169";
96436
- try {
96437
- const pkgResolved = __require.resolve("context-mode/package.json");
96438
- const pkgRaw = readFileSync45(pkgResolved, "utf8");
96439
- const pkg = JSON.parse(pkgRaw);
96440
- if (pkg.version)
96441
- version4 = pkg.version;
96442
- } catch {}
96443
- const body = stripDisciplineHeader(raw);
96444
- const withVersion = body.includes("<!-- discipline") ? body : `${body}
97611
+ const pkgResolved = __require.resolve("context-mode/package.json");
97612
+ const pkgRaw = readFileSync45(pkgResolved, "utf8");
97613
+ const pkg = JSON.parse(pkgRaw);
97614
+ if (pkg.version)
97615
+ version4 = pkg.version;
97616
+ } catch {}
97617
+ const body = stripDisciplineHeader(raw);
97618
+ const text = body.includes("<!-- discipline") ? body : `${body}
96445
97619
 
96446
97620
  <!-- discipline v${version4} Elastic-2.0 -->`;
96447
- cachedRuntimeFull = withVersion;
96448
- log("context-discipline loaded", { path: p, version: version4 });
96449
- return withVersion;
97621
+ return { text, version: version4, path: p };
97622
+ }
97623
+ function loadDiscipline(kind, hasGrepGlob = true) {
97624
+ const fallbackKey = hasGrepGlob ? "1" : "0";
97625
+ const runtimeCache = kind === "full" ? cachedRuntimeFull : cachedRuntimeCompact;
97626
+ const fallbackCache = kind === "full" ? cachedFallbackFull : cachedFallbackCompact;
97627
+ const label = kind === "full" ? "context-discipline" : "compact-discipline";
97628
+ if (runtimeCache[fallbackKey])
97629
+ return runtimeCache[fallbackKey];
97630
+ if (fallbackCache[fallbackKey])
97631
+ return fallbackCache[fallbackKey];
97632
+ try {
97633
+ const { text, version: version4, path: path13 } = readDisciplineFile();
97634
+ runtimeCache[fallbackKey] = text;
97635
+ log(`${label} loaded`, { path: path13, version: version4 });
97636
+ return text;
96450
97637
  } catch (e) {
96451
- log("context-discipline fallback", { error: String(e) });
96452
- const fallback = fallbackFullDiscipline(hasGrepGlob);
96453
- cachedFallbackFull[fallbackKey] = fallback;
97638
+ log(`${label} fallback`, { error: String(e) });
97639
+ const fallback = kind === "full" ? fallbackFullDiscipline(hasGrepGlob) : fallbackCompactDiscipline(hasGrepGlob);
97640
+ fallbackCache[fallbackKey] = fallback;
96454
97641
  return fallback;
96455
97642
  }
96456
97643
  }
97644
+ function loadContextModeDiscipline(hasGrepGlob = true) {
97645
+ return loadDiscipline("full", hasGrepGlob);
97646
+ }
96457
97647
  function buildContextDisciplineSection(hasContextMode = false, hasGrepGlob = true) {
96458
97648
  if (!hasContextMode)
96459
97649
  return "";
@@ -96473,37 +97663,7 @@ function buildHeadroomSection(hasHeadroom = false) {
96473
97663
  **Headroom L4 is transport-level (CacheAligner->ContentRouter->CCR). It complements L1 RTK, L2 context-mode, L3 DCP \u2014 do not duplicate their discipline.**`;
96474
97664
  }
96475
97665
  function loadCompactContextDiscipline(hasGrepGlob = true) {
96476
- if (cachedRuntimeCompact)
96477
- return cachedRuntimeCompact;
96478
- const fallbackKey = hasGrepGlob ? "1" : "0";
96479
- if (cachedFallbackCompact[fallbackKey])
96480
- return cachedFallbackCompact[fallbackKey];
96481
- try {
96482
- const p = resolveContextModeDisciplinePath();
96483
- if (!p)
96484
- throw new Error("discipline file not found");
96485
- const raw = readFileSync45(p, "utf8");
96486
- let version4 = "1.0.169";
96487
- try {
96488
- const pkgResolved = __require.resolve("context-mode/package.json");
96489
- const pkgRaw = readFileSync45(pkgResolved, "utf8");
96490
- const pkg = JSON.parse(pkgRaw);
96491
- if (pkg.version)
96492
- version4 = pkg.version;
96493
- } catch {}
96494
- const body = stripDisciplineHeader(raw);
96495
- const withVersion = body.includes("<!-- discipline") ? body : `${body}
96496
-
96497
- <!-- discipline v${version4} Elastic-2.0 -->`;
96498
- cachedRuntimeCompact = withVersion;
96499
- log("compact-discipline loaded", { path: p, version: version4 });
96500
- return withVersion;
96501
- } catch (e) {
96502
- log("compact-discipline fallback", { error: String(e) });
96503
- const fallback = fallbackCompactDiscipline(hasGrepGlob);
96504
- cachedFallbackCompact[fallbackKey] = fallback;
96505
- return fallback;
96506
- }
97666
+ return loadDiscipline("compact", hasGrepGlob);
96507
97667
  }
96508
97668
  function buildCompactContextDisciplineSection(hasContextMode = false, hasGrepGlob = true) {
96509
97669
  if (!hasContextMode)
@@ -97555,7 +98715,7 @@ function createBddContractAgent(model) {
97555
98715
  createBddContractAgent.mode = MODE2;
97556
98716
 
97557
98717
  // src/agents/builtin-agents/resolve-file-uri.ts
97558
- import { existsSync as existsSync76, readFileSync as readFileSync46 } from "fs";
98718
+ import { existsSync as existsSync77, readFileSync as readFileSync46 } from "fs";
97559
98719
  import { homedir as homedir12 } from "os";
97560
98720
  import { isAbsolute as isAbsolute6, resolve as resolve10 } from "path";
97561
98721
  function resolvePromptAppend(promptAppend, configDir) {
@@ -97570,7 +98730,7 @@ function resolvePromptAppend(promptAppend, configDir) {
97570
98730
  } catch {
97571
98731
  return `[WARNING: Malformed file URI (invalid percent-encoding): ${promptAppend}]`;
97572
98732
  }
97573
- if (!existsSync76(filePath)) {
98733
+ if (!existsSync77(filePath)) {
97574
98734
  return `[WARNING: Could not resolve file URI: ${promptAppend}]`;
97575
98735
  }
97576
98736
  try {
@@ -97924,7 +99084,7 @@ function buildKeymakerPrompt(availableAgents = [], availableTools = [], availabl
97924
99084
  const hardBlocks = buildHardBlocksSection();
97925
99085
  const antiPatterns = buildAntiPatternsSection();
97926
99086
  const hasContextMode = availableTools.some((t) => t.name.startsWith("ctx_"));
97927
- const hasGrepGlob = hasGrepGlobToolNames(availableTools.map((t) => t.name));
99087
+ const hasGrepGlob = resolveGrepGlobUsable(availableTools.map((t) => t.name));
97928
99088
  const contextDiscipline = buildContextDisciplineSection(hasContextMode, hasGrepGlob);
97929
99089
  const hasHeadroom = availableTools.some((t) => t.name.startsWith("headroom_"));
97930
99090
  const headroomDiscipline = buildHeadroomSection(hasHeadroom);
@@ -98113,6 +99273,11 @@ result = task(..., run_in_background=false) // Never wait synchronously for exp
98113
99273
  - Collect results with \`background_output(task_id="...")\` when needed
98114
99274
  - BEFORE final answer: \`background_cancel(all=true)\` to clean up
98115
99275
 
99276
+ **Interpreting terminal task states** \u2014 \`background_output\` and the completion \`<system-reminder>\` report one of:
99277
+ - \`completed\` / \`error\` / \`cancelled\` / \`interrupt\` \u2014 as named.
99278
+ - \`stopped\` (notification label \`STOPPED\`) \u2014 ended without a terminal result (queue-saturated, no output, or nested depth cap); partial output may exist.
99279
+ - \`statusUncertain\` (label \`STATUS UNCERTAIN\`) \u2014 liveness could not be determined after a restart; re-check with \`background_output\`, never assume success or failure.
99280
+
98116
99281
  ### Search Stop Conditions
98117
99282
 
98118
99283
  STOP searching when:
@@ -98601,7 +99766,7 @@ function buildDynamicMorpheusPrompt(availableAgents, availableTools = [], availa
98601
99766
  const hardBlocks = buildHardBlocksSection();
98602
99767
  const antiPatterns = buildAntiPatternsSection();
98603
99768
  const hasContextMode = availableTools.some((t) => t.name.startsWith("ctx_"));
98604
- const hasGrepGlob = hasGrepGlobToolNames(availableTools.map((t) => t.name));
99769
+ const hasGrepGlob = resolveGrepGlobUsable(availableTools.map((t) => t.name));
98605
99770
  const contextDiscipline = buildContextDisciplineSection(hasContextMode, hasGrepGlob);
98606
99771
  const hasHeadroom = availableTools.some((t) => t.name.startsWith("headroom_"));
98607
99772
  const headroomDiscipline = buildHeadroomSection(hasHeadroom);
@@ -98746,6 +99911,11 @@ result = task(..., run_in_background=false) // Never wait synchronously for exp
98746
99911
  3. When results needed: \`background_output(task_id="...")\`
98747
99912
  4. BEFORE final answer: \`background_cancel(all=true)\`
98748
99913
 
99914
+ **Interpreting terminal task states** \u2014 \`background_output\` and the completion \`<system-reminder>\` report one of:
99915
+ - \`completed\` / \`error\` / \`cancelled\` / \`interrupt\` \u2014 as named.
99916
+ - \`stopped\` (notification label \`STOPPED\`) \u2014 ended without a terminal result (queue-saturated, no output, or nested depth cap); partial output may exist.
99917
+ - \`statusUncertain\` (label \`STATUS UNCERTAIN\`) \u2014 liveness could not be determined after a restart; re-check with \`background_output\`, never assume success or failure.
99918
+
98749
99919
  ### Search Stop Conditions
98750
99920
 
98751
99921
  STOP searching when:
@@ -102999,12 +104169,12 @@ function setAvailableToolNames(names) {
102999
104169
  function getAvailableToolNames() {
103000
104170
  return _availableToolNames;
103001
104171
  }
103002
- function injectContextDiscipline(availableToolNames, agents) {
104172
+ function injectContextDiscipline(availableToolNames, agents, contextMode) {
103003
104173
  const hasContextMode = availableToolNames.some((n) => n.startsWith("ctx_"));
103004
104174
  const hasHeadroom = availableToolNames.some((n) => n.startsWith("headroom_"));
103005
104175
  if (!(hasContextMode || hasHeadroom))
103006
104176
  return;
103007
- const hasGrepGlob = hasGrepGlobToolNames(availableToolNames);
104177
+ const hasGrepGlob = resolveGrepGlobUsable(availableToolNames, contextMode ?? getContextModeForPrompts());
103008
104178
  const exploreAgents = new Set(["trinity", "operator", "seraph", "smith", "merovingian", "construct", "bdd-contract"]);
103009
104179
  for (const [name, cfg] of Object.entries(agents)) {
103010
104180
  if (name === "morpheus" || name === "keymaker")
@@ -103045,6 +104215,7 @@ async function applyAgentConfig(params) {
103045
104215
  disabledSkills.add("tdd-enforcer");
103046
104216
  }
103047
104217
  const useTaskSystem = isTaskSystemEnabled(params.pluginConfig);
104218
+ setContextModeForPrompts(params.pluginConfig.context_mode);
103048
104219
  const availableToolNames = getAvailableToolNames();
103049
104220
  const builtinAgents = await createBuiltinAgents(migratedDisabledAgents, params.pluginConfig.agents, params.ctx.directory, undefined, params.pluginConfig.categories, allDiscoveredSkills, params.ctx.client, browserProvider, currentModel, disabledSkills, useTaskSystem, params.pluginConfig.global_model, availableToolNames);
103050
104221
  const rawPluginAgents = params.pluginComponents.agents;
@@ -103120,7 +104291,7 @@ async function applyAgentConfig(params) {
103120
104291
  params.config.agent = reorderAgentsByPriority(params.config.agent);
103121
104292
  }
103122
104293
  if (params.config.agent) {
103123
- injectContextDiscipline(availableToolNames, params.config.agent);
104294
+ injectContextDiscipline(availableToolNames, params.config.agent, params.pluginConfig.context_mode);
103124
104295
  }
103125
104296
  const agentResult = params.config.agent;
103126
104297
  log("[config-handler] agents loaded", { agentKeys: Object.keys(agentResult) });
@@ -103457,6 +104628,7 @@ function createManagers(args) {
103457
104628
  const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
103458
104629
  const backgroundManager = new BackgroundManager(ctx, pluginConfig.background_task, {
103459
104630
  tmuxConfig,
104631
+ handoffCompression: pluginConfig.dcp?.handoffCompression,
103460
104632
  onSubagentSessionCreated: async (event) => {
103461
104633
  log("[index] onSubagentSessionCreated callback received", {
103462
104634
  sessionID: event.sessionID,
@@ -103554,7 +104726,7 @@ function filterDisabledTools(tools, disabledTools) {
103554
104726
  // src/plugin/tool-registry.ts
103555
104727
  function createToolRegistry(args) {
103556
104728
  const { ctx, pluginConfig, managers, skillContext, availableCategories } = args;
103557
- const backgroundTools = createBackgroundTools(managers.backgroundManager, ctx.client);
104729
+ const backgroundTools = createBackgroundTools(managers.backgroundManager, ctx.client, ctx.directory);
103558
104730
  const delegateAgent = createDelegateAgent(ctx, managers.backgroundManager, pluginConfig.disabled_agents ?? []);
103559
104731
  const isMultimodalLookerEnabled = !(pluginConfig.disabled_agents ?? []).some((agent) => agent.toLowerCase() === "construct");
103560
104732
  const lookAt = isMultimodalLookerEnabled ? createLookAt(ctx) : null;
@@ -103834,6 +105006,20 @@ var CircuitBreakerConfigSchema = exports_external.object({
103834
105006
  maxToolCalls: exports_external.number().int().min(10).optional(),
103835
105007
  consecutiveThreshold: exports_external.number().int().min(5).optional()
103836
105008
  });
105009
+ var NestedAdmissionConfigSchema = exports_external.object({
105010
+ enabled: exports_external.boolean().optional(),
105011
+ mode: exports_external.enum(["bypass", "reserve"]).optional(),
105012
+ maxDepth: exports_external.number().int().min(1).max(5).optional()
105013
+ });
105014
+ var WakeSchedulerConfigSchema = exports_external.object({
105015
+ enabled: exports_external.boolean().optional(),
105016
+ intervalMs: exports_external.number().min(60000).optional()
105017
+ });
105018
+ var JobBoardConfigSchema = exports_external.object({
105019
+ enabled: exports_external.boolean().optional(),
105020
+ strategy: exports_external.enum(["latest", "checkpoint-compatible"]).optional(),
105021
+ maxRetainedSnapshots: exports_external.number().int().min(1).max(100).optional()
105022
+ });
103837
105023
  var BackgroundTaskConfigSchema = exports_external.object({
103838
105024
  defaultConcurrency: exports_external.number().min(1).optional(),
103839
105025
  providerConcurrency: exports_external.record(exports_external.string(), exports_external.number().min(0)).optional(),
@@ -103841,7 +105027,13 @@ var BackgroundTaskConfigSchema = exports_external.object({
103841
105027
  staleTimeoutMs: exports_external.number().min(60000).optional(),
103842
105028
  messageStalenessTimeoutMs: exports_external.number().min(60000).optional(),
103843
105029
  maxToolCalls: exports_external.number().int().min(10).optional(),
103844
- circuitBreaker: CircuitBreakerConfigSchema.optional()
105030
+ circuitBreaker: CircuitBreakerConfigSchema.optional(),
105031
+ admissionTimeoutMs: exports_external.union([exports_external.literal(0), exports_external.number().min(60000)]).optional(),
105032
+ nestedAdmission: NestedAdmissionConfigSchema.optional(),
105033
+ wakeScheduler: WakeSchedulerConfigSchema.optional(),
105034
+ jobBoard: JobBoardConfigSchema.optional(),
105035
+ wallClockTimeoutMs: exports_external.union([exports_external.literal(0), exports_external.number().int().min(60000).max(2147483647)]).optional(),
105036
+ wallClockAbortGraceMs: exports_external.number().int().min(1000).max(60000).optional()
103845
105037
  });
103846
105038
  // src/config/schema/browser-automation.ts
103847
105039
  var BrowserAutomationProviderSchema = exports_external.enum([
@@ -103915,7 +105107,7 @@ var CommentCheckerConfigSchema = exports_external.object({
103915
105107
  var ContextModeConfigSchema = exports_external.object({
103916
105108
  enabled: exports_external.boolean().default(true),
103917
105109
  enforce: exports_external.boolean().default(false),
103918
- blocked_tools: exports_external.array(exports_external.string()).default(["read", "grep", "glob"])
105110
+ blocked_tools: exports_external.array(exports_external.string()).default(["grep", "glob"])
103919
105111
  });
103920
105112
  // src/config/schema/evolution.ts
103921
105113
  var EvolutionWatcherConfigSchema = exports_external.object({
@@ -105210,6 +106402,7 @@ var MatrixxPlugin = async (ctx) => {
105210
106402
  injectServerAuthIntoClient(ctx.client);
105211
106403
  startBackgroundCheck();
105212
106404
  const pluginConfig = await loadPluginConfig(ctx.directory, ctx);
106405
+ setContextModeForPrompts(pluginConfig.context_mode);
105213
106406
  const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
105214
106407
  const isHookEnabled = (hookName) => !disabledHooks.has(hookName);
105215
106408
  const safeHookEnabled = pluginConfig.experimental?.safe_hook_creation ?? true;