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/README.md +3 -1
- package/dist/core/agents/manager.d.ts +8 -0
- package/dist/index.js +2072 -2015
- package/package.json +1 -1
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/
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
if (
|
|
15754
|
-
|
|
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
|
-
|
|
15757
|
-
|
|
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
|
-
|
|
15823
|
-
|
|
15824
|
-
|
|
15825
|
-
|
|
15826
|
-
|
|
15827
|
-
|
|
15828
|
-
|
|
15829
|
-
|
|
15830
|
-
|
|
15831
|
-
|
|
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
|
-
|
|
15836
|
-
|
|
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
|
-
|
|
15853
|
-
|
|
15854
|
-
|
|
15855
|
-
|
|
15856
|
-
|
|
15857
|
-
|
|
15858
|
-
|
|
15859
|
-
|
|
15860
|
-
|
|
15861
|
-
|
|
15862
|
-
|
|
15863
|
-
|
|
15864
|
-
|
|
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
|
-
|
|
15876
|
-
|
|
15877
|
-
|
|
15878
|
-
|
|
15879
|
-
|
|
15880
|
-
|
|
15881
|
-
|
|
15882
|
-
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
}
|
|
15888
|
-
|
|
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
|
-
|
|
15901
|
-
|
|
15902
|
-
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
|
|
15906
|
-
|
|
15907
|
-
|
|
15908
|
-
|
|
15909
|
-
|
|
15910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
return
|
|
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/
|
|
15961
|
-
import
|
|
15962
|
-
import
|
|
15963
|
-
var
|
|
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
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
15970
|
-
|
|
15971
|
-
if (
|
|
15972
|
-
|
|
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
|
-
|
|
15977
|
-
return
|
|
15753
|
+
get(id) {
|
|
15754
|
+
return this.tasks.get(id);
|
|
15978
15755
|
}
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
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
|
-
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
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
|
-
|
|
15801
|
+
this.notifications.set(task.parentSessionID, queue);
|
|
16047
15802
|
}
|
|
16048
|
-
|
|
16049
|
-
return this.
|
|
15803
|
+
getNotifications(parentSessionID) {
|
|
15804
|
+
return this.notifications.get(parentSessionID) ?? [];
|
|
16050
15805
|
}
|
|
16051
|
-
|
|
16052
|
-
|
|
15806
|
+
clearNotifications(parentSessionID) {
|
|
15807
|
+
this.notifications.delete(parentSessionID);
|
|
16053
15808
|
}
|
|
16054
|
-
|
|
16055
|
-
|
|
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
|
-
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
if (
|
|
16061
|
-
this.
|
|
16062
|
-
|
|
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
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16079
|
-
|
|
16080
|
-
|
|
16081
|
-
|
|
16082
|
-
|
|
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
|
-
|
|
16085
|
-
|
|
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/
|
|
16091
|
-
|
|
16092
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
16123
|
-
var
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
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
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
|
|
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
|
-
|
|
16152
|
-
|
|
16153
|
-
|
|
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
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
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
|
-
|
|
16174
|
-
|
|
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/
|
|
16179
|
-
var
|
|
16180
|
-
|
|
16181
|
-
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16185
|
-
|
|
16186
|
-
|
|
16187
|
-
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
|
|
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
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
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/
|
|
16220
|
-
var
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
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/
|
|
16241
|
-
var
|
|
16242
|
-
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
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
|
-
*
|
|
16260
|
-
* Priority: explicit limit > model > provider > agent > default
|
|
16141
|
+
* Set concurrency controller (can be set after init)
|
|
16261
16142
|
*/
|
|
16262
|
-
|
|
16263
|
-
|
|
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
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
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
|
-
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16292
|
-
|
|
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
|
-
|
|
16303
|
-
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
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
|
-
*
|
|
16179
|
+
* Get all running tasks (newest first)
|
|
16320
16180
|
*/
|
|
16321
|
-
|
|
16322
|
-
|
|
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
|
-
|
|
16348
|
-
|
|
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
|
-
|
|
16351
|
-
|
|
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
|
-
*
|
|
16197
|
+
* Format duration since task started
|
|
16355
16198
|
*/
|
|
16356
|
-
|
|
16357
|
-
const
|
|
16358
|
-
|
|
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 `
|
|
16217
|
+
return ` [${total}/${limit}]`;
|
|
16361
16218
|
}
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
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
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
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
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
|
|
16390
|
-
|
|
16391
|
-
|
|
16392
|
-
|
|
16393
|
-
|
|
16394
|
-
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
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
|
-
|
|
16414
|
-
|
|
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
|
-
|
|
16427
|
-
|
|
16428
|
-
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
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
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
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
|
-
*
|
|
16328
|
+
* Show progress toast (for long-running tasks)
|
|
16456
16329
|
*/
|
|
16457
|
-
|
|
16458
|
-
return
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
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
|
-
*
|
|
16468
|
-
* Archives old completed tasks to disk
|
|
16350
|
+
* Clear all tracked tasks
|
|
16469
16351
|
*/
|
|
16470
|
-
|
|
16471
|
-
|
|
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
|
-
*
|
|
16356
|
+
* Get task count stats
|
|
16495
16357
|
*/
|
|
16496
|
-
|
|
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
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
|
|
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
|
-
|
|
16510
|
-
|
|
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
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
}
|
|
16555
|
-
|
|
16556
|
-
|
|
16557
|
-
|
|
16558
|
-
|
|
16559
|
-
|
|
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
|
-
|
|
16448
|
+
/**
|
|
16449
|
+
* Compact the WAL by writing only the current active tasks
|
|
16450
|
+
*/
|
|
16451
|
+
async compact(activeTasks) {
|
|
16602
16452
|
try {
|
|
16603
|
-
|
|
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
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
|
|
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
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
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
|
-
|
|
16622
|
-
}
|
|
16595
|
+
};
|
|
16623
16596
|
|
|
16624
|
-
// src/core/
|
|
16625
|
-
var
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
});
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
});
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
})
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
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/
|
|
16751
|
-
var
|
|
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/
|
|
16754
|
-
var
|
|
16755
|
-
|
|
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.
|
|
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
|
-
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
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
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
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
|
-
|
|
16856
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
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
|
-
|
|
16862
|
-
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
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
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
if (
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
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
|
-
|
|
16894
|
-
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
16900
|
-
|
|
16901
|
-
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
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
|
-
|
|
16916
|
-
|
|
16788
|
+
}
|
|
16789
|
+
};
|
|
16917
16790
|
|
|
16918
|
-
|
|
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
|
-
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
16934
|
-
|
|
16935
|
-
const
|
|
16936
|
-
if (
|
|
16937
|
-
const
|
|
16938
|
-
const
|
|
16939
|
-
const
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
|
|
16949
|
-
|
|
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
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
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
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
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
|
-
|
|
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("
|
|
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/
|
|
17095
|
-
var
|
|
17096
|
-
constructor(client,
|
|
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.
|
|
17102
|
-
this.
|
|
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
|
-
*
|
|
17106
|
-
*
|
|
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
|
-
|
|
17110
|
-
const
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
log(
|
|
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
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
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
|
|
17306
|
-
|
|
17307
|
-
|
|
17308
|
-
|
|
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
|
-
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
|
|
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
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
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
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
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.
|
|
17422
|
-
this.
|
|
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
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17050
|
+
return _ParallelAgentManager._instance;
|
|
17455
17051
|
}
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
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
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
|
|
17497
|
-
}
|
|
17498
|
-
|
|
17499
|
-
|
|
17500
|
-
|
|
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.
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17564
|
-
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
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
|
-
|
|
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,
|
|
17579
|
-
task.concurrencyKey = void 0;
|
|
17166
|
+
this.concurrency.reportResult(task.concurrencyKey, false);
|
|
17580
17167
|
}
|
|
17581
|
-
this.store.untrackPending(task.parentSessionID,
|
|
17582
|
-
this.
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
|
|
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
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
|
|
17596
|
-
|
|
17597
|
-
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
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/
|
|
17614
|
-
|
|
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
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
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
|
|
17669
|
-
if (!
|
|
17670
|
-
|
|
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
|
|
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
|
-
|
|
17685
|
-
return
|
|
17639
|
+
generateId() {
|
|
17640
|
+
return `${ID_PREFIX.JOB}${randomBytes(4).toString("hex")}`;
|
|
17686
17641
|
}
|
|
17687
|
-
|
|
17688
|
-
|
|
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
|
-
|
|
17691
|
-
|
|
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
|
-
|
|
17694
|
-
return this.
|
|
17711
|
+
get(taskId) {
|
|
17712
|
+
return this.tasks.get(taskId);
|
|
17695
17713
|
}
|
|
17696
|
-
|
|
17697
|
-
return this.
|
|
17714
|
+
getAll() {
|
|
17715
|
+
return Array.from(this.tasks.values());
|
|
17698
17716
|
}
|
|
17699
|
-
|
|
17700
|
-
|
|
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
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
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
|
-
|
|
17739
|
-
this.
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
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
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
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
|
-
|
|
17762
|
-
// ========================================================================
|
|
17763
|
-
findBySession(sessionID) {
|
|
17764
|
-
return this.store.getAll().find((t) => t.sessionID === sessionID);
|
|
17747
|
+
getStatusEmoji(status) {
|
|
17748
|
+
return getStatusIndicator(status);
|
|
17765
17749
|
}
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
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
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
|
|
17824
|
-
|
|
17825
|
-
|
|
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 (
|
|
17837
|
-
|
|
17838
|
-
|
|
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;
|