oh-my-opencode-slim 2.0.0-beta.11 → 2.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/constants.d.ts +3 -0
- package/dist/index.js +727 -97
- package/dist/tools/cancel-task.d.ts +6 -0
- package/dist/tui.js +15 -0
- package/dist/utils/background-job-board.d.ts +10 -1
- package/dist/utils/task.d.ts +2 -0
- package/package.json +1 -1
|
@@ -21,6 +21,9 @@ export declare const MAX_POLL_TIME_MS: number;
|
|
|
21
21
|
export declare const FALLBACK_FAILOVER_TIMEOUT_MS = 15000;
|
|
22
22
|
export declare const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
23
23
|
export declare const PHASE_REMINDER_TEXT = "!IMPORTANT! Scheduler workflow: plan lanes/dependencies \u2192 dispatch background specialists \u2192 track task IDs \u2192 wait for hook-driven completion or use task_status only when needed \u2192 reconcile terminal results \u2192 verify. Do not consume running-job output or advance dependent work. !END!";
|
|
24
|
+
export declare const WRITABLE_FILE_OPERATIONS_RULES = "**File Operations Rules**:\n- Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.\n- Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.\n- Shell is acceptable for bulk or mechanical filesystem changes when it is clearer or safer than many individual edits (for example: truncate generated logs, remove build artifacts, batch rename/move files), especially when the user explicitly asks for that shell operation.\n- Before destructive or broad shell operations, verify the target set and quote paths. Prefer a dry-run/listing first when practical.\n- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.";
|
|
25
|
+
export declare const READONLY_FILE_OPERATIONS_RULES = "**File Operations Rules**:\n- READ-ONLY: inspect and report; do not modify files.\n- Prefer dedicated file tools for codebase inspection: glob/grep/ast_grep_search for discovery and read for file contents.\n- Bash is allowed for non-mutating diagnostics and shell-native inspection when it is the clearest tool, but not for modifying files.\n- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.";
|
|
26
|
+
export declare const NO_SHELL_READONLY_FILE_OPERATIONS_RULES = "**File Operations Rules**:\n- READ-ONLY: inspect and report; do not modify files.\n- Use glob/grep/ast_grep_search for discovery and read for file contents.\n- Do not use bash or shell commands.";
|
|
24
27
|
export declare const TMUX_SPAWN_DELAY_MS = 500;
|
|
25
28
|
export declare const COUNCILLOR_STAGGER_MS = 250;
|
|
26
29
|
export declare const STABLE_POLLS_THRESHOLD = 3;
|
package/dist/index.js
CHANGED
|
@@ -18309,6 +18309,21 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
|
18309
18309
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
18310
18310
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
18311
18311
|
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion or use task_status only when needed → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
|
|
18312
|
+
var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
18313
|
+
- Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
|
|
18314
|
+
- Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
|
|
18315
|
+
- Shell is acceptable for bulk or mechanical filesystem changes when it is clearer or safer than many individual edits (for example: truncate generated logs, remove build artifacts, batch rename/move files), especially when the user explicitly asks for that shell operation.
|
|
18316
|
+
- Before destructive or broad shell operations, verify the target set and quote paths. Prefer a dry-run/listing first when practical.
|
|
18317
|
+
- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
|
|
18318
|
+
var READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
18319
|
+
- READ-ONLY: inspect and report; do not modify files.
|
|
18320
|
+
- Prefer dedicated file tools for codebase inspection: glob/grep/ast_grep_search for discovery and read for file contents.
|
|
18321
|
+
- Bash is allowed for non-mutating diagnostics and shell-native inspection when it is the clearest tool, but not for modifying files.
|
|
18322
|
+
- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
|
|
18323
|
+
var NO_SHELL_READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
18324
|
+
- READ-ONLY: inspect and report; do not modify files.
|
|
18325
|
+
- Use glob/grep/ast_grep_search for discovery and read for file contents.
|
|
18326
|
+
- Do not use bash or shell commands.`;
|
|
18312
18327
|
var TMUX_SPAWN_DELAY_MS = 500;
|
|
18313
18328
|
var COUNCILLOR_STAGGER_MS = 250;
|
|
18314
18329
|
var DEFAULT_DISABLED_AGENTS = ["observer"];
|
|
@@ -19093,12 +19108,7 @@ Review available agents and lane rules.
|
|
|
19093
19108
|
- Do not immediately wait after spawning independent background tasks unless the next step truly depends on their result
|
|
19094
19109
|
- Reconcile results, resolve conflicts, and gate dependent lanes
|
|
19095
19110
|
|
|
19096
|
-
|
|
19097
|
-
- Always use dedicated file tools for file I/O.
|
|
19098
|
-
- Search files/code with \`glob\`, \`grep\`, or \`ast_grep_search\`.
|
|
19099
|
-
- Read files with \`read\`. Never use \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or bash commands to read file contents.
|
|
19100
|
-
- Edit files with \`apply_patch\`. Never use shell redirection, \`echo\`, \`printf\`, or heredocs for file content unless no file tool can do the job.
|
|
19101
|
-
- Use \`bash\` only for execution: git, package managers, tests, builds, scripts, or diagnostics.
|
|
19111
|
+
${WRITABLE_FILE_OPERATIONS_RULES}
|
|
19102
19112
|
|
|
19103
19113
|
## 4. Plan and Parallelize
|
|
19104
19114
|
Build a short work graph before dispatching:
|
|
@@ -19120,12 +19130,15 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
|
|
|
19120
19130
|
- Continue orchestration while tasks run only when useful: planning, scheduling independent lanes, preparing synthesis, or asking needed user questions.
|
|
19121
19131
|
- If no useful independent work remains, stop after a brief status response; do not call \`task_status\` just to wait. OpenCode will resume you when the background completion event arrives.
|
|
19122
19132
|
- Use \`task_status(wait: true, timeout_ms: ...)\` only when you actively need a result before a dependent step or final response and no completion event has arrived yet.
|
|
19133
|
+
- If \`task_status(wait: true)\` times out and reports the task still \`running\`, the delegated lane is still owned by that specialist. Do not treat the timeout as failure, cancellation, or permission to do the same work yourself.
|
|
19134
|
+
- For dependent work, either call \`task_status(wait: true)\` again with the same reasonable interval, or stop with a brief waiting status and let the completion event resume you.
|
|
19123
19135
|
- Parallel background tasks are allowed only when their write scopes do not conflict.
|
|
19124
19136
|
- Final response requires relevant tasks to be terminal and reconciled.
|
|
19125
19137
|
|
|
19126
19138
|
### Background Job Discipline
|
|
19127
19139
|
- Every background task owns its declared lane until terminal.
|
|
19128
19140
|
- Do not duplicate, undermine, or race a running lane.
|
|
19141
|
+
- A polling timeout is not terminal. The lane remains running until a terminal completion/error/cancel event is observed or the user explicitly cancels it.
|
|
19129
19142
|
- After dispatch, classify the next step:
|
|
19130
19143
|
1. independent: continue,
|
|
19131
19144
|
2. dependent: wait/poll,
|
|
@@ -19255,11 +19268,7 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
|
|
|
19255
19268
|
- Be transparent about trade-offs when different approaches have valid pros/cons
|
|
19256
19269
|
- Don't just average responses — choose the best approach and improve upon it
|
|
19257
19270
|
|
|
19258
|
-
|
|
19259
|
-
- Use dedicated tools for file I/O if local files must be inspected
|
|
19260
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19261
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19262
|
-
- Use bash only for execution/diagnostics, never for file I/O
|
|
19271
|
+
${READONLY_FILE_OPERATIONS_RULES}
|
|
19263
19272
|
|
|
19264
19273
|
**Required Output Format**:
|
|
19265
19274
|
Always include these sections in your final response:
|
|
@@ -19372,11 +19381,7 @@ var COUNCILLOR_PROMPT = `You are a councillor in a multi-model council.
|
|
|
19372
19381
|
|
|
19373
19382
|
You CANNOT edit files, write files, run shell commands, or delegate to other agents. You are an advisor, not an implementer.
|
|
19374
19383
|
|
|
19375
|
-
|
|
19376
|
-
- READ-ONLY: do not modify files
|
|
19377
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19378
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19379
|
-
- Do not use bash or shell commands
|
|
19384
|
+
${NO_SHELL_READONLY_FILE_OPERATIONS_RULES}
|
|
19380
19385
|
|
|
19381
19386
|
**Behavior**:
|
|
19382
19387
|
- **Examine the codebase** before answering — your read access is what makes council valuable. Don't guess at code you can see.
|
|
@@ -19466,12 +19471,7 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
|
|
|
19466
19471
|
- Prioritize visual excellence—code perfection comes second
|
|
19467
19472
|
- Use grounded, normal, regular english - don't use jargon or overly technical language
|
|
19468
19473
|
|
|
19469
|
-
|
|
19470
|
-
- Always use dedicated file tools for file I/O
|
|
19471
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19472
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19473
|
-
- Edit/write files with write, edit, or apply_patch. Never use shell redirection, echo, printf, or heredocs for file content unless no file tool can do the job
|
|
19474
|
-
- Use bash only for execution: git, package managers, tests, builds, scripts, or diagnostics
|
|
19474
|
+
${WRITABLE_FILE_OPERATIONS_RULES}
|
|
19475
19475
|
|
|
19476
19476
|
## Review Responsibilities
|
|
19477
19477
|
- Review existing UI for usability, responsiveness, visual consistency, and polish when asked
|
|
@@ -19510,11 +19510,7 @@ var EXPLORER_PROMPT = `You are Explorer - a fast codebase navigation specialist.
|
|
|
19510
19510
|
- **Structural patterns** (function shapes, class structures): ast_grep_search
|
|
19511
19511
|
- **File discovery** (find by name/extension): glob
|
|
19512
19512
|
|
|
19513
|
-
|
|
19514
|
-
- READ-ONLY: Search and report, don't modify files
|
|
19515
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19516
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19517
|
-
- Use bash only for execution/diagnostics, never for file I/O
|
|
19513
|
+
${READONLY_FILE_OPERATIONS_RULES}
|
|
19518
19514
|
|
|
19519
19515
|
**Behavior**:
|
|
19520
19516
|
- Be fast and thorough
|
|
@@ -19570,12 +19566,7 @@ var FIXER_PROMPT = `You are Fixer - a fast, focused implementation specialist.
|
|
|
19570
19566
|
- Run relevant validation when requested or clearly applicable (otherwise note as skipped with reason)
|
|
19571
19567
|
- Report completion with summary of changes
|
|
19572
19568
|
|
|
19573
|
-
|
|
19574
|
-
- Always use dedicated file tools for file I/O
|
|
19575
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19576
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19577
|
-
- Edit/write files with write, edit, or apply_patch. Never use shell redirection, echo, printf, or heredocs for file content unless no file tool can do the job
|
|
19578
|
-
- Use bash only for execution: git, package managers, tests, builds, scripts, or diagnostics
|
|
19569
|
+
${WRITABLE_FILE_OPERATIONS_RULES}
|
|
19579
19570
|
|
|
19580
19571
|
**Constraints**:
|
|
19581
19572
|
- NO external research (no websearch, context7, grep_app)
|
|
@@ -19642,11 +19633,7 @@ var LIBRARIAN_PROMPT = `You are Librarian - a research specialist for codebases
|
|
|
19642
19633
|
- grep_app: Search GitHub repositories
|
|
19643
19634
|
- websearch: General web search for docs
|
|
19644
19635
|
|
|
19645
|
-
|
|
19646
|
-
- Use dedicated tools for file I/O when local files must be inspected
|
|
19647
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19648
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19649
|
-
- Use bash only for execution/diagnostics, never for file I/O
|
|
19636
|
+
${READONLY_FILE_OPERATIONS_RULES}
|
|
19650
19637
|
|
|
19651
19638
|
**Behavior**:
|
|
19652
19639
|
- Provide evidence-based answers with sources
|
|
@@ -19693,10 +19680,7 @@ var OBSERVER_PROMPT = `You are Observer — a visual analysis specialist.
|
|
|
19693
19680
|
- Match the language of the request
|
|
19694
19681
|
- If info not found, state clearly what's missing
|
|
19695
19682
|
|
|
19696
|
-
|
|
19697
|
-
- READ-ONLY: do not modify files
|
|
19698
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19699
|
-
- Use bash only for execution/diagnostics, never for file I/O
|
|
19683
|
+
${READONLY_FILE_OPERATIONS_RULES}
|
|
19700
19684
|
`;
|
|
19701
19685
|
function createObserverAgent(model, customPrompt, customAppendPrompt) {
|
|
19702
19686
|
let prompt = OBSERVER_PROMPT;
|
|
@@ -19742,11 +19726,7 @@ var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor and code rev
|
|
|
19742
19726
|
- Focus on strategy, not execution
|
|
19743
19727
|
- Point to specific files/lines when relevant
|
|
19744
19728
|
|
|
19745
|
-
|
|
19746
|
-
- READ-ONLY: do not modify files
|
|
19747
|
-
- Search files/code with glob, grep, or ast_grep_search
|
|
19748
|
-
- Read files with read. Never use cat, head, tail, sed, awk, or bash commands to read file contents
|
|
19749
|
-
- Use bash only for execution/diagnostics, never for file I/O
|
|
19729
|
+
${READONLY_FILE_OPERATIONS_RULES}
|
|
19750
19730
|
`;
|
|
19751
19731
|
function createOracleAgent(model, customPrompt, customAppendPrompt) {
|
|
19752
19732
|
let prompt = ORACLE_PROMPT;
|
|
@@ -22662,6 +22642,10 @@ function createDisplayNameMentionRewriter(config) {
|
|
|
22662
22642
|
};
|
|
22663
22643
|
}
|
|
22664
22644
|
// src/utils/task.ts
|
|
22645
|
+
var TRANSIENT_PROCESS_ERROR_TEXT = new Set([
|
|
22646
|
+
"Task is not running in this process and has no final output.",
|
|
22647
|
+
"Task is not running in this process and has not produced output."
|
|
22648
|
+
]);
|
|
22665
22649
|
function parseTaskIdFromTaskOutput(output) {
|
|
22666
22650
|
const lines = output.split(/\r?\n/);
|
|
22667
22651
|
for (const line of lines) {
|
|
@@ -22697,6 +22681,19 @@ function parseTaskStatusOutput(output) {
|
|
|
22697
22681
|
result: parseTaskResultFromOutput(output)
|
|
22698
22682
|
};
|
|
22699
22683
|
}
|
|
22684
|
+
function classifyTaskStatusOutput(status) {
|
|
22685
|
+
if (status.timedOut)
|
|
22686
|
+
return "timeout";
|
|
22687
|
+
if (status.state === "running")
|
|
22688
|
+
return "running";
|
|
22689
|
+
if (status.state === "completed" || status.state === "cancelled") {
|
|
22690
|
+
return "terminal";
|
|
22691
|
+
}
|
|
22692
|
+
if (TRANSIENT_PROCESS_ERROR_TEXT.has(status.result ?? "")) {
|
|
22693
|
+
return "transient_process_error";
|
|
22694
|
+
}
|
|
22695
|
+
return "unknown_error";
|
|
22696
|
+
}
|
|
22700
22697
|
function parseTaskStateFromOutput(output) {
|
|
22701
22698
|
for (const line of getTaskHeader(output).split(/\r?\n/)) {
|
|
22702
22699
|
const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
|
|
@@ -22755,11 +22752,15 @@ class BackgroundJobBoard {
|
|
|
22755
22752
|
objective: input.objective ?? existing.objective,
|
|
22756
22753
|
state: "running",
|
|
22757
22754
|
timedOut: false,
|
|
22755
|
+
statusUncertain: false,
|
|
22756
|
+
cancellationRequested: false,
|
|
22758
22757
|
terminalUnreconciled: false,
|
|
22759
22758
|
completedAt: undefined,
|
|
22760
22759
|
resultSummary: undefined,
|
|
22760
|
+
lastStatusError: undefined,
|
|
22761
22761
|
terminalState: undefined,
|
|
22762
22762
|
lastLaunchedAt: now,
|
|
22763
|
+
lastLiveBusyAt: now,
|
|
22763
22764
|
lastUsedAt: now,
|
|
22764
22765
|
updatedAt: now
|
|
22765
22766
|
};
|
|
@@ -22774,9 +22775,12 @@ class BackgroundJobBoard {
|
|
|
22774
22775
|
objective: input.objective,
|
|
22775
22776
|
state: "running",
|
|
22776
22777
|
timedOut: false,
|
|
22778
|
+
statusUncertain: false,
|
|
22779
|
+
cancellationRequested: false,
|
|
22777
22780
|
terminalUnreconciled: false,
|
|
22778
22781
|
launchedAt: now,
|
|
22779
22782
|
lastLaunchedAt: now,
|
|
22783
|
+
lastLiveBusyAt: now,
|
|
22780
22784
|
lastUsedAt: now,
|
|
22781
22785
|
updatedAt: now,
|
|
22782
22786
|
alias: this.nextAlias(input.parentSessionID, input.agent),
|
|
@@ -22798,11 +22802,13 @@ class BackgroundJobBoard {
|
|
|
22798
22802
|
...existing,
|
|
22799
22803
|
state: input.state,
|
|
22800
22804
|
timedOut: input.timedOut ?? false,
|
|
22805
|
+
statusUncertain: input.statusUncertain ?? false,
|
|
22801
22806
|
terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
|
|
22802
22807
|
updatedAt: now,
|
|
22803
22808
|
completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
|
|
22804
22809
|
terminalState: terminal ? input.state : existing.terminalState,
|
|
22805
|
-
resultSummary: input.resultSummary ?? existing.resultSummary
|
|
22810
|
+
resultSummary: input.resultSummary ?? existing.resultSummary,
|
|
22811
|
+
lastStatusError: input.lastStatusError
|
|
22806
22812
|
};
|
|
22807
22813
|
this.jobs.set(input.taskID, updated);
|
|
22808
22814
|
this.trimReusable(input.taskID);
|
|
@@ -22819,6 +22825,36 @@ class BackgroundJobBoard {
|
|
|
22819
22825
|
resultSummary: status.result
|
|
22820
22826
|
});
|
|
22821
22827
|
}
|
|
22828
|
+
markRunningFromLiveSession(taskID, now = Date.now()) {
|
|
22829
|
+
const existing = this.jobs.get(taskID);
|
|
22830
|
+
if (!existing)
|
|
22831
|
+
return;
|
|
22832
|
+
const isStaleTerminal = TERMINAL_STATES.has(existing.state) || existing.state === "reconciled";
|
|
22833
|
+
if (!isStaleTerminal || existing.cancellationRequested) {
|
|
22834
|
+
const updated2 = {
|
|
22835
|
+
...existing,
|
|
22836
|
+
lastLiveBusyAt: now
|
|
22837
|
+
};
|
|
22838
|
+
this.jobs.set(taskID, updated2);
|
|
22839
|
+
return updated2;
|
|
22840
|
+
}
|
|
22841
|
+
const updated = {
|
|
22842
|
+
...existing,
|
|
22843
|
+
state: "running",
|
|
22844
|
+
timedOut: false,
|
|
22845
|
+
statusUncertain: false,
|
|
22846
|
+
cancellationRequested: false,
|
|
22847
|
+
terminalUnreconciled: false,
|
|
22848
|
+
updatedAt: now,
|
|
22849
|
+
lastLiveBusyAt: now,
|
|
22850
|
+
completedAt: undefined,
|
|
22851
|
+
terminalState: undefined,
|
|
22852
|
+
resultSummary: undefined,
|
|
22853
|
+
lastStatusError: undefined
|
|
22854
|
+
};
|
|
22855
|
+
this.jobs.set(taskID, updated);
|
|
22856
|
+
return updated;
|
|
22857
|
+
}
|
|
22822
22858
|
markReconciled(taskID, now = Date.now()) {
|
|
22823
22859
|
const existing = this.jobs.get(taskID);
|
|
22824
22860
|
if (!existing)
|
|
@@ -22830,6 +22866,7 @@ class BackgroundJobBoard {
|
|
|
22830
22866
|
...existing,
|
|
22831
22867
|
state: "reconciled",
|
|
22832
22868
|
terminalUnreconciled: false,
|
|
22869
|
+
statusUncertain: false,
|
|
22833
22870
|
updatedAt: now,
|
|
22834
22871
|
lastUsedAt: now,
|
|
22835
22872
|
terminalState: existing.terminalState ?? terminalStateOf(existing.state)
|
|
@@ -22838,24 +22875,29 @@ class BackgroundJobBoard {
|
|
|
22838
22875
|
this.trimReusable(taskID);
|
|
22839
22876
|
return updated;
|
|
22840
22877
|
}
|
|
22841
|
-
markCancelled(taskID, reason, now = Date.now()) {
|
|
22878
|
+
markCancelled(taskID, reason, now = Date.now(), options = {}) {
|
|
22842
22879
|
const existing = this.jobs.get(taskID);
|
|
22843
22880
|
if (!existing)
|
|
22844
22881
|
return;
|
|
22845
|
-
if (
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22882
|
+
if (!options.force) {
|
|
22883
|
+
if (existing.state === "reconciled")
|
|
22884
|
+
return existing;
|
|
22885
|
+
if (TERMINAL_STATES.has(existing.state))
|
|
22886
|
+
return existing;
|
|
22887
|
+
}
|
|
22849
22888
|
const summary = normalizeCancelReason(reason);
|
|
22850
22889
|
const updated = {
|
|
22851
22890
|
...existing,
|
|
22852
22891
|
state: "cancelled",
|
|
22853
22892
|
timedOut: false,
|
|
22893
|
+
statusUncertain: false,
|
|
22894
|
+
cancellationRequested: true,
|
|
22854
22895
|
terminalUnreconciled: true,
|
|
22855
22896
|
updatedAt: now,
|
|
22856
22897
|
completedAt: existing.completedAt ?? now,
|
|
22857
22898
|
terminalState: "cancelled",
|
|
22858
|
-
resultSummary: summary
|
|
22899
|
+
resultSummary: summary,
|
|
22900
|
+
lastStatusError: undefined
|
|
22859
22901
|
};
|
|
22860
22902
|
this.jobs.set(taskID, updated);
|
|
22861
22903
|
return updated;
|
|
@@ -22925,7 +22967,7 @@ class BackgroundJobBoard {
|
|
|
22925
22967
|
return [
|
|
22926
22968
|
"### Background Job Board",
|
|
22927
22969
|
"SENTINEL: background-job-board-v2",
|
|
22928
|
-
"Use task_status for running jobs. Reconcile terminal jobs before final response. Reuse only completed
|
|
22970
|
+
"Use task_status for running jobs. Reconcile terminal jobs before final response. Reuse only completed sessions for the same specialist/context; never reuse cancelled or errored sessions.",
|
|
22929
22971
|
"",
|
|
22930
22972
|
"#### Active / Unreconciled",
|
|
22931
22973
|
...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
|
|
@@ -22953,8 +22995,10 @@ class BackgroundJobBoard {
|
|
|
22953
22995
|
}
|
|
22954
22996
|
}
|
|
22955
22997
|
formatReusableJob(job) {
|
|
22998
|
+
const terminal = job.terminalState ?? terminalStateOf(job.state);
|
|
22999
|
+
const reconciliation = job.terminalUnreconciled ? "unreconciled" : "reconciled";
|
|
22956
23000
|
const lines = [
|
|
22957
|
-
`- ${job.alias} / ${job.taskID} / ${job.agent} /
|
|
23001
|
+
`- ${job.alias} / ${job.taskID} / ${job.agent} / ${terminal ?? job.state}, ${reconciliation}`,
|
|
22958
23002
|
` Objective: ${job.objective || job.description}`
|
|
22959
23003
|
];
|
|
22960
23004
|
const context = formatContextFiles(job.contextFiles, this.readContextMaxFiles);
|
|
@@ -22979,7 +23023,8 @@ function deriveTaskSessionLabel(input) {
|
|
|
22979
23023
|
return firstPromptLine ? firstPromptLine.slice(0, 48) : `recent ${input.agentType} task`;
|
|
22980
23024
|
}
|
|
22981
23025
|
function isReusable(job) {
|
|
22982
|
-
|
|
23026
|
+
const terminal = job.terminalState ?? terminalStateOf(job.state);
|
|
23027
|
+
return terminal === "completed" && !job.terminalUnreconciled;
|
|
22983
23028
|
}
|
|
22984
23029
|
function terminalStateOf(state) {
|
|
22985
23030
|
return state === "completed" || state === "error" || state === "cancelled" ? state : undefined;
|
|
@@ -22999,13 +23044,15 @@ function formatJob(job, now = Date.now()) {
|
|
|
22999
23044
|
const ageMs = now - job.lastLaunchedAt;
|
|
23000
23045
|
const isResume = job.lastLaunchedAt !== job.launchedAt;
|
|
23001
23046
|
const ageLabel = job.state === "running" && ageMs < 30000 ? ` [${isResume ? "resumed" : "just launched"}, ${Math.floor(ageMs / 1000)}s ago]` : "";
|
|
23002
|
-
const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
|
|
23047
|
+
const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.statusUncertain ? `${job.state}, status uncertain` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
|
|
23003
23048
|
const lines = [
|
|
23004
23049
|
`- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
|
|
23005
23050
|
` Objective: ${job.objective || job.description}`
|
|
23006
23051
|
];
|
|
23007
23052
|
if (job.resultSummary && job.terminalUnreconciled) {
|
|
23008
23053
|
lines.push(` Result: ${singleLine(job.resultSummary)}`);
|
|
23054
|
+
} else if (job.lastStatusError && job.statusUncertain) {
|
|
23055
|
+
lines.push(` Status: ${singleLine(job.lastStatusError)}`);
|
|
23009
23056
|
}
|
|
23010
23057
|
return lines.join(`
|
|
23011
23058
|
`);
|
|
@@ -24086,14 +24133,44 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24086
24133
|
const status = parseTaskStatusOutput(output);
|
|
24087
24134
|
if (!status)
|
|
24088
24135
|
return;
|
|
24136
|
+
log("[task-session-manager] parsed task status output", {
|
|
24137
|
+
taskID: status.taskID,
|
|
24138
|
+
state: status.state,
|
|
24139
|
+
timedOut: status.timedOut,
|
|
24140
|
+
hasResult: Boolean(status.result)
|
|
24141
|
+
});
|
|
24142
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24143
|
+
if (isLateCancelledTaskError(existing, status.state)) {
|
|
24144
|
+
log("[task-session-manager] suppressed late cancelled task error", {
|
|
24145
|
+
taskID: status.taskID,
|
|
24146
|
+
alias: existing?.alias,
|
|
24147
|
+
state: existing?.state,
|
|
24148
|
+
terminalState: existing?.terminalState,
|
|
24149
|
+
result: status.result
|
|
24150
|
+
});
|
|
24151
|
+
return existing;
|
|
24152
|
+
}
|
|
24089
24153
|
const updated = backgroundJobBoard.updateStatus({
|
|
24090
24154
|
taskID: status.taskID,
|
|
24091
24155
|
state: status.state,
|
|
24092
24156
|
timedOut: status.timedOut,
|
|
24093
24157
|
resultSummary: status.result
|
|
24094
24158
|
});
|
|
24095
|
-
if (!updated)
|
|
24159
|
+
if (!updated) {
|
|
24160
|
+
log("[task-session-manager] ignored status for unknown background job", {
|
|
24161
|
+
taskID: status.taskID,
|
|
24162
|
+
state: status.state
|
|
24163
|
+
});
|
|
24096
24164
|
return;
|
|
24165
|
+
}
|
|
24166
|
+
log("[task-session-manager] background job status updated", {
|
|
24167
|
+
taskID: updated.taskID,
|
|
24168
|
+
alias: updated.alias,
|
|
24169
|
+
parentSessionID: updated.parentSessionID,
|
|
24170
|
+
state: updated.state,
|
|
24171
|
+
terminalUnreconciled: updated.terminalUnreconciled,
|
|
24172
|
+
timedOut: updated.timedOut
|
|
24173
|
+
});
|
|
24097
24174
|
if (updated.terminalUnreconciled) {
|
|
24098
24175
|
pendingManagedTaskIds.delete(updated.taskID);
|
|
24099
24176
|
backgroundJobBoard.addContext(updated.taskID, contextFilesForPrompt(contextByTask.get(updated.taskID)));
|
|
@@ -24101,6 +24178,61 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24101
24178
|
}
|
|
24102
24179
|
return updated;
|
|
24103
24180
|
}
|
|
24181
|
+
async function handleTransientTaskStatusOutput(output) {
|
|
24182
|
+
if (typeof output.output !== "string")
|
|
24183
|
+
return false;
|
|
24184
|
+
const status = parseTaskStatusOutput(output.output);
|
|
24185
|
+
if (!status)
|
|
24186
|
+
return false;
|
|
24187
|
+
if (classifyTaskStatusOutput(status) !== "transient_process_error") {
|
|
24188
|
+
return false;
|
|
24189
|
+
}
|
|
24190
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24191
|
+
const liveStatus = existing && existing.state === "running" ? undefined : await getLiveSessionStatus(status.taskID);
|
|
24192
|
+
const recentLiveBusy = !!existing?.lastLiveBusyAt && (!existing.completedAt || existing.lastLiveBusyAt >= existing.completedAt);
|
|
24193
|
+
const isStillRunning = existing?.state === "running" || recentLiveBusy || liveStatus === "busy" || liveStatus === "retry";
|
|
24194
|
+
if (!isStillRunning)
|
|
24195
|
+
return false;
|
|
24196
|
+
const updated = existing?.state === "running" ? backgroundJobBoard.updateStatus({
|
|
24197
|
+
taskID: status.taskID,
|
|
24198
|
+
state: "running",
|
|
24199
|
+
statusUncertain: true,
|
|
24200
|
+
lastStatusError: status.result
|
|
24201
|
+
}) : undefined;
|
|
24202
|
+
log("[task-session-manager] classified transient task_status error", {
|
|
24203
|
+
taskID: status.taskID,
|
|
24204
|
+
alias: existing?.alias,
|
|
24205
|
+
parentSessionID: existing?.parentSessionID,
|
|
24206
|
+
previousState: existing?.state,
|
|
24207
|
+
updatedState: updated?.state,
|
|
24208
|
+
liveStatus,
|
|
24209
|
+
recentLiveBusy
|
|
24210
|
+
});
|
|
24211
|
+
return true;
|
|
24212
|
+
}
|
|
24213
|
+
async function getLiveSessionStatus(sessionID) {
|
|
24214
|
+
try {
|
|
24215
|
+
const response = await _ctx.client.session.status();
|
|
24216
|
+
const data = response.data;
|
|
24217
|
+
if (!isObjectRecord(data))
|
|
24218
|
+
return;
|
|
24219
|
+
const item = data[sessionID];
|
|
24220
|
+
if (item === undefined)
|
|
24221
|
+
return "idle";
|
|
24222
|
+
if (isObjectRecord(item) && typeof item.type === "string") {
|
|
24223
|
+
return item.type;
|
|
24224
|
+
}
|
|
24225
|
+
const directType = data.type;
|
|
24226
|
+
if (typeof directType === "string")
|
|
24227
|
+
return directType;
|
|
24228
|
+
const nestedStatus = data.status;
|
|
24229
|
+
if (!isObjectRecord(nestedStatus))
|
|
24230
|
+
return;
|
|
24231
|
+
return typeof nestedStatus.type === "string" ? nestedStatus.type : undefined;
|
|
24232
|
+
} catch {
|
|
24233
|
+
return;
|
|
24234
|
+
}
|
|
24235
|
+
}
|
|
24104
24236
|
function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
|
|
24105
24237
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
24106
24238
|
return;
|
|
@@ -24113,16 +24245,36 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24113
24245
|
const status = parseTaskStatusOutput(part.text);
|
|
24114
24246
|
if (!status)
|
|
24115
24247
|
return;
|
|
24248
|
+
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
24249
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24250
|
+
if (isFailed && isLateCancelledTaskError(existing, status.state)) {
|
|
24251
|
+
part.text = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
|
|
24252
|
+
log("[task-session-manager] normalized late cancelled injected failure", {
|
|
24253
|
+
taskID: status.taskID,
|
|
24254
|
+
alias: existing?.alias,
|
|
24255
|
+
state: existing?.state,
|
|
24256
|
+
terminalState: existing?.terminalState,
|
|
24257
|
+
result: status.result
|
|
24258
|
+
});
|
|
24259
|
+
rememberProcessedInjectedCompletion(occurrenceId);
|
|
24260
|
+
return existing;
|
|
24261
|
+
}
|
|
24116
24262
|
if (isCompleted && status.state !== "completed")
|
|
24117
24263
|
return;
|
|
24118
24264
|
if (isFailed && status.state !== "error")
|
|
24119
24265
|
return;
|
|
24120
|
-
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
24121
24266
|
if (processedInjectedCompletions.has(occurrenceId))
|
|
24122
24267
|
return;
|
|
24123
24268
|
const updated = updateBackgroundJobFromOutput(part.text);
|
|
24124
24269
|
if (!updated)
|
|
24125
24270
|
return;
|
|
24271
|
+
log("[task-session-manager] processed injected background completion", {
|
|
24272
|
+
taskID: updated.taskID,
|
|
24273
|
+
alias: updated.alias,
|
|
24274
|
+
parentSessionID: updated.parentSessionID,
|
|
24275
|
+
state: updated.state,
|
|
24276
|
+
occurrenceId
|
|
24277
|
+
});
|
|
24126
24278
|
rememberProcessedInjectedCompletion(occurrenceId);
|
|
24127
24279
|
return updated;
|
|
24128
24280
|
}
|
|
@@ -24179,6 +24331,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24179
24331
|
const taskIDs = backgroundJobBoard.list(parentSessionID).filter((job) => job.terminalUnreconciled).map((job) => job.taskID);
|
|
24180
24332
|
if (taskIDs.length === 0)
|
|
24181
24333
|
return;
|
|
24334
|
+
log("[task-session-manager] terminal jobs injected for reconciliation", {
|
|
24335
|
+
parentSessionID,
|
|
24336
|
+
taskIDs
|
|
24337
|
+
});
|
|
24182
24338
|
const existing = terminalJobsInjectedByParent.get(parentSessionID) ?? new Set;
|
|
24183
24339
|
for (const taskID of taskIDs) {
|
|
24184
24340
|
existing.add(taskID);
|
|
@@ -24189,6 +24345,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24189
24345
|
const taskIDs = terminalJobsInjectedByParent.get(parentSessionID);
|
|
24190
24346
|
if (!taskIDs)
|
|
24191
24347
|
return;
|
|
24348
|
+
log("[task-session-manager] reconciling injected terminal jobs", {
|
|
24349
|
+
parentSessionID,
|
|
24350
|
+
taskIDs: [...taskIDs]
|
|
24351
|
+
});
|
|
24192
24352
|
for (const taskID of taskIDs) {
|
|
24193
24353
|
backgroundJobBoard.markReconciled(taskID);
|
|
24194
24354
|
}
|
|
@@ -24262,6 +24422,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24262
24422
|
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
24263
24423
|
return;
|
|
24264
24424
|
}
|
|
24425
|
+
normalizeLateCancelledToolStatus(output);
|
|
24426
|
+
if (await handleTransientTaskStatusOutput(output)) {
|
|
24427
|
+
return;
|
|
24428
|
+
}
|
|
24265
24429
|
updateBackgroundJobFromOutput(output.output);
|
|
24266
24430
|
return;
|
|
24267
24431
|
}
|
|
@@ -24272,13 +24436,21 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24272
24436
|
return;
|
|
24273
24437
|
const launch = parseTaskLaunchOutput(output.output);
|
|
24274
24438
|
if (launch) {
|
|
24275
|
-
backgroundJobBoard.registerLaunch({
|
|
24439
|
+
const record = backgroundJobBoard.registerLaunch({
|
|
24276
24440
|
taskID: launch.taskID,
|
|
24277
24441
|
parentSessionID: pending.parentSessionId,
|
|
24278
24442
|
agent: pending.agentType,
|
|
24279
24443
|
description: pending.label,
|
|
24280
24444
|
objective: pending.label
|
|
24281
24445
|
});
|
|
24446
|
+
log("[task-session-manager] background task launch registered", {
|
|
24447
|
+
taskID: record.taskID,
|
|
24448
|
+
alias: record.alias,
|
|
24449
|
+
parentSessionID: record.parentSessionID,
|
|
24450
|
+
agent: record.agent,
|
|
24451
|
+
description: record.description,
|
|
24452
|
+
state: record.state
|
|
24453
|
+
});
|
|
24282
24454
|
backgroundJobBoard.addContext(launch.taskID, contextFilesForPrompt(contextByTask.get(launch.taskID)));
|
|
24283
24455
|
pendingManagedTaskIds.add(launch.taskID);
|
|
24284
24456
|
return;
|
|
@@ -24344,6 +24516,11 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24344
24516
|
event: async (input) => {
|
|
24345
24517
|
if (input.event.type === "session.created") {
|
|
24346
24518
|
const info = input.event.properties?.info;
|
|
24519
|
+
log("[task-session-manager] session.created observed", {
|
|
24520
|
+
sessionID: info?.id,
|
|
24521
|
+
parentSessionID: info?.parentID,
|
|
24522
|
+
managesParent: info?.parentID ? options.shouldManageSession(info.parentID) : false
|
|
24523
|
+
});
|
|
24347
24524
|
if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
|
|
24348
24525
|
pendingManagedTaskIds.add(info.id);
|
|
24349
24526
|
}
|
|
@@ -24351,6 +24528,11 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24351
24528
|
}
|
|
24352
24529
|
if (input.event.type === "session.idle" || input.event.type === "session.status" && input.event.properties?.status?.type === "idle") {
|
|
24353
24530
|
const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
24531
|
+
log("[task-session-manager] idle/status idle observed", {
|
|
24532
|
+
sessionID: sessionId2,
|
|
24533
|
+
managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
|
|
24534
|
+
terminalJobsPending: sessionId2 ? terminalJobsInjectedByParent.get(sessionId2)?.size ?? 0 : 0
|
|
24535
|
+
});
|
|
24354
24536
|
if (sessionId2 && options.shouldManageSession(sessionId2)) {
|
|
24355
24537
|
reconcileInjectedTerminalJobs(sessionId2);
|
|
24356
24538
|
}
|
|
@@ -24363,11 +24545,49 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24363
24545
|
}
|
|
24364
24546
|
return;
|
|
24365
24547
|
}
|
|
24548
|
+
if (input.event.type === "session.status" && input.event.properties?.status?.type === "busy") {
|
|
24549
|
+
const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
24550
|
+
const before = sessionId2 ? backgroundJobBoard.get(sessionId2) : undefined;
|
|
24551
|
+
const updated = sessionId2 ? backgroundJobBoard.markRunningFromLiveSession(sessionId2) : undefined;
|
|
24552
|
+
if (before?.cancellationRequested) {
|
|
24553
|
+
log("[task-session-manager] busy observed after cancel request", {
|
|
24554
|
+
sessionID: sessionId2,
|
|
24555
|
+
previousState: before.state,
|
|
24556
|
+
previousTerminalState: before.terminalState,
|
|
24557
|
+
terminalUnreconciled: before.terminalUnreconciled,
|
|
24558
|
+
resultSummary: before.resultSummary,
|
|
24559
|
+
updatedState: updated?.state,
|
|
24560
|
+
updatedCancellationRequested: updated?.cancellationRequested
|
|
24561
|
+
});
|
|
24562
|
+
}
|
|
24563
|
+
log("[task-session-manager] busy/status busy observed", {
|
|
24564
|
+
sessionID: sessionId2,
|
|
24565
|
+
managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
|
|
24566
|
+
previousState: before?.state,
|
|
24567
|
+
previousTerminalState: before?.terminalState,
|
|
24568
|
+
previousCancellationRequested: before?.cancellationRequested,
|
|
24569
|
+
previousLastLiveBusyAt: before?.lastLiveBusyAt,
|
|
24570
|
+
updatedState: updated?.state,
|
|
24571
|
+
updatedCancellationRequested: updated?.cancellationRequested,
|
|
24572
|
+
updatedLastLiveBusyAt: updated?.lastLiveBusyAt
|
|
24573
|
+
});
|
|
24574
|
+
return;
|
|
24575
|
+
}
|
|
24366
24576
|
if (input.event.type !== "session.deleted")
|
|
24367
24577
|
return;
|
|
24368
24578
|
const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
24369
24579
|
if (!sessionId)
|
|
24370
24580
|
return;
|
|
24581
|
+
log("[task-session-manager] session.deleted observed; clearing job state", {
|
|
24582
|
+
sessionID: sessionId,
|
|
24583
|
+
deletedJob: backgroundJobBoard.get(sessionId) ? {
|
|
24584
|
+
state: backgroundJobBoard.get(sessionId)?.state,
|
|
24585
|
+
parentSessionID: backgroundJobBoard.get(sessionId)?.parentSessionID,
|
|
24586
|
+
alias: backgroundJobBoard.get(sessionId)?.alias
|
|
24587
|
+
} : undefined,
|
|
24588
|
+
childJobCount: backgroundJobBoard.list(sessionId).length,
|
|
24589
|
+
managesSession: options.shouldManageSession(sessionId)
|
|
24590
|
+
});
|
|
24371
24591
|
backgroundJobBoard.drop(sessionId);
|
|
24372
24592
|
backgroundJobBoard.clearParent(sessionId);
|
|
24373
24593
|
terminalJobsInjectedByParent.delete(sessionId);
|
|
@@ -24382,6 +24602,45 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24382
24602
|
}
|
|
24383
24603
|
}
|
|
24384
24604
|
};
|
|
24605
|
+
function normalizeLateCancelledToolStatus(output) {
|
|
24606
|
+
if (typeof output.output !== "string")
|
|
24607
|
+
return;
|
|
24608
|
+
const status = parseTaskStatusOutput(output.output);
|
|
24609
|
+
if (!status)
|
|
24610
|
+
return;
|
|
24611
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24612
|
+
if (!isLateCancelledTaskError(existing, status.state))
|
|
24613
|
+
return;
|
|
24614
|
+
log("[task-session-manager] normalized late cancelled task_status output", {
|
|
24615
|
+
taskID: status.taskID,
|
|
24616
|
+
alias: existing?.alias,
|
|
24617
|
+
state: existing?.state,
|
|
24618
|
+
terminalState: existing?.terminalState,
|
|
24619
|
+
result: status.result
|
|
24620
|
+
});
|
|
24621
|
+
output.output = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
|
|
24622
|
+
if (isObjectRecord(output) && isObjectRecord(output.metadata)) {
|
|
24623
|
+
output.metadata.state = "cancelled";
|
|
24624
|
+
}
|
|
24625
|
+
}
|
|
24626
|
+
}
|
|
24627
|
+
function isLateCancelledTaskError(job, state) {
|
|
24628
|
+
if (state !== "error")
|
|
24629
|
+
return false;
|
|
24630
|
+
if (!job?.cancellationRequested)
|
|
24631
|
+
return false;
|
|
24632
|
+
return job.state === "cancelled" || job.terminalState === "cancelled";
|
|
24633
|
+
}
|
|
24634
|
+
function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
|
|
24635
|
+
return [
|
|
24636
|
+
`task_id: ${taskID}`,
|
|
24637
|
+
"state: cancelled",
|
|
24638
|
+
"",
|
|
24639
|
+
"<task_error>",
|
|
24640
|
+
summary,
|
|
24641
|
+
"</task_error>"
|
|
24642
|
+
].join(`
|
|
24643
|
+
`);
|
|
24385
24644
|
}
|
|
24386
24645
|
// src/hooks/todo-continuation/index.ts
|
|
24387
24646
|
import { tool } from "@opencode-ai/plugin";
|
|
@@ -29690,6 +29949,7 @@ class MultiplexerSessionManager {
|
|
|
29690
29949
|
parentId,
|
|
29691
29950
|
title,
|
|
29692
29951
|
directory,
|
|
29952
|
+
ownerInstanceId: this.instanceId,
|
|
29693
29953
|
createdAt: now,
|
|
29694
29954
|
lastSeenAt: now,
|
|
29695
29955
|
seenInStatus: false
|
|
@@ -29715,7 +29975,9 @@ class MultiplexerSessionManager {
|
|
|
29715
29975
|
instanceId: this.instanceId,
|
|
29716
29976
|
sessionId: sessionId2,
|
|
29717
29977
|
tracked: this.sessions.has(sessionId2),
|
|
29718
|
-
known: this.knownSessions.has(sessionId2)
|
|
29978
|
+
known: this.knownSessions.has(sessionId2),
|
|
29979
|
+
ownerInstanceId: this.sessions.get(sessionId2)?.ownerInstanceId,
|
|
29980
|
+
backgroundJobState: this.backgroundJobBoard?.get(sessionId2)?.state
|
|
29719
29981
|
});
|
|
29720
29982
|
await this.closeSession(sessionId2, "idle");
|
|
29721
29983
|
return;
|
|
@@ -29726,6 +29988,14 @@ class MultiplexerSessionManager {
|
|
|
29726
29988
|
if (!sessionId)
|
|
29727
29989
|
return;
|
|
29728
29990
|
if (event.properties?.status?.type === "idle") {
|
|
29991
|
+
log("[multiplexer-session-manager] session status idle received", {
|
|
29992
|
+
instanceId: this.instanceId,
|
|
29993
|
+
sessionId,
|
|
29994
|
+
tracked: this.sessions.has(sessionId),
|
|
29995
|
+
known: this.knownSessions.has(sessionId),
|
|
29996
|
+
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
29997
|
+
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
|
|
29998
|
+
});
|
|
29729
29999
|
await this.closeSession(sessionId, "idle");
|
|
29730
30000
|
return;
|
|
29731
30001
|
}
|
|
@@ -29734,7 +30004,9 @@ class MultiplexerSessionManager {
|
|
|
29734
30004
|
instanceId: this.instanceId,
|
|
29735
30005
|
sessionId,
|
|
29736
30006
|
tracked: this.sessions.has(sessionId),
|
|
29737
|
-
known: this.knownSessions.has(sessionId)
|
|
30007
|
+
known: this.knownSessions.has(sessionId),
|
|
30008
|
+
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
30009
|
+
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
|
|
29738
30010
|
});
|
|
29739
30011
|
await this.respawnIfKnown(sessionId);
|
|
29740
30012
|
}
|
|
@@ -29749,7 +30021,11 @@ class MultiplexerSessionManager {
|
|
|
29749
30021
|
return;
|
|
29750
30022
|
log("[multiplexer-session-manager] session deleted, closing pane", {
|
|
29751
30023
|
instanceId: this.instanceId,
|
|
29752
|
-
sessionId
|
|
30024
|
+
sessionId,
|
|
30025
|
+
tracked: this.sessions.has(sessionId),
|
|
30026
|
+
known: this.knownSessions.has(sessionId),
|
|
30027
|
+
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
30028
|
+
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
|
|
29753
30029
|
});
|
|
29754
30030
|
await this.closeSession(sessionId, "deleted");
|
|
29755
30031
|
}
|
|
@@ -29780,6 +30056,15 @@ class MultiplexerSessionManager {
|
|
|
29780
30056
|
const now = Date.now();
|
|
29781
30057
|
const sessionsToClose = [];
|
|
29782
30058
|
for (const [sessionId, tracked] of this.sessions.entries()) {
|
|
30059
|
+
if (tracked.ownerInstanceId !== this.instanceId) {
|
|
30060
|
+
log("[multiplexer-session-manager] skipping non-owner poll close", {
|
|
30061
|
+
instanceId: this.instanceId,
|
|
30062
|
+
ownerInstanceId: tracked.ownerInstanceId,
|
|
30063
|
+
sessionId,
|
|
30064
|
+
paneId: tracked.paneId
|
|
30065
|
+
});
|
|
30066
|
+
continue;
|
|
30067
|
+
}
|
|
29783
30068
|
const status = allStatuses[sessionId];
|
|
29784
30069
|
const isIdle = status?.type === "idle";
|
|
29785
30070
|
if (status) {
|
|
@@ -29790,7 +30075,7 @@ class MultiplexerSessionManager {
|
|
|
29790
30075
|
tracked.missingSince = now;
|
|
29791
30076
|
}
|
|
29792
30077
|
const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
|
|
29793
|
-
const shouldKeepRunningBackgroundJob = missingTooLong && this.isRunningBackgroundJob(sessionId);
|
|
30078
|
+
const shouldKeepRunningBackgroundJob = (isIdle || missingTooLong) && this.isRunningBackgroundJob(sessionId);
|
|
29794
30079
|
if (isIdle || missingTooLong) {
|
|
29795
30080
|
if (shouldKeepRunningBackgroundJob) {
|
|
29796
30081
|
log("[multiplexer-session-manager] keeping running background pane", {
|
|
@@ -29848,12 +30133,44 @@ class MultiplexerSessionManager {
|
|
|
29848
30133
|
});
|
|
29849
30134
|
return;
|
|
29850
30135
|
}
|
|
30136
|
+
if (reason !== "deleted" && tracked.ownerInstanceId !== this.instanceId) {
|
|
30137
|
+
log("[multiplexer-session-manager] close skipped; non-owner instance", {
|
|
30138
|
+
instanceId: this.instanceId,
|
|
30139
|
+
ownerInstanceId: tracked.ownerInstanceId,
|
|
30140
|
+
sessionId,
|
|
30141
|
+
paneId: tracked.paneId,
|
|
30142
|
+
reason
|
|
30143
|
+
});
|
|
30144
|
+
return;
|
|
30145
|
+
}
|
|
30146
|
+
if (reason === "deleted" && tracked.ownerInstanceId !== this.instanceId) {
|
|
30147
|
+
log("[multiplexer-session-manager] closing deleted pane as non-owner", {
|
|
30148
|
+
instanceId: this.instanceId,
|
|
30149
|
+
ownerInstanceId: tracked.ownerInstanceId,
|
|
30150
|
+
sessionId,
|
|
30151
|
+
paneId: tracked.paneId,
|
|
30152
|
+
reason
|
|
30153
|
+
});
|
|
30154
|
+
}
|
|
30155
|
+
if (reason === "idle" && this.isRunningBackgroundJob(sessionId)) {
|
|
30156
|
+
log("[multiplexer-session-manager] close skipped; background job running", {
|
|
30157
|
+
instanceId: this.instanceId,
|
|
30158
|
+
sessionId,
|
|
30159
|
+
paneId: tracked.paneId,
|
|
30160
|
+
reason,
|
|
30161
|
+
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state
|
|
30162
|
+
});
|
|
30163
|
+
return;
|
|
30164
|
+
}
|
|
29851
30165
|
this.sessions.delete(sessionId);
|
|
29852
30166
|
log("[multiplexer-session-manager] closing session pane", {
|
|
29853
30167
|
instanceId: this.instanceId,
|
|
29854
30168
|
sessionId,
|
|
29855
30169
|
paneId: tracked.paneId,
|
|
29856
|
-
reason
|
|
30170
|
+
reason,
|
|
30171
|
+
backgroundJobState: this.backgroundJobBoard?.get(sessionId)?.state,
|
|
30172
|
+
parentId: tracked.parentId,
|
|
30173
|
+
title: tracked.title
|
|
29857
30174
|
});
|
|
29858
30175
|
const closePromise = this.multiplexer.closePane(tracked.paneId).then(() => {
|
|
29859
30176
|
return;
|
|
@@ -29927,6 +30244,7 @@ class MultiplexerSessionManager {
|
|
|
29927
30244
|
parentId: known.parentId,
|
|
29928
30245
|
title: known.title,
|
|
29929
30246
|
directory: known.directory,
|
|
30247
|
+
ownerInstanceId: this.instanceId,
|
|
29930
30248
|
createdAt: now,
|
|
29931
30249
|
lastSeenAt: now,
|
|
29932
30250
|
seenInStatus: false
|
|
@@ -30552,6 +30870,9 @@ import {
|
|
|
30552
30870
|
tool as tool3
|
|
30553
30871
|
} from "@opencode-ai/plugin";
|
|
30554
30872
|
var z4 = tool3.schema;
|
|
30873
|
+
|
|
30874
|
+
class SessionStillRunningError extends Error {
|
|
30875
|
+
}
|
|
30555
30876
|
function createCancelTaskTool(options) {
|
|
30556
30877
|
const cancel_task = tool3({
|
|
30557
30878
|
description: `Cancel a tracked background specialist task.
|
|
@@ -30575,42 +30896,68 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
30575
30896
|
if (!requested)
|
|
30576
30897
|
throw new Error("cancel_task requires task_id");
|
|
30577
30898
|
const job = options.backgroundJobBoard.resolve(parentSessionID, requested);
|
|
30899
|
+
log("[cancel-task] request received", {
|
|
30900
|
+
parentSessionID,
|
|
30901
|
+
requested,
|
|
30902
|
+
resolvedTaskID: job?.taskID,
|
|
30903
|
+
alias: job?.alias,
|
|
30904
|
+
state: job?.state,
|
|
30905
|
+
terminalState: job?.terminalState,
|
|
30906
|
+
cancellationRequested: job?.cancellationRequested
|
|
30907
|
+
});
|
|
30578
30908
|
if (!job) {
|
|
30579
|
-
|
|
30580
|
-
|
|
30581
|
-
|
|
30582
|
-
|
|
30583
|
-
|
|
30584
|
-
|
|
30585
|
-
|
|
30586
|
-
|
|
30587
|
-
|
|
30588
|
-
|
|
30589
|
-
|
|
30590
|
-
|
|
30591
|
-
|
|
30592
|
-
|
|
30593
|
-
|
|
30594
|
-
|
|
30595
|
-
|
|
30596
|
-
|
|
30597
|
-
|
|
30598
|
-
|
|
30909
|
+
if (isSessionID(requested)) {
|
|
30910
|
+
if (requested === parentSessionID) {
|
|
30911
|
+
log("[cancel-task] rejected parent session cancellation", {
|
|
30912
|
+
parentSessionID,
|
|
30913
|
+
taskID: requested
|
|
30914
|
+
});
|
|
30915
|
+
return unknownTaskOutput(requested, "cannot cancel parent session");
|
|
30916
|
+
}
|
|
30917
|
+
const knownJob = options.backgroundJobBoard.get(requested);
|
|
30918
|
+
if (knownJob && knownJob.parentSessionID !== parentSessionID) {
|
|
30919
|
+
log("[cancel-task] rejected unowned tracked raw session", {
|
|
30920
|
+
parentSessionID,
|
|
30921
|
+
taskID: requested,
|
|
30922
|
+
ownerParentSessionID: knownJob.parentSessionID
|
|
30923
|
+
});
|
|
30924
|
+
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
30925
|
+
}
|
|
30926
|
+
const parentID = await getSessionParentID(options.client, requested);
|
|
30927
|
+
if (parentID !== parentSessionID) {
|
|
30928
|
+
log("[cancel-task] rejected raw session without parent ownership", {
|
|
30929
|
+
parentSessionID,
|
|
30930
|
+
taskID: requested,
|
|
30931
|
+
actualParentID: parentID
|
|
30932
|
+
});
|
|
30933
|
+
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
30934
|
+
}
|
|
30935
|
+
log("[cancel-task] falling back to owned raw session abort", {
|
|
30936
|
+
parentSessionID,
|
|
30937
|
+
taskID: requested
|
|
30938
|
+
});
|
|
30939
|
+
return cancelSessionByID(options, requested, args.reason);
|
|
30940
|
+
}
|
|
30941
|
+
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
30599
30942
|
}
|
|
30600
30943
|
try {
|
|
30601
|
-
await
|
|
30944
|
+
await abortAndVerifySession(options, job.taskID);
|
|
30602
30945
|
} catch (error) {
|
|
30603
|
-
const
|
|
30604
|
-
|
|
30605
|
-
|
|
30606
|
-
|
|
30607
|
-
|
|
30608
|
-
|
|
30609
|
-
|
|
30610
|
-
|
|
30946
|
+
const stillRunning = error instanceof SessionStillRunningError;
|
|
30947
|
+
log("[cancel-task] abort failed", {
|
|
30948
|
+
taskID: job.taskID,
|
|
30949
|
+
stillRunning,
|
|
30950
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30951
|
+
});
|
|
30952
|
+
options.backgroundJobBoard.updateStatus({
|
|
30953
|
+
taskID: job.taskID,
|
|
30954
|
+
state: "running",
|
|
30955
|
+
statusUncertain: true,
|
|
30956
|
+
lastStatusError: error instanceof Error ? error.message : String(error)
|
|
30957
|
+
});
|
|
30611
30958
|
return [
|
|
30612
30959
|
`task_id: ${job.taskID}`,
|
|
30613
|
-
|
|
30960
|
+
"state: running",
|
|
30614
30961
|
"",
|
|
30615
30962
|
"<task_error>",
|
|
30616
30963
|
error instanceof Error ? error.message : String(error),
|
|
@@ -30618,7 +30965,14 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
30618
30965
|
].join(`
|
|
30619
30966
|
`);
|
|
30620
30967
|
}
|
|
30621
|
-
const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason);
|
|
30968
|
+
const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason, Date.now(), { force: true });
|
|
30969
|
+
log("[cancel-task] marked job cancelled after verified abort", {
|
|
30970
|
+
taskID: job.taskID,
|
|
30971
|
+
alias: job.alias,
|
|
30972
|
+
previousState: job.state,
|
|
30973
|
+
state: cancelled?.state,
|
|
30974
|
+
cancellationRequested: cancelled?.cancellationRequested
|
|
30975
|
+
});
|
|
30622
30976
|
return [
|
|
30623
30977
|
`task_id: ${job.taskID}`,
|
|
30624
30978
|
`state: ${cancelled?.state ?? "cancelled"}`,
|
|
@@ -30632,6 +30986,282 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
30632
30986
|
});
|
|
30633
30987
|
return { cancel_task };
|
|
30634
30988
|
}
|
|
30989
|
+
async function cancelSessionByID(options, taskID, reason) {
|
|
30990
|
+
try {
|
|
30991
|
+
await abortAndVerifySession(options, taskID);
|
|
30992
|
+
} catch (error) {
|
|
30993
|
+
const stillRunning = error instanceof SessionStillRunningError;
|
|
30994
|
+
log("[cancel-task] raw session abort failed", {
|
|
30995
|
+
taskID,
|
|
30996
|
+
stillRunning,
|
|
30997
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30998
|
+
});
|
|
30999
|
+
return [
|
|
31000
|
+
`task_id: ${taskID}`,
|
|
31001
|
+
`state: ${stillRunning ? "running" : "error"}`,
|
|
31002
|
+
"",
|
|
31003
|
+
"<task_error>",
|
|
31004
|
+
error instanceof Error ? error.message : String(error),
|
|
31005
|
+
"</task_error>"
|
|
31006
|
+
].join(`
|
|
31007
|
+
`);
|
|
31008
|
+
}
|
|
31009
|
+
return [
|
|
31010
|
+
`task_id: ${taskID}`,
|
|
31011
|
+
"state: cancelled",
|
|
31012
|
+
"",
|
|
31013
|
+
"<task_error>",
|
|
31014
|
+
normalizeCancelReason2(reason),
|
|
31015
|
+
"</task_error>"
|
|
31016
|
+
].join(`
|
|
31017
|
+
`);
|
|
31018
|
+
}
|
|
31019
|
+
async function abortAndVerifySession(options, taskID) {
|
|
31020
|
+
log("[cancel-task] abort attempt starting", { taskID });
|
|
31021
|
+
const abortStartedAt = Date.now();
|
|
31022
|
+
try {
|
|
31023
|
+
await abortSessionWithTimeout(options.client, taskID, options.abortTimeoutMs ?? 1e4);
|
|
31024
|
+
log("[cancel-task] abort call returned", { taskID });
|
|
31025
|
+
} catch (error) {
|
|
31026
|
+
log("[cancel-task] abort call failed", {
|
|
31027
|
+
taskID,
|
|
31028
|
+
error: error instanceof Error ? error.message : String(error),
|
|
31029
|
+
canDelete: canDeleteSession(options.client)
|
|
31030
|
+
});
|
|
31031
|
+
if (!canDeleteSession(options.client))
|
|
31032
|
+
throw error;
|
|
31033
|
+
}
|
|
31034
|
+
if (canDeleteSession(options.client)) {
|
|
31035
|
+
await deleteAndVerifySession(options, taskID, "cancel-task-after-abort");
|
|
31036
|
+
return;
|
|
31037
|
+
}
|
|
31038
|
+
const verifyAbortMs = options.verifyAbortMs ?? 8000;
|
|
31039
|
+
const stableStoppedMs = options.stableStoppedMs ?? 3000;
|
|
31040
|
+
const retryIntervalMs = options.abortRetryIntervalMs ?? 150;
|
|
31041
|
+
const deadline = Date.now() + verifyAbortMs;
|
|
31042
|
+
log("[cancel-task] abort verification starting", {
|
|
31043
|
+
taskID,
|
|
31044
|
+
verifyAbortMs,
|
|
31045
|
+
stableStoppedMs,
|
|
31046
|
+
retryIntervalMs
|
|
31047
|
+
});
|
|
31048
|
+
let attempts = 0;
|
|
31049
|
+
let stableStoppedSince;
|
|
31050
|
+
let lastStatus;
|
|
31051
|
+
while (Date.now() <= deadline) {
|
|
31052
|
+
attempts += 1;
|
|
31053
|
+
const statusSnapshot = await getSessionStatus(options.client, taskID);
|
|
31054
|
+
lastStatus = statusSnapshot.status;
|
|
31055
|
+
log("[cancel-task] abort verification status", {
|
|
31056
|
+
taskID,
|
|
31057
|
+
attempts,
|
|
31058
|
+
status: statusSnapshot.status,
|
|
31059
|
+
statusSource: statusSnapshot.source,
|
|
31060
|
+
statusKeys: statusSnapshot.keys,
|
|
31061
|
+
stableStoppedSince,
|
|
31062
|
+
stableStoppedForMs: stableStoppedSince ? Date.now() - stableStoppedSince : 0,
|
|
31063
|
+
boardState: options.backgroundJobBoard.get(taskID)?.state,
|
|
31064
|
+
boardLastLiveBusyAt: options.backgroundJobBoard.get(taskID)?.lastLiveBusyAt
|
|
31065
|
+
});
|
|
31066
|
+
const boardLastLiveBusyAt = options.backgroundJobBoard.get(taskID)?.lastLiveBusyAt;
|
|
31067
|
+
if (boardLastLiveBusyAt && boardLastLiveBusyAt >= abortStartedAt) {
|
|
31068
|
+
log("[cancel-task] abort verification saw board busy after abort", {
|
|
31069
|
+
taskID,
|
|
31070
|
+
attempts,
|
|
31071
|
+
abortStartedAt,
|
|
31072
|
+
boardLastLiveBusyAt,
|
|
31073
|
+
status: statusSnapshot.status,
|
|
31074
|
+
statusSource: statusSnapshot.source
|
|
31075
|
+
});
|
|
31076
|
+
await deleteAndVerifySession(options, taskID, "board-busy-after-abort");
|
|
31077
|
+
return;
|
|
31078
|
+
}
|
|
31079
|
+
if (statusSnapshot.status === "busy" || statusSnapshot.status === "retry") {
|
|
31080
|
+
if (stableStoppedSince !== undefined) {
|
|
31081
|
+
log("[cancel-task] abort verification saw busy after idle", {
|
|
31082
|
+
taskID,
|
|
31083
|
+
attempts,
|
|
31084
|
+
stableStoppedForMs: Date.now() - stableStoppedSince
|
|
31085
|
+
});
|
|
31086
|
+
await deleteAndVerifySession(options, taskID, "busy-after-idle");
|
|
31087
|
+
return;
|
|
31088
|
+
}
|
|
31089
|
+
stableStoppedSince = undefined;
|
|
31090
|
+
await abortSessionWithTimeout(options.client, taskID, options.abortTimeoutMs ?? 1e4);
|
|
31091
|
+
log("[cancel-task] abort retry returned", {
|
|
31092
|
+
taskID,
|
|
31093
|
+
attempts,
|
|
31094
|
+
status: statusSnapshot.status
|
|
31095
|
+
});
|
|
31096
|
+
await delay(retryIntervalMs);
|
|
31097
|
+
continue;
|
|
31098
|
+
}
|
|
31099
|
+
stableStoppedSince ??= Date.now();
|
|
31100
|
+
if (Date.now() - stableStoppedSince >= stableStoppedMs) {
|
|
31101
|
+
log("[cancel-task] abort verified stopped", {
|
|
31102
|
+
taskID,
|
|
31103
|
+
attempts,
|
|
31104
|
+
status: statusSnapshot.status,
|
|
31105
|
+
stableStoppedMs
|
|
31106
|
+
});
|
|
31107
|
+
return;
|
|
31108
|
+
}
|
|
31109
|
+
await delay(retryIntervalMs);
|
|
31110
|
+
}
|
|
31111
|
+
log("[cancel-task] abort verification timed out", {
|
|
31112
|
+
taskID,
|
|
31113
|
+
attempts,
|
|
31114
|
+
lastStatus,
|
|
31115
|
+
stableStoppedSince
|
|
31116
|
+
});
|
|
31117
|
+
if (lastStatus === "busy" || lastStatus === "retry") {
|
|
31118
|
+
await deleteAndVerifySession(options, taskID, "still-busy-after-abort");
|
|
31119
|
+
return;
|
|
31120
|
+
}
|
|
31121
|
+
throw new SessionStillRunningError(`Session abort returned but task did not stay stopped: ${taskID}`);
|
|
31122
|
+
}
|
|
31123
|
+
async function deleteAndVerifySession(options, taskID, reason) {
|
|
31124
|
+
const session2 = options.client.session;
|
|
31125
|
+
if (!session2.delete) {
|
|
31126
|
+
log("[cancel-task] session delete unavailable", { taskID, reason });
|
|
31127
|
+
throw new SessionStillRunningError(`Session resumed after abort and delete is unavailable: ${taskID}`);
|
|
31128
|
+
}
|
|
31129
|
+
log("[cancel-task] deleting session after unstable abort", {
|
|
31130
|
+
taskID,
|
|
31131
|
+
reason
|
|
31132
|
+
});
|
|
31133
|
+
try {
|
|
31134
|
+
await withTimeout(session2.delete({ path: { id: taskID } }), options.deleteTimeoutMs ?? 1e4, `Session delete timed out after ${options.deleteTimeoutMs ?? 1e4}ms`);
|
|
31135
|
+
log("[cancel-task] session delete returned", { taskID, reason });
|
|
31136
|
+
} catch (error) {
|
|
31137
|
+
log("[cancel-task] session delete failed; verifying live state", {
|
|
31138
|
+
taskID,
|
|
31139
|
+
reason,
|
|
31140
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31141
|
+
});
|
|
31142
|
+
const status = await getSessionStatus(options.client, taskID);
|
|
31143
|
+
log("[cancel-task] delete failure verification status", {
|
|
31144
|
+
taskID,
|
|
31145
|
+
reason,
|
|
31146
|
+
status: status.status,
|
|
31147
|
+
statusSource: status.source,
|
|
31148
|
+
statusKeys: status.keys
|
|
31149
|
+
});
|
|
31150
|
+
if (status.status === "busy" || status.status === "retry") {
|
|
31151
|
+
throw new SessionStillRunningError(`Session delete failed and task is still busy: ${taskID}`);
|
|
31152
|
+
}
|
|
31153
|
+
if (status.status !== "idle")
|
|
31154
|
+
throw error;
|
|
31155
|
+
}
|
|
31156
|
+
const deadline = Date.now() + (options.deleteVerifyMs ?? 1500);
|
|
31157
|
+
const stableStoppedMs = options.deleteStableStoppedMs ?? 300;
|
|
31158
|
+
const retryIntervalMs = options.abortRetryIntervalMs ?? 150;
|
|
31159
|
+
let stableStoppedSince;
|
|
31160
|
+
let attempts = 0;
|
|
31161
|
+
let lastStatus;
|
|
31162
|
+
while (Date.now() <= deadline) {
|
|
31163
|
+
attempts += 1;
|
|
31164
|
+
const status = await getSessionStatus(options.client, taskID);
|
|
31165
|
+
lastStatus = status.status;
|
|
31166
|
+
log("[cancel-task] delete verification status", {
|
|
31167
|
+
taskID,
|
|
31168
|
+
reason,
|
|
31169
|
+
attempts,
|
|
31170
|
+
status: status.status,
|
|
31171
|
+
statusSource: status.source,
|
|
31172
|
+
statusKeys: status.keys,
|
|
31173
|
+
stableStoppedSince
|
|
31174
|
+
});
|
|
31175
|
+
if (status.status === "busy" || status.status === "retry") {
|
|
31176
|
+
stableStoppedSince = undefined;
|
|
31177
|
+
await delay(retryIntervalMs);
|
|
31178
|
+
continue;
|
|
31179
|
+
}
|
|
31180
|
+
stableStoppedSince ??= Date.now();
|
|
31181
|
+
if (Date.now() - stableStoppedSince >= stableStoppedMs)
|
|
31182
|
+
return;
|
|
31183
|
+
await delay(retryIntervalMs);
|
|
31184
|
+
}
|
|
31185
|
+
throw new SessionStillRunningError(`Session delete returned but task did not stay stopped: ${taskID} (${lastStatus ?? "unknown"})`);
|
|
31186
|
+
}
|
|
31187
|
+
function canDeleteSession(client) {
|
|
31188
|
+
const session2 = client.session;
|
|
31189
|
+
return typeof session2.delete === "function";
|
|
31190
|
+
}
|
|
31191
|
+
async function getSessionStatus(client, taskID) {
|
|
31192
|
+
try {
|
|
31193
|
+
const result = await client.session.status();
|
|
31194
|
+
const data = result.data;
|
|
31195
|
+
if (!isObjectRecord2(data)) {
|
|
31196
|
+
return { status: undefined, source: "invalid-data", keys: [] };
|
|
31197
|
+
}
|
|
31198
|
+
const keys = Object.keys(data).slice(0, 20);
|
|
31199
|
+
const item = data[taskID];
|
|
31200
|
+
if (item === undefined) {
|
|
31201
|
+
return { status: "idle", source: "missing-from-map", keys };
|
|
31202
|
+
}
|
|
31203
|
+
if (isObjectRecord2(item) && typeof item.type === "string") {
|
|
31204
|
+
return { status: item.type, source: "task-map-entry", keys };
|
|
31205
|
+
}
|
|
31206
|
+
if (typeof data.type === "string") {
|
|
31207
|
+
return { status: data.type, source: "legacy-data-type", keys };
|
|
31208
|
+
}
|
|
31209
|
+
const nested = data.status;
|
|
31210
|
+
if (isObjectRecord2(nested) && typeof nested.type === "string") {
|
|
31211
|
+
return { status: nested.type, source: "legacy-data-status", keys };
|
|
31212
|
+
}
|
|
31213
|
+
return { status: undefined, source: "unknown-shape", keys };
|
|
31214
|
+
} catch (error) {
|
|
31215
|
+
log("[cancel-task] session status lookup failed", {
|
|
31216
|
+
taskID,
|
|
31217
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31218
|
+
});
|
|
31219
|
+
return { status: undefined, source: "lookup-error", keys: [] };
|
|
31220
|
+
}
|
|
31221
|
+
}
|
|
31222
|
+
function delay(ms) {
|
|
31223
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
31224
|
+
}
|
|
31225
|
+
function isObjectRecord2(value) {
|
|
31226
|
+
return typeof value === "object" && value !== null;
|
|
31227
|
+
}
|
|
31228
|
+
function isSessionID(value) {
|
|
31229
|
+
return /^ses_[\w-]+$/.test(value);
|
|
31230
|
+
}
|
|
31231
|
+
function normalizeCancelReason2(reason) {
|
|
31232
|
+
const normalized = reason?.replace(/\s+/g, " ").trim();
|
|
31233
|
+
return normalized ? `cancelled: ${normalized}` : "cancelled";
|
|
31234
|
+
}
|
|
31235
|
+
async function getSessionParentID(client, taskID) {
|
|
31236
|
+
const session2 = client.session;
|
|
31237
|
+
if (!session2.get)
|
|
31238
|
+
return;
|
|
31239
|
+
try {
|
|
31240
|
+
const response = await session2.get({ path: { id: taskID } });
|
|
31241
|
+
const data = response.data;
|
|
31242
|
+
if (!isObjectRecord2(data))
|
|
31243
|
+
return;
|
|
31244
|
+
const parentID = data.parentID;
|
|
31245
|
+
return typeof parentID === "string" ? parentID : undefined;
|
|
31246
|
+
} catch (error) {
|
|
31247
|
+
log("[cancel-task] session metadata lookup failed", {
|
|
31248
|
+
taskID,
|
|
31249
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31250
|
+
});
|
|
31251
|
+
return;
|
|
31252
|
+
}
|
|
31253
|
+
}
|
|
31254
|
+
function unknownTaskOutput(taskID, message) {
|
|
31255
|
+
return [
|
|
31256
|
+
`task_id: ${taskID}`,
|
|
31257
|
+
"state: unknown",
|
|
31258
|
+
"",
|
|
31259
|
+
"<task_error>",
|
|
31260
|
+
message,
|
|
31261
|
+
"</task_error>"
|
|
31262
|
+
].join(`
|
|
31263
|
+
`);
|
|
31264
|
+
}
|
|
30635
31265
|
// src/tools/council.ts
|
|
30636
31266
|
import {
|
|
30637
31267
|
tool as tool4
|
|
@@ -5,6 +5,12 @@ interface CancelTaskToolOptions {
|
|
|
5
5
|
backgroundJobBoard: BackgroundJobBoard;
|
|
6
6
|
shouldManageSession: (sessionID: string) => boolean;
|
|
7
7
|
abortTimeoutMs?: number;
|
|
8
|
+
verifyAbortMs?: number;
|
|
9
|
+
abortRetryIntervalMs?: number;
|
|
10
|
+
stableStoppedMs?: number;
|
|
11
|
+
deleteTimeoutMs?: number;
|
|
12
|
+
deleteVerifyMs?: number;
|
|
13
|
+
deleteStableStoppedMs?: number;
|
|
8
14
|
}
|
|
9
15
|
export declare function createCancelTaskTool(options: CancelTaskToolOptions): Record<string, ToolDefinition>;
|
|
10
16
|
export {};
|
package/dist/tui.js
CHANGED
|
@@ -69,6 +69,21 @@ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
|
69
69
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
70
70
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
71
71
|
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion or use task_status only when needed → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
|
|
72
|
+
var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
73
|
+
- Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
|
|
74
|
+
- Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
|
|
75
|
+
- Shell is acceptable for bulk or mechanical filesystem changes when it is clearer or safer than many individual edits (for example: truncate generated logs, remove build artifacts, batch rename/move files), especially when the user explicitly asks for that shell operation.
|
|
76
|
+
- Before destructive or broad shell operations, verify the target set and quote paths. Prefer a dry-run/listing first when practical.
|
|
77
|
+
- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
|
|
78
|
+
var READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
79
|
+
- READ-ONLY: inspect and report; do not modify files.
|
|
80
|
+
- Prefer dedicated file tools for codebase inspection: glob/grep/ast_grep_search for discovery and read for file contents.
|
|
81
|
+
- Bash is allowed for non-mutating diagnostics and shell-native inspection when it is the clearest tool, but not for modifying files.
|
|
82
|
+
- Do not use cat/head/tail/sed/awk only to read code into context; use read/grep unless a shell pipeline is genuinely the better diagnostic.`;
|
|
83
|
+
var NO_SHELL_READONLY_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
84
|
+
- READ-ONLY: inspect and report; do not modify files.
|
|
85
|
+
- Use glob/grep/ast_grep_search for discovery and read for file contents.
|
|
86
|
+
- Do not use bash or shell commands.`;
|
|
72
87
|
var TMUX_SPAWN_DELAY_MS = 500;
|
|
73
88
|
var COUNCILLOR_STAGGER_MS = 250;
|
|
74
89
|
var DEFAULT_DISABLED_AGENTS = ["observer"];
|
|
@@ -14,12 +14,16 @@ export interface BackgroundJobRecord {
|
|
|
14
14
|
objective?: string;
|
|
15
15
|
state: BackgroundJobState;
|
|
16
16
|
timedOut: boolean;
|
|
17
|
+
statusUncertain: boolean;
|
|
18
|
+
cancellationRequested: boolean;
|
|
17
19
|
terminalUnreconciled: boolean;
|
|
18
20
|
launchedAt: number;
|
|
19
21
|
lastLaunchedAt: number;
|
|
20
22
|
updatedAt: number;
|
|
23
|
+
lastLiveBusyAt?: number;
|
|
21
24
|
completedAt?: number;
|
|
22
25
|
resultSummary?: string;
|
|
26
|
+
lastStatusError?: string;
|
|
23
27
|
alias: string;
|
|
24
28
|
lastUsedAt: number;
|
|
25
29
|
terminalState?: TaskOutputState;
|
|
@@ -42,7 +46,9 @@ export interface BackgroundJobStatusInput {
|
|
|
42
46
|
taskID: string;
|
|
43
47
|
state: TaskOutputState;
|
|
44
48
|
timedOut?: boolean;
|
|
49
|
+
statusUncertain?: boolean;
|
|
45
50
|
resultSummary?: string;
|
|
51
|
+
lastStatusError?: string;
|
|
46
52
|
now?: number;
|
|
47
53
|
}
|
|
48
54
|
export declare class BackgroundJobBoard {
|
|
@@ -55,8 +61,11 @@ export declare class BackgroundJobBoard {
|
|
|
55
61
|
registerLaunch(input: BackgroundJobLaunchInput): BackgroundJobRecord;
|
|
56
62
|
updateStatus(input: BackgroundJobStatusInput): BackgroundJobRecord | undefined;
|
|
57
63
|
updateFromStatusOutput(output: string): BackgroundJobRecord | undefined;
|
|
64
|
+
markRunningFromLiveSession(taskID: string, now?: number): BackgroundJobRecord | undefined;
|
|
58
65
|
markReconciled(taskID: string, now?: number): BackgroundJobRecord | undefined;
|
|
59
|
-
markCancelled(taskID: string, reason?: string, now?: number
|
|
66
|
+
markCancelled(taskID: string, reason?: string, now?: number, options?: {
|
|
67
|
+
force?: boolean;
|
|
68
|
+
}): BackgroundJobRecord | undefined;
|
|
60
69
|
get(taskID: string): BackgroundJobRecord | undefined;
|
|
61
70
|
resolve(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
|
|
62
71
|
resolveForStatus(parentSessionID: string, taskIDOrAlias: string): BackgroundJobRecord | undefined;
|
package/dist/utils/task.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ export interface TaskStatusOutput {
|
|
|
13
13
|
timedOut: boolean;
|
|
14
14
|
result?: string;
|
|
15
15
|
}
|
|
16
|
+
export type TaskStatusClassification = 'running' | 'terminal' | 'timeout' | 'transient_process_error' | 'unknown_error';
|
|
16
17
|
export declare function parseTaskIdFromTaskOutput(output: string): string | undefined;
|
|
17
18
|
export declare function parseTaskLaunchOutput(output: string): TaskLaunchOutput | undefined;
|
|
18
19
|
export declare function parseTaskStatusOutput(output: string): TaskStatusOutput | undefined;
|
|
20
|
+
export declare function classifyTaskStatusOutput(status: TaskStatusOutput): TaskStatusClassification;
|
|
19
21
|
export declare function parseTaskStateFromOutput(output: string): TaskOutputState | undefined;
|
|
20
22
|
export declare function parseTaskResultFromOutput(output: string): string | undefined;
|
package/package.json
CHANGED