opencode-orchestrator 1.0.26 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15592,141 +15592,6 @@ Never claim completion without proof.
15592
15592
  }
15593
15593
  });
15594
15594
 
15595
- // src/tools/slashCommand.ts
15596
- var COMMANDER_SYSTEM_PROMPT = commander.systemPrompt;
15597
- var MISSION_MODE_TEMPLATE = `${COMMANDER_SYSTEM_PROMPT}
15598
-
15599
- <mission>
15600
- <task>
15601
- $ARGUMENTS
15602
- </task>
15603
-
15604
- <execution_rules>
15605
- 1. Complete this mission without user intervention
15606
- 2. Use your full capabilities: research, implement, verify
15607
- 3. Output "${MISSION_SEAL.PATTERN}" when done
15608
- </execution_rules>
15609
- </mission>`;
15610
- var COMMANDS = {
15611
- "task": {
15612
- description: "MISSION MODE - Execute task autonomously until complete",
15613
- template: MISSION_MODE_TEMPLATE,
15614
- argumentHint: '"mission goal"'
15615
- },
15616
- "plan": {
15617
- description: "Create a task plan without executing",
15618
- template: `<delegate>
15619
- <agent>${AGENT_NAMES.PLANNER}</agent>
15620
- <objective>Create parallel task plan for: $ARGUMENTS</objective>
15621
- <success>Valid .opencode/todo.md with tasks, each having id, description, agent, size, dependencies</success>
15622
- <must_do>
15623
- - Maximize parallelism by grouping independent tasks
15624
- - Assign correct agent to each task (${AGENT_NAMES.WORKER} or ${AGENT_NAMES.REVIEWER})
15625
- - Include clear success criteria for each task
15626
- - Research before planning if unfamiliar technology
15627
- </must_do>
15628
- <must_not>
15629
- - Do not implement any tasks, only plan
15630
- - Do not create tasks that depend on each other unnecessarily
15631
- </must_not>
15632
- <context>
15633
- - This is planning only, no execution
15634
- - Output to .opencode/todo.md
15635
- </context>
15636
- </delegate>`,
15637
- argumentHint: '"complex task to plan"'
15638
- },
15639
- "agents": {
15640
- description: "Show the 4-agent architecture",
15641
- template: `## OpenCode Orchestrator - 4-Agent Architecture
15642
-
15643
- | Agent | Role | Capabilities |
15644
- |-------|------|--------------|
15645
- | **${AGENT_NAMES.COMMANDER}** | [MASTER] | Master Orchestrator: mission control, parallel coordination |
15646
- | **${AGENT_NAMES.PLANNER}** | [STRATEGIST] | Planning, research, documentation analysis |
15647
- | **${AGENT_NAMES.WORKER}** | [EXECUTOR] | Implementation, coding, terminal tasks |
15648
- | **${AGENT_NAMES.REVIEWER}** | [VERIFIER] | Verification, testing, context sanity checks |
15649
-
15650
- ## Parallel Execution System
15651
- \`\`\`
15652
- Up to 50 Worker Sessions running simultaneously
15653
- Max 10 per agent type (auto-queues excess)
15654
- Auto-timeout: 60 min | Auto-cleanup: 30 min
15655
- \`\`\`
15656
-
15657
- ## Execution Flow
15658
- \`\`\`
15659
- THINK \u2192 PLAN \u2192 DELEGATE \u2192 EXECUTE \u2192 VERIFY \u2192 COMPLETE
15660
- L1: Fast Track (simple fixes)
15661
- L2: Normal Track (features)
15662
- L3: Deep Track (complex refactoring)
15663
- \`\`\`
15664
-
15665
- ## Anti-Hallucination
15666
- - ${AGENT_NAMES.PLANNER} researches BEFORE implementation
15667
- - ${AGENT_NAMES.WORKER} caches official documentation
15668
- - Never assumes - always verifies from sources
15669
-
15670
- ## Usage
15671
- - Select **${AGENT_NAMES.COMMANDER}** and type your request
15672
- - Or use \`/task "your mission"\` explicitly
15673
- - ${AGENT_NAMES.COMMANDER} automatically coordinates all agents`
15674
- }
15675
- };
15676
- function createSlashcommandTool() {
15677
- const commandList = Object.entries(COMMANDS).map(([name, cmd]) => {
15678
- const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : "";
15679
- return `- /${name}${hint}: ${cmd.description}`;
15680
- }).join("\n");
15681
- return tool({
15682
- description: `Available commands:
15683
- ${commandList}`,
15684
- args: {
15685
- command: tool.schema.string().describe("Command name (without slash)")
15686
- },
15687
- async execute(args) {
15688
- const cmdName = (args.command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
15689
- const cmdArgs = (args.command || "").replace(/^\/?\\S+\s*/, "");
15690
- if (!cmdName) return `Commands:
15691
- ${commandList}`;
15692
- const command = COMMANDS[cmdName];
15693
- if (!command) return `Unknown command: /${cmdName}
15694
-
15695
- ${commandList}`;
15696
- return command.template.replace(/\$ARGUMENTS/g, cmdArgs || PROMPTS.CONTINUE_DEFAULT);
15697
- }
15698
- });
15699
- }
15700
-
15701
- // src/tools/rust.ts
15702
- import { spawn } from "child_process";
15703
- import { existsSync as existsSync2 } from "fs";
15704
-
15705
- // src/utils/binary.ts
15706
- import { join, dirname } from "path";
15707
- import { fileURLToPath } from "url";
15708
- import { platform, arch } from "os";
15709
- import { existsSync } from "fs";
15710
- var __dirname = dirname(fileURLToPath(import.meta.url));
15711
- function getBinaryPath() {
15712
- const binDir = join(__dirname, "..", "..", "bin");
15713
- const os2 = platform();
15714
- const cpu = arch();
15715
- let binaryName;
15716
- if (os2 === PLATFORM.WIN32) {
15717
- binaryName = "orchestrator-windows-x64.exe";
15718
- } else if (os2 === PLATFORM.DARWIN) {
15719
- binaryName = cpu === "arm64" ? "orchestrator-macos-arm64" : "orchestrator-macos-x64";
15720
- } else {
15721
- binaryName = cpu === "arm64" ? "orchestrator-linux-arm64" : "orchestrator-linux-x64";
15722
- }
15723
- let binaryPath = join(binDir, binaryName);
15724
- if (!existsSync(binaryPath)) {
15725
- binaryPath = join(binDir, os2 === PLATFORM.WIN32 ? "orchestrator.exe" : "orchestrator");
15726
- }
15727
- return binaryPath;
15728
- }
15729
-
15730
15595
  // src/core/agents/logger.ts
15731
15596
  import * as fs from "fs";
15732
15597
  import * as os from "os";
@@ -15747,1614 +15612,1340 @@ function getLogPath() {
15747
15612
  return LOG_FILE;
15748
15613
  }
15749
15614
 
15750
- // src/tools/rust.ts
15751
- async function callRustTool(name, args) {
15752
- const binary = getBinaryPath();
15753
- if (!existsSync2(binary)) {
15754
- return JSON.stringify({ error: `Binary not found: ${binary}` });
15615
+ // src/core/agents/concurrency.ts
15616
+ var DEBUG2 = process.env.DEBUG_PARALLEL_AGENT === "true";
15617
+ var log2 = (...args) => {
15618
+ if (DEBUG2) log("[concurrency]", ...args);
15619
+ };
15620
+ var ConcurrencyController = class {
15621
+ counts = /* @__PURE__ */ new Map();
15622
+ queues = /* @__PURE__ */ new Map();
15623
+ limits = /* @__PURE__ */ new Map();
15624
+ config;
15625
+ successStreak = /* @__PURE__ */ new Map();
15626
+ failureCount = /* @__PURE__ */ new Map();
15627
+ constructor(config2) {
15628
+ this.config = config2 ?? {};
15755
15629
  }
15756
- return new Promise((resolve2) => {
15757
- const proc = spawn(binary, ["serve"], { stdio: ["pipe", "pipe", "pipe"] });
15758
- let stdout = "";
15759
- proc.stdout.on("data", (data) => {
15760
- stdout += data.toString();
15761
- });
15762
- proc.stderr.on("data", (data) => {
15763
- const msg = data.toString().trim();
15764
- if (msg) log(`[rust-stderr] ${msg}`);
15765
- });
15766
- const request = JSON.stringify({
15767
- jsonrpc: "2.0",
15768
- id: Date.now(),
15769
- method: "tools/call",
15770
- params: { name, arguments: args }
15771
- });
15772
- proc.stdin.write(request + "\n");
15773
- proc.stdin.end();
15774
- const timeout = setTimeout(() => {
15775
- proc.kill();
15776
- resolve2(JSON.stringify({ error: "Timeout" }));
15777
- }, 6e4);
15778
- proc.on("close", (code) => {
15779
- clearTimeout(timeout);
15780
- if (code !== 0 && code !== null) {
15781
- log(`Rust process exited with code ${code}`);
15782
- }
15783
- try {
15784
- const lines = stdout.trim().split("\n");
15785
- for (let i = lines.length - 1; i >= 0; i--) {
15786
- try {
15787
- const response = JSON.parse(lines[i]);
15788
- if (response.result || response.error) {
15789
- const text = response?.result?.content?.[0]?.text;
15790
- return resolve2(text || JSON.stringify(response.result));
15791
- }
15792
- } catch {
15793
- continue;
15794
- }
15795
- }
15796
- resolve2(stdout || "No output");
15797
- } catch {
15798
- resolve2(stdout || "No output");
15799
- }
15800
- });
15801
- });
15802
- }
15803
-
15804
- // src/tools/search.ts
15805
- var grepSearchTool = (directory) => tool({
15806
- description: "Search code patterns using regex. Returns matching lines with file paths and line numbers.",
15807
- args: {
15808
- pattern: tool.schema.string().describe("Regex pattern to search for"),
15809
- dir: tool.schema.string().optional().describe("Directory to search (defaults to project root)"),
15810
- max_results: tool.schema.number().optional().describe("Max results (default: 100)"),
15811
- timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds (default: 30000)")
15812
- },
15813
- async execute(args) {
15814
- return callRustTool("grep_search", {
15815
- pattern: args.pattern,
15816
- directory: args.dir || directory,
15817
- max_results: args.max_results,
15818
- timeout_ms: args.timeout_ms
15819
- });
15630
+ setLimit(key, limit) {
15631
+ this.limits.set(key, limit);
15820
15632
  }
15821
- });
15822
- var globSearchTool = (directory) => tool({
15823
- description: "Find files matching a glob pattern. Returns list of file paths.",
15824
- args: {
15825
- pattern: tool.schema.string().describe("Glob pattern (e.g., '**/*.ts', 'src/**/*.md')"),
15826
- dir: tool.schema.string().optional().describe("Directory to search (defaults to project root)")
15827
- },
15828
- async execute(args) {
15829
- return callRustTool("glob_search", {
15830
- pattern: args.pattern,
15831
- directory: args.dir || directory
15832
- });
15633
+ /**
15634
+ * Get concurrency limit for a key.
15635
+ * Priority: explicit limit > model > provider > agent > default
15636
+ */
15637
+ getConcurrencyLimit(key) {
15638
+ const explicitLimit = this.limits.get(key);
15639
+ if (explicitLimit !== void 0) {
15640
+ return explicitLimit === 0 ? Infinity : explicitLimit;
15641
+ }
15642
+ if (this.config.modelConcurrency?.[key] !== void 0) {
15643
+ const limit = this.config.modelConcurrency[key];
15644
+ return limit === 0 ? Infinity : limit;
15645
+ }
15646
+ const provider = key.split("/")[0];
15647
+ if (this.config.providerConcurrency?.[provider] !== void 0) {
15648
+ const limit = this.config.providerConcurrency[provider];
15649
+ return limit === 0 ? Infinity : limit;
15650
+ }
15651
+ if (this.config.agentConcurrency?.[key] !== void 0) {
15652
+ const limit = this.config.agentConcurrency[key];
15653
+ return limit === 0 ? Infinity : limit;
15654
+ }
15655
+ return this.config.defaultConcurrency ?? PARALLEL_TASK.DEFAULT_CONCURRENCY;
15833
15656
  }
15834
- });
15835
- var mgrepTool = (directory) => tool({
15836
- description: `Search multiple patterns (runs grep for each pattern).`,
15837
- args: {
15838
- patterns: tool.schema.array(tool.schema.string()).describe("Array of regex patterns"),
15839
- dir: tool.schema.string().optional().describe("Directory (defaults to project root)"),
15840
- max_results_per_pattern: tool.schema.number().optional().describe("Max results per pattern (default: 50)"),
15841
- timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds (default: 60000)")
15842
- },
15843
- async execute(args) {
15844
- return callRustTool("mgrep", {
15845
- patterns: args.patterns,
15846
- directory: args.dir || directory,
15847
- max_results_per_pattern: args.max_results_per_pattern,
15848
- timeout_ms: args.timeout_ms
15849
- });
15657
+ // Backwards compatible alias
15658
+ getLimit(key) {
15659
+ return this.getConcurrencyLimit(key);
15850
15660
  }
15851
- });
15852
- var sedReplaceTool = (directory) => tool({
15853
- description: `Find and replace patterns in files (sed-like). Supports regex. Use dry_run=true to preview changes.`,
15854
- args: {
15855
- pattern: tool.schema.string().describe("Regex pattern to find"),
15856
- replacement: tool.schema.string().describe("Replacement string"),
15857
- file: tool.schema.string().optional().describe("Single file to modify"),
15858
- dir: tool.schema.string().optional().describe("Directory to search (modifies all matching files)"),
15859
- dry_run: tool.schema.boolean().optional().describe("Preview changes without modifying files (default: false)"),
15860
- backup: tool.schema.boolean().optional().describe("Create .bak backup before modifying (default: false)"),
15861
- timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds")
15862
- },
15863
- async execute(args) {
15864
- return callRustTool("sed_replace", {
15865
- pattern: args.pattern,
15866
- replacement: args.replacement,
15867
- file: args.file,
15868
- directory: args.dir || (args.file ? void 0 : directory),
15869
- dry_run: args.dry_run,
15870
- backup: args.backup,
15871
- timeout_ms: args.timeout_ms
15661
+ async acquire(key) {
15662
+ const limit = this.getConcurrencyLimit(key);
15663
+ if (limit === Infinity) return;
15664
+ const current = this.counts.get(key) ?? 0;
15665
+ if (current < limit) {
15666
+ this.counts.set(key, current + 1);
15667
+ log2(`Acquired ${key}: ${current + 1}/${limit}`);
15668
+ return;
15669
+ }
15670
+ log2(`Queueing ${key}: ${current}/${limit}`);
15671
+ return new Promise((resolve2) => {
15672
+ const queue = this.queues.get(key) ?? [];
15673
+ queue.push(resolve2);
15674
+ this.queues.set(key, queue);
15872
15675
  });
15873
15676
  }
15874
- });
15875
- var diffTool = () => tool({
15876
- description: `Compare two files or strings and show differences.`,
15877
- args: {
15878
- file1: tool.schema.string().optional().describe("First file to compare"),
15879
- file2: tool.schema.string().optional().describe("Second file to compare"),
15880
- content1: tool.schema.string().optional().describe("First string to compare"),
15881
- content2: tool.schema.string().optional().describe("Second string to compare"),
15882
- ignore_whitespace: tool.schema.boolean().optional().describe("Ignore whitespace differences")
15883
- },
15884
- async execute(args) {
15885
- return callRustTool("diff", args);
15886
- }
15887
- });
15888
- var jqTool = () => tool({
15889
- description: `Query and manipulate JSON using jq expressions.`,
15890
- args: {
15891
- json_input: tool.schema.string().optional().describe("JSON string to query"),
15892
- file: tool.schema.string().optional().describe("JSON file to query"),
15893
- expression: tool.schema.string().describe("jq expression (e.g., '.foo.bar', '.[] | select(.x > 1)')"),
15894
- raw_output: tool.schema.boolean().optional().describe("Raw output (no JSON encoding for strings)")
15895
- },
15896
- async execute(args) {
15897
- return callRustTool("jq", args);
15677
+ release(key) {
15678
+ const limit = this.getConcurrencyLimit(key);
15679
+ if (limit === Infinity) return;
15680
+ const queue = this.queues.get(key);
15681
+ if (queue && queue.length > 0) {
15682
+ const next = queue.shift();
15683
+ log2(`Released ${key}: next in queue`);
15684
+ next();
15685
+ } else {
15686
+ const current = this.counts.get(key) ?? 0;
15687
+ if (current > 0) {
15688
+ this.counts.set(key, current - 1);
15689
+ log2(`Released ${key}: ${current - 1}/${limit}`);
15690
+ }
15691
+ }
15898
15692
  }
15899
- });
15900
- var httpTool = () => tool({
15901
- description: `Make HTTP requests (GET, POST, PUT, DELETE, etc).`,
15902
- args: {
15903
- url: tool.schema.string().describe("URL to request"),
15904
- method: tool.schema.string().optional().describe("HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD)"),
15905
- headers: tool.schema.object({}).optional().describe("Request headers as JSON object"),
15906
- body: tool.schema.string().optional().describe("Request body"),
15907
- timeout_ms: tool.schema.number().optional().describe("Request timeout in milliseconds")
15908
- },
15909
- async execute(args) {
15910
- return callRustTool("http", args);
15693
+ /**
15694
+ * Report success/failure to adjust concurrency dynamically
15695
+ */
15696
+ reportResult(key, success2) {
15697
+ if (success2) {
15698
+ const streak = (this.successStreak.get(key) ?? 0) + 1;
15699
+ this.successStreak.set(key, streak);
15700
+ this.failureCount.set(key, 0);
15701
+ if (streak % 5 === 0) {
15702
+ const currentLimit = this.getConcurrencyLimit(key);
15703
+ if (currentLimit < PARALLEL_TASK.MAX_CONCURRENCY) {
15704
+ this.setLimit(key, currentLimit + 1);
15705
+ log(`[concurrency] Auto-scaling UP for ${key}: ${currentLimit + 1}`);
15706
+ }
15707
+ }
15708
+ } else {
15709
+ const failures = (this.failureCount.get(key) ?? 0) + 1;
15710
+ this.failureCount.set(key, failures);
15711
+ this.successStreak.set(key, 0);
15712
+ if (failures >= 2) {
15713
+ const currentLimit = this.getConcurrencyLimit(key);
15714
+ const minLimit = 1;
15715
+ if (currentLimit > minLimit) {
15716
+ this.setLimit(key, currentLimit - 1);
15717
+ log(`[concurrency] Auto-scaling DOWN for ${key}: ${currentLimit - 1} (due to ${failures} failures)`);
15718
+ }
15719
+ }
15720
+ }
15911
15721
  }
15912
- });
15913
- var fileStatsTool = (directory) => tool({
15914
- description: `Analyze file/directory statistics (file counts, sizes, line counts, etc).`,
15915
- args: {
15916
- dir: tool.schema.string().optional().describe("Directory to analyze (defaults to project root)"),
15917
- max_depth: tool.schema.number().optional().describe("Maximum directory depth to analyze")
15918
- },
15919
- async execute(args) {
15920
- return callRustTool("file_stats", {
15921
- directory: args.dir || directory,
15922
- max_depth: args.max_depth
15923
- });
15722
+ getQueueLength(key) {
15723
+ return this.queues.get(key)?.length ?? 0;
15924
15724
  }
15925
- });
15926
- var gitDiffTool = (directory) => tool({
15927
- description: `Show git diff of uncommitted changes.`,
15928
- args: {
15929
- dir: tool.schema.string().optional().describe("Repository directory (defaults to project root)"),
15930
- staged_only: tool.schema.boolean().optional().describe("Show only staged changes")
15931
- },
15932
- async execute(args) {
15933
- return callRustTool("git_diff", {
15934
- directory: args.dir || directory,
15935
- staged_only: args.staged_only
15936
- });
15725
+ getActiveCount(key) {
15726
+ return this.counts.get(key) ?? 0;
15937
15727
  }
15938
- });
15939
- var gitStatusTool = (directory) => tool({
15940
- description: `Show git status (modified, added, deleted files).`,
15941
- args: {
15942
- dir: tool.schema.string().optional().describe("Repository directory (defaults to project root)")
15943
- },
15944
- async execute(args) {
15945
- return callRustTool("git_status", {
15946
- directory: args.dir || directory
15947
- });
15728
+ /**
15729
+ * Get formatted concurrency info string (e.g., "2/5 slots")
15730
+ */
15731
+ getConcurrencyInfo(key) {
15732
+ const active = this.getActiveCount(key);
15733
+ const limit = this.getConcurrencyLimit(key);
15734
+ if (limit === Infinity) return "";
15735
+ return ` (${active}/${limit} slots)`;
15948
15736
  }
15949
- });
15950
-
15951
- // src/core/commands/types/background-task-status.ts
15952
- var BACKGROUND_TASK_STATUS = {
15953
- PENDING: STATUS_LABEL.PENDING,
15954
- RUNNING: STATUS_LABEL.RUNNING,
15955
- DONE: STATUS_LABEL.DONE,
15956
- ERROR: STATUS_LABEL.ERROR,
15957
- TIMEOUT: STATUS_LABEL.TIMEOUT
15958
15737
  };
15959
15738
 
15960
- // src/core/commands/manager.ts
15961
- import { spawn as spawn2 } from "node:child_process";
15962
- import { randomBytes } from "node:crypto";
15963
- var BackgroundTaskManager = class _BackgroundTaskManager {
15964
- static _instance;
15739
+ // src/core/agents/task-store.ts
15740
+ import * as fs2 from "node:fs/promises";
15741
+ import * as path2 from "node:path";
15742
+ var TaskStore = class {
15965
15743
  tasks = /* @__PURE__ */ new Map();
15966
- debugMode = process.env.DEBUG_BG_TASK === "true";
15967
- // Disabled by default
15968
- constructor() {
15969
- }
15970
- static get instance() {
15971
- if (!_BackgroundTaskManager._instance) {
15972
- _BackgroundTaskManager._instance = new _BackgroundTaskManager();
15744
+ pendingByParent = /* @__PURE__ */ new Map();
15745
+ notifications = /* @__PURE__ */ new Map();
15746
+ archivedCount = 0;
15747
+ set(id, task) {
15748
+ this.tasks.set(id, task);
15749
+ if (this.tasks.size > MEMORY_LIMITS.MAX_TASKS_IN_MEMORY) {
15750
+ this.gc();
15973
15751
  }
15974
- return _BackgroundTaskManager._instance;
15975
15752
  }
15976
- generateId() {
15977
- return `${ID_PREFIX.JOB}${randomBytes(4).toString("hex")}`;
15753
+ get(id) {
15754
+ return this.tasks.get(id);
15978
15755
  }
15979
- debug(taskId, message) {
15980
- if (this.debugMode) {
15981
- const ts = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
15982
- log(`[BG ${ts}] ${taskId}: ${message}`);
15756
+ getAll() {
15757
+ return Array.from(this.tasks.values());
15758
+ }
15759
+ getRunning() {
15760
+ return this.getAll().filter((t) => t.status === TASK_STATUS.RUNNING || t.status === TASK_STATUS.PENDING);
15761
+ }
15762
+ getByParent(parentSessionID) {
15763
+ return this.getAll().filter((t) => t.parentSessionID === parentSessionID);
15764
+ }
15765
+ delete(id) {
15766
+ return this.tasks.delete(id);
15767
+ }
15768
+ clear() {
15769
+ this.tasks.clear();
15770
+ this.pendingByParent.clear();
15771
+ this.notifications.clear();
15772
+ }
15773
+ // Pending tracking
15774
+ trackPending(parentSessionID, taskId) {
15775
+ const pending2 = this.pendingByParent.get(parentSessionID) ?? /* @__PURE__ */ new Set();
15776
+ pending2.add(taskId);
15777
+ this.pendingByParent.set(parentSessionID, pending2);
15778
+ }
15779
+ untrackPending(parentSessionID, taskId) {
15780
+ const pending2 = this.pendingByParent.get(parentSessionID);
15781
+ if (pending2) {
15782
+ pending2.delete(taskId);
15783
+ if (pending2.size === 0) {
15784
+ this.pendingByParent.delete(parentSessionID);
15785
+ }
15983
15786
  }
15984
15787
  }
15985
- run(options) {
15986
- const id = this.generateId();
15987
- const { command, cwd = process.cwd(), timeout = 3e5, label } = options;
15988
- const isWindows = process.platform === PLATFORM.WIN32;
15989
- const shell = isWindows ? "cmd.exe" : CLI_NAME.SH;
15990
- const shellFlag = isWindows ? "/c" : "-c";
15991
- const task = {
15992
- id,
15993
- command,
15994
- args: [shellFlag, command],
15995
- cwd,
15996
- label,
15997
- status: STATUS_LABEL.RUNNING,
15998
- output: "",
15999
- errorOutput: "",
16000
- exitCode: null,
16001
- startTime: Date.now(),
16002
- timeout
16003
- };
16004
- this.tasks.set(id, task);
16005
- this.debug(id, `Starting: ${command}`);
16006
- try {
16007
- const proc = spawn2(shell, task.args, {
16008
- cwd,
16009
- stdio: ["ignore", "pipe", "pipe"],
16010
- detached: false
16011
- });
16012
- task.process = proc;
16013
- proc.stdout?.on("data", (data) => {
16014
- task.output += data.toString();
16015
- });
16016
- proc.stderr?.on("data", (data) => {
16017
- task.errorOutput += data.toString();
16018
- });
16019
- proc.on("close", (code) => {
16020
- task.exitCode = code;
16021
- task.endTime = Date.now();
16022
- task.status = code === 0 ? STATUS_LABEL.DONE : STATUS_LABEL.ERROR;
16023
- task.process = void 0;
16024
- this.debug(id, `Done (code=${code})`);
16025
- });
16026
- proc.on("error", (err) => {
16027
- task.status = STATUS_LABEL.ERROR;
16028
- task.errorOutput += `
16029
- Process error: ${err.message}`;
16030
- task.endTime = Date.now();
16031
- task.process = void 0;
16032
- });
16033
- setTimeout(() => {
16034
- if (task.status === STATUS_LABEL.RUNNING && task.process) {
16035
- task.process.kill("SIGKILL");
16036
- task.status = STATUS_LABEL.TIMEOUT;
16037
- task.endTime = Date.now();
16038
- this.debug(id, "Timeout");
16039
- }
16040
- }, timeout);
16041
- } catch (err) {
16042
- task.status = STATUS_LABEL.ERROR;
16043
- task.errorOutput = `Spawn failed: ${err instanceof Error ? err.message : String(err)}`;
16044
- task.endTime = Date.now();
15788
+ getPendingCount(parentSessionID) {
15789
+ return this.pendingByParent.get(parentSessionID)?.size ?? 0;
15790
+ }
15791
+ hasPending(parentSessionID) {
15792
+ return this.getPendingCount(parentSessionID) > 0;
15793
+ }
15794
+ // Notifications with limit
15795
+ queueNotification(task) {
15796
+ const queue = this.notifications.get(task.parentSessionID) ?? [];
15797
+ queue.push(task);
15798
+ if (queue.length > MEMORY_LIMITS.MAX_NOTIFICATIONS_PER_PARENT) {
15799
+ queue.shift();
16045
15800
  }
16046
- return task;
15801
+ this.notifications.set(task.parentSessionID, queue);
16047
15802
  }
16048
- get(taskId) {
16049
- return this.tasks.get(taskId);
15803
+ getNotifications(parentSessionID) {
15804
+ return this.notifications.get(parentSessionID) ?? [];
16050
15805
  }
16051
- getAll() {
16052
- return Array.from(this.tasks.values());
15806
+ clearNotifications(parentSessionID) {
15807
+ this.notifications.delete(parentSessionID);
16053
15808
  }
16054
- getByStatus(status) {
16055
- return this.getAll().filter((t) => t.status === status);
15809
+ cleanEmptyNotifications() {
15810
+ for (const [sessionID, queue] of this.notifications.entries()) {
15811
+ if (queue.length === 0) {
15812
+ this.notifications.delete(sessionID);
15813
+ }
15814
+ }
16056
15815
  }
16057
- clearCompleted() {
16058
- let count = 0;
16059
- for (const [id, task] of this.tasks) {
16060
- if (task.status !== STATUS_LABEL.RUNNING && task.status !== STATUS_LABEL.PENDING) {
16061
- this.tasks.delete(id);
16062
- count++;
15816
+ clearNotificationsForTask(taskId) {
15817
+ for (const [sessionID, tasks] of this.notifications.entries()) {
15818
+ const filtered = tasks.filter((t) => t.id !== taskId);
15819
+ if (filtered.length === 0) {
15820
+ this.notifications.delete(sessionID);
15821
+ } else if (filtered.length !== tasks.length) {
15822
+ this.notifications.set(sessionID, filtered);
16063
15823
  }
16064
15824
  }
16065
- return count;
16066
15825
  }
16067
- kill(taskId) {
16068
- const task = this.tasks.get(taskId);
16069
- if (task?.process) {
16070
- task.process.kill("SIGKILL");
16071
- task.status = STATUS_LABEL.ERROR;
16072
- task.errorOutput += "\nKilled by user";
16073
- task.endTime = Date.now();
16074
- return true;
15826
+ // =========================================================================
15827
+ // Garbage Collection & Memory Management
15828
+ // =========================================================================
15829
+ /**
15830
+ * Get memory statistics
15831
+ */
15832
+ getStats() {
15833
+ return {
15834
+ tasksInMemory: this.tasks.size,
15835
+ runningTasks: this.getRunning().length,
15836
+ archivedTasks: this.archivedCount,
15837
+ notificationQueues: this.notifications.size,
15838
+ pendingParents: this.pendingByParent.size
15839
+ };
15840
+ }
15841
+ /**
15842
+ * Garbage collect completed tasks
15843
+ * Archives old completed tasks to disk
15844
+ */
15845
+ async gc() {
15846
+ const now = Date.now();
15847
+ const toRemove = [];
15848
+ const toArchive = [];
15849
+ for (const [id, task] of this.tasks) {
15850
+ if (task.status === TASK_STATUS.RUNNING) continue;
15851
+ const completedAt = task.completedAt?.getTime() ?? 0;
15852
+ const age = now - completedAt;
15853
+ if (age > MEMORY_LIMITS.ARCHIVE_AGE_MS && task.status === TASK_STATUS.COMPLETED) {
15854
+ toArchive.push(task);
15855
+ toRemove.push(id);
15856
+ } else if (age > MEMORY_LIMITS.ERROR_CLEANUP_AGE_MS && (task.status === TASK_STATUS.ERROR || task.status === TASK_STATUS.CANCELLED)) {
15857
+ toRemove.push(id);
15858
+ }
16075
15859
  }
16076
- return false;
15860
+ if (toArchive.length > 0) {
15861
+ await this.archiveTasks(toArchive);
15862
+ }
15863
+ for (const id of toRemove) {
15864
+ this.tasks.delete(id);
15865
+ }
15866
+ return toRemove.length;
16077
15867
  }
16078
- formatDuration(task) {
16079
- const end = task.endTime || Date.now();
16080
- const seconds = (end - task.startTime) / 1e3;
16081
- if (seconds < 60) return `${seconds.toFixed(1)}s`;
16082
- return `${Math.floor(seconds / 60)}m ${(seconds % 60).toFixed(0)}s`;
15868
+ /**
15869
+ * Archive tasks to disk for later analysis
15870
+ */
15871
+ async archiveTasks(tasks) {
15872
+ try {
15873
+ await fs2.mkdir(PATHS.TASK_ARCHIVE, { recursive: true });
15874
+ const date5 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15875
+ const filename = `tasks_${date5}.jsonl`;
15876
+ const filepath = path2.join(PATHS.TASK_ARCHIVE, filename);
15877
+ const lines = tasks.map((task) => JSON.stringify({
15878
+ id: task.id,
15879
+ agent: task.agent,
15880
+ prompt: task.prompt.slice(0, 200),
15881
+ // Truncate
15882
+ status: task.status,
15883
+ startedAt: task.startedAt,
15884
+ completedAt: task.completedAt,
15885
+ parentSessionID: task.parentSessionID
15886
+ }));
15887
+ await fs2.appendFile(filepath, lines.join("\n") + "\n");
15888
+ this.archivedCount += tasks.length;
15889
+ } catch (error45) {
15890
+ }
16083
15891
  }
16084
- getStatusEmoji(status) {
16085
- return getStatusIndicator(status);
15892
+ /**
15893
+ * Force cleanup of all completed tasks
15894
+ */
15895
+ forceCleanup() {
15896
+ const toRemove = [];
15897
+ for (const [id, task] of this.tasks) {
15898
+ if (task.status !== TASK_STATUS.RUNNING) {
15899
+ toRemove.push(id);
15900
+ }
15901
+ }
15902
+ for (const id of toRemove) {
15903
+ this.tasks.delete(id);
15904
+ }
15905
+ return toRemove.length;
16086
15906
  }
16087
15907
  };
16088
- var backgroundTaskManager = BackgroundTaskManager.instance;
16089
15908
 
16090
- // src/tools/background-cmd/run.ts
16091
- var runBackgroundTool = tool({
16092
- description: `Run a shell command in the background and get a task ID.
15909
+ // src/core/agents/format.ts
15910
+ function formatDuration(start, end) {
15911
+ const duration3 = (end ?? /* @__PURE__ */ new Date()).getTime() - start.getTime();
15912
+ const seconds = Math.floor(duration3 / 1e3);
15913
+ const minutes = Math.floor(seconds / 60);
15914
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
15915
+ return `${seconds}s`;
15916
+ }
15917
+ function buildNotificationMessage(tasks) {
15918
+ const summary = tasks.map((t) => {
15919
+ const status = t.status === TASK_STATUS.COMPLETED ? "\u2705" : "\u274C";
15920
+ return `${status} \`${t.id}\`: ${t.description}`;
15921
+ }).join("\n");
15922
+ return `<system-notification>
15923
+ **All Parallel Tasks Complete**
16093
15924
 
16094
- <purpose>
16095
- Execute long-running commands (builds, tests) without blocking.
16096
- Use check_background to get results.
16097
- </purpose>`,
16098
- args: {
16099
- command: tool.schema.string().describe("Shell command to execute"),
16100
- cwd: tool.schema.string().optional().describe("Working directory"),
16101
- timeout: tool.schema.number().optional().describe(`Timeout in ms (default: ${BACKGROUND_TASK.DEFAULT_TIMEOUT_MS})`),
16102
- label: tool.schema.string().optional().describe("Task label")
16103
- },
16104
- async execute(args) {
16105
- const { command, cwd, timeout, label } = args;
16106
- const task = backgroundTaskManager.run({
16107
- command,
16108
- cwd: cwd || process.cwd(),
16109
- timeout: timeout || BACKGROUND_TASK.DEFAULT_TIMEOUT_MS,
16110
- label
16111
- });
16112
- const displayLabel = label ? ` (${label})` : "";
16113
- return `\u{1F680} **Background Task Started**${displayLabel}
16114
- | Task ID | \`${task.id}\` |
16115
- | Command | \`${command}\` |
16116
- | Status | ${backgroundTaskManager.getStatusEmoji(task.status)} ${task.status} |
15925
+ ${summary}
16117
15926
 
16118
- \u{1F4CC} Use \`check_background({ taskId: "${task.id}" })\` to get results.`;
16119
- }
15927
+ Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
15928
+ </system-notification>`;
15929
+ }
15930
+
15931
+ // src/core/notification/presets/index.ts
15932
+ var presets_exports = {};
15933
+ __export(presets_exports, {
15934
+ allTasksComplete: () => allTasksComplete,
15935
+ concurrencyAcquired: () => concurrencyAcquired,
15936
+ concurrencyReleased: () => concurrencyReleased,
15937
+ documentCached: () => documentCached,
15938
+ errorRecovery: () => errorRecovery,
15939
+ missionComplete: () => missionComplete,
15940
+ missionStarted: () => missionStarted,
15941
+ parallelTasksLaunched: () => parallelTasksLaunched,
15942
+ researchStarted: () => researchStarted,
15943
+ sessionCompleted: () => sessionCompleted,
15944
+ sessionCreated: () => sessionCreated,
15945
+ sessionResumed: () => sessionResumed,
15946
+ taskCompleted: () => taskCompleted,
15947
+ taskFailed: () => taskFailed,
15948
+ taskStarted: () => taskStarted,
15949
+ toolExecuted: () => toolExecuted,
15950
+ warningMaxDepth: () => warningMaxDepth,
15951
+ warningMaxRetries: () => warningMaxRetries,
15952
+ warningRateLimited: () => warningRateLimited
16120
15953
  });
16121
15954
 
16122
- // src/tools/background-cmd/check.ts
16123
- var checkBackgroundTool = tool({
16124
- description: `Check the status and output of a background task.`,
16125
- args: {
16126
- taskId: tool.schema.string().describe("Task ID from run_background"),
16127
- tailLines: tool.schema.number().optional().describe("Limit output to last N lines")
16128
- },
16129
- async execute(args) {
16130
- const { taskId, tailLines } = args;
16131
- const task = backgroundTaskManager.get(taskId);
16132
- if (!task) {
16133
- const allTasks = backgroundTaskManager.getAll();
16134
- if (allTasks.length === 0) {
16135
- return `\u274C Task \`${taskId}\` not found. No background tasks exist.`;
16136
- }
16137
- const taskList = allTasks.map((t) => `- \`${t.id}\`: ${t.command.substring(0, 30)}...`).join("\n");
16138
- return `\u274C Task \`${taskId}\` not found.
16139
-
16140
- **Available:**
16141
- ${taskList}`;
15955
+ // src/core/notification/toast-core.ts
15956
+ var tuiClient = null;
15957
+ function initToastClient(client) {
15958
+ tuiClient = client;
15959
+ }
15960
+ var toasts = [];
15961
+ var handlers = [];
15962
+ function show(options) {
15963
+ const toast = {
15964
+ id: `toast_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
15965
+ title: options.title,
15966
+ message: options.message,
15967
+ variant: options.variant || "info",
15968
+ timestamp: /* @__PURE__ */ new Date(),
15969
+ duration: options.duration ?? 5e3,
15970
+ dismissed: false
15971
+ };
15972
+ toasts.push(toast);
15973
+ if (toasts.length > HISTORY.MAX_TOAST) {
15974
+ toasts.shift();
15975
+ }
15976
+ for (const handler of handlers) {
15977
+ try {
15978
+ handler(toast);
15979
+ } catch (error45) {
16142
15980
  }
16143
- const duration3 = backgroundTaskManager.formatDuration(task);
16144
- const statusEmoji = backgroundTaskManager.getStatusEmoji(task.status);
16145
- let output = task.output;
16146
- let stderr = task.errorOutput;
16147
- if (tailLines && tailLines > 0) {
16148
- output = output.split("\n").slice(-tailLines).join("\n");
16149
- stderr = stderr.split("\n").slice(-tailLines).join("\n");
15981
+ }
15982
+ if (tuiClient) {
15983
+ const client = tuiClient;
15984
+ if (client.tui?.showToast) {
15985
+ client.tui.showToast({
15986
+ body: {
15987
+ title: toast.title,
15988
+ message: toast.message,
15989
+ variant: toast.variant,
15990
+ duration: toast.duration
15991
+ }
15992
+ }).catch(() => {
15993
+ });
16150
15994
  }
16151
- const maxLen = 1e4;
16152
- if (output.length > maxLen) output = `[...truncated...]\\n` + output.slice(-maxLen);
16153
- if (stderr.length > maxLen) stderr = `[...truncated...]\\n` + stderr.slice(-maxLen);
16154
- let result = `${statusEmoji} **Task ${task.id}**${task.label ? ` (${task.label})` : ""}
16155
- | Command | \`${task.command}\` |
16156
- | Status | ${statusEmoji} **${task.status.toUpperCase()}** |
16157
- | Duration | ${duration3}${task.status === STATUS_LABEL.RUNNING ? " (ongoing)" : ""} |
16158
- ${task.exitCode !== null ? `| Exit Code | ${task.exitCode} |` : ""}`;
16159
- if (output.trim()) result += `
16160
-
16161
- \u{1F4E4} **stdout:**
16162
- \`\`\`
16163
- ${output.trim()}
16164
- \`\`\``;
16165
- if (stderr.trim()) result += `
15995
+ }
15996
+ return toast;
15997
+ }
16166
15998
 
16167
- \u26A0\uFE0F **stderr:**
16168
- \`\`\`
16169
- ${stderr.trim()}
16170
- \`\`\``;
16171
- if (task.status === STATUS_LABEL.RUNNING) result += `
15999
+ // src/core/notification/presets/task-lifecycle.ts
16000
+ var taskStarted = (taskId, agent) => show({
16001
+ title: "Task Started",
16002
+ message: `${agent}: ${taskId}`,
16003
+ variant: "info",
16004
+ duration: 3e3
16005
+ });
16006
+ var taskCompleted = (taskId, agent) => show({
16007
+ title: "Task Completed",
16008
+ message: `${agent}: ${taskId}`,
16009
+ variant: "success",
16010
+ duration: 3e3
16011
+ });
16012
+ var taskFailed = (taskId, error45) => show({
16013
+ title: "Task Failed",
16014
+ message: `${taskId}: ${error45}`,
16015
+ variant: "error",
16016
+ duration: 0
16017
+ });
16018
+ var allTasksComplete = (count) => show({
16019
+ title: "All Tasks Complete",
16020
+ message: `${count} tasks finished successfully`,
16021
+ variant: "success",
16022
+ duration: 5e3
16023
+ });
16172
16024
 
16173
- \u23F3 Still running... check again.`;
16174
- return result;
16175
- }
16025
+ // src/core/notification/presets/session.ts
16026
+ var sessionCreated = (sessionId, agent) => show({
16027
+ title: "Session Created",
16028
+ message: `${agent} - ${sessionId.slice(0, 12)}...`,
16029
+ variant: STATUS_LABEL.INFO,
16030
+ duration: TOAST_DURATION.SHORT
16031
+ });
16032
+ var sessionResumed = (sessionId, agent) => show({
16033
+ title: "Session Resumed",
16034
+ message: `${agent} - ${sessionId.slice(0, 12)}...`,
16035
+ variant: STATUS_LABEL.INFO,
16036
+ duration: TOAST_DURATION.SHORT
16037
+ });
16038
+ var sessionCompleted = (sessionId, duration3) => show({
16039
+ title: "Session Completed",
16040
+ message: `${sessionId.slice(0, 12)}... (${duration3})`,
16041
+ variant: STATUS_LABEL.SUCCESS,
16042
+ duration: TOAST_DURATION.MEDIUM
16176
16043
  });
16177
16044
 
16178
- // src/tools/background-cmd/list.ts
16179
- var listBackgroundTool = tool({
16180
- description: `List all background tasks and their status.`,
16181
- args: {
16182
- status: tool.schema.enum([
16183
- FILTER_STATUS.ALL,
16184
- BACKGROUND_STATUS.RUNNING,
16185
- BACKGROUND_STATUS.DONE,
16186
- BACKGROUND_STATUS.ERROR
16187
- ]).optional().describe("Filter by status")
16188
- },
16189
- async execute(args) {
16190
- const { status = FILTER_STATUS.ALL } = args;
16191
- let tasks;
16192
- if (status === FILTER_STATUS.ALL) {
16193
- tasks = backgroundTaskManager.getAll();
16194
- } else {
16195
- tasks = backgroundTaskManager.getByStatus(status);
16196
- }
16197
- if (tasks.length === 0) {
16198
- return `No background tasks${status !== FILTER_STATUS.ALL ? ` with status "${status}"` : ""}`;
16199
- }
16200
- tasks.sort((a, b) => b.startTime - a.startTime);
16201
- const rows = tasks.map((t) => {
16202
- const indicator = backgroundTaskManager.getStatusEmoji(t.status);
16203
- const cmd = t.command.length > 25 ? t.command.slice(0, 22) + "..." : t.command;
16204
- const label = t.label ? ` [${t.label}]` : "";
16205
- return `| \`${t.id}\` | ${indicator} ${t.status} | ${cmd}${label} | ${backgroundTaskManager.formatDuration(t)} |`;
16206
- }).join("\n");
16207
- const running = tasks.filter((t) => t.status === BACKGROUND_STATUS.RUNNING).length;
16208
- const done = tasks.filter((t) => t.status === BACKGROUND_STATUS.DONE).length;
16209
- const error45 = tasks.filter((t) => t.status === BACKGROUND_STATUS.ERROR || t.status === BACKGROUND_STATUS.TIMEOUT).length;
16210
- return `Background Tasks (${tasks.length})
16211
- Running: ${running} | Done: ${done} | Error: ${error45}
16045
+ // src/core/notification/presets/parallel.ts
16046
+ var parallelTasksLaunched = (count, agents) => show({
16047
+ title: "Parallel Tasks Launched",
16048
+ message: `${count} tasks: ${agents.join(", ")}`,
16049
+ variant: "info",
16050
+ duration: TOAST_DURATION.DEFAULT
16051
+ });
16052
+ var concurrencyAcquired = (agent, slot) => show({
16053
+ title: "Concurrency Slot",
16054
+ message: `${agent} acquired ${slot}`,
16055
+ variant: "info",
16056
+ duration: TOAST_DURATION.SHORT
16057
+ });
16058
+ var concurrencyReleased = (agent) => show({
16059
+ title: "Slot Released",
16060
+ message: agent,
16061
+ variant: "info",
16062
+ duration: TOAST_DURATION.EXTRA_SHORT
16063
+ });
16212
16064
 
16213
- | ID | Status | Command | Duration |
16214
- |----|--------|---------|----------|
16215
- ${rows}`;
16216
- }
16065
+ // src/core/notification/presets/mission.ts
16066
+ var missionComplete = (summary) => show({
16067
+ title: "Mission Complete",
16068
+ message: summary,
16069
+ variant: "success",
16070
+ duration: 0
16071
+ });
16072
+ var missionStarted = (description) => show({
16073
+ title: "Mission Started",
16074
+ message: description.slice(0, 100),
16075
+ variant: "info",
16076
+ duration: 4e3
16217
16077
  });
16218
16078
 
16219
- // src/tools/background-cmd/kill.ts
16220
- var killBackgroundTool = tool({
16221
- description: `Kill a running background task.`,
16222
- args: {
16223
- taskId: tool.schema.string().describe("Task ID to kill")
16224
- },
16225
- async execute(args) {
16226
- const { taskId } = args;
16227
- const task = backgroundTaskManager.get(taskId);
16228
- if (!task) return `\u274C Task \`${taskId}\` not found.`;
16229
- if (task.status !== STATUS_LABEL.RUNNING) return `\u26A0\uFE0F Task \`${taskId}\` is not running (${task.status}).`;
16230
- const killed = backgroundTaskManager.kill(taskId);
16231
- if (killed) {
16232
- return `\u{1F6D1} Task \`${taskId}\` killed.
16233
- Command: \`${task.command}\`
16234
- Duration: ${backgroundTaskManager.formatDuration(task)}`;
16235
- }
16236
- return `\u26A0\uFE0F Could not kill task \`${taskId}\`.`;
16237
- }
16079
+ // src/core/notification/presets/tools.ts
16080
+ var toolExecuted = (toolName, target) => show({
16081
+ title: toolName,
16082
+ message: target.slice(0, 80),
16083
+ variant: "info",
16084
+ duration: TOAST_DURATION.SHORT
16085
+ });
16086
+ var documentCached = (filename) => show({
16087
+ title: "Document Cached",
16088
+ message: `${PATHS.DOCS}/${filename}`,
16089
+ variant: "info",
16090
+ duration: TOAST_DURATION.SHORT
16091
+ });
16092
+ var researchStarted = (topic) => show({
16093
+ title: "Research Started",
16094
+ message: topic,
16095
+ variant: "info",
16096
+ duration: TOAST_DURATION.MEDIUM
16238
16097
  });
16239
16098
 
16240
- // src/core/agents/concurrency.ts
16241
- var DEBUG2 = process.env.DEBUG_PARALLEL_AGENT === "true";
16242
- var log2 = (...args) => {
16243
- if (DEBUG2) log("[concurrency]", ...args);
16244
- };
16245
- var ConcurrencyController = class {
16246
- counts = /* @__PURE__ */ new Map();
16247
- queues = /* @__PURE__ */ new Map();
16248
- limits = /* @__PURE__ */ new Map();
16249
- config;
16250
- successStreak = /* @__PURE__ */ new Map();
16251
- failureCount = /* @__PURE__ */ new Map();
16252
- constructor(config2) {
16253
- this.config = config2 ?? {};
16254
- }
16255
- setLimit(key, limit) {
16256
- this.limits.set(key, limit);
16099
+ // src/core/notification/presets/warnings.ts
16100
+ var warningRateLimited = () => show({
16101
+ title: "Rate Limited",
16102
+ message: "Waiting before retry...",
16103
+ variant: "warning",
16104
+ duration: TOAST_DURATION.LONG
16105
+ });
16106
+ var errorRecovery = (action) => show({
16107
+ title: "Error Recovery",
16108
+ message: `Attempting: ${action}`,
16109
+ variant: "warning",
16110
+ duration: TOAST_DURATION.MEDIUM
16111
+ });
16112
+ var warningMaxDepth = (depth) => show({
16113
+ title: "Max Depth Reached",
16114
+ message: `Recursion blocked at depth ${depth}`,
16115
+ variant: "warning",
16116
+ duration: TOAST_DURATION.LONG
16117
+ });
16118
+ var warningMaxRetries = () => show({
16119
+ title: "Max Retries Exceeded",
16120
+ message: "Automatic recovery has stopped. Manual intervention may be needed.",
16121
+ variant: "error",
16122
+ duration: TOAST_DURATION.PERSISTENT
16123
+ });
16124
+
16125
+ // src/core/notification/presets.ts
16126
+ var presets = presets_exports;
16127
+
16128
+ // src/core/notification/task-toast-manager.ts
16129
+ var TaskToastManager = class {
16130
+ tasks = /* @__PURE__ */ new Map();
16131
+ client = null;
16132
+ concurrency = null;
16133
+ /**
16134
+ * Initialize the manager with OpenCode client
16135
+ */
16136
+ init(client, concurrency) {
16137
+ this.client = client;
16138
+ this.concurrency = concurrency ?? null;
16257
16139
  }
16258
16140
  /**
16259
- * Get concurrency limit for a key.
16260
- * Priority: explicit limit > model > provider > agent > default
16141
+ * Set concurrency controller (can be set after init)
16261
16142
  */
16262
- getConcurrencyLimit(key) {
16263
- const explicitLimit = this.limits.get(key);
16264
- if (explicitLimit !== void 0) {
16265
- return explicitLimit === 0 ? Infinity : explicitLimit;
16266
- }
16267
- if (this.config.modelConcurrency?.[key] !== void 0) {
16268
- const limit = this.config.modelConcurrency[key];
16269
- return limit === 0 ? Infinity : limit;
16270
- }
16271
- const provider = key.split("/")[0];
16272
- if (this.config.providerConcurrency?.[provider] !== void 0) {
16273
- const limit = this.config.providerConcurrency[provider];
16274
- return limit === 0 ? Infinity : limit;
16275
- }
16276
- if (this.config.agentConcurrency?.[key] !== void 0) {
16277
- const limit = this.config.agentConcurrency[key];
16278
- return limit === 0 ? Infinity : limit;
16279
- }
16280
- return this.config.defaultConcurrency ?? PARALLEL_TASK.DEFAULT_CONCURRENCY;
16143
+ setConcurrencyController(concurrency) {
16144
+ this.concurrency = concurrency;
16281
16145
  }
16282
- // Backwards compatible alias
16283
- getLimit(key) {
16284
- return this.getConcurrencyLimit(key);
16146
+ /**
16147
+ * Add a new task and show consolidated toast
16148
+ */
16149
+ addTask(task) {
16150
+ const trackedTask = {
16151
+ id: task.id,
16152
+ description: task.description,
16153
+ agent: task.agent,
16154
+ status: task.status ?? STATUS_LABEL.RUNNING,
16155
+ startedAt: /* @__PURE__ */ new Date(),
16156
+ isBackground: task.isBackground,
16157
+ parentSessionID: task.parentSessionID,
16158
+ sessionID: task.sessionID
16159
+ };
16160
+ this.tasks.set(task.id, trackedTask);
16161
+ this.showTaskListToast(trackedTask);
16285
16162
  }
16286
- async acquire(key) {
16287
- const limit = this.getConcurrencyLimit(key);
16288
- if (limit === Infinity) return;
16289
- const current = this.counts.get(key) ?? 0;
16290
- if (current < limit) {
16291
- this.counts.set(key, current + 1);
16292
- log2(`Acquired ${key}: ${current + 1}/${limit}`);
16293
- return;
16163
+ /**
16164
+ * Update task status
16165
+ */
16166
+ updateTask(id, status) {
16167
+ const task = this.tasks.get(id);
16168
+ if (task) {
16169
+ task.status = status;
16294
16170
  }
16295
- log2(`Queueing ${key}: ${current}/${limit}`);
16296
- return new Promise((resolve2) => {
16297
- const queue = this.queues.get(key) ?? [];
16298
- queue.push(resolve2);
16299
- this.queues.set(key, queue);
16300
- });
16301
16171
  }
16302
- release(key) {
16303
- const limit = this.getConcurrencyLimit(key);
16304
- if (limit === Infinity) return;
16305
- const queue = this.queues.get(key);
16306
- if (queue && queue.length > 0) {
16307
- const next = queue.shift();
16308
- log2(`Released ${key}: next in queue`);
16309
- next();
16310
- } else {
16311
- const current = this.counts.get(key) ?? 0;
16312
- if (current > 0) {
16313
- this.counts.set(key, current - 1);
16314
- log2(`Released ${key}: ${current - 1}/${limit}`);
16315
- }
16316
- }
16172
+ /**
16173
+ * Remove a task
16174
+ */
16175
+ removeTask(id) {
16176
+ this.tasks.delete(id);
16317
16177
  }
16318
16178
  /**
16319
- * Report success/failure to adjust concurrency dynamically
16179
+ * Get all running tasks (newest first)
16320
16180
  */
16321
- reportResult(key, success2) {
16322
- if (success2) {
16323
- const streak = (this.successStreak.get(key) ?? 0) + 1;
16324
- this.successStreak.set(key, streak);
16325
- this.failureCount.set(key, 0);
16326
- if (streak % 5 === 0) {
16327
- const currentLimit = this.getConcurrencyLimit(key);
16328
- if (currentLimit < PARALLEL_TASK.MAX_CONCURRENCY) {
16329
- this.setLimit(key, currentLimit + 1);
16330
- log(`[concurrency] Auto-scaling UP for ${key}: ${currentLimit + 1}`);
16331
- }
16332
- }
16333
- } else {
16334
- const failures = (this.failureCount.get(key) ?? 0) + 1;
16335
- this.failureCount.set(key, failures);
16336
- this.successStreak.set(key, 0);
16337
- if (failures >= 2) {
16338
- const currentLimit = this.getConcurrencyLimit(key);
16339
- const minLimit = 1;
16340
- if (currentLimit > minLimit) {
16341
- this.setLimit(key, currentLimit - 1);
16342
- log(`[concurrency] Auto-scaling DOWN for ${key}: ${currentLimit - 1} (due to ${failures} failures)`);
16343
- }
16344
- }
16345
- }
16181
+ getRunningTasks() {
16182
+ return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.RUNNING).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
16346
16183
  }
16347
- getQueueLength(key) {
16348
- return this.queues.get(key)?.length ?? 0;
16184
+ /**
16185
+ * Get all queued tasks (oldest first - FIFO)
16186
+ */
16187
+ getQueuedTasks() {
16188
+ return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.QUEUED).sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
16349
16189
  }
16350
- getActiveCount(key) {
16351
- return this.counts.get(key) ?? 0;
16190
+ /**
16191
+ * Get tasks by parent session
16192
+ */
16193
+ getTasksByParent(parentSessionID) {
16194
+ return Array.from(this.tasks.values()).filter((t) => t.parentSessionID === parentSessionID);
16352
16195
  }
16353
16196
  /**
16354
- * Get formatted concurrency info string (e.g., "2/5 slots")
16197
+ * Format duration since task started
16355
16198
  */
16356
- getConcurrencyInfo(key) {
16357
- const active = this.getActiveCount(key);
16358
- const limit = this.getConcurrencyLimit(key);
16199
+ formatDuration(startedAt) {
16200
+ const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
16201
+ if (seconds < 60) return `${seconds}s`;
16202
+ const minutes = Math.floor(seconds / 60);
16203
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
16204
+ const hours = Math.floor(minutes / 60);
16205
+ return `${hours}h ${minutes % 60}m`;
16206
+ }
16207
+ /**
16208
+ * Get concurrency info string (e.g., " [2/5]")
16209
+ */
16210
+ getConcurrencyInfo() {
16211
+ if (!this.concurrency) return "";
16212
+ const running = this.getRunningTasks();
16213
+ const queued = this.getQueuedTasks();
16214
+ const total = running.length + queued.length;
16215
+ const limit = this.concurrency.getConcurrencyLimit("default");
16359
16216
  if (limit === Infinity) return "";
16360
- return ` (${active}/${limit} slots)`;
16217
+ return ` [${total}/${limit}]`;
16361
16218
  }
16362
- };
16363
-
16364
- // src/core/agents/task-store.ts
16365
- import * as fs2 from "node:fs/promises";
16366
- import * as path2 from "node:path";
16367
- var TaskStore = class {
16368
- tasks = /* @__PURE__ */ new Map();
16369
- pendingByParent = /* @__PURE__ */ new Map();
16370
- notifications = /* @__PURE__ */ new Map();
16371
- archivedCount = 0;
16372
- set(id, task) {
16373
- this.tasks.set(id, task);
16374
- if (this.tasks.size > MEMORY_LIMITS.MAX_TASKS_IN_MEMORY) {
16375
- this.gc();
16219
+ /**
16220
+ * Build consolidated task list message
16221
+ */
16222
+ buildTaskListMessage(newTask) {
16223
+ const running = this.getRunningTasks();
16224
+ const queued = this.getQueuedTasks();
16225
+ const concurrencyInfo = this.getConcurrencyInfo();
16226
+ const lines = [];
16227
+ if (running.length > 0) {
16228
+ lines.push(`Running (${running.length}):${concurrencyInfo}`);
16229
+ for (const task of running) {
16230
+ const duration3 = this.formatDuration(task.startedAt);
16231
+ const bgTag = task.isBackground ? "[B]" : "[F]";
16232
+ const isNew = newTask && task.id === newTask.id ? " <- NEW" : "";
16233
+ lines.push(`${bgTag} ${task.description} (${task.agent}) - ${duration3}${isNew}`);
16234
+ }
16376
16235
  }
16236
+ if (queued.length > 0) {
16237
+ if (lines.length > 0) lines.push("");
16238
+ lines.push(`Queued (${queued.length}):`);
16239
+ for (const task of queued) {
16240
+ const bgTag = task.isBackground ? "[W]" : "[P]";
16241
+ lines.push(`${bgTag} ${task.description} (${task.agent})`);
16242
+ }
16243
+ }
16244
+ return lines.join("\n");
16377
16245
  }
16378
- get(id) {
16379
- return this.tasks.get(id);
16380
- }
16381
- getAll() {
16382
- return Array.from(this.tasks.values());
16246
+ /**
16247
+ * Show consolidated toast with all running/queued tasks
16248
+ */
16249
+ showTaskListToast(newTask) {
16250
+ if (!this.client) return;
16251
+ const tuiClient2 = this.client;
16252
+ if (!tuiClient2.tui?.showToast) return;
16253
+ const message = this.buildTaskListMessage(newTask);
16254
+ const running = this.getRunningTasks();
16255
+ const queued = this.getQueuedTasks();
16256
+ const title = newTask.isBackground ? `Background Task Started` : `Task Started`;
16257
+ tuiClient2.tui.showToast({
16258
+ body: {
16259
+ title,
16260
+ message: message || `${newTask.description} (${newTask.agent})`,
16261
+ variant: STATUS_LABEL.INFO,
16262
+ duration: running.length + queued.length > 2 ? 5e3 : 3e3
16263
+ }
16264
+ }).catch(() => {
16265
+ });
16383
16266
  }
16384
- getRunning() {
16385
- return this.getAll().filter((t) => t.status === TASK_STATUS.RUNNING || t.status === TASK_STATUS.PENDING);
16386
- }
16387
- getByParent(parentSessionID) {
16388
- return this.getAll().filter((t) => t.parentSessionID === parentSessionID);
16389
- }
16390
- delete(id) {
16391
- return this.tasks.delete(id);
16392
- }
16393
- clear() {
16394
- this.tasks.clear();
16395
- this.pendingByParent.clear();
16396
- this.notifications.clear();
16397
- }
16398
- // Pending tracking
16399
- trackPending(parentSessionID, taskId) {
16400
- const pending2 = this.pendingByParent.get(parentSessionID) ?? /* @__PURE__ */ new Set();
16401
- pending2.add(taskId);
16402
- this.pendingByParent.set(parentSessionID, pending2);
16403
- }
16404
- untrackPending(parentSessionID, taskId) {
16405
- const pending2 = this.pendingByParent.get(parentSessionID);
16406
- if (pending2) {
16407
- pending2.delete(taskId);
16408
- if (pending2.size === 0) {
16409
- this.pendingByParent.delete(parentSessionID);
16410
- }
16267
+ /**
16268
+ * Show task completion toast
16269
+ */
16270
+ showCompletionToast(info) {
16271
+ if (!this.client) return;
16272
+ const tuiClient2 = this.client;
16273
+ if (!tuiClient2.tui?.showToast) return;
16274
+ this.removeTask(info.id);
16275
+ const remaining = this.getRunningTasks();
16276
+ const queued = this.getQueuedTasks();
16277
+ let message;
16278
+ let title;
16279
+ let variant;
16280
+ if (info.status === STATUS_LABEL.ERROR || info.status === STATUS_LABEL.CANCELLED || info.status === STATUS_LABEL.FAILED) {
16281
+ title = info.status === STATUS_LABEL.ERROR ? "Task Failed" : "Task Cancelled";
16282
+ message = `[FAIL] "${info.description}" ${info.status}
16283
+ ${info.error || ""}`;
16284
+ variant = STATUS_LABEL.ERROR;
16285
+ } else {
16286
+ title = "Task Completed";
16287
+ message = `[DONE] "${info.description}" finished in ${info.duration}`;
16288
+ variant = STATUS_LABEL.SUCCESS;
16411
16289
  }
16412
- }
16413
- getPendingCount(parentSessionID) {
16414
- return this.pendingByParent.get(parentSessionID)?.size ?? 0;
16415
- }
16416
- hasPending(parentSessionID) {
16417
- return this.getPendingCount(parentSessionID) > 0;
16418
- }
16419
- // Notifications with limit
16420
- queueNotification(task) {
16421
- const queue = this.notifications.get(task.parentSessionID) ?? [];
16422
- queue.push(task);
16423
- if (queue.length > MEMORY_LIMITS.MAX_NOTIFICATIONS_PER_PARENT) {
16424
- queue.shift();
16290
+ if (remaining.length > 0 || queued.length > 0) {
16291
+ message += `
16292
+
16293
+ Still running: ${remaining.length} | Queued: ${queued.length}`;
16425
16294
  }
16426
- this.notifications.set(task.parentSessionID, queue);
16427
- }
16428
- getNotifications(parentSessionID) {
16429
- return this.notifications.get(parentSessionID) ?? [];
16430
- }
16431
- clearNotifications(parentSessionID) {
16432
- this.notifications.delete(parentSessionID);
16433
- }
16434
- cleanEmptyNotifications() {
16435
- for (const [sessionID, queue] of this.notifications.entries()) {
16436
- if (queue.length === 0) {
16437
- this.notifications.delete(sessionID);
16295
+ tuiClient2.tui.showToast({
16296
+ body: {
16297
+ title,
16298
+ message,
16299
+ variant,
16300
+ duration: 5e3
16438
16301
  }
16439
- }
16302
+ }).catch(() => {
16303
+ });
16440
16304
  }
16441
- clearNotificationsForTask(taskId) {
16442
- for (const [sessionID, tasks] of this.notifications.entries()) {
16443
- const filtered = tasks.filter((t) => t.id !== taskId);
16444
- if (filtered.length === 0) {
16445
- this.notifications.delete(sessionID);
16446
- } else if (filtered.length !== tasks.length) {
16447
- this.notifications.set(sessionID, filtered);
16305
+ /**
16306
+ * Show all-tasks-complete summary toast
16307
+ */
16308
+ showAllCompleteToast(parentSessionID, completedTasks) {
16309
+ if (!this.client) return;
16310
+ const tuiClient2 = this.client;
16311
+ if (!tuiClient2.tui?.showToast) return;
16312
+ const successCount = completedTasks.filter((t) => t.status === STATUS_LABEL.COMPLETED).length;
16313
+ const failCount = completedTasks.filter((t) => t.status === STATUS_LABEL.ERROR || t.status === STATUS_LABEL.CANCELLED || t.status === STATUS_LABEL.FAILED).length;
16314
+ const taskList = completedTasks.map((t) => `- [${t.status === STATUS_LABEL.COMPLETED ? "OK" : "FAIL"}] ${t.description} (${t.duration})`).join("\n");
16315
+ tuiClient2.tui.showToast({
16316
+ body: {
16317
+ title: "All Tasks Completed",
16318
+ message: `${successCount} succeeded, ${failCount} failed
16319
+
16320
+ ${taskList}`,
16321
+ variant: failCount > 0 ? STATUS_LABEL.WARNING : STATUS_LABEL.SUCCESS,
16322
+ duration: 7e3
16448
16323
  }
16449
- }
16324
+ }).catch(() => {
16325
+ });
16450
16326
  }
16451
- // =========================================================================
16452
- // Garbage Collection & Memory Management
16453
- // =========================================================================
16454
16327
  /**
16455
- * Get memory statistics
16328
+ * Show progress toast (for long-running tasks)
16456
16329
  */
16457
- getStats() {
16458
- return {
16459
- tasksInMemory: this.tasks.size,
16460
- runningTasks: this.getRunning().length,
16461
- archivedTasks: this.archivedCount,
16462
- notificationQueues: this.notifications.size,
16463
- pendingParents: this.pendingByParent.size
16464
- };
16330
+ showProgressToast(taskId, progress) {
16331
+ if (!this.client) return;
16332
+ const tuiClient2 = this.client;
16333
+ if (!tuiClient2.tui?.showToast) return;
16334
+ const task = this.tasks.get(taskId);
16335
+ if (!task) return;
16336
+ const percentage = Math.round(progress.current / progress.total * 100);
16337
+ const progressBar = `[${"#".repeat(Math.floor(percentage / 10))}${"-".repeat(10 - Math.floor(percentage / 10))}]`;
16338
+ tuiClient2.tui.showToast({
16339
+ body: {
16340
+ title: `Task Progress: ${task.description}`,
16341
+ message: `${progressBar} ${percentage}%
16342
+ ${progress.message || ""}`,
16343
+ variant: STATUS_LABEL.INFO,
16344
+ duration: 2e3
16345
+ }
16346
+ }).catch(() => {
16347
+ });
16465
16348
  }
16466
16349
  /**
16467
- * Garbage collect completed tasks
16468
- * Archives old completed tasks to disk
16350
+ * Clear all tracked tasks
16469
16351
  */
16470
- async gc() {
16471
- const now = Date.now();
16472
- const toRemove = [];
16473
- const toArchive = [];
16474
- for (const [id, task] of this.tasks) {
16475
- if (task.status === TASK_STATUS.RUNNING) continue;
16476
- const completedAt = task.completedAt?.getTime() ?? 0;
16477
- const age = now - completedAt;
16478
- if (age > MEMORY_LIMITS.ARCHIVE_AGE_MS && task.status === TASK_STATUS.COMPLETED) {
16479
- toArchive.push(task);
16480
- toRemove.push(id);
16481
- } else if (age > MEMORY_LIMITS.ERROR_CLEANUP_AGE_MS && (task.status === TASK_STATUS.ERROR || task.status === TASK_STATUS.CANCELLED)) {
16482
- toRemove.push(id);
16483
- }
16484
- }
16485
- if (toArchive.length > 0) {
16486
- await this.archiveTasks(toArchive);
16487
- }
16488
- for (const id of toRemove) {
16489
- this.tasks.delete(id);
16490
- }
16491
- return toRemove.length;
16352
+ clear() {
16353
+ this.tasks.clear();
16492
16354
  }
16493
16355
  /**
16494
- * Archive tasks to disk for later analysis
16356
+ * Get task count stats
16495
16357
  */
16496
- async archiveTasks(tasks) {
16358
+ getStats() {
16359
+ const running = this.getRunningTasks().length;
16360
+ const queued = this.getQueuedTasks().length;
16361
+ return { running, queued, total: this.tasks.size };
16362
+ }
16363
+ };
16364
+ var instance = null;
16365
+ function getTaskToastManager() {
16366
+ return instance;
16367
+ }
16368
+ function initTaskToastManager(client, concurrency) {
16369
+ if (!instance) {
16370
+ instance = new TaskToastManager();
16371
+ }
16372
+ instance.init(client, concurrency);
16373
+ return instance;
16374
+ }
16375
+
16376
+ // src/core/agents/persistence/task-wal.ts
16377
+ import * as fs3 from "node:fs/promises";
16378
+ import * as path3 from "node:path";
16379
+ var TaskWAL = class {
16380
+ walPath;
16381
+ initialized = false;
16382
+ constructor(customPath) {
16383
+ this.walPath = customPath || path3.resolve(process.cwd(), ".opencode/archive/tasks/active_tasks.jsonl");
16384
+ }
16385
+ async init() {
16386
+ if (this.initialized) return;
16497
16387
  try {
16498
- await fs2.mkdir(PATHS.TASK_ARCHIVE, { recursive: true });
16499
- const date5 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16500
- const filename = `tasks_${date5}.jsonl`;
16501
- const filepath = path2.join(PATHS.TASK_ARCHIVE, filename);
16502
- const lines = tasks.map((task) => JSON.stringify({
16388
+ const dir = path3.dirname(this.walPath);
16389
+ await fs3.mkdir(dir, { recursive: true });
16390
+ this.initialized = true;
16391
+ } catch (error45) {
16392
+ log("Failed to initialize Task WAL directory:", error45);
16393
+ }
16394
+ }
16395
+ async log(action, task) {
16396
+ if (!this.initialized) await this.init();
16397
+ const entry = {
16398
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16399
+ action,
16400
+ taskId: task.id,
16401
+ data: action === WAL_ACTIONS.DELETE ? { id: task.id } : {
16503
16402
  id: task.id,
16403
+ sessionID: task.sessionID,
16404
+ parentSessionID: task.parentSessionID,
16405
+ description: task.description,
16504
16406
  agent: task.agent,
16505
- prompt: task.prompt.slice(0, 200),
16506
- // Truncate
16507
16407
  status: task.status,
16508
16408
  startedAt: task.startedAt,
16509
- completedAt: task.completedAt,
16510
- parentSessionID: task.parentSessionID
16511
- }));
16512
- await fs2.appendFile(filepath, lines.join("\n") + "\n");
16513
- this.archivedCount += tasks.length;
16514
- } catch (error45) {
16515
- }
16516
- }
16517
- /**
16518
- * Force cleanup of all completed tasks
16519
- */
16520
- forceCleanup() {
16521
- const toRemove = [];
16522
- for (const [id, task] of this.tasks) {
16523
- if (task.status !== TASK_STATUS.RUNNING) {
16524
- toRemove.push(id);
16409
+ depth: task.depth,
16410
+ prompt: action === WAL_ACTIONS.LAUNCH ? task.prompt : void 0
16411
+ // Only log prompt on launch to save space
16525
16412
  }
16413
+ };
16414
+ try {
16415
+ await fs3.appendFile(this.walPath, JSON.stringify(entry) + "\n");
16416
+ } catch (error45) {
16526
16417
  }
16527
- for (const id of toRemove) {
16528
- this.tasks.delete(id);
16529
- }
16530
- return toRemove.length;
16531
16418
  }
16532
- };
16533
-
16534
- // src/core/agents/format.ts
16535
- function formatDuration(start, end) {
16536
- const duration3 = (end ?? /* @__PURE__ */ new Date()).getTime() - start.getTime();
16537
- const seconds = Math.floor(duration3 / 1e3);
16538
- const minutes = Math.floor(seconds / 60);
16539
- if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
16540
- return `${seconds}s`;
16541
- }
16542
- function buildNotificationMessage(tasks) {
16543
- const summary = tasks.map((t) => {
16544
- const status = t.status === TASK_STATUS.COMPLETED ? "\u2705" : "\u274C";
16545
- return `${status} \`${t.id}\`: ${t.description}`;
16546
- }).join("\n");
16547
- return `<system-notification>
16548
- **All Parallel Tasks Complete**
16549
-
16550
- ${summary}
16551
-
16552
- Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
16553
- </system-notification>`;
16554
- }
16555
-
16556
- // src/core/notification/presets/index.ts
16557
- var presets_exports = {};
16558
- __export(presets_exports, {
16559
- allTasksComplete: () => allTasksComplete,
16560
- concurrencyAcquired: () => concurrencyAcquired,
16561
- concurrencyReleased: () => concurrencyReleased,
16562
- documentCached: () => documentCached,
16563
- errorRecovery: () => errorRecovery,
16564
- missionComplete: () => missionComplete,
16565
- missionStarted: () => missionStarted,
16566
- parallelTasksLaunched: () => parallelTasksLaunched,
16567
- researchStarted: () => researchStarted,
16568
- sessionCompleted: () => sessionCompleted,
16569
- sessionCreated: () => sessionCreated,
16570
- sessionResumed: () => sessionResumed,
16571
- taskCompleted: () => taskCompleted,
16572
- taskFailed: () => taskFailed,
16573
- taskStarted: () => taskStarted,
16574
- toolExecuted: () => toolExecuted,
16575
- warningMaxDepth: () => warningMaxDepth,
16576
- warningMaxRetries: () => warningMaxRetries,
16577
- warningRateLimited: () => warningRateLimited
16578
- });
16579
-
16580
- // src/core/notification/toast-core.ts
16581
- var tuiClient = null;
16582
- function initToastClient(client) {
16583
- tuiClient = client;
16584
- }
16585
- var toasts = [];
16586
- var handlers = [];
16587
- function show(options) {
16588
- const toast = {
16589
- id: `toast_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
16590
- title: options.title,
16591
- message: options.message,
16592
- variant: options.variant || "info",
16593
- timestamp: /* @__PURE__ */ new Date(),
16594
- duration: options.duration ?? 5e3,
16595
- dismissed: false
16596
- };
16597
- toasts.push(toast);
16598
- if (toasts.length > HISTORY.MAX_TOAST) {
16599
- toasts.shift();
16419
+ async readAll() {
16420
+ if (!this.initialized) await this.init();
16421
+ const tasks = /* @__PURE__ */ new Map();
16422
+ try {
16423
+ const content = await fs3.readFile(this.walPath, "utf-8");
16424
+ const lines = content.split("\n").filter(Boolean);
16425
+ for (const line of lines) {
16426
+ try {
16427
+ const entry = JSON.parse(line);
16428
+ if (entry.action === WAL_ACTIONS.DELETE) {
16429
+ tasks.delete(entry.taskId);
16430
+ } else if (entry.action === WAL_ACTIONS.LAUNCH) {
16431
+ tasks.set(entry.taskId, entry.data);
16432
+ } else {
16433
+ const existing = tasks.get(entry.taskId);
16434
+ if (existing) {
16435
+ Object.assign(existing, entry.data);
16436
+ }
16437
+ }
16438
+ } catch {
16439
+ }
16440
+ }
16441
+ } catch (error45) {
16442
+ if (error45.code !== "ENOENT") {
16443
+ log("Error reading Task WAL:", error45);
16444
+ }
16445
+ }
16446
+ return tasks;
16600
16447
  }
16601
- for (const handler of handlers) {
16448
+ /**
16449
+ * Compact the WAL by writing only the current active tasks
16450
+ */
16451
+ async compact(activeTasks) {
16602
16452
  try {
16603
- handler(toast);
16453
+ const tempPath = `${this.walPath}.tmp`;
16454
+ const content = activeTasks.map((task) => JSON.stringify({
16455
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16456
+ action: WAL_ACTIONS.LAUNCH,
16457
+ taskId: task.id,
16458
+ data: task
16459
+ })).join("\n") + "\n";
16460
+ await fs3.writeFile(tempPath, content);
16461
+ await fs3.rename(tempPath, this.walPath);
16604
16462
  } catch (error45) {
16463
+ log("Failed to compact Task WAL:", error45);
16605
16464
  }
16606
16465
  }
16607
- if (tuiClient) {
16608
- const client = tuiClient;
16609
- if (client.tui?.showToast) {
16610
- client.tui.showToast({
16466
+ };
16467
+ var taskWAL = new TaskWAL();
16468
+
16469
+ // src/core/agents/manager/task-launcher.ts
16470
+ var TaskLauncher = class {
16471
+ constructor(client, directory, store, concurrency, onTaskError, startPolling) {
16472
+ this.client = client;
16473
+ this.directory = directory;
16474
+ this.store = store;
16475
+ this.concurrency = concurrency;
16476
+ this.onTaskError = onTaskError;
16477
+ this.startPolling = startPolling;
16478
+ }
16479
+ /**
16480
+ * Unified launch method - handles both single and multiple tasks efficiently.
16481
+ * All session creations happen in parallel immediately.
16482
+ * Concurrency acquisition and prompt firing happen in the background.
16483
+ */
16484
+ async launch(inputs) {
16485
+ const isArray = Array.isArray(inputs);
16486
+ const taskInputs = isArray ? inputs : [inputs];
16487
+ if (taskInputs.length === 0) return isArray ? [] : null;
16488
+ log(`[task-launcher.ts] Batch launching ${taskInputs.length} task(s)`);
16489
+ const startTime = Date.now();
16490
+ const tasks = await Promise.all(taskInputs.map(
16491
+ (input) => this.prepareTask(input).catch((error45) => {
16492
+ log(`[task-launcher.ts] Failed to prepare task ${input.description}:`, error45);
16493
+ return null;
16494
+ })
16495
+ ));
16496
+ const successfulTasks = tasks.filter((t) => t !== null);
16497
+ successfulTasks.forEach((task) => {
16498
+ this.executeBackground(task).catch((error45) => {
16499
+ log(`[task-launcher.ts] Background execution failed for ${task.id}:`, error45);
16500
+ this.onTaskError(task.id, error45);
16501
+ });
16502
+ });
16503
+ const elapsed = Date.now() - startTime;
16504
+ log(`[task-launcher.ts] Batch launch prepared: ${successfulTasks.length} tasks in ${elapsed}ms`);
16505
+ if (successfulTasks.length > 0) {
16506
+ this.startPolling();
16507
+ }
16508
+ return isArray ? successfulTasks : successfulTasks[0] || null;
16509
+ }
16510
+ /**
16511
+ * Prepare task: Create session and registration without blocking on concurrency
16512
+ */
16513
+ async prepareTask(input) {
16514
+ const currentDepth = input.depth ?? 0;
16515
+ if (currentDepth >= PARALLEL_TASK.MAX_DEPTH) {
16516
+ log(`[task-launcher.ts] Task depth limit reached (${currentDepth}/${PARALLEL_TASK.MAX_DEPTH}). Generation blocked.`);
16517
+ throw new Error(`Maximum task depth (${PARALLEL_TASK.MAX_DEPTH}) reached. To prevent infinite recursion, no further sub-tasks can be spawned.`);
16518
+ }
16519
+ const createResult = await this.client.session.create({
16520
+ body: {
16521
+ parentID: input.parentSessionID,
16522
+ title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${input.description}`
16523
+ },
16524
+ query: { directory: this.directory }
16525
+ });
16526
+ if (createResult.error || !createResult.data?.id) {
16527
+ throw new Error(`Session creation failed: ${createResult.error || "No ID"}`);
16528
+ }
16529
+ const sessionID = createResult.data.id;
16530
+ const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
16531
+ const task = {
16532
+ id: taskId,
16533
+ sessionID,
16534
+ parentSessionID: input.parentSessionID,
16535
+ description: input.description,
16536
+ prompt: input.prompt,
16537
+ agent: input.agent,
16538
+ status: TASK_STATUS.PENDING,
16539
+ // Start as PENDING
16540
+ startedAt: /* @__PURE__ */ new Date(),
16541
+ concurrencyKey: input.agent,
16542
+ depth: (input.depth ?? 0) + 1,
16543
+ mode: input.mode || "normal",
16544
+ groupID: input.groupID
16545
+ };
16546
+ this.store.set(taskId, task);
16547
+ this.store.trackPending(input.parentSessionID, taskId);
16548
+ taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
16549
+ });
16550
+ const toastManager = getTaskToastManager();
16551
+ if (toastManager) {
16552
+ toastManager.addTask({
16553
+ id: taskId,
16554
+ description: input.description,
16555
+ agent: input.agent,
16556
+ isBackground: true,
16557
+ parentSessionID: input.parentSessionID,
16558
+ sessionID
16559
+ });
16560
+ }
16561
+ presets.sessionCreated(sessionID, input.agent);
16562
+ return task;
16563
+ }
16564
+ /**
16565
+ * Background execution: Acquire slot and fire prompt
16566
+ */
16567
+ async executeBackground(task) {
16568
+ try {
16569
+ await this.concurrency.acquire(task.agent);
16570
+ task.status = TASK_STATUS.RUNNING;
16571
+ task.startedAt = /* @__PURE__ */ new Date();
16572
+ this.store.set(task.id, task);
16573
+ taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
16574
+ });
16575
+ await this.client.session.prompt({
16576
+ path: { id: task.sessionID },
16611
16577
  body: {
16612
- title: toast.title,
16613
- message: toast.message,
16614
- variant: toast.variant,
16615
- duration: toast.duration
16578
+ agent: task.agent,
16579
+ tools: {
16580
+ // HPFA: Allow agents to delegate sub-tasks (Fractal Spawning)
16581
+ delegate_task: true,
16582
+ get_task_result: true,
16583
+ list_tasks: true,
16584
+ cancel_task: true
16585
+ },
16586
+ parts: [{ type: PART_TYPES.TEXT, text: task.prompt }]
16616
16587
  }
16617
- }).catch(() => {
16618
16588
  });
16589
+ log(`[task-launcher.ts] Task ${task.id} (${task.agent}) started running`);
16590
+ } catch (error45) {
16591
+ this.concurrency.release(task.agent);
16592
+ throw error45;
16619
16593
  }
16620
16594
  }
16621
- return toast;
16622
- }
16595
+ };
16623
16596
 
16624
- // src/core/notification/presets/task-lifecycle.ts
16625
- var taskStarted = (taskId, agent) => show({
16626
- title: "Task Started",
16627
- message: `${agent}: ${taskId}`,
16628
- variant: "info",
16629
- duration: 3e3
16630
- });
16631
- var taskCompleted = (taskId, agent) => show({
16632
- title: "Task Completed",
16633
- message: `${agent}: ${taskId}`,
16634
- variant: "success",
16635
- duration: 3e3
16636
- });
16637
- var taskFailed = (taskId, error45) => show({
16638
- title: "Task Failed",
16639
- message: `${taskId}: ${error45}`,
16640
- variant: "error",
16641
- duration: 0
16642
- });
16643
- var allTasksComplete = (count) => show({
16644
- title: "All Tasks Complete",
16645
- message: `${count} tasks finished successfully`,
16646
- variant: "success",
16647
- duration: 5e3
16648
- });
16649
-
16650
- // src/core/notification/presets/session.ts
16651
- var sessionCreated = (sessionId, agent) => show({
16652
- title: "Session Created",
16653
- message: `${agent} - ${sessionId.slice(0, 12)}...`,
16654
- variant: STATUS_LABEL.INFO,
16655
- duration: TOAST_DURATION.SHORT
16656
- });
16657
- var sessionResumed = (sessionId, agent) => show({
16658
- title: "Session Resumed",
16659
- message: `${agent} - ${sessionId.slice(0, 12)}...`,
16660
- variant: STATUS_LABEL.INFO,
16661
- duration: TOAST_DURATION.SHORT
16662
- });
16663
- var sessionCompleted = (sessionId, duration3) => show({
16664
- title: "Session Completed",
16665
- message: `${sessionId.slice(0, 12)}... (${duration3})`,
16666
- variant: STATUS_LABEL.SUCCESS,
16667
- duration: TOAST_DURATION.MEDIUM
16668
- });
16669
-
16670
- // src/core/notification/presets/parallel.ts
16671
- var parallelTasksLaunched = (count, agents) => show({
16672
- title: "Parallel Tasks Launched",
16673
- message: `${count} tasks: ${agents.join(", ")}`,
16674
- variant: "info",
16675
- duration: TOAST_DURATION.DEFAULT
16676
- });
16677
- var concurrencyAcquired = (agent, slot) => show({
16678
- title: "Concurrency Slot",
16679
- message: `${agent} acquired ${slot}`,
16680
- variant: "info",
16681
- duration: TOAST_DURATION.SHORT
16682
- });
16683
- var concurrencyReleased = (agent) => show({
16684
- title: "Slot Released",
16685
- message: agent,
16686
- variant: "info",
16687
- duration: TOAST_DURATION.EXTRA_SHORT
16688
- });
16689
-
16690
- // src/core/notification/presets/mission.ts
16691
- var missionComplete = (summary) => show({
16692
- title: "Mission Complete",
16693
- message: summary,
16694
- variant: "success",
16695
- duration: 0
16696
- });
16697
- var missionStarted = (description) => show({
16698
- title: "Mission Started",
16699
- message: description.slice(0, 100),
16700
- variant: "info",
16701
- duration: 4e3
16702
- });
16703
-
16704
- // src/core/notification/presets/tools.ts
16705
- var toolExecuted = (toolName, target) => show({
16706
- title: toolName,
16707
- message: target.slice(0, 80),
16708
- variant: "info",
16709
- duration: TOAST_DURATION.SHORT
16710
- });
16711
- var documentCached = (filename) => show({
16712
- title: "Document Cached",
16713
- message: `${PATHS.DOCS}/${filename}`,
16714
- variant: "info",
16715
- duration: TOAST_DURATION.SHORT
16716
- });
16717
- var researchStarted = (topic) => show({
16718
- title: "Research Started",
16719
- message: topic,
16720
- variant: "info",
16721
- duration: TOAST_DURATION.MEDIUM
16722
- });
16723
-
16724
- // src/core/notification/presets/warnings.ts
16725
- var warningRateLimited = () => show({
16726
- title: "Rate Limited",
16727
- message: "Waiting before retry...",
16728
- variant: "warning",
16729
- duration: TOAST_DURATION.LONG
16730
- });
16731
- var errorRecovery = (action) => show({
16732
- title: "Error Recovery",
16733
- message: `Attempting: ${action}`,
16734
- variant: "warning",
16735
- duration: TOAST_DURATION.MEDIUM
16736
- });
16737
- var warningMaxDepth = (depth) => show({
16738
- title: "Max Depth Reached",
16739
- message: `Recursion blocked at depth ${depth}`,
16740
- variant: "warning",
16741
- duration: TOAST_DURATION.LONG
16742
- });
16743
- var warningMaxRetries = () => show({
16744
- title: "Max Retries Exceeded",
16745
- message: "Automatic recovery has stopped. Manual intervention may be needed.",
16746
- variant: "error",
16747
- duration: TOAST_DURATION.PERSISTENT
16748
- });
16597
+ // src/core/agents/manager/task-resumer.ts
16598
+ var TaskResumer = class {
16599
+ constructor(client, store, findBySession, startPolling, notifyParentIfAllComplete) {
16600
+ this.client = client;
16601
+ this.store = store;
16602
+ this.findBySession = findBySession;
16603
+ this.startPolling = startPolling;
16604
+ this.notifyParentIfAllComplete = notifyParentIfAllComplete;
16605
+ }
16606
+ async resume(input) {
16607
+ const existingTask = this.findBySession(input.sessionId);
16608
+ if (!existingTask) {
16609
+ throw new Error(`Task not found for session: ${input.sessionId}`);
16610
+ }
16611
+ existingTask.status = TASK_STATUS.RUNNING;
16612
+ existingTask.completedAt = void 0;
16613
+ existingTask.error = void 0;
16614
+ existingTask.result = void 0;
16615
+ existingTask.parentSessionID = input.parentSessionID;
16616
+ existingTask.startedAt = /* @__PURE__ */ new Date();
16617
+ existingTask.stablePolls = 0;
16618
+ this.store.trackPending(input.parentSessionID, existingTask.id);
16619
+ this.startPolling();
16620
+ taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
16621
+ });
16622
+ log(`Resuming task ${existingTask.id} in session ${existingTask.sessionID}`);
16623
+ this.client.session.prompt({
16624
+ path: { id: existingTask.sessionID },
16625
+ body: {
16626
+ agent: existingTask.agent,
16627
+ parts: [{ type: PART_TYPES.TEXT, text: input.prompt }]
16628
+ }
16629
+ }).catch((error45) => {
16630
+ log(`Resume prompt error for ${existingTask.id}:`, error45);
16631
+ existingTask.status = TASK_STATUS.ERROR;
16632
+ existingTask.error = error45 instanceof Error ? error45.message : String(error45);
16633
+ existingTask.completedAt = /* @__PURE__ */ new Date();
16634
+ this.store.untrackPending(input.parentSessionID, existingTask.id);
16635
+ this.store.queueNotification(existingTask);
16636
+ this.notifyParentIfAllComplete(input.parentSessionID).catch(() => {
16637
+ });
16638
+ taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
16639
+ });
16640
+ });
16641
+ return existingTask;
16642
+ }
16643
+ };
16749
16644
 
16750
- // src/core/notification/presets.ts
16751
- var presets = presets_exports;
16645
+ // src/core/agents/config.ts
16646
+ var CONFIG = {
16647
+ TASK_TTL_MS: PARALLEL_TASK.TTL_MS,
16648
+ CLEANUP_DELAY_MS: PARALLEL_TASK.CLEANUP_DELAY_MS,
16649
+ MIN_STABILITY_MS: PARALLEL_TASK.MIN_STABILITY_MS,
16650
+ POLL_INTERVAL_MS: PARALLEL_TASK.POLL_INTERVAL_MS
16651
+ };
16752
16652
 
16753
- // src/core/notification/task-toast-manager.ts
16754
- var TaskToastManager = class {
16755
- tasks = /* @__PURE__ */ new Map();
16756
- client = null;
16757
- concurrency = null;
16758
- /**
16759
- * Initialize the manager with OpenCode client
16760
- */
16761
- init(client, concurrency) {
16653
+ // src/core/agents/manager/task-poller.ts
16654
+ var TaskPoller = class {
16655
+ constructor(client, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete) {
16762
16656
  this.client = client;
16763
- this.concurrency = concurrency ?? null;
16764
- }
16765
- /**
16766
- * Set concurrency controller (can be set after init)
16767
- */
16768
- setConcurrencyController(concurrency) {
16657
+ this.store = store;
16769
16658
  this.concurrency = concurrency;
16659
+ this.notifyParentIfAllComplete = notifyParentIfAllComplete;
16660
+ this.scheduleCleanup = scheduleCleanup;
16661
+ this.pruneExpiredTasks = pruneExpiredTasks;
16662
+ this.onTaskComplete = onTaskComplete;
16770
16663
  }
16771
- /**
16772
- * Add a new task and show consolidated toast
16773
- */
16774
- addTask(task) {
16775
- const trackedTask = {
16776
- id: task.id,
16777
- description: task.description,
16778
- agent: task.agent,
16779
- status: task.status ?? STATUS_LABEL.RUNNING,
16780
- startedAt: /* @__PURE__ */ new Date(),
16781
- isBackground: task.isBackground,
16782
- parentSessionID: task.parentSessionID,
16783
- sessionID: task.sessionID
16784
- };
16785
- this.tasks.set(task.id, trackedTask);
16786
- this.showTaskListToast(trackedTask);
16664
+ pollingInterval;
16665
+ start() {
16666
+ if (this.pollingInterval) return;
16667
+ log("[task-poller.ts] start() - polling started");
16668
+ this.pollingInterval = setInterval(() => this.poll(), CONFIG.POLL_INTERVAL_MS);
16669
+ this.pollingInterval.unref();
16787
16670
  }
16788
- /**
16789
- * Update task status
16790
- */
16791
- updateTask(id, status) {
16792
- const task = this.tasks.get(id);
16793
- if (task) {
16794
- task.status = status;
16671
+ stop() {
16672
+ if (this.pollingInterval) {
16673
+ clearInterval(this.pollingInterval);
16674
+ this.pollingInterval = void 0;
16795
16675
  }
16796
16676
  }
16797
- /**
16798
- * Remove a task
16799
- */
16800
- removeTask(id) {
16801
- this.tasks.delete(id);
16802
- }
16803
- /**
16804
- * Get all running tasks (newest first)
16805
- */
16806
- getRunningTasks() {
16807
- return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.RUNNING).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
16808
- }
16809
- /**
16810
- * Get all queued tasks (oldest first - FIFO)
16811
- */
16812
- getQueuedTasks() {
16813
- return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.QUEUED).sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
16814
- }
16815
- /**
16816
- * Get tasks by parent session
16817
- */
16818
- getTasksByParent(parentSessionID) {
16819
- return Array.from(this.tasks.values()).filter((t) => t.parentSessionID === parentSessionID);
16677
+ isRunning() {
16678
+ return !!this.pollingInterval;
16820
16679
  }
16821
- /**
16822
- * Format duration since task started
16823
- */
16824
- formatDuration(startedAt) {
16825
- const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
16826
- if (seconds < 60) return `${seconds}s`;
16827
- const minutes = Math.floor(seconds / 60);
16828
- if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
16829
- const hours = Math.floor(minutes / 60);
16830
- return `${hours}h ${minutes % 60}m`;
16831
- }
16832
- /**
16833
- * Get concurrency info string (e.g., " [2/5]")
16834
- */
16835
- getConcurrencyInfo() {
16836
- if (!this.concurrency) return "";
16837
- const running = this.getRunningTasks();
16838
- const queued = this.getQueuedTasks();
16839
- const total = running.length + queued.length;
16840
- const limit = this.concurrency.getConcurrencyLimit("default");
16841
- if (limit === Infinity) return "";
16842
- return ` [${total}/${limit}]`;
16843
- }
16844
- /**
16845
- * Build consolidated task list message
16846
- */
16847
- buildTaskListMessage(newTask) {
16848
- const running = this.getRunningTasks();
16849
- const queued = this.getQueuedTasks();
16850
- const concurrencyInfo = this.getConcurrencyInfo();
16851
- const lines = [];
16852
- if (running.length > 0) {
16853
- lines.push(`Running (${running.length}):${concurrencyInfo}`);
16680
+ async poll() {
16681
+ this.pruneExpiredTasks();
16682
+ const running = this.store.getRunning();
16683
+ if (running.length === 0) {
16684
+ this.stop();
16685
+ return;
16686
+ }
16687
+ log("[task-poller.ts] poll() checking", running.length, "running tasks");
16688
+ try {
16689
+ const statusResult = await this.client.session.status();
16690
+ const allStatuses = statusResult.data ?? {};
16854
16691
  for (const task of running) {
16855
- const duration3 = this.formatDuration(task.startedAt);
16856
- const bgTag = task.isBackground ? "[B]" : "[F]";
16857
- const isNew = newTask && task.id === newTask.id ? " <- NEW" : "";
16858
- lines.push(`${bgTag} ${task.description} (${task.agent}) - ${duration3}${isNew}`);
16692
+ try {
16693
+ if (task.status === TASK_STATUS.PENDING) continue;
16694
+ const sessionStatus = allStatuses[task.sessionID];
16695
+ if (sessionStatus?.type === SESSION_STATUS.IDLE) {
16696
+ const elapsed2 = Date.now() - task.startedAt.getTime();
16697
+ if (elapsed2 < CONFIG.MIN_STABILITY_MS) continue;
16698
+ if (!task.hasStartedOutputting && !await this.validateSessionHasOutput(task.sessionID, task)) continue;
16699
+ await this.completeTask(task);
16700
+ continue;
16701
+ }
16702
+ await this.updateTaskProgress(task);
16703
+ const elapsed = Date.now() - task.startedAt.getTime();
16704
+ if (elapsed >= CONFIG.MIN_STABILITY_MS && task.stablePolls && task.stablePolls >= 3) {
16705
+ if (task.hasStartedOutputting || await this.validateSessionHasOutput(task.sessionID, task)) {
16706
+ log(`Task ${task.id} stable for 3 polls, completing...`);
16707
+ await this.completeTask(task);
16708
+ }
16709
+ }
16710
+ } catch (error45) {
16711
+ log(`Poll error for task ${task.id}:`, error45);
16712
+ }
16859
16713
  }
16714
+ } catch (error45) {
16715
+ log("Polling error:", error45);
16860
16716
  }
16861
- if (queued.length > 0) {
16862
- if (lines.length > 0) lines.push("");
16863
- lines.push(`Queued (${queued.length}):`);
16864
- for (const task of queued) {
16865
- const bgTag = task.isBackground ? "[W]" : "[P]";
16866
- lines.push(`${bgTag} ${task.description} (${task.agent})`);
16717
+ }
16718
+ async validateSessionHasOutput(sessionID, task) {
16719
+ try {
16720
+ const response = await this.client.session.messages({ path: { id: sessionID } });
16721
+ const messages = response.data ?? [];
16722
+ const hasOutput = messages.some((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT && m.parts?.some((p) => p.type === PART_TYPES.TEXT && p.text?.trim() || p.type === PART_TYPES.TOOL));
16723
+ if (hasOutput && task) {
16724
+ task.hasStartedOutputting = true;
16867
16725
  }
16726
+ return hasOutput;
16727
+ } catch {
16728
+ return true;
16868
16729
  }
16869
- return lines.join("\n");
16870
16730
  }
16871
- /**
16872
- * Show consolidated toast with all running/queued tasks
16873
- */
16874
- showTaskListToast(newTask) {
16875
- if (!this.client) return;
16876
- const tuiClient2 = this.client;
16877
- if (!tuiClient2.tui?.showToast) return;
16878
- const message = this.buildTaskListMessage(newTask);
16879
- const running = this.getRunningTasks();
16880
- const queued = this.getQueuedTasks();
16881
- const title = newTask.isBackground ? `Background Task Started` : `Task Started`;
16882
- tuiClient2.tui.showToast({
16883
- body: {
16884
- title,
16885
- message: message || `${newTask.description} (${newTask.agent})`,
16886
- variant: STATUS_LABEL.INFO,
16887
- duration: running.length + queued.length > 2 ? 5e3 : 3e3
16888
- }
16889
- }).catch(() => {
16731
+ async completeTask(task) {
16732
+ log("[task-poller.ts] completeTask() called for", task.id, task.agent);
16733
+ task.status = TASK_STATUS.COMPLETED;
16734
+ task.completedAt = /* @__PURE__ */ new Date();
16735
+ if (task.concurrencyKey) {
16736
+ this.concurrency.release(task.concurrencyKey);
16737
+ this.concurrency.reportResult(task.concurrencyKey, true);
16738
+ task.concurrencyKey = void 0;
16739
+ }
16740
+ this.store.untrackPending(task.parentSessionID, task.id);
16741
+ this.store.queueNotification(task);
16742
+ await this.notifyParentIfAllComplete(task.parentSessionID);
16743
+ this.scheduleCleanup(task.id);
16744
+ taskWAL.log(WAL_ACTIONS.COMPLETE, task).catch(() => {
16890
16745
  });
16746
+ if (this.onTaskComplete) {
16747
+ Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
16748
+ }
16749
+ const duration3 = formatDuration(task.startedAt, task.completedAt);
16750
+ presets.sessionCompleted(task.sessionID, duration3);
16751
+ log(`Completed ${task.id} (${duration3})`);
16891
16752
  }
16892
- /**
16893
- * Show task completion toast
16894
- */
16895
- showCompletionToast(info) {
16896
- if (!this.client) return;
16897
- const tuiClient2 = this.client;
16898
- if (!tuiClient2.tui?.showToast) return;
16899
- this.removeTask(info.id);
16900
- const remaining = this.getRunningTasks();
16901
- const queued = this.getQueuedTasks();
16902
- let message;
16903
- let title;
16904
- let variant;
16905
- if (info.status === STATUS_LABEL.ERROR || info.status === STATUS_LABEL.CANCELLED || info.status === STATUS_LABEL.FAILED) {
16906
- title = info.status === STATUS_LABEL.ERROR ? "Task Failed" : "Task Cancelled";
16907
- message = `[FAIL] "${info.description}" ${info.status}
16908
- ${info.error || ""}`;
16909
- variant = STATUS_LABEL.ERROR;
16910
- } else {
16911
- title = "Task Completed";
16912
- message = `[DONE] "${info.description}" finished in ${info.duration}`;
16913
- variant = STATUS_LABEL.SUCCESS;
16753
+ async updateTaskProgress(task) {
16754
+ try {
16755
+ const result = await this.client.session.messages({ path: { id: task.sessionID } });
16756
+ if (result.error) return;
16757
+ const messages = result.data ?? [];
16758
+ const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
16759
+ let toolCalls = 0;
16760
+ let lastTool;
16761
+ let lastMessage;
16762
+ for (const msg of assistantMsgs) {
16763
+ for (const part of msg.parts ?? []) {
16764
+ if (part.type === PART_TYPES.TOOL_USE || part.tool) {
16765
+ toolCalls++;
16766
+ lastTool = part.tool || part.name;
16767
+ }
16768
+ if (part.type === PART_TYPES.TEXT && part.text) {
16769
+ lastMessage = part.text;
16770
+ }
16771
+ }
16772
+ }
16773
+ task.progress = {
16774
+ toolCalls,
16775
+ lastTool,
16776
+ lastMessage: lastMessage?.slice(0, 100),
16777
+ lastUpdate: /* @__PURE__ */ new Date()
16778
+ };
16779
+ const currentMsgCount = messages.length;
16780
+ if (task.lastMsgCount === currentMsgCount) {
16781
+ task.stablePolls = (task.stablePolls ?? 0) + 1;
16782
+ } else {
16783
+ task.stablePolls = 0;
16784
+ }
16785
+ task.lastMsgCount = currentMsgCount;
16786
+ } catch {
16914
16787
  }
16915
- if (remaining.length > 0 || queued.length > 0) {
16916
- message += `
16788
+ }
16789
+ };
16917
16790
 
16918
- Still running: ${remaining.length} | Queued: ${queued.length}`;
16791
+ // src/core/agents/manager/task-cleaner.ts
16792
+ init_store();
16793
+ var TaskCleaner = class {
16794
+ constructor(client, store, concurrency) {
16795
+ this.client = client;
16796
+ this.store = store;
16797
+ this.concurrency = concurrency;
16798
+ }
16799
+ pruneExpiredTasks() {
16800
+ const now = Date.now();
16801
+ for (const [taskId, task] of this.store.getAll().map((t) => [t.id, t])) {
16802
+ const age = now - task.startedAt.getTime();
16803
+ if (age <= CONFIG.TASK_TTL_MS) continue;
16804
+ log(`Timeout: ${taskId}`);
16805
+ if (task.status === TASK_STATUS.RUNNING) {
16806
+ task.status = TASK_STATUS.TIMEOUT;
16807
+ task.error = "Task exceeded 30 minute time limit";
16808
+ task.completedAt = /* @__PURE__ */ new Date();
16809
+ if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
16810
+ this.store.untrackPending(task.parentSessionID, taskId);
16811
+ const toastManager = getTaskToastManager();
16812
+ if (toastManager) {
16813
+ toastManager.showCompletionToast({
16814
+ id: taskId,
16815
+ description: task.description,
16816
+ duration: formatDuration(task.startedAt, task.completedAt),
16817
+ status: TASK_STATUS.ERROR,
16818
+ error: task.error
16819
+ });
16820
+ }
16821
+ }
16822
+ this.client.session.delete({ path: { id: task.sessionID } }).catch(() => {
16823
+ });
16824
+ clear(task.sessionID);
16825
+ this.store.delete(taskId);
16826
+ taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
16827
+ });
16919
16828
  }
16920
- tuiClient2.tui.showToast({
16921
- body: {
16922
- title,
16923
- message,
16924
- variant,
16925
- duration: 5e3
16829
+ this.store.cleanEmptyNotifications();
16830
+ }
16831
+ scheduleCleanup(taskId) {
16832
+ const task = this.store.get(taskId);
16833
+ const sessionID = task?.sessionID;
16834
+ setTimeout(async () => {
16835
+ if (sessionID) {
16836
+ try {
16837
+ await this.client.session.delete({ path: { id: sessionID } });
16838
+ clear(sessionID);
16839
+ } catch {
16840
+ }
16926
16841
  }
16927
- }).catch(() => {
16928
- });
16842
+ this.store.delete(taskId);
16843
+ if (task) taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
16844
+ });
16845
+ log(`Cleaned up ${taskId}`);
16846
+ }, CONFIG.CLEANUP_DELAY_MS);
16929
16847
  }
16930
16848
  /**
16931
- * Show all-tasks-complete summary toast
16849
+ * Notify parent session when task(s) complete.
16850
+ * Uses noReply strategy:
16851
+ * - Individual completion: noReply=true (silent notification, save tokens)
16852
+ * - All complete: noReply=false (AI should process and report results)
16932
16853
  */
16933
- showAllCompleteToast(parentSessionID, completedTasks) {
16934
- if (!this.client) return;
16935
- const tuiClient2 = this.client;
16936
- if (!tuiClient2.tui?.showToast) return;
16937
- const successCount = completedTasks.filter((t) => t.status === STATUS_LABEL.COMPLETED).length;
16938
- const failCount = completedTasks.filter((t) => t.status === STATUS_LABEL.ERROR || t.status === STATUS_LABEL.CANCELLED || t.status === STATUS_LABEL.FAILED).length;
16939
- const taskList = completedTasks.map((t) => `- [${t.status === STATUS_LABEL.COMPLETED ? "OK" : "FAIL"}] ${t.description} (${t.duration})`).join("\n");
16940
- tuiClient2.tui.showToast({
16941
- body: {
16942
- title: "All Tasks Completed",
16943
- message: `${successCount} succeeded, ${failCount} failed
16944
-
16945
- ${taskList}`,
16946
- variant: failCount > 0 ? STATUS_LABEL.WARNING : STATUS_LABEL.SUCCESS,
16947
- duration: 7e3
16948
- }
16949
- }).catch(() => {
16950
- });
16951
- }
16952
- /**
16953
- * Show progress toast (for long-running tasks)
16954
- */
16955
- showProgressToast(taskId, progress) {
16956
- if (!this.client) return;
16957
- const tuiClient2 = this.client;
16958
- if (!tuiClient2.tui?.showToast) return;
16959
- const task = this.tasks.get(taskId);
16960
- if (!task) return;
16961
- const percentage = Math.round(progress.current / progress.total * 100);
16962
- const progressBar = `[${"#".repeat(Math.floor(percentage / 10))}${"-".repeat(10 - Math.floor(percentage / 10))}]`;
16963
- tuiClient2.tui.showToast({
16964
- body: {
16965
- title: `Task Progress: ${task.description}`,
16966
- message: `${progressBar} ${percentage}%
16967
- ${progress.message || ""}`,
16968
- variant: STATUS_LABEL.INFO,
16969
- duration: 2e3
16854
+ async notifyParentIfAllComplete(parentSessionID) {
16855
+ const pendingCount = this.store.getPendingCount(parentSessionID);
16856
+ const notifications = this.store.getNotifications(parentSessionID);
16857
+ if (notifications.length === 0) return;
16858
+ const allComplete = pendingCount === 0;
16859
+ const toastManager = getTaskToastManager();
16860
+ const completionInfos = notifications.map((task) => ({
16861
+ id: task.id,
16862
+ description: task.description,
16863
+ duration: formatDuration(task.startedAt, task.completedAt),
16864
+ status: task.status,
16865
+ error: task.error
16866
+ }));
16867
+ if (allComplete && completionInfos.length > 1 && toastManager) {
16868
+ toastManager.showAllCompleteToast(parentSessionID, completionInfos);
16869
+ } else if (toastManager) {
16870
+ for (const info of completionInfos) {
16871
+ toastManager.showCompletionToast(info);
16970
16872
  }
16971
- }).catch(() => {
16972
- });
16973
- }
16974
- /**
16975
- * Clear all tracked tasks
16976
- */
16977
- clear() {
16978
- this.tasks.clear();
16979
- }
16980
- /**
16981
- * Get task count stats
16982
- */
16983
- getStats() {
16984
- const running = this.getRunningTasks().length;
16985
- const queued = this.getQueuedTasks().length;
16986
- return { running, queued, total: this.tasks.size };
16987
- }
16988
- };
16989
- var instance = null;
16990
- function getTaskToastManager() {
16991
- return instance;
16992
- }
16993
- function initTaskToastManager(client, concurrency) {
16994
- if (!instance) {
16995
- instance = new TaskToastManager();
16996
- }
16997
- instance.init(client, concurrency);
16998
- return instance;
16999
- }
17000
-
17001
- // src/core/agents/persistence/task-wal.ts
17002
- import * as fs3 from "node:fs/promises";
17003
- import * as path3 from "node:path";
17004
- var TaskWAL = class {
17005
- walPath;
17006
- initialized = false;
17007
- constructor(customPath) {
17008
- this.walPath = customPath || path3.resolve(process.cwd(), ".opencode/archive/tasks/active_tasks.jsonl");
17009
- }
17010
- async init() {
17011
- if (this.initialized) return;
17012
- try {
17013
- const dir = path3.dirname(this.walPath);
17014
- await fs3.mkdir(dir, { recursive: true });
17015
- this.initialized = true;
17016
- } catch (error45) {
17017
- log("Failed to initialize Task WAL directory:", error45);
17018
16873
  }
17019
- }
17020
- async log(action, task) {
17021
- if (!this.initialized) await this.init();
17022
- const entry = {
17023
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
17024
- action,
17025
- taskId: task.id,
17026
- data: action === WAL_ACTIONS.DELETE ? { id: task.id } : {
17027
- id: task.id,
17028
- sessionID: task.sessionID,
17029
- parentSessionID: task.parentSessionID,
17030
- description: task.description,
17031
- agent: task.agent,
17032
- status: task.status,
17033
- startedAt: task.startedAt,
17034
- depth: task.depth,
17035
- prompt: action === WAL_ACTIONS.LAUNCH ? task.prompt : void 0
17036
- // Only log prompt on launch to save space
17037
- }
17038
- };
17039
- try {
17040
- await fs3.appendFile(this.walPath, JSON.stringify(entry) + "\n");
17041
- } catch (error45) {
16874
+ let message;
16875
+ if (allComplete) {
16876
+ message = buildNotificationMessage(notifications);
16877
+ message += `
16878
+
16879
+ **ACTION REQUIRED:** All background tasks are complete. Use \`get_task_result(taskId)\` to retrieve outputs and continue with the mission.`;
16880
+ } else {
16881
+ const completedCount = notifications.length;
16882
+ message = `[BACKGROUND UPDATE] ${completedCount} task(s) completed, ${pendingCount} still running.
16883
+ Completed: ${notifications.map((t) => `\`${t.id}\``).join(", ")}
16884
+ You will be notified when ALL tasks complete. Continue productive work.`;
17042
16885
  }
17043
- }
17044
- async readAll() {
17045
- if (!this.initialized) await this.init();
17046
- const tasks = /* @__PURE__ */ new Map();
17047
16886
  try {
17048
- const content = await fs3.readFile(this.walPath, "utf-8");
17049
- const lines = content.split("\n").filter(Boolean);
17050
- for (const line of lines) {
17051
- try {
17052
- const entry = JSON.parse(line);
17053
- if (entry.action === WAL_ACTIONS.DELETE) {
17054
- tasks.delete(entry.taskId);
17055
- } else if (entry.action === WAL_ACTIONS.LAUNCH) {
17056
- tasks.set(entry.taskId, entry.data);
17057
- } else {
17058
- const existing = tasks.get(entry.taskId);
17059
- if (existing) {
17060
- Object.assign(existing, entry.data);
17061
- }
17062
- }
17063
- } catch {
16887
+ await this.client.session.prompt({
16888
+ path: { id: parentSessionID },
16889
+ body: {
16890
+ // Key optimization: only trigger AI response when ALL complete
16891
+ noReply: !allComplete,
16892
+ parts: [{ type: PART_TYPES.TEXT, text: message }]
17064
16893
  }
17065
- }
17066
- } catch (error45) {
17067
- if (error45.code !== "ENOENT") {
17068
- log("Error reading Task WAL:", error45);
17069
- }
17070
- }
17071
- return tasks;
17072
- }
17073
- /**
17074
- * Compact the WAL by writing only the current active tasks
17075
- */
17076
- async compact(activeTasks) {
17077
- try {
17078
- const tempPath = `${this.walPath}.tmp`;
17079
- const content = activeTasks.map((task) => JSON.stringify({
17080
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
17081
- action: WAL_ACTIONS.LAUNCH,
17082
- taskId: task.id,
17083
- data: task
17084
- })).join("\n") + "\n";
17085
- await fs3.writeFile(tempPath, content);
17086
- await fs3.rename(tempPath, this.walPath);
16894
+ });
16895
+ log(`Notified parent ${parentSessionID} (allComplete=${allComplete}, noReply=${!allComplete})`);
17087
16896
  } catch (error45) {
17088
- log("Failed to compact Task WAL:", error45);
16897
+ log("Notification error:", error45);
17089
16898
  }
16899
+ this.store.clearNotifications(parentSessionID);
17090
16900
  }
17091
16901
  };
17092
- var taskWAL = new TaskWAL();
17093
16902
 
17094
- // src/core/agents/manager/task-launcher.ts
17095
- var TaskLauncher = class {
17096
- constructor(client, directory, store, concurrency, onTaskError, startPolling) {
16903
+ // src/core/agents/manager/event-handler.ts
16904
+ var EventHandler = class {
16905
+ constructor(client, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
17097
16906
  this.client = client;
17098
- this.directory = directory;
17099
16907
  this.store = store;
17100
16908
  this.concurrency = concurrency;
17101
- this.onTaskError = onTaskError;
17102
- this.startPolling = startPolling;
16909
+ this.findBySession = findBySession;
16910
+ this.notifyParentIfAllComplete = notifyParentIfAllComplete;
16911
+ this.scheduleCleanup = scheduleCleanup;
16912
+ this.validateSessionHasOutput = validateSessionHasOutput2;
16913
+ this.onTaskComplete = onTaskComplete;
17103
16914
  }
17104
16915
  /**
17105
- * Unified launch method - handles both single and multiple tasks efficiently.
17106
- * All session creations happen in parallel immediately.
17107
- * Concurrency acquisition and prompt firing happen in the background.
16916
+ * Handle OpenCode session events for proper resource cleanup.
16917
+ * Call this from your plugin's event hook.
17108
16918
  */
17109
- async launch(inputs) {
17110
- const isArray = Array.isArray(inputs);
17111
- const taskInputs = isArray ? inputs : [inputs];
17112
- if (taskInputs.length === 0) return isArray ? [] : null;
17113
- log(`[task-launcher.ts] Batch launching ${taskInputs.length} task(s)`);
17114
- const startTime = Date.now();
17115
- const tasks = await Promise.all(taskInputs.map(
17116
- (input) => this.prepareTask(input).catch((error45) => {
17117
- log(`[task-launcher.ts] Failed to prepare task ${input.description}:`, error45);
17118
- return null;
17119
- })
17120
- ));
17121
- const successfulTasks = tasks.filter((t) => t !== null);
17122
- successfulTasks.forEach((task) => {
17123
- this.executeBackground(task).catch((error45) => {
17124
- log(`[task-launcher.ts] Background execution failed for ${task.id}:`, error45);
17125
- this.onTaskError(task.id, error45);
16919
+ handle(event) {
16920
+ const props = event.properties;
16921
+ if (event.type === SESSION_EVENTS.IDLE) {
16922
+ const sessionID = props?.sessionID;
16923
+ if (!sessionID) return;
16924
+ const task = this.findBySession(sessionID);
16925
+ if (!task || task.status !== TASK_STATUS.RUNNING) return;
16926
+ this.handleSessionIdle(task).catch((err) => {
16927
+ log("Error handling session.idle:", err);
17126
16928
  });
17127
- });
17128
- const elapsed = Date.now() - startTime;
17129
- log(`[task-launcher.ts] Batch launch prepared: ${successfulTasks.length} tasks in ${elapsed}ms`);
17130
- if (successfulTasks.length > 0) {
17131
- this.startPolling();
17132
16929
  }
17133
- return isArray ? successfulTasks : successfulTasks[0] || null;
17134
- }
17135
- /**
17136
- * Prepare task: Create session and registration without blocking on concurrency
17137
- */
17138
- async prepareTask(input) {
17139
- const currentDepth = input.depth ?? 0;
17140
- if (currentDepth >= PARALLEL_TASK.MAX_DEPTH) {
17141
- log(`[task-launcher.ts] Task depth limit reached (${currentDepth}/${PARALLEL_TASK.MAX_DEPTH}). Generation blocked.`);
17142
- throw new Error(`Maximum task depth (${PARALLEL_TASK.MAX_DEPTH}) reached. To prevent infinite recursion, no further sub-tasks can be spawned.`);
16930
+ if (event.type === SESSION_EVENTS.DELETED) {
16931
+ const sessionID = props?.info?.id ?? props?.sessionID;
16932
+ if (!sessionID) return;
16933
+ const task = this.findBySession(sessionID);
16934
+ if (!task) return;
16935
+ this.handleSessionDeleted(task);
17143
16936
  }
17144
- const createResult = await this.client.session.create({
17145
- body: {
17146
- parentID: input.parentSessionID,
17147
- title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${input.description}`
17148
- },
17149
- query: { directory: this.directory }
17150
- });
17151
- if (createResult.error || !createResult.data?.id) {
17152
- throw new Error(`Session creation failed: ${createResult.error || "No ID"}`);
17153
- }
17154
- const sessionID = createResult.data.id;
17155
- const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
17156
- const task = {
17157
- id: taskId,
17158
- sessionID,
17159
- parentSessionID: input.parentSessionID,
17160
- description: input.description,
17161
- prompt: input.prompt,
17162
- agent: input.agent,
17163
- status: TASK_STATUS.PENDING,
17164
- // Start as PENDING
17165
- startedAt: /* @__PURE__ */ new Date(),
17166
- concurrencyKey: input.agent,
17167
- depth: (input.depth ?? 0) + 1,
17168
- mode: input.mode || "normal",
17169
- groupID: input.groupID
17170
- };
17171
- this.store.set(taskId, task);
17172
- this.store.trackPending(input.parentSessionID, taskId);
17173
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
17174
- });
17175
- const toastManager = getTaskToastManager();
17176
- if (toastManager) {
17177
- toastManager.addTask({
17178
- id: taskId,
17179
- description: input.description,
17180
- agent: input.agent,
17181
- isBackground: true,
17182
- parentSessionID: input.parentSessionID,
17183
- sessionID
17184
- });
17185
- }
17186
- presets.sessionCreated(sessionID, input.agent);
17187
- return task;
17188
- }
17189
- /**
17190
- * Background execution: Acquire slot and fire prompt
17191
- */
17192
- async executeBackground(task) {
17193
- try {
17194
- await this.concurrency.acquire(task.agent);
17195
- task.status = TASK_STATUS.RUNNING;
17196
- task.startedAt = /* @__PURE__ */ new Date();
17197
- this.store.set(task.id, task);
17198
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
17199
- });
17200
- await this.client.session.prompt({
17201
- path: { id: task.sessionID },
17202
- body: {
17203
- agent: task.agent,
17204
- tools: {
17205
- // HPFA: Allow agents to delegate sub-tasks (Fractal Spawning)
17206
- delegate_task: true,
17207
- get_task_result: true,
17208
- list_tasks: true,
17209
- cancel_task: true
17210
- },
17211
- parts: [{ type: PART_TYPES.TEXT, text: task.prompt }]
17212
- }
17213
- });
17214
- log(`[task-launcher.ts] Task ${task.id} (${task.agent}) started running`);
17215
- } catch (error45) {
17216
- this.concurrency.release(task.agent);
17217
- throw error45;
17218
- }
17219
- }
17220
- };
17221
-
17222
- // src/core/agents/manager/task-resumer.ts
17223
- var TaskResumer = class {
17224
- constructor(client, store, findBySession, startPolling, notifyParentIfAllComplete) {
17225
- this.client = client;
17226
- this.store = store;
17227
- this.findBySession = findBySession;
17228
- this.startPolling = startPolling;
17229
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
17230
- }
17231
- async resume(input) {
17232
- const existingTask = this.findBySession(input.sessionId);
17233
- if (!existingTask) {
17234
- throw new Error(`Task not found for session: ${input.sessionId}`);
17235
- }
17236
- existingTask.status = TASK_STATUS.RUNNING;
17237
- existingTask.completedAt = void 0;
17238
- existingTask.error = void 0;
17239
- existingTask.result = void 0;
17240
- existingTask.parentSessionID = input.parentSessionID;
17241
- existingTask.startedAt = /* @__PURE__ */ new Date();
17242
- existingTask.stablePolls = 0;
17243
- this.store.trackPending(input.parentSessionID, existingTask.id);
17244
- this.startPolling();
17245
- taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
17246
- });
17247
- log(`Resuming task ${existingTask.id} in session ${existingTask.sessionID}`);
17248
- this.client.session.prompt({
17249
- path: { id: existingTask.sessionID },
17250
- body: {
17251
- agent: existingTask.agent,
17252
- parts: [{ type: PART_TYPES.TEXT, text: input.prompt }]
17253
- }
17254
- }).catch((error45) => {
17255
- log(`Resume prompt error for ${existingTask.id}:`, error45);
17256
- existingTask.status = TASK_STATUS.ERROR;
17257
- existingTask.error = error45 instanceof Error ? error45.message : String(error45);
17258
- existingTask.completedAt = /* @__PURE__ */ new Date();
17259
- this.store.untrackPending(input.parentSessionID, existingTask.id);
17260
- this.store.queueNotification(existingTask);
17261
- this.notifyParentIfAllComplete(input.parentSessionID).catch(() => {
17262
- });
17263
- taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
17264
- });
17265
- });
17266
- return existingTask;
17267
- }
17268
- };
17269
-
17270
- // src/core/agents/config.ts
17271
- var CONFIG = {
17272
- TASK_TTL_MS: PARALLEL_TASK.TTL_MS,
17273
- CLEANUP_DELAY_MS: PARALLEL_TASK.CLEANUP_DELAY_MS,
17274
- MIN_STABILITY_MS: PARALLEL_TASK.MIN_STABILITY_MS,
17275
- POLL_INTERVAL_MS: PARALLEL_TASK.POLL_INTERVAL_MS
17276
- };
17277
-
17278
- // src/core/agents/manager/task-poller.ts
17279
- var TaskPoller = class {
17280
- constructor(client, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete) {
17281
- this.client = client;
17282
- this.store = store;
17283
- this.concurrency = concurrency;
17284
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
17285
- this.scheduleCleanup = scheduleCleanup;
17286
- this.pruneExpiredTasks = pruneExpiredTasks;
17287
- this.onTaskComplete = onTaskComplete;
17288
- }
17289
- pollingInterval;
17290
- start() {
17291
- if (this.pollingInterval) return;
17292
- log("[task-poller.ts] start() - polling started");
17293
- this.pollingInterval = setInterval(() => this.poll(), CONFIG.POLL_INTERVAL_MS);
17294
- this.pollingInterval.unref();
17295
- }
17296
- stop() {
17297
- if (this.pollingInterval) {
17298
- clearInterval(this.pollingInterval);
17299
- this.pollingInterval = void 0;
17300
- }
17301
- }
17302
- isRunning() {
17303
- return !!this.pollingInterval;
17304
16937
  }
17305
- async poll() {
17306
- this.pruneExpiredTasks();
17307
- const running = this.store.getRunning();
17308
- if (running.length === 0) {
17309
- this.stop();
16938
+ async handleSessionIdle(task) {
16939
+ const elapsed = Date.now() - task.startedAt.getTime();
16940
+ if (elapsed < CONFIG.MIN_STABILITY_MS) {
16941
+ log(`Session idle but too early for ${task.id}, waiting...`);
17310
16942
  return;
17311
16943
  }
17312
- log("[task-poller.ts] poll() checking", running.length, "running tasks");
17313
- try {
17314
- const statusResult = await this.client.session.status();
17315
- const allStatuses = statusResult.data ?? {};
17316
- for (const task of running) {
17317
- try {
17318
- if (task.status === TASK_STATUS.PENDING) continue;
17319
- const sessionStatus = allStatuses[task.sessionID];
17320
- if (sessionStatus?.type === SESSION_STATUS.IDLE) {
17321
- const elapsed2 = Date.now() - task.startedAt.getTime();
17322
- if (elapsed2 < CONFIG.MIN_STABILITY_MS) continue;
17323
- if (!task.hasStartedOutputting && !await this.validateSessionHasOutput(task.sessionID, task)) continue;
17324
- await this.completeTask(task);
17325
- continue;
17326
- }
17327
- await this.updateTaskProgress(task);
17328
- const elapsed = Date.now() - task.startedAt.getTime();
17329
- if (elapsed >= CONFIG.MIN_STABILITY_MS && task.stablePolls && task.stablePolls >= 3) {
17330
- if (task.hasStartedOutputting || await this.validateSessionHasOutput(task.sessionID, task)) {
17331
- log(`Task ${task.id} stable for 3 polls, completing...`);
17332
- await this.completeTask(task);
17333
- }
17334
- }
17335
- } catch (error45) {
17336
- log(`Poll error for task ${task.id}:`, error45);
17337
- }
17338
- }
17339
- } catch (error45) {
17340
- log("Polling error:", error45);
17341
- }
17342
- }
17343
- async validateSessionHasOutput(sessionID, task) {
17344
- try {
17345
- const response = await this.client.session.messages({ path: { id: sessionID } });
17346
- const messages = response.data ?? [];
17347
- const hasOutput = messages.some((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT && m.parts?.some((p) => p.type === PART_TYPES.TEXT && p.text?.trim() || p.type === PART_TYPES.TOOL));
17348
- if (hasOutput && task) {
17349
- task.hasStartedOutputting = true;
17350
- }
17351
- return hasOutput;
17352
- } catch {
17353
- return true;
16944
+ const hasOutput = await this.validateSessionHasOutput(task.sessionID);
16945
+ if (!hasOutput) {
16946
+ log(`Session idle but no output for ${task.id}, waiting...`);
16947
+ return;
17354
16948
  }
17355
- }
17356
- async completeTask(task) {
17357
- log("[task-poller.ts] completeTask() called for", task.id, task.agent);
17358
16949
  task.status = TASK_STATUS.COMPLETED;
17359
16950
  task.completedAt = /* @__PURE__ */ new Date();
17360
16951
  if (task.concurrencyKey) {
@@ -17371,477 +16962,943 @@ var TaskPoller = class {
17371
16962
  if (this.onTaskComplete) {
17372
16963
  Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
17373
16964
  }
17374
- const duration3 = formatDuration(task.startedAt, task.completedAt);
17375
- presets.sessionCompleted(task.sessionID, duration3);
17376
- log(`Completed ${task.id} (${duration3})`);
17377
- }
17378
- async updateTaskProgress(task) {
17379
- try {
17380
- const result = await this.client.session.messages({ path: { id: task.sessionID } });
17381
- if (result.error) return;
17382
- const messages = result.data ?? [];
17383
- const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
17384
- let toolCalls = 0;
17385
- let lastTool;
17386
- let lastMessage;
17387
- for (const msg of assistantMsgs) {
17388
- for (const part of msg.parts ?? []) {
17389
- if (part.type === PART_TYPES.TOOL_USE || part.tool) {
17390
- toolCalls++;
17391
- lastTool = part.tool || part.name;
17392
- }
17393
- if (part.type === PART_TYPES.TEXT && part.text) {
17394
- lastMessage = part.text;
17395
- }
17396
- }
17397
- }
17398
- task.progress = {
17399
- toolCalls,
17400
- lastTool,
17401
- lastMessage: lastMessage?.slice(0, 100),
17402
- lastUpdate: /* @__PURE__ */ new Date()
17403
- };
17404
- const currentMsgCount = messages.length;
17405
- if (task.lastMsgCount === currentMsgCount) {
17406
- task.stablePolls = (task.stablePolls ?? 0) + 1;
17407
- } else {
17408
- task.stablePolls = 0;
17409
- }
17410
- task.lastMsgCount = currentMsgCount;
17411
- } catch {
17412
- }
16965
+ log(`Task ${task.id} completed via session.idle event (${formatDuration(task.startedAt, task.completedAt)})`);
16966
+ }
16967
+ handleSessionDeleted(task) {
16968
+ log(`Session deleted event for task ${task.id}`);
16969
+ if (task.status === TASK_STATUS.RUNNING) {
16970
+ task.status = TASK_STATUS.ERROR;
16971
+ task.error = "Session deleted";
16972
+ task.completedAt = /* @__PURE__ */ new Date();
16973
+ }
16974
+ if (task.concurrencyKey) {
16975
+ this.concurrency.release(task.concurrencyKey);
16976
+ this.concurrency.reportResult(task.concurrencyKey, false);
16977
+ task.concurrencyKey = void 0;
16978
+ }
16979
+ this.store.untrackPending(task.parentSessionID, task.id);
16980
+ this.store.clearNotificationsForTask(task.id);
16981
+ this.store.delete(task.id);
16982
+ taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
16983
+ });
16984
+ log(`Cleaned up deleted session task: ${task.id}`);
17413
16985
  }
17414
16986
  };
17415
16987
 
17416
- // src/core/agents/manager/task-cleaner.ts
17417
- init_store();
17418
- var TaskCleaner = class {
17419
- constructor(client, store, concurrency) {
16988
+ // src/core/agents/manager.ts
16989
+ var ParallelAgentManager = class _ParallelAgentManager {
16990
+ static _instance;
16991
+ store = new TaskStore();
16992
+ client;
16993
+ directory;
16994
+ concurrency = new ConcurrencyController();
16995
+ // Composed components
16996
+ launcher;
16997
+ resumer;
16998
+ poller;
16999
+ cleaner;
17000
+ eventHandler;
17001
+ constructor(client, directory) {
17420
17002
  this.client = client;
17421
- this.store = store;
17422
- this.concurrency = concurrency;
17003
+ this.directory = directory;
17004
+ this.cleaner = new TaskCleaner(client, this.store, this.concurrency);
17005
+ this.poller = new TaskPoller(
17006
+ client,
17007
+ this.store,
17008
+ this.concurrency,
17009
+ (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
17010
+ (taskId) => this.cleaner.scheduleCleanup(taskId),
17011
+ () => this.cleaner.pruneExpiredTasks(),
17012
+ (task) => this.handleTaskComplete(task)
17013
+ );
17014
+ this.launcher = new TaskLauncher(
17015
+ client,
17016
+ directory,
17017
+ this.store,
17018
+ this.concurrency,
17019
+ (taskId, error45) => this.handleTaskError(taskId, error45),
17020
+ () => this.poller.start()
17021
+ );
17022
+ this.resumer = new TaskResumer(
17023
+ client,
17024
+ this.store,
17025
+ (sessionID) => this.findBySession(sessionID),
17026
+ () => this.poller.start(),
17027
+ (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
17028
+ );
17029
+ this.eventHandler = new EventHandler(
17030
+ client,
17031
+ this.store,
17032
+ this.concurrency,
17033
+ (sessionID) => this.findBySession(sessionID),
17034
+ (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
17035
+ (taskId) => this.cleaner.scheduleCleanup(taskId),
17036
+ (sessionID) => this.poller.validateSessionHasOutput(sessionID),
17037
+ (task) => this.handleTaskComplete(task)
17038
+ );
17039
+ this.recoverActiveTasks().catch((err) => {
17040
+ log("Recovery error:", err);
17041
+ });
17423
17042
  }
17424
- pruneExpiredTasks() {
17425
- const now = Date.now();
17426
- for (const [taskId, task] of this.store.getAll().map((t) => [t.id, t])) {
17427
- const age = now - task.startedAt.getTime();
17428
- if (age <= CONFIG.TASK_TTL_MS) continue;
17429
- log(`Timeout: ${taskId}`);
17430
- if (task.status === TASK_STATUS.RUNNING) {
17431
- task.status = TASK_STATUS.TIMEOUT;
17432
- task.error = "Task exceeded 30 minute time limit";
17433
- task.completedAt = /* @__PURE__ */ new Date();
17434
- if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
17435
- this.store.untrackPending(task.parentSessionID, taskId);
17436
- const toastManager = getTaskToastManager();
17437
- if (toastManager) {
17438
- toastManager.showCompletionToast({
17439
- id: taskId,
17440
- description: task.description,
17441
- duration: formatDuration(task.startedAt, task.completedAt),
17442
- status: TASK_STATUS.ERROR,
17443
- error: task.error
17444
- });
17445
- }
17043
+ static getInstance(client, directory) {
17044
+ if (!_ParallelAgentManager._instance) {
17045
+ if (!client || !directory) {
17046
+ throw new Error("ParallelAgentManager requires client and directory on first call");
17446
17047
  }
17447
- this.client.session.delete({ path: { id: task.sessionID } }).catch(() => {
17448
- });
17449
- clear(task.sessionID);
17450
- this.store.delete(taskId);
17451
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17452
- });
17048
+ _ParallelAgentManager._instance = new _ParallelAgentManager(client, directory);
17453
17049
  }
17454
- this.store.cleanEmptyNotifications();
17050
+ return _ParallelAgentManager._instance;
17455
17051
  }
17456
- scheduleCleanup(taskId) {
17457
- const task = this.store.get(taskId);
17458
- const sessionID = task?.sessionID;
17459
- setTimeout(async () => {
17460
- if (sessionID) {
17461
- try {
17462
- await this.client.session.delete({ path: { id: sessionID } });
17463
- clear(sessionID);
17464
- } catch {
17465
- }
17466
- }
17467
- this.store.delete(taskId);
17468
- if (task) taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17469
- });
17470
- log(`Cleaned up ${taskId}`);
17471
- }, CONFIG.CLEANUP_DELAY_MS);
17052
+ // ========================================================================
17053
+ // Public API
17054
+ // ========================================================================
17055
+ async launch(inputs) {
17056
+ this.cleaner.pruneExpiredTasks();
17057
+ return this.launcher.launch(inputs);
17472
17058
  }
17473
- /**
17474
- * Notify parent session when task(s) complete.
17475
- * Uses noReply strategy:
17476
- * - Individual completion: noReply=true (silent notification, save tokens)
17477
- * - All complete: noReply=false (AI should process and report results)
17478
- */
17479
- async notifyParentIfAllComplete(parentSessionID) {
17480
- const pendingCount = this.store.getPendingCount(parentSessionID);
17481
- const notifications = this.store.getNotifications(parentSessionID);
17482
- if (notifications.length === 0) return;
17483
- const allComplete = pendingCount === 0;
17484
- const toastManager = getTaskToastManager();
17485
- const completionInfos = notifications.map((task) => ({
17486
- id: task.id,
17487
- description: task.description,
17488
- duration: formatDuration(task.startedAt, task.completedAt),
17489
- status: task.status,
17490
- error: task.error
17491
- }));
17492
- if (allComplete && completionInfos.length > 1 && toastManager) {
17493
- toastManager.showAllCompleteToast(parentSessionID, completionInfos);
17494
- } else if (toastManager) {
17495
- for (const info of completionInfos) {
17496
- toastManager.showCompletionToast(info);
17497
- }
17498
- }
17499
- let message;
17500
- if (allComplete) {
17501
- message = buildNotificationMessage(notifications);
17502
- message += `
17503
-
17504
- **ACTION REQUIRED:** All background tasks are complete. Use \`get_task_result(taskId)\` to retrieve outputs and continue with the mission.`;
17505
- } else {
17506
- const completedCount = notifications.length;
17507
- message = `[BACKGROUND UPDATE] ${completedCount} task(s) completed, ${pendingCount} still running.
17508
- Completed: ${notifications.map((t) => `\`${t.id}\``).join(", ")}
17509
- You will be notified when ALL tasks complete. Continue productive work.`;
17059
+ async resume(input) {
17060
+ return this.resumer.resume(input);
17061
+ }
17062
+ getTask(id) {
17063
+ return this.store.get(id);
17064
+ }
17065
+ getRunningTasks() {
17066
+ return this.store.getRunning();
17067
+ }
17068
+ getAllTasks() {
17069
+ return this.store.getAll();
17070
+ }
17071
+ getTasksByParent(parentSessionID) {
17072
+ return this.store.getByParent(parentSessionID);
17073
+ }
17074
+ async cancelTask(taskId) {
17075
+ const task = this.store.get(taskId);
17076
+ if (!task || task.status !== TASK_STATUS.RUNNING) return false;
17077
+ task.status = TASK_STATUS.ERROR;
17078
+ task.error = "Cancelled by user";
17079
+ task.completedAt = /* @__PURE__ */ new Date();
17080
+ if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
17081
+ this.store.untrackPending(task.parentSessionID, taskId);
17082
+ try {
17083
+ await this.client.session.delete({ path: { id: task.sessionID } });
17084
+ log(`Session ${task.sessionID.slice(0, 8)}... deleted`);
17085
+ } catch {
17086
+ log(`Session ${task.sessionID.slice(0, 8)}... already gone`);
17510
17087
  }
17088
+ this.cleaner.scheduleCleanup(taskId);
17089
+ taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17090
+ });
17091
+ log(`Cancelled ${taskId}`);
17092
+ return true;
17093
+ }
17094
+ async getResult(taskId) {
17095
+ const task = this.store.get(taskId);
17096
+ if (!task) return null;
17097
+ if (task.result) return task.result;
17098
+ if (task.status === TASK_STATUS.ERROR) return `Error: ${task.error}`;
17099
+ if (task.status === TASK_STATUS.RUNNING) return null;
17511
17100
  try {
17512
- await this.client.session.prompt({
17513
- path: { id: parentSessionID },
17514
- body: {
17515
- // Key optimization: only trigger AI response when ALL complete
17516
- noReply: !allComplete,
17517
- parts: [{ type: PART_TYPES.TEXT, text: message }]
17518
- }
17519
- });
17520
- log(`Notified parent ${parentSessionID} (allComplete=${allComplete}, noReply=${!allComplete})`);
17101
+ const result = await this.client.session.messages({ path: { id: task.sessionID } });
17102
+ if (result.error) return `Error: ${result.error}`;
17103
+ const messages = result.data ?? [];
17104
+ const lastMsg = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT).reverse()[0];
17105
+ if (!lastMsg) return "(No response)";
17106
+ const text = lastMsg.parts?.filter((p) => p.type === PART_TYPES.TEXT || p.type === PART_TYPES.REASONING).map((p) => p.text ?? "").filter(Boolean).join("\n") ?? "";
17107
+ task.result = text;
17108
+ return text;
17521
17109
  } catch (error45) {
17522
- log("Notification error:", error45);
17110
+ return `Error: ${error45 instanceof Error ? error45.message : String(error45)}`;
17523
17111
  }
17524
- this.store.clearNotifications(parentSessionID);
17525
17112
  }
17526
- };
17527
-
17528
- // src/core/agents/manager/event-handler.ts
17529
- var EventHandler = class {
17530
- constructor(client, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
17531
- this.client = client;
17532
- this.store = store;
17533
- this.concurrency = concurrency;
17534
- this.findBySession = findBySession;
17535
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
17536
- this.scheduleCleanup = scheduleCleanup;
17537
- this.validateSessionHasOutput = validateSessionHasOutput2;
17538
- this.onTaskComplete = onTaskComplete;
17113
+ setConcurrencyLimit(agentType, limit) {
17114
+ this.concurrency.setLimit(agentType, limit);
17539
17115
  }
17540
- /**
17541
- * Handle OpenCode session events for proper resource cleanup.
17542
- * Call this from your plugin's event hook.
17543
- */
17544
- handle(event) {
17545
- const props = event.properties;
17546
- if (event.type === SESSION_EVENTS.IDLE) {
17547
- const sessionID = props?.sessionID;
17548
- if (!sessionID) return;
17549
- const task = this.findBySession(sessionID);
17550
- if (!task || task.status !== TASK_STATUS.RUNNING) return;
17551
- this.handleSessionIdle(task).catch((err) => {
17552
- log("Error handling session.idle:", err);
17553
- });
17554
- }
17555
- if (event.type === SESSION_EVENTS.DELETED) {
17556
- const sessionID = props?.info?.id ?? props?.sessionID;
17557
- if (!sessionID) return;
17558
- const task = this.findBySession(sessionID);
17559
- if (!task) return;
17560
- this.handleSessionDeleted(task);
17561
- }
17116
+ getPendingCount(parentSessionID) {
17117
+ return this.store.getPendingCount(parentSessionID);
17562
17118
  }
17563
- async handleSessionIdle(task) {
17564
- const elapsed = Date.now() - task.startedAt.getTime();
17565
- if (elapsed < CONFIG.MIN_STABILITY_MS) {
17566
- log(`Session idle but too early for ${task.id}, waiting...`);
17567
- return;
17568
- }
17569
- const hasOutput = await this.validateSessionHasOutput(task.sessionID);
17570
- if (!hasOutput) {
17571
- log(`Session idle but no output for ${task.id}, waiting...`);
17572
- return;
17119
+ getConcurrency() {
17120
+ return this.concurrency;
17121
+ }
17122
+ cleanup() {
17123
+ this.poller.stop();
17124
+ this.store.clear();
17125
+ Promise.resolve().then(() => (init_store(), store_exports)).then((store) => store.clearAll()).catch(() => {
17126
+ });
17127
+ }
17128
+ formatDuration = formatDuration;
17129
+ /**
17130
+ * Get orchestration stats
17131
+ */
17132
+ getStats() {
17133
+ const running = this.store.getRunning().length;
17134
+ const all = this.store.getAll().length;
17135
+ let queued = 0;
17136
+ const concurrencyKeys = ["default", AGENT_NAMES.WORKER, AGENT_NAMES.REVIEWER, AGENT_NAMES.PLANNER];
17137
+ for (const key of concurrencyKeys) {
17138
+ queued += this.concurrency.getQueueLength(key);
17573
17139
  }
17574
- task.status = TASK_STATUS.COMPLETED;
17140
+ return {
17141
+ running,
17142
+ queued,
17143
+ total: all
17144
+ };
17145
+ }
17146
+ // ========================================================================
17147
+ // Event Handling
17148
+ // ========================================================================
17149
+ handleEvent(event) {
17150
+ this.eventHandler.handle(event);
17151
+ }
17152
+ // ========================================================================
17153
+ // Private Helpers
17154
+ // ========================================================================
17155
+ findBySession(sessionID) {
17156
+ return this.store.getAll().find((t) => t.sessionID === sessionID);
17157
+ }
17158
+ handleTaskError(taskId, error45) {
17159
+ const task = this.store.get(taskId);
17160
+ if (!task) return;
17161
+ task.status = TASK_STATUS.ERROR;
17162
+ task.error = error45 instanceof Error ? error45.message : String(error45);
17575
17163
  task.completedAt = /* @__PURE__ */ new Date();
17576
17164
  if (task.concurrencyKey) {
17577
17165
  this.concurrency.release(task.concurrencyKey);
17578
- this.concurrency.reportResult(task.concurrencyKey, true);
17579
- task.concurrencyKey = void 0;
17166
+ this.concurrency.reportResult(task.concurrencyKey, false);
17580
17167
  }
17581
- this.store.untrackPending(task.parentSessionID, task.id);
17582
- this.store.queueNotification(task);
17583
- await this.notifyParentIfAllComplete(task.parentSessionID);
17584
- this.scheduleCleanup(task.id);
17585
- taskWAL.log(WAL_ACTIONS.COMPLETE, task).catch(() => {
17168
+ this.store.untrackPending(task.parentSessionID, taskId);
17169
+ this.cleaner.notifyParentIfAllComplete(task.parentSessionID);
17170
+ this.cleaner.scheduleCleanup(taskId);
17171
+ taskWAL.log(WAL_ACTIONS.UPDATE, task).catch(() => {
17172
+ });
17173
+ }
17174
+ async handleTaskComplete(task) {
17175
+ if (task.agent === AGENT_NAMES.WORKER && task.mode !== "race") {
17176
+ log(`[MSVP] Triggering 1\uCC28 \uB9AC\uBDF0 (Unit Review) for task ${task.id}`);
17177
+ try {
17178
+ await this.launch({
17179
+ agent: AGENT_NAMES.REVIEWER,
17180
+ description: `1\uCC28 \uB9AC\uBDF0: ${task.description}`,
17181
+ prompt: `\uC9C4\uD589\uB41C \uC791\uC5C5(\`${task.description}\`)\uC5D0 \uB300\uD574 1\uCC28 \uB9AC\uBDF0(\uC720\uB2DB \uAC80\uC99D)\uB97C \uC218\uD589\uD558\uC138\uC694.
17182
+ \uC8FC\uC694 \uC810\uAC80 \uC0AC\uD56D:
17183
+ 1. \uD574\uB2F9 \uBAA8\uB4C8\uC758 \uC720\uB2DB \uD14C\uC2A4\uD2B8 \uCF54\uB4DC \uC791\uC131 \uC5EC\uBD80 \uBC0F \uD1B5\uACFC \uD655\uC778
17184
+ 2. \uCF54\uB4DC \uD488\uC9C8 \uBC0F \uBAA8\uB4C8\uC131 \uC900\uC218 \uC5EC\uBD80
17185
+ 3. \uBC1C\uACAC\uB41C \uACB0\uD568 \uC989\uC2DC \uC218\uC815 \uC9C0\uC2DC \uB610\uB294 \uB9AC\uD3EC\uD2B8
17186
+
17187
+ \uC774 \uC791\uC5C5\uC740 \uC804\uCCB4 \uD1B5\uD569 \uC804 \uBD80\uD488 \uB2E8\uC704\uC758 \uC644\uACB0\uC131\uC744 \uBCF4\uC7A5\uD558\uAE30 \uC704\uD568\uC785\uB2C8\uB2E4.`,
17188
+ parentSessionID: task.parentSessionID,
17189
+ depth: task.depth,
17190
+ groupID: task.groupID || task.id
17191
+ // Group reviews with their origins
17192
+ });
17193
+ } catch (error45) {
17194
+ log(`[MSVP] Failed to trigger review for ${task.id}:`, error45);
17195
+ }
17196
+ }
17197
+ }
17198
+ async recoverActiveTasks() {
17199
+ const tasks = await taskWAL.readAll();
17200
+ if (tasks.size === 0) return;
17201
+ log(`Attempting to recover ${tasks.size} tasks from WAL...`);
17202
+ let recoveredCount = 0;
17203
+ for (const task of tasks.values()) {
17204
+ if (task.status === TASK_STATUS.RUNNING) {
17205
+ try {
17206
+ const status = await this.client.session.get({ path: { id: task.sessionID } });
17207
+ if (!status.error) {
17208
+ this.store.set(task.id, task);
17209
+ this.store.trackPending(task.parentSessionID, task.id);
17210
+ const toastManager = getTaskToastManager();
17211
+ if (toastManager) {
17212
+ toastManager.addTask({
17213
+ id: task.id,
17214
+ description: task.description,
17215
+ agent: task.agent,
17216
+ isBackground: true,
17217
+ parentSessionID: task.parentSessionID,
17218
+ sessionID: task.sessionID
17219
+ });
17220
+ }
17221
+ recoveredCount++;
17222
+ }
17223
+ } catch {
17224
+ }
17225
+ } else {
17226
+ }
17227
+ }
17228
+ if (recoveredCount > 0) {
17229
+ log(`Recovered ${recoveredCount} active tasks.`);
17230
+ this.poller.start();
17231
+ }
17232
+ }
17233
+ };
17234
+ var parallelAgentManager = {
17235
+ getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager)
17236
+ };
17237
+
17238
+ // src/tools/slashCommand.ts
17239
+ var COMMANDER_SYSTEM_PROMPT = commander.systemPrompt;
17240
+ var MISSION_MODE_TEMPLATE = `${COMMANDER_SYSTEM_PROMPT}
17241
+
17242
+ <mission>
17243
+ <task>
17244
+ $ARGUMENTS
17245
+ </task>
17246
+
17247
+ <execution_rules>
17248
+ 1. Complete this mission without user intervention
17249
+ 2. Use your full capabilities: research, implement, verify
17250
+ 3. Output "${MISSION_SEAL.PATTERN}" when done
17251
+ </execution_rules>
17252
+ </mission>`;
17253
+ var COMMANDS = {
17254
+ "task": {
17255
+ description: "MISSION MODE - Execute task autonomously until complete",
17256
+ template: MISSION_MODE_TEMPLATE,
17257
+ argumentHint: '"mission goal"'
17258
+ },
17259
+ "plan": {
17260
+ description: "Create a task plan without executing",
17261
+ template: `<delegate>
17262
+ <agent>${AGENT_NAMES.PLANNER}</agent>
17263
+ <objective>Create parallel task plan for: $ARGUMENTS</objective>
17264
+ <success>Valid .opencode/todo.md with tasks, each having id, description, agent, size, dependencies</success>
17265
+ <must_do>
17266
+ - Maximize parallelism by grouping independent tasks
17267
+ - Assign correct agent to each task (${AGENT_NAMES.WORKER} or ${AGENT_NAMES.REVIEWER})
17268
+ - Include clear success criteria for each task
17269
+ - Research before planning if unfamiliar technology
17270
+ </must_do>
17271
+ <must_not>
17272
+ - Do not implement any tasks, only plan
17273
+ - Do not create tasks that depend on each other unnecessarily
17274
+ </must_not>
17275
+ <context>
17276
+ - This is planning only, no execution
17277
+ - Output to .opencode/todo.md
17278
+ </context>
17279
+ </delegate>`,
17280
+ argumentHint: '"complex task to plan"'
17281
+ },
17282
+ "agents": {
17283
+ description: "Show the 4-agent architecture",
17284
+ template: `## OpenCode Orchestrator - 4-Agent Architecture
17285
+
17286
+ | Agent | Role | Capabilities |
17287
+ |-------|------|--------------|
17288
+ | **${AGENT_NAMES.COMMANDER}** | [MASTER] | Master Orchestrator: mission control, parallel coordination |
17289
+ | **${AGENT_NAMES.PLANNER}** | [STRATEGIST] | Planning, research, documentation analysis |
17290
+ | **${AGENT_NAMES.WORKER}** | [EXECUTOR] | Implementation, coding, terminal tasks |
17291
+ | **${AGENT_NAMES.REVIEWER}** | [VERIFIER] | Verification, testing, context sanity checks |
17292
+
17293
+ ## Parallel Execution System
17294
+ \`\`\`
17295
+ Up to 50 Worker Sessions running simultaneously
17296
+ Max 10 per agent type (auto-queues excess)
17297
+ Auto-timeout: 60 min | Auto-cleanup: 30 min
17298
+ \`\`\`
17299
+
17300
+ ## Execution Flow
17301
+ \`\`\`
17302
+ THINK \u2192 PLAN \u2192 DELEGATE \u2192 EXECUTE \u2192 VERIFY \u2192 COMPLETE
17303
+ L1: Fast Track (simple fixes)
17304
+ L2: Normal Track (features)
17305
+ L3: Deep Track (complex refactoring)
17306
+ \`\`\`
17307
+
17308
+ ## Anti-Hallucination
17309
+ - ${AGENT_NAMES.PLANNER} researches BEFORE implementation
17310
+ - ${AGENT_NAMES.WORKER} caches official documentation
17311
+ - Never assumes - always verifies from sources
17312
+
17313
+ ## Usage
17314
+ - Select **${AGENT_NAMES.COMMANDER}** and type your request
17315
+ - Or use \`/task "your mission"\` explicitly
17316
+ - ${AGENT_NAMES.COMMANDER} automatically coordinates all agents`
17317
+ },
17318
+ "monitor": {
17319
+ description: "SYS MONITOR - Check real-time orchestration status and connections",
17320
+ template: "$DYNAMIC_MONITOR_STATUS"
17321
+ }
17322
+ };
17323
+ function createSlashcommandTool() {
17324
+ const commandList = Object.entries(COMMANDS).map(([name, cmd]) => {
17325
+ const hint = cmd.argumentHint ? ` ${cmd.argumentHint}` : "";
17326
+ return `- /${name}${hint}: ${cmd.description}`;
17327
+ }).join("\n");
17328
+ return tool({
17329
+ description: `Available commands:
17330
+ ${commandList}`,
17331
+ args: {
17332
+ command: tool.schema.string().describe("Command name (without slash)")
17333
+ },
17334
+ async execute(args) {
17335
+ const cmdName = (args.command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
17336
+ const cmdArgs = (args.command || "").replace(/^\/?\\S+\s*/, "");
17337
+ if (!cmdName) return `Commands:
17338
+ ${commandList}`;
17339
+ const command = COMMANDS[cmdName];
17340
+ if (!command) return `Unknown command: /${cmdName}
17341
+
17342
+ ${commandList}`;
17343
+ if (cmdName === "monitor") {
17344
+ try {
17345
+ const manager = ParallelAgentManager.getInstance();
17346
+ const stats2 = manager.getStats?.() || { running: 0, queued: 0, total: 0 };
17347
+ const concurrency = manager.getConcurrency();
17348
+ const toastManager = getTaskToastManager();
17349
+ const logPath = getLogPath();
17350
+ const activeTasks = manager.getRunningTasks();
17351
+ const taskList = activeTasks.length > 0 ? activeTasks.map((t) => `- [${t.agent}] ${t.description.slice(0, 40)}... (${manager.formatDuration(t.startedAt)})`).join("\n") : "No active background tasks.";
17352
+ const tuiConnected = toastManager?.client ? "\u2705 CONNECTED" : "\u274C DISCONNECTED";
17353
+ return `## \u{1F5A5}\uFE0F System Monitor: opencode-orchestrator
17354
+
17355
+ ### \u{1F684} Orchestration Status
17356
+ - **Active Workers**: ${stats2.running}
17357
+ - **Queued Tasks**: ${stats2.queued}
17358
+ - **Total Lifetime Tasks**: ${stats2.total}
17359
+
17360
+ ### \u{1F517} Connection Status
17361
+ - **TUI (Toast/UI)**: ${tuiConnected}
17362
+ - **Storage (WAL)**: \u2705 ENABLED
17363
+ - **Log Feed**: \`${logPath}\`
17364
+
17365
+ ### \u26A1 Concurrency Slots
17366
+ - **Default**: ${concurrency.getActiveCount("default")}/${concurrency.getConcurrencyLimit("default")}
17367
+ - **Worker**: ${concurrency.getActiveCount(AGENT_NAMES.WORKER)}/${concurrency.getConcurrencyLimit(AGENT_NAMES.WORKER)}
17368
+ - **Reviewer**: ${concurrency.getActiveCount(AGENT_NAMES.REVIEWER)}/${concurrency.getConcurrencyLimit(AGENT_NAMES.REVIEWER)}
17369
+
17370
+ ### \u{1F4CA} Active Background Swarm
17371
+ ${taskList}
17372
+
17373
+ ---
17374
+ *Use \`tail -f ${logPath}\` for real-time debug stream.*`;
17375
+ } catch (err) {
17376
+ return `Error fetching monitor status: ${err instanceof Error ? err.message : String(err)}`;
17377
+ }
17378
+ }
17379
+ return command.template.replace(/\$ARGUMENTS/g, cmdArgs || PROMPTS.CONTINUE_DEFAULT);
17380
+ }
17381
+ });
17382
+ }
17383
+
17384
+ // src/tools/rust.ts
17385
+ import { spawn } from "child_process";
17386
+ import { existsSync as existsSync2 } from "fs";
17387
+
17388
+ // src/utils/binary.ts
17389
+ import { join as join3, dirname as dirname2 } from "path";
17390
+ import { fileURLToPath } from "url";
17391
+ import { platform, arch } from "os";
17392
+ import { existsSync } from "fs";
17393
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
17394
+ function getBinaryPath() {
17395
+ const binDir = join3(__dirname, "..", "..", "bin");
17396
+ const os2 = platform();
17397
+ const cpu = arch();
17398
+ let binaryName;
17399
+ if (os2 === PLATFORM.WIN32) {
17400
+ binaryName = "orchestrator-windows-x64.exe";
17401
+ } else if (os2 === PLATFORM.DARWIN) {
17402
+ binaryName = cpu === "arm64" ? "orchestrator-macos-arm64" : "orchestrator-macos-x64";
17403
+ } else {
17404
+ binaryName = cpu === "arm64" ? "orchestrator-linux-arm64" : "orchestrator-linux-x64";
17405
+ }
17406
+ let binaryPath = join3(binDir, binaryName);
17407
+ if (!existsSync(binaryPath)) {
17408
+ binaryPath = join3(binDir, os2 === PLATFORM.WIN32 ? "orchestrator.exe" : "orchestrator");
17409
+ }
17410
+ return binaryPath;
17411
+ }
17412
+
17413
+ // src/tools/rust.ts
17414
+ async function callRustTool(name, args) {
17415
+ const binary = getBinaryPath();
17416
+ if (!existsSync2(binary)) {
17417
+ return JSON.stringify({ error: `Binary not found: ${binary}` });
17418
+ }
17419
+ return new Promise((resolve2) => {
17420
+ const proc = spawn(binary, ["serve"], { stdio: ["pipe", "pipe", "pipe"] });
17421
+ let stdout = "";
17422
+ proc.stdout.on("data", (data) => {
17423
+ stdout += data.toString();
17424
+ });
17425
+ proc.stderr.on("data", (data) => {
17426
+ const msg = data.toString().trim();
17427
+ if (msg) log(`[rust-stderr] ${msg}`);
17428
+ });
17429
+ const request = JSON.stringify({
17430
+ jsonrpc: "2.0",
17431
+ id: Date.now(),
17432
+ method: "tools/call",
17433
+ params: { name, arguments: args }
17434
+ });
17435
+ proc.stdin.write(request + "\n");
17436
+ proc.stdin.end();
17437
+ const timeout = setTimeout(() => {
17438
+ proc.kill();
17439
+ resolve2(JSON.stringify({ error: "Timeout" }));
17440
+ }, 6e4);
17441
+ proc.on("close", (code) => {
17442
+ clearTimeout(timeout);
17443
+ if (code !== 0 && code !== null) {
17444
+ log(`Rust process exited with code ${code}`);
17445
+ }
17446
+ try {
17447
+ const lines = stdout.trim().split("\n");
17448
+ for (let i = lines.length - 1; i >= 0; i--) {
17449
+ try {
17450
+ const response = JSON.parse(lines[i]);
17451
+ if (response.result || response.error) {
17452
+ const text = response?.result?.content?.[0]?.text;
17453
+ return resolve2(text || JSON.stringify(response.result));
17454
+ }
17455
+ } catch {
17456
+ continue;
17457
+ }
17458
+ }
17459
+ resolve2(stdout || "No output");
17460
+ } catch {
17461
+ resolve2(stdout || "No output");
17462
+ }
17463
+ });
17464
+ });
17465
+ }
17466
+
17467
+ // src/tools/search.ts
17468
+ var grepSearchTool = (directory) => tool({
17469
+ description: "Search code patterns using regex. Returns matching lines with file paths and line numbers.",
17470
+ args: {
17471
+ pattern: tool.schema.string().describe("Regex pattern to search for"),
17472
+ dir: tool.schema.string().optional().describe("Directory to search (defaults to project root)"),
17473
+ max_results: tool.schema.number().optional().describe("Max results (default: 100)"),
17474
+ timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds (default: 30000)")
17475
+ },
17476
+ async execute(args) {
17477
+ return callRustTool("grep_search", {
17478
+ pattern: args.pattern,
17479
+ directory: args.dir || directory,
17480
+ max_results: args.max_results,
17481
+ timeout_ms: args.timeout_ms
17482
+ });
17483
+ }
17484
+ });
17485
+ var globSearchTool = (directory) => tool({
17486
+ description: "Find files matching a glob pattern. Returns list of file paths.",
17487
+ args: {
17488
+ pattern: tool.schema.string().describe("Glob pattern (e.g., '**/*.ts', 'src/**/*.md')"),
17489
+ dir: tool.schema.string().optional().describe("Directory to search (defaults to project root)")
17490
+ },
17491
+ async execute(args) {
17492
+ return callRustTool("glob_search", {
17493
+ pattern: args.pattern,
17494
+ directory: args.dir || directory
17495
+ });
17496
+ }
17497
+ });
17498
+ var mgrepTool = (directory) => tool({
17499
+ description: `Search multiple patterns (runs grep for each pattern).`,
17500
+ args: {
17501
+ patterns: tool.schema.array(tool.schema.string()).describe("Array of regex patterns"),
17502
+ dir: tool.schema.string().optional().describe("Directory (defaults to project root)"),
17503
+ max_results_per_pattern: tool.schema.number().optional().describe("Max results per pattern (default: 50)"),
17504
+ timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds (default: 60000)")
17505
+ },
17506
+ async execute(args) {
17507
+ return callRustTool("mgrep", {
17508
+ patterns: args.patterns,
17509
+ directory: args.dir || directory,
17510
+ max_results_per_pattern: args.max_results_per_pattern,
17511
+ timeout_ms: args.timeout_ms
17512
+ });
17513
+ }
17514
+ });
17515
+ var sedReplaceTool = (directory) => tool({
17516
+ description: `Find and replace patterns in files (sed-like). Supports regex. Use dry_run=true to preview changes.`,
17517
+ args: {
17518
+ pattern: tool.schema.string().describe("Regex pattern to find"),
17519
+ replacement: tool.schema.string().describe("Replacement string"),
17520
+ file: tool.schema.string().optional().describe("Single file to modify"),
17521
+ dir: tool.schema.string().optional().describe("Directory to search (modifies all matching files)"),
17522
+ dry_run: tool.schema.boolean().optional().describe("Preview changes without modifying files (default: false)"),
17523
+ backup: tool.schema.boolean().optional().describe("Create .bak backup before modifying (default: false)"),
17524
+ timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds")
17525
+ },
17526
+ async execute(args) {
17527
+ return callRustTool("sed_replace", {
17528
+ pattern: args.pattern,
17529
+ replacement: args.replacement,
17530
+ file: args.file,
17531
+ directory: args.dir || (args.file ? void 0 : directory),
17532
+ dry_run: args.dry_run,
17533
+ backup: args.backup,
17534
+ timeout_ms: args.timeout_ms
17535
+ });
17536
+ }
17537
+ });
17538
+ var diffTool = () => tool({
17539
+ description: `Compare two files or strings and show differences.`,
17540
+ args: {
17541
+ file1: tool.schema.string().optional().describe("First file to compare"),
17542
+ file2: tool.schema.string().optional().describe("Second file to compare"),
17543
+ content1: tool.schema.string().optional().describe("First string to compare"),
17544
+ content2: tool.schema.string().optional().describe("Second string to compare"),
17545
+ ignore_whitespace: tool.schema.boolean().optional().describe("Ignore whitespace differences")
17546
+ },
17547
+ async execute(args) {
17548
+ return callRustTool("diff", args);
17549
+ }
17550
+ });
17551
+ var jqTool = () => tool({
17552
+ description: `Query and manipulate JSON using jq expressions.`,
17553
+ args: {
17554
+ json_input: tool.schema.string().optional().describe("JSON string to query"),
17555
+ file: tool.schema.string().optional().describe("JSON file to query"),
17556
+ expression: tool.schema.string().describe("jq expression (e.g., '.foo.bar', '.[] | select(.x > 1)')"),
17557
+ raw_output: tool.schema.boolean().optional().describe("Raw output (no JSON encoding for strings)")
17558
+ },
17559
+ async execute(args) {
17560
+ return callRustTool("jq", args);
17561
+ }
17562
+ });
17563
+ var httpTool = () => tool({
17564
+ description: `Make HTTP requests (GET, POST, PUT, DELETE, etc).`,
17565
+ args: {
17566
+ url: tool.schema.string().describe("URL to request"),
17567
+ method: tool.schema.string().optional().describe("HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD)"),
17568
+ headers: tool.schema.object({}).optional().describe("Request headers as JSON object"),
17569
+ body: tool.schema.string().optional().describe("Request body"),
17570
+ timeout_ms: tool.schema.number().optional().describe("Request timeout in milliseconds")
17571
+ },
17572
+ async execute(args) {
17573
+ return callRustTool("http", args);
17574
+ }
17575
+ });
17576
+ var fileStatsTool = (directory) => tool({
17577
+ description: `Analyze file/directory statistics (file counts, sizes, line counts, etc).`,
17578
+ args: {
17579
+ dir: tool.schema.string().optional().describe("Directory to analyze (defaults to project root)"),
17580
+ max_depth: tool.schema.number().optional().describe("Maximum directory depth to analyze")
17581
+ },
17582
+ async execute(args) {
17583
+ return callRustTool("file_stats", {
17584
+ directory: args.dir || directory,
17585
+ max_depth: args.max_depth
17586
+ });
17587
+ }
17588
+ });
17589
+ var gitDiffTool = (directory) => tool({
17590
+ description: `Show git diff of uncommitted changes.`,
17591
+ args: {
17592
+ dir: tool.schema.string().optional().describe("Repository directory (defaults to project root)"),
17593
+ staged_only: tool.schema.boolean().optional().describe("Show only staged changes")
17594
+ },
17595
+ async execute(args) {
17596
+ return callRustTool("git_diff", {
17597
+ directory: args.dir || directory,
17598
+ staged_only: args.staged_only
17586
17599
  });
17587
- if (this.onTaskComplete) {
17588
- Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
17589
- }
17590
- log(`Task ${task.id} completed via session.idle event (${formatDuration(task.startedAt, task.completedAt)})`);
17591
17600
  }
17592
- handleSessionDeleted(task) {
17593
- log(`Session deleted event for task ${task.id}`);
17594
- if (task.status === TASK_STATUS.RUNNING) {
17595
- task.status = TASK_STATUS.ERROR;
17596
- task.error = "Session deleted";
17597
- task.completedAt = /* @__PURE__ */ new Date();
17598
- }
17599
- if (task.concurrencyKey) {
17600
- this.concurrency.release(task.concurrencyKey);
17601
- this.concurrency.reportResult(task.concurrencyKey, false);
17602
- task.concurrencyKey = void 0;
17603
- }
17604
- this.store.untrackPending(task.parentSessionID, task.id);
17605
- this.store.clearNotificationsForTask(task.id);
17606
- this.store.delete(task.id);
17607
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17601
+ });
17602
+ var gitStatusTool = (directory) => tool({
17603
+ description: `Show git status (modified, added, deleted files).`,
17604
+ args: {
17605
+ dir: tool.schema.string().optional().describe("Repository directory (defaults to project root)")
17606
+ },
17607
+ async execute(args) {
17608
+ return callRustTool("git_status", {
17609
+ directory: args.dir || directory
17608
17610
  });
17609
- log(`Cleaned up deleted session task: ${task.id}`);
17610
17611
  }
17612
+ });
17613
+
17614
+ // src/core/commands/types/background-task-status.ts
17615
+ var BACKGROUND_TASK_STATUS = {
17616
+ PENDING: STATUS_LABEL.PENDING,
17617
+ RUNNING: STATUS_LABEL.RUNNING,
17618
+ DONE: STATUS_LABEL.DONE,
17619
+ ERROR: STATUS_LABEL.ERROR,
17620
+ TIMEOUT: STATUS_LABEL.TIMEOUT
17611
17621
  };
17612
17622
 
17613
- // src/core/agents/manager.ts
17614
- var ParallelAgentManager = class _ParallelAgentManager {
17623
+ // src/core/commands/manager.ts
17624
+ import { spawn as spawn2 } from "node:child_process";
17625
+ import { randomBytes } from "node:crypto";
17626
+ var BackgroundTaskManager = class _BackgroundTaskManager {
17615
17627
  static _instance;
17616
- store = new TaskStore();
17617
- client;
17618
- directory;
17619
- concurrency = new ConcurrencyController();
17620
- // Composed components
17621
- launcher;
17622
- resumer;
17623
- poller;
17624
- cleaner;
17625
- eventHandler;
17626
- constructor(client, directory) {
17627
- this.client = client;
17628
- this.directory = directory;
17629
- this.cleaner = new TaskCleaner(client, this.store, this.concurrency);
17630
- this.poller = new TaskPoller(
17631
- client,
17632
- this.store,
17633
- this.concurrency,
17634
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
17635
- (taskId) => this.cleaner.scheduleCleanup(taskId),
17636
- () => this.cleaner.pruneExpiredTasks(),
17637
- (task) => this.handleTaskComplete(task)
17638
- );
17639
- this.launcher = new TaskLauncher(
17640
- client,
17641
- directory,
17642
- this.store,
17643
- this.concurrency,
17644
- (taskId, error45) => this.handleTaskError(taskId, error45),
17645
- () => this.poller.start()
17646
- );
17647
- this.resumer = new TaskResumer(
17648
- client,
17649
- this.store,
17650
- (sessionID) => this.findBySession(sessionID),
17651
- () => this.poller.start(),
17652
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
17653
- );
17654
- this.eventHandler = new EventHandler(
17655
- client,
17656
- this.store,
17657
- this.concurrency,
17658
- (sessionID) => this.findBySession(sessionID),
17659
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
17660
- (taskId) => this.cleaner.scheduleCleanup(taskId),
17661
- (sessionID) => this.poller.validateSessionHasOutput(sessionID),
17662
- (task) => this.handleTaskComplete(task)
17663
- );
17664
- this.recoverActiveTasks().catch((err) => {
17665
- log("Recovery error:", err);
17666
- });
17628
+ tasks = /* @__PURE__ */ new Map();
17629
+ debugMode = process.env.DEBUG_BG_TASK === "true";
17630
+ // Disabled by default
17631
+ constructor() {
17667
17632
  }
17668
- static getInstance(client, directory) {
17669
- if (!_ParallelAgentManager._instance) {
17670
- if (!client || !directory) {
17671
- throw new Error("ParallelAgentManager requires client and directory on first call");
17672
- }
17673
- _ParallelAgentManager._instance = new _ParallelAgentManager(client, directory);
17633
+ static get instance() {
17634
+ if (!_BackgroundTaskManager._instance) {
17635
+ _BackgroundTaskManager._instance = new _BackgroundTaskManager();
17674
17636
  }
17675
- return _ParallelAgentManager._instance;
17676
- }
17677
- // ========================================================================
17678
- // Public API
17679
- // ========================================================================
17680
- async launch(inputs) {
17681
- this.cleaner.pruneExpiredTasks();
17682
- return this.launcher.launch(inputs);
17637
+ return _BackgroundTaskManager._instance;
17683
17638
  }
17684
- async resume(input) {
17685
- return this.resumer.resume(input);
17639
+ generateId() {
17640
+ return `${ID_PREFIX.JOB}${randomBytes(4).toString("hex")}`;
17686
17641
  }
17687
- getTask(id) {
17688
- return this.store.get(id);
17642
+ debug(taskId, message) {
17643
+ if (this.debugMode) {
17644
+ const ts = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
17645
+ log(`[BG ${ts}] ${taskId}: ${message}`);
17646
+ }
17689
17647
  }
17690
- getRunningTasks() {
17691
- return this.store.getRunning();
17648
+ run(options) {
17649
+ const id = this.generateId();
17650
+ const { command, cwd = process.cwd(), timeout = 3e5, label } = options;
17651
+ const isWindows = process.platform === PLATFORM.WIN32;
17652
+ const shell = isWindows ? "cmd.exe" : CLI_NAME.SH;
17653
+ const shellFlag = isWindows ? "/c" : "-c";
17654
+ const task = {
17655
+ id,
17656
+ command,
17657
+ args: [shellFlag, command],
17658
+ cwd,
17659
+ label,
17660
+ status: STATUS_LABEL.RUNNING,
17661
+ output: "",
17662
+ errorOutput: "",
17663
+ exitCode: null,
17664
+ startTime: Date.now(),
17665
+ timeout
17666
+ };
17667
+ this.tasks.set(id, task);
17668
+ this.debug(id, `Starting: ${command}`);
17669
+ try {
17670
+ const proc = spawn2(shell, task.args, {
17671
+ cwd,
17672
+ stdio: ["ignore", "pipe", "pipe"],
17673
+ detached: false
17674
+ });
17675
+ task.process = proc;
17676
+ proc.stdout?.on("data", (data) => {
17677
+ task.output += data.toString();
17678
+ });
17679
+ proc.stderr?.on("data", (data) => {
17680
+ task.errorOutput += data.toString();
17681
+ });
17682
+ proc.on("close", (code) => {
17683
+ task.exitCode = code;
17684
+ task.endTime = Date.now();
17685
+ task.status = code === 0 ? STATUS_LABEL.DONE : STATUS_LABEL.ERROR;
17686
+ task.process = void 0;
17687
+ this.debug(id, `Done (code=${code})`);
17688
+ });
17689
+ proc.on("error", (err) => {
17690
+ task.status = STATUS_LABEL.ERROR;
17691
+ task.errorOutput += `
17692
+ Process error: ${err.message}`;
17693
+ task.endTime = Date.now();
17694
+ task.process = void 0;
17695
+ });
17696
+ setTimeout(() => {
17697
+ if (task.status === STATUS_LABEL.RUNNING && task.process) {
17698
+ task.process.kill("SIGKILL");
17699
+ task.status = STATUS_LABEL.TIMEOUT;
17700
+ task.endTime = Date.now();
17701
+ this.debug(id, "Timeout");
17702
+ }
17703
+ }, timeout);
17704
+ } catch (err) {
17705
+ task.status = STATUS_LABEL.ERROR;
17706
+ task.errorOutput = `Spawn failed: ${err instanceof Error ? err.message : String(err)}`;
17707
+ task.endTime = Date.now();
17708
+ }
17709
+ return task;
17692
17710
  }
17693
- getAllTasks() {
17694
- return this.store.getAll();
17711
+ get(taskId) {
17712
+ return this.tasks.get(taskId);
17695
17713
  }
17696
- getTasksByParent(parentSessionID) {
17697
- return this.store.getByParent(parentSessionID);
17714
+ getAll() {
17715
+ return Array.from(this.tasks.values());
17698
17716
  }
17699
- async cancelTask(taskId) {
17700
- const task = this.store.get(taskId);
17701
- if (!task || task.status !== TASK_STATUS.RUNNING) return false;
17702
- task.status = TASK_STATUS.ERROR;
17703
- task.error = "Cancelled by user";
17704
- task.completedAt = /* @__PURE__ */ new Date();
17705
- if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
17706
- this.store.untrackPending(task.parentSessionID, taskId);
17707
- try {
17708
- await this.client.session.delete({ path: { id: task.sessionID } });
17709
- log(`Session ${task.sessionID.slice(0, 8)}... deleted`);
17710
- } catch {
17711
- log(`Session ${task.sessionID.slice(0, 8)}... already gone`);
17712
- }
17713
- this.cleaner.scheduleCleanup(taskId);
17714
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
17715
- });
17716
- log(`Cancelled ${taskId}`);
17717
- return true;
17717
+ getByStatus(status) {
17718
+ return this.getAll().filter((t) => t.status === status);
17718
17719
  }
17719
- async getResult(taskId) {
17720
- const task = this.store.get(taskId);
17721
- if (!task) return null;
17722
- if (task.result) return task.result;
17723
- if (task.status === TASK_STATUS.ERROR) return `Error: ${task.error}`;
17724
- if (task.status === TASK_STATUS.RUNNING) return null;
17725
- try {
17726
- const result = await this.client.session.messages({ path: { id: task.sessionID } });
17727
- if (result.error) return `Error: ${result.error}`;
17728
- const messages = result.data ?? [];
17729
- const lastMsg = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT).reverse()[0];
17730
- if (!lastMsg) return "(No response)";
17731
- const text = lastMsg.parts?.filter((p) => p.type === PART_TYPES.TEXT || p.type === PART_TYPES.REASONING).map((p) => p.text ?? "").filter(Boolean).join("\n") ?? "";
17732
- task.result = text;
17733
- return text;
17734
- } catch (error45) {
17735
- return `Error: ${error45 instanceof Error ? error45.message : String(error45)}`;
17720
+ clearCompleted() {
17721
+ let count = 0;
17722
+ for (const [id, task] of this.tasks) {
17723
+ if (task.status !== STATUS_LABEL.RUNNING && task.status !== STATUS_LABEL.PENDING) {
17724
+ this.tasks.delete(id);
17725
+ count++;
17726
+ }
17736
17727
  }
17728
+ return count;
17737
17729
  }
17738
- setConcurrencyLimit(agentType, limit) {
17739
- this.concurrency.setLimit(agentType, limit);
17740
- }
17741
- getPendingCount(parentSessionID) {
17742
- return this.store.getPendingCount(parentSessionID);
17743
- }
17744
- getConcurrency() {
17745
- return this.concurrency;
17746
- }
17747
- cleanup() {
17748
- this.poller.stop();
17749
- this.store.clear();
17750
- Promise.resolve().then(() => (init_store(), store_exports)).then((store) => store.clearAll()).catch(() => {
17751
- });
17730
+ kill(taskId) {
17731
+ const task = this.tasks.get(taskId);
17732
+ if (task?.process) {
17733
+ task.process.kill("SIGKILL");
17734
+ task.status = STATUS_LABEL.ERROR;
17735
+ task.errorOutput += "\nKilled by user";
17736
+ task.endTime = Date.now();
17737
+ return true;
17738
+ }
17739
+ return false;
17752
17740
  }
17753
- formatDuration = formatDuration;
17754
- // ========================================================================
17755
- // Event Handling
17756
- // ========================================================================
17757
- handleEvent(event) {
17758
- this.eventHandler.handle(event);
17741
+ formatDuration(task) {
17742
+ const end = task.endTime || Date.now();
17743
+ const seconds = (end - task.startTime) / 1e3;
17744
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
17745
+ return `${Math.floor(seconds / 60)}m ${(seconds % 60).toFixed(0)}s`;
17759
17746
  }
17760
- // ========================================================================
17761
- // Private Helpers
17762
- // ========================================================================
17763
- findBySession(sessionID) {
17764
- return this.store.getAll().find((t) => t.sessionID === sessionID);
17747
+ getStatusEmoji(status) {
17748
+ return getStatusIndicator(status);
17765
17749
  }
17766
- handleTaskError(taskId, error45) {
17767
- const task = this.store.get(taskId);
17768
- if (!task) return;
17769
- task.status = TASK_STATUS.ERROR;
17770
- task.error = error45 instanceof Error ? error45.message : String(error45);
17771
- task.completedAt = /* @__PURE__ */ new Date();
17772
- if (task.concurrencyKey) {
17773
- this.concurrency.release(task.concurrencyKey);
17774
- this.concurrency.reportResult(task.concurrencyKey, false);
17775
- }
17776
- this.store.untrackPending(task.parentSessionID, taskId);
17777
- this.cleaner.notifyParentIfAllComplete(task.parentSessionID);
17778
- this.cleaner.scheduleCleanup(taskId);
17779
- taskWAL.log(WAL_ACTIONS.UPDATE, task).catch(() => {
17750
+ };
17751
+ var backgroundTaskManager = BackgroundTaskManager.instance;
17752
+
17753
+ // src/tools/background-cmd/run.ts
17754
+ var runBackgroundTool = tool({
17755
+ description: `Run a shell command in the background and get a task ID.
17756
+
17757
+ <purpose>
17758
+ Execute long-running commands (builds, tests) without blocking.
17759
+ Use check_background to get results.
17760
+ </purpose>`,
17761
+ args: {
17762
+ command: tool.schema.string().describe("Shell command to execute"),
17763
+ cwd: tool.schema.string().optional().describe("Working directory"),
17764
+ timeout: tool.schema.number().optional().describe(`Timeout in ms (default: ${BACKGROUND_TASK.DEFAULT_TIMEOUT_MS})`),
17765
+ label: tool.schema.string().optional().describe("Task label")
17766
+ },
17767
+ async execute(args) {
17768
+ const { command, cwd, timeout, label } = args;
17769
+ const task = backgroundTaskManager.run({
17770
+ command,
17771
+ cwd: cwd || process.cwd(),
17772
+ timeout: timeout || BACKGROUND_TASK.DEFAULT_TIMEOUT_MS,
17773
+ label
17780
17774
  });
17775
+ const displayLabel = label ? ` (${label})` : "";
17776
+ return `\u{1F680} **Background Task Started**${displayLabel}
17777
+ | Task ID | \`${task.id}\` |
17778
+ | Command | \`${command}\` |
17779
+ | Status | ${backgroundTaskManager.getStatusEmoji(task.status)} ${task.status} |
17780
+
17781
+ \u{1F4CC} Use \`check_background({ taskId: "${task.id}" })\` to get results.`;
17781
17782
  }
17782
- async handleTaskComplete(task) {
17783
- if (task.agent === AGENT_NAMES.WORKER && task.mode !== "race") {
17784
- log(`[MSVP] Triggering 1\uCC28 \uB9AC\uBDF0 (Unit Review) for task ${task.id}`);
17785
- try {
17786
- await this.launch({
17787
- agent: AGENT_NAMES.REVIEWER,
17788
- description: `1\uCC28 \uB9AC\uBDF0: ${task.description}`,
17789
- prompt: `\uC9C4\uD589\uB41C \uC791\uC5C5(\`${task.description}\`)\uC5D0 \uB300\uD574 1\uCC28 \uB9AC\uBDF0(\uC720\uB2DB \uAC80\uC99D)\uB97C \uC218\uD589\uD558\uC138\uC694.
17790
- \uC8FC\uC694 \uC810\uAC80 \uC0AC\uD56D:
17791
- 1. \uD574\uB2F9 \uBAA8\uB4C8\uC758 \uC720\uB2DB \uD14C\uC2A4\uD2B8 \uCF54\uB4DC \uC791\uC131 \uC5EC\uBD80 \uBC0F \uD1B5\uACFC \uD655\uC778
17792
- 2. \uCF54\uB4DC \uD488\uC9C8 \uBC0F \uBAA8\uB4C8\uC131 \uC900\uC218 \uC5EC\uBD80
17793
- 3. \uBC1C\uACAC\uB41C \uACB0\uD568 \uC989\uC2DC \uC218\uC815 \uC9C0\uC2DC \uB610\uB294 \uB9AC\uD3EC\uD2B8
17783
+ });
17794
17784
 
17795
- \uC774 \uC791\uC5C5\uC740 \uC804\uCCB4 \uD1B5\uD569 \uC804 \uBD80\uD488 \uB2E8\uC704\uC758 \uC644\uACB0\uC131\uC744 \uBCF4\uC7A5\uD558\uAE30 \uC704\uD568\uC785\uB2C8\uB2E4.`,
17796
- parentSessionID: task.parentSessionID,
17797
- depth: task.depth,
17798
- groupID: task.groupID || task.id
17799
- // Group reviews with their origins
17800
- });
17801
- } catch (error45) {
17802
- log(`[MSVP] Failed to trigger review for ${task.id}:`, error45);
17785
+ // src/tools/background-cmd/check.ts
17786
+ var checkBackgroundTool = tool({
17787
+ description: `Check the status and output of a background task.`,
17788
+ args: {
17789
+ taskId: tool.schema.string().describe("Task ID from run_background"),
17790
+ tailLines: tool.schema.number().optional().describe("Limit output to last N lines")
17791
+ },
17792
+ async execute(args) {
17793
+ const { taskId, tailLines } = args;
17794
+ const task = backgroundTaskManager.get(taskId);
17795
+ if (!task) {
17796
+ const allTasks = backgroundTaskManager.getAll();
17797
+ if (allTasks.length === 0) {
17798
+ return `\u274C Task \`${taskId}\` not found. No background tasks exist.`;
17803
17799
  }
17800
+ const taskList = allTasks.map((t) => `- \`${t.id}\`: ${t.command.substring(0, 30)}...`).join("\n");
17801
+ return `\u274C Task \`${taskId}\` not found.
17802
+
17803
+ **Available:**
17804
+ ${taskList}`;
17804
17805
  }
17806
+ const duration3 = backgroundTaskManager.formatDuration(task);
17807
+ const statusEmoji = backgroundTaskManager.getStatusEmoji(task.status);
17808
+ let output = task.output;
17809
+ let stderr = task.errorOutput;
17810
+ if (tailLines && tailLines > 0) {
17811
+ output = output.split("\n").slice(-tailLines).join("\n");
17812
+ stderr = stderr.split("\n").slice(-tailLines).join("\n");
17813
+ }
17814
+ const maxLen = 1e4;
17815
+ if (output.length > maxLen) output = `[...truncated...]\\n` + output.slice(-maxLen);
17816
+ if (stderr.length > maxLen) stderr = `[...truncated...]\\n` + stderr.slice(-maxLen);
17817
+ let result = `${statusEmoji} **Task ${task.id}**${task.label ? ` (${task.label})` : ""}
17818
+ | Command | \`${task.command}\` |
17819
+ | Status | ${statusEmoji} **${task.status.toUpperCase()}** |
17820
+ | Duration | ${duration3}${task.status === STATUS_LABEL.RUNNING ? " (ongoing)" : ""} |
17821
+ ${task.exitCode !== null ? `| Exit Code | ${task.exitCode} |` : ""}`;
17822
+ if (output.trim()) result += `
17823
+
17824
+ \u{1F4E4} **stdout:**
17825
+ \`\`\`
17826
+ ${output.trim()}
17827
+ \`\`\``;
17828
+ if (stderr.trim()) result += `
17829
+
17830
+ \u26A0\uFE0F **stderr:**
17831
+ \`\`\`
17832
+ ${stderr.trim()}
17833
+ \`\`\``;
17834
+ if (task.status === STATUS_LABEL.RUNNING) result += `
17835
+
17836
+ \u23F3 Still running... check again.`;
17837
+ return result;
17805
17838
  }
17806
- async recoverActiveTasks() {
17807
- const tasks = await taskWAL.readAll();
17808
- if (tasks.size === 0) return;
17809
- log(`Attempting to recover ${tasks.size} tasks from WAL...`);
17810
- let recoveredCount = 0;
17811
- for (const task of tasks.values()) {
17812
- if (task.status === TASK_STATUS.RUNNING) {
17813
- try {
17814
- const status = await this.client.session.get({ path: { id: task.sessionID } });
17815
- if (!status.error) {
17816
- this.store.set(task.id, task);
17817
- this.store.trackPending(task.parentSessionID, task.id);
17818
- const toastManager = getTaskToastManager();
17819
- if (toastManager) {
17820
- toastManager.addTask({
17821
- id: task.id,
17822
- description: task.description,
17823
- agent: task.agent,
17824
- isBackground: true,
17825
- parentSessionID: task.parentSessionID,
17826
- sessionID: task.sessionID
17827
- });
17828
- }
17829
- recoveredCount++;
17830
- }
17831
- } catch {
17832
- }
17833
- } else {
17834
- }
17839
+ });
17840
+
17841
+ // src/tools/background-cmd/list.ts
17842
+ var listBackgroundTool = tool({
17843
+ description: `List all background tasks and their status.`,
17844
+ args: {
17845
+ status: tool.schema.enum([
17846
+ FILTER_STATUS.ALL,
17847
+ BACKGROUND_STATUS.RUNNING,
17848
+ BACKGROUND_STATUS.DONE,
17849
+ BACKGROUND_STATUS.ERROR
17850
+ ]).optional().describe("Filter by status")
17851
+ },
17852
+ async execute(args) {
17853
+ const { status = FILTER_STATUS.ALL } = args;
17854
+ let tasks;
17855
+ if (status === FILTER_STATUS.ALL) {
17856
+ tasks = backgroundTaskManager.getAll();
17857
+ } else {
17858
+ tasks = backgroundTaskManager.getByStatus(status);
17835
17859
  }
17836
- if (recoveredCount > 0) {
17837
- log(`Recovered ${recoveredCount} active tasks.`);
17838
- this.poller.start();
17860
+ if (tasks.length === 0) {
17861
+ return `No background tasks${status !== FILTER_STATUS.ALL ? ` with status "${status}"` : ""}`;
17862
+ }
17863
+ tasks.sort((a, b) => b.startTime - a.startTime);
17864
+ const rows = tasks.map((t) => {
17865
+ const indicator = backgroundTaskManager.getStatusEmoji(t.status);
17866
+ const cmd = t.command.length > 25 ? t.command.slice(0, 22) + "..." : t.command;
17867
+ const label = t.label ? ` [${t.label}]` : "";
17868
+ return `| \`${t.id}\` | ${indicator} ${t.status} | ${cmd}${label} | ${backgroundTaskManager.formatDuration(t)} |`;
17869
+ }).join("\n");
17870
+ const running = tasks.filter((t) => t.status === BACKGROUND_STATUS.RUNNING).length;
17871
+ const done = tasks.filter((t) => t.status === BACKGROUND_STATUS.DONE).length;
17872
+ const error45 = tasks.filter((t) => t.status === BACKGROUND_STATUS.ERROR || t.status === BACKGROUND_STATUS.TIMEOUT).length;
17873
+ return `Background Tasks (${tasks.length})
17874
+ Running: ${running} | Done: ${done} | Error: ${error45}
17875
+
17876
+ | ID | Status | Command | Duration |
17877
+ |----|--------|---------|----------|
17878
+ ${rows}`;
17879
+ }
17880
+ });
17881
+
17882
+ // src/tools/background-cmd/kill.ts
17883
+ var killBackgroundTool = tool({
17884
+ description: `Kill a running background task.`,
17885
+ args: {
17886
+ taskId: tool.schema.string().describe("Task ID to kill")
17887
+ },
17888
+ async execute(args) {
17889
+ const { taskId } = args;
17890
+ const task = backgroundTaskManager.get(taskId);
17891
+ if (!task) return `\u274C Task \`${taskId}\` not found.`;
17892
+ if (task.status !== STATUS_LABEL.RUNNING) return `\u26A0\uFE0F Task \`${taskId}\` is not running (${task.status}).`;
17893
+ const killed = backgroundTaskManager.kill(taskId);
17894
+ if (killed) {
17895
+ return `\u{1F6D1} Task \`${taskId}\` killed.
17896
+ Command: \`${task.command}\`
17897
+ Duration: ${backgroundTaskManager.formatDuration(task)}`;
17839
17898
  }
17899
+ return `\u26A0\uFE0F Could not kill task \`${taskId}\`.`;
17840
17900
  }
17841
- };
17842
- var parallelAgentManager = {
17843
- getInstance: ParallelAgentManager.getInstance.bind(ParallelAgentManager)
17844
- };
17901
+ });
17845
17902
 
17846
17903
  // src/tools/parallel/delegate-task.ts
17847
17904
  var MIN_IDLE_TIME_MS = PARALLEL_TASK.MIN_IDLE_TIME_MS;