oh-my-opencode-slim 2.0.0-beta.12 → 2.0.0-beta.14
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.ja-JP.md +15 -16
- package/README.ko-KR.md +632 -0
- package/README.md +41 -18
- package/README.zh-CN.md +15 -15
- package/dist/cli/background-subagents.d.ts +13 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +251 -25
- package/dist/cli/install.d.ts +6 -1
- package/dist/cli/types.d.ts +5 -0
- package/dist/config/schema.d.ts +0 -13
- package/dist/hooks/index.d.ts +0 -1
- package/dist/index.js +638 -895
- package/dist/tools/cancel-task.d.ts +6 -0
- package/dist/tools/preset-manager.d.ts +6 -7
- package/dist/tui.js +14 -8
- package/dist/utils/background-job-board.d.ts +9 -1
- package/dist/utils/task.d.ts +2 -0
- package/oh-my-opencode-slim.schema.json +0 -31
- package/package.json +3 -1
- package/src/skills/codemap.md +3 -1
- package/src/skills/oh-my-opencode-slim/SKILL.md +326 -0
- package/dist/hooks/todo-continuation/index.d.ts +0 -55
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +0 -35
package/dist/index.js
CHANGED
|
@@ -18196,6 +18196,13 @@ function getCustomOpenCodeConfigDir() {
|
|
|
18196
18196
|
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
18197
18197
|
return configDir || undefined;
|
|
18198
18198
|
}
|
|
18199
|
+
function getConfigDir() {
|
|
18200
|
+
const customConfigDir = getCustomOpenCodeConfigDir();
|
|
18201
|
+
if (customConfigDir) {
|
|
18202
|
+
return customConfigDir;
|
|
18203
|
+
}
|
|
18204
|
+
return getDefaultOpenCodeConfigDir();
|
|
18205
|
+
}
|
|
18199
18206
|
function getConfigSearchDirs() {
|
|
18200
18207
|
const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
|
|
18201
18208
|
return dirs.filter((dir, index) => {
|
|
@@ -18203,7 +18210,7 @@ function getConfigSearchDirs() {
|
|
|
18203
18210
|
});
|
|
18204
18211
|
}
|
|
18205
18212
|
function getOpenCodeConfigPaths() {
|
|
18206
|
-
const configDir =
|
|
18213
|
+
const configDir = getConfigDir();
|
|
18207
18214
|
return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
|
|
18208
18215
|
}
|
|
18209
18216
|
|
|
@@ -18232,6 +18239,12 @@ var CUSTOM_SKILLS = [
|
|
|
18232
18239
|
description: "Heavy/complex coding sessions and large modifications workflow",
|
|
18233
18240
|
allowedAgents: ["orchestrator"],
|
|
18234
18241
|
sourcePath: "src/skills/deepwork"
|
|
18242
|
+
},
|
|
18243
|
+
{
|
|
18244
|
+
name: "oh-my-opencode-slim",
|
|
18245
|
+
description: "Configure, customize, and safely improve oh-my-opencode-slim setups",
|
|
18246
|
+
allowedAgents: ["orchestrator"],
|
|
18247
|
+
sourcePath: "src/skills/oh-my-opencode-slim"
|
|
18235
18248
|
}
|
|
18236
18249
|
];
|
|
18237
18250
|
|
|
@@ -18597,12 +18610,6 @@ var DivoomConfigSchema = z2.object({
|
|
|
18597
18610
|
posterizeBits: z2.number().int().min(1).max(8).default(3),
|
|
18598
18611
|
gifs: z2.record(z2.string(), z2.string().min(1)).optional()
|
|
18599
18612
|
});
|
|
18600
|
-
var TodoContinuationConfigSchema = z2.object({
|
|
18601
|
-
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
18602
|
-
cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
|
|
18603
|
-
autoEnable: z2.boolean().default(false).describe("Automatically enable auto-continue when the orchestrator session has enough todos"),
|
|
18604
|
-
autoEnableThreshold: z2.number().int().min(1).max(50).default(4).describe("Number of todos that triggers auto-enable (only used when autoEnable is true)")
|
|
18605
|
-
});
|
|
18606
18613
|
var FailoverConfigSchema = z2.object({
|
|
18607
18614
|
enabled: z2.boolean().default(true),
|
|
18608
18615
|
timeoutMs: z2.number().min(0).default(15000),
|
|
@@ -18649,7 +18656,6 @@ var PluginConfigSchema = z2.object({
|
|
|
18649
18656
|
interview: InterviewConfigSchema.optional(),
|
|
18650
18657
|
backgroundJobs: BackgroundJobsConfigSchema.optional(),
|
|
18651
18658
|
divoom: DivoomConfigSchema.optional(),
|
|
18652
|
-
todoContinuation: TodoContinuationConfigSchema.optional(),
|
|
18653
18659
|
fallback: FailoverConfigSchema.optional(),
|
|
18654
18660
|
council: CouncilConfigSchema.optional()
|
|
18655
18661
|
}).superRefine((value, ctx) => {
|
|
@@ -18903,34 +18909,32 @@ function parseModelReference(model) {
|
|
|
18903
18909
|
async function promptWithTimeout(client, args, timeoutMs, signal) {
|
|
18904
18910
|
if (signal?.aborted)
|
|
18905
18911
|
throw new Error("Prompt cancelled");
|
|
18906
|
-
if (timeoutMs <= 0) {
|
|
18907
|
-
await client.session.prompt(args);
|
|
18908
|
-
return;
|
|
18909
|
-
}
|
|
18910
18912
|
const sessionId = args.path.id;
|
|
18913
|
+
const hasTimeout = timeoutMs > 0;
|
|
18911
18914
|
let timer;
|
|
18912
18915
|
let onAbort;
|
|
18913
18916
|
try {
|
|
18914
18917
|
const promptPromise = client.session.prompt(args);
|
|
18915
18918
|
promptPromise.catch(() => {});
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
new Promise((_, reject) => {
|
|
18919
|
+
const racers = [promptPromise];
|
|
18920
|
+
if (hasTimeout) {
|
|
18921
|
+
racers.push(new Promise((_, reject) => {
|
|
18919
18922
|
timer = setTimeout(() => {
|
|
18920
18923
|
reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
|
|
18921
18924
|
}, timeoutMs);
|
|
18922
|
-
})
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
|
|
18925
|
+
}));
|
|
18926
|
+
}
|
|
18927
|
+
if (signal) {
|
|
18928
|
+
racers.push(new Promise((_, reject) => {
|
|
18926
18929
|
if (signal.aborted) {
|
|
18927
18930
|
reject(new Error("Prompt cancelled"));
|
|
18928
18931
|
return;
|
|
18929
18932
|
}
|
|
18930
18933
|
onAbort = () => reject(new Error("Prompt cancelled"));
|
|
18931
18934
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
18932
|
-
})
|
|
18933
|
-
|
|
18935
|
+
}));
|
|
18936
|
+
}
|
|
18937
|
+
await Promise.race(racers);
|
|
18934
18938
|
} catch (error) {
|
|
18935
18939
|
if (error instanceof OperationTimeoutError) {
|
|
18936
18940
|
try {
|
|
@@ -19161,13 +19165,6 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
|
|
|
19161
19165
|
- If multiple remembered sessions fit, prefer the most recently used matching session.
|
|
19162
19166
|
- Prefer re-uses over creating new sessions all the time
|
|
19163
19167
|
|
|
19164
|
-
### Auto-Continue
|
|
19165
|
-
When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
|
|
19166
|
-
- **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
|
|
19167
|
-
- **Don't enable when:** User is in an interactive/conversational flow, or each step needs explicit review
|
|
19168
|
-
- Use the \`auto_continue\` tool with \`enabled: true\` to activate. The system will automatically resume you when incomplete todos remain after you stop.
|
|
19169
|
-
- The user can toggle this anytime via the \`/auto-continue\` command.
|
|
19170
|
-
|
|
19171
19168
|
### Validation routing
|
|
19172
19169
|
- Validation is a workflow stage owned by the Orchestrator, not a separate specialist
|
|
19173
19170
|
${enabledValidationRouting}
|
|
@@ -20039,10 +20036,6 @@ function setActiveRuntimePresetWithPrevious(name) {
|
|
|
20039
20036
|
previousRuntimePreset = activeRuntimePreset;
|
|
20040
20037
|
activeRuntimePreset = name;
|
|
20041
20038
|
}
|
|
20042
|
-
function rollbackRuntimePreset(previous) {
|
|
20043
|
-
activeRuntimePreset = previous;
|
|
20044
|
-
previousRuntimePreset = null;
|
|
20045
|
-
}
|
|
20046
20039
|
|
|
20047
20040
|
// src/utils/logger.ts
|
|
20048
20041
|
import * as fs2 from "node:fs";
|
|
@@ -20055,7 +20048,7 @@ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
20055
20048
|
var logFile = null;
|
|
20056
20049
|
var writeChain = Promise.resolve();
|
|
20057
20050
|
function getLogDir() {
|
|
20058
|
-
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
|
|
20051
|
+
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode/log");
|
|
20059
20052
|
}
|
|
20060
20053
|
function cleanupOldLogs(logDir) {
|
|
20061
20054
|
try {
|
|
@@ -22642,6 +22635,10 @@ function createDisplayNameMentionRewriter(config) {
|
|
|
22642
22635
|
};
|
|
22643
22636
|
}
|
|
22644
22637
|
// src/utils/task.ts
|
|
22638
|
+
var TRANSIENT_PROCESS_ERROR_TEXT = new Set([
|
|
22639
|
+
"Task is not running in this process and has no final output.",
|
|
22640
|
+
"Task is not running in this process and has not produced output."
|
|
22641
|
+
]);
|
|
22645
22642
|
function parseTaskIdFromTaskOutput(output) {
|
|
22646
22643
|
const lines = output.split(/\r?\n/);
|
|
22647
22644
|
for (const line of lines) {
|
|
@@ -22677,6 +22674,19 @@ function parseTaskStatusOutput(output) {
|
|
|
22677
22674
|
result: parseTaskResultFromOutput(output)
|
|
22678
22675
|
};
|
|
22679
22676
|
}
|
|
22677
|
+
function classifyTaskStatusOutput(status) {
|
|
22678
|
+
if (status.timedOut)
|
|
22679
|
+
return "timeout";
|
|
22680
|
+
if (status.state === "running")
|
|
22681
|
+
return "running";
|
|
22682
|
+
if (status.state === "completed" || status.state === "cancelled") {
|
|
22683
|
+
return "terminal";
|
|
22684
|
+
}
|
|
22685
|
+
if (TRANSIENT_PROCESS_ERROR_TEXT.has(status.result ?? "")) {
|
|
22686
|
+
return "transient_process_error";
|
|
22687
|
+
}
|
|
22688
|
+
return "unknown_error";
|
|
22689
|
+
}
|
|
22680
22690
|
function parseTaskStateFromOutput(output) {
|
|
22681
22691
|
for (const line of getTaskHeader(output).split(/\r?\n/)) {
|
|
22682
22692
|
const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
|
|
@@ -22735,11 +22745,15 @@ class BackgroundJobBoard {
|
|
|
22735
22745
|
objective: input.objective ?? existing.objective,
|
|
22736
22746
|
state: "running",
|
|
22737
22747
|
timedOut: false,
|
|
22748
|
+
statusUncertain: false,
|
|
22749
|
+
cancellationRequested: false,
|
|
22738
22750
|
terminalUnreconciled: false,
|
|
22739
22751
|
completedAt: undefined,
|
|
22740
22752
|
resultSummary: undefined,
|
|
22753
|
+
lastStatusError: undefined,
|
|
22741
22754
|
terminalState: undefined,
|
|
22742
22755
|
lastLaunchedAt: now,
|
|
22756
|
+
lastLiveBusyAt: now,
|
|
22743
22757
|
lastUsedAt: now,
|
|
22744
22758
|
updatedAt: now
|
|
22745
22759
|
};
|
|
@@ -22754,9 +22768,12 @@ class BackgroundJobBoard {
|
|
|
22754
22768
|
objective: input.objective,
|
|
22755
22769
|
state: "running",
|
|
22756
22770
|
timedOut: false,
|
|
22771
|
+
statusUncertain: false,
|
|
22772
|
+
cancellationRequested: false,
|
|
22757
22773
|
terminalUnreconciled: false,
|
|
22758
22774
|
launchedAt: now,
|
|
22759
22775
|
lastLaunchedAt: now,
|
|
22776
|
+
lastLiveBusyAt: now,
|
|
22760
22777
|
lastUsedAt: now,
|
|
22761
22778
|
updatedAt: now,
|
|
22762
22779
|
alias: this.nextAlias(input.parentSessionID, input.agent),
|
|
@@ -22778,11 +22795,13 @@ class BackgroundJobBoard {
|
|
|
22778
22795
|
...existing,
|
|
22779
22796
|
state: input.state,
|
|
22780
22797
|
timedOut: input.timedOut ?? false,
|
|
22798
|
+
statusUncertain: input.statusUncertain ?? false,
|
|
22781
22799
|
terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
|
|
22782
22800
|
updatedAt: now,
|
|
22783
22801
|
completedAt: terminal ? existing.completedAt ?? now : existing.completedAt,
|
|
22784
22802
|
terminalState: terminal ? input.state : existing.terminalState,
|
|
22785
|
-
resultSummary: input.resultSummary ?? existing.resultSummary
|
|
22803
|
+
resultSummary: input.resultSummary ?? existing.resultSummary,
|
|
22804
|
+
lastStatusError: input.lastStatusError
|
|
22786
22805
|
};
|
|
22787
22806
|
this.jobs.set(input.taskID, updated);
|
|
22788
22807
|
this.trimReusable(input.taskID);
|
|
@@ -22803,18 +22822,28 @@ class BackgroundJobBoard {
|
|
|
22803
22822
|
const existing = this.jobs.get(taskID);
|
|
22804
22823
|
if (!existing)
|
|
22805
22824
|
return;
|
|
22806
|
-
const
|
|
22807
|
-
if (!
|
|
22808
|
-
|
|
22825
|
+
const isStaleTerminal = TERMINAL_STATES.has(existing.state) || existing.state === "reconciled";
|
|
22826
|
+
if (!isStaleTerminal || existing.cancellationRequested) {
|
|
22827
|
+
const updated2 = {
|
|
22828
|
+
...existing,
|
|
22829
|
+
lastLiveBusyAt: now
|
|
22830
|
+
};
|
|
22831
|
+
this.jobs.set(taskID, updated2);
|
|
22832
|
+
return updated2;
|
|
22833
|
+
}
|
|
22809
22834
|
const updated = {
|
|
22810
22835
|
...existing,
|
|
22811
22836
|
state: "running",
|
|
22812
22837
|
timedOut: false,
|
|
22838
|
+
statusUncertain: false,
|
|
22839
|
+
cancellationRequested: false,
|
|
22813
22840
|
terminalUnreconciled: false,
|
|
22814
22841
|
updatedAt: now,
|
|
22842
|
+
lastLiveBusyAt: now,
|
|
22815
22843
|
completedAt: undefined,
|
|
22816
22844
|
terminalState: undefined,
|
|
22817
|
-
resultSummary: undefined
|
|
22845
|
+
resultSummary: undefined,
|
|
22846
|
+
lastStatusError: undefined
|
|
22818
22847
|
};
|
|
22819
22848
|
this.jobs.set(taskID, updated);
|
|
22820
22849
|
return updated;
|
|
@@ -22830,6 +22859,7 @@ class BackgroundJobBoard {
|
|
|
22830
22859
|
...existing,
|
|
22831
22860
|
state: "reconciled",
|
|
22832
22861
|
terminalUnreconciled: false,
|
|
22862
|
+
statusUncertain: false,
|
|
22833
22863
|
updatedAt: now,
|
|
22834
22864
|
lastUsedAt: now,
|
|
22835
22865
|
terminalState: existing.terminalState ?? terminalStateOf(existing.state)
|
|
@@ -22838,24 +22868,29 @@ class BackgroundJobBoard {
|
|
|
22838
22868
|
this.trimReusable(taskID);
|
|
22839
22869
|
return updated;
|
|
22840
22870
|
}
|
|
22841
|
-
markCancelled(taskID, reason, now = Date.now()) {
|
|
22871
|
+
markCancelled(taskID, reason, now = Date.now(), options = {}) {
|
|
22842
22872
|
const existing = this.jobs.get(taskID);
|
|
22843
22873
|
if (!existing)
|
|
22844
22874
|
return;
|
|
22845
|
-
if (
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22875
|
+
if (!options.force) {
|
|
22876
|
+
if (existing.state === "reconciled")
|
|
22877
|
+
return existing;
|
|
22878
|
+
if (TERMINAL_STATES.has(existing.state))
|
|
22879
|
+
return existing;
|
|
22880
|
+
}
|
|
22849
22881
|
const summary = normalizeCancelReason(reason);
|
|
22850
22882
|
const updated = {
|
|
22851
22883
|
...existing,
|
|
22852
22884
|
state: "cancelled",
|
|
22853
22885
|
timedOut: false,
|
|
22886
|
+
statusUncertain: false,
|
|
22887
|
+
cancellationRequested: true,
|
|
22854
22888
|
terminalUnreconciled: true,
|
|
22855
22889
|
updatedAt: now,
|
|
22856
22890
|
completedAt: existing.completedAt ?? now,
|
|
22857
22891
|
terminalState: "cancelled",
|
|
22858
|
-
resultSummary: summary
|
|
22892
|
+
resultSummary: summary,
|
|
22893
|
+
lastStatusError: undefined
|
|
22859
22894
|
};
|
|
22860
22895
|
this.jobs.set(taskID, updated);
|
|
22861
22896
|
return updated;
|
|
@@ -22925,7 +22960,7 @@ class BackgroundJobBoard {
|
|
|
22925
22960
|
return [
|
|
22926
22961
|
"### Background Job Board",
|
|
22927
22962
|
"SENTINEL: background-job-board-v2",
|
|
22928
|
-
"Use task_status for running jobs. Reconcile terminal jobs before final response. Reuse
|
|
22963
|
+
"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
22964
|
"",
|
|
22930
22965
|
"#### Active / Unreconciled",
|
|
22931
22966
|
...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
|
|
@@ -22981,7 +23016,8 @@ function deriveTaskSessionLabel(input) {
|
|
|
22981
23016
|
return firstPromptLine ? firstPromptLine.slice(0, 48) : `recent ${input.agentType} task`;
|
|
22982
23017
|
}
|
|
22983
23018
|
function isReusable(job) {
|
|
22984
|
-
|
|
23019
|
+
const terminal = job.terminalState ?? terminalStateOf(job.state);
|
|
23020
|
+
return terminal === "completed" && !job.terminalUnreconciled;
|
|
22985
23021
|
}
|
|
22986
23022
|
function terminalStateOf(state) {
|
|
22987
23023
|
return state === "completed" || state === "error" || state === "cancelled" ? state : undefined;
|
|
@@ -23001,13 +23037,15 @@ function formatJob(job, now = Date.now()) {
|
|
|
23001
23037
|
const ageMs = now - job.lastLaunchedAt;
|
|
23002
23038
|
const isResume = job.lastLaunchedAt !== job.launchedAt;
|
|
23003
23039
|
const ageLabel = job.state === "running" && ageMs < 30000 ? ` [${isResume ? "resumed" : "just launched"}, ${Math.floor(ageMs / 1000)}s ago]` : "";
|
|
23004
|
-
const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
|
|
23040
|
+
const status = job.terminalUnreconciled ? `${job.state}, unreconciled` : job.statusUncertain ? `${job.state}, status uncertain` : job.timedOut ? `${job.state}, timed out` : `${job.state}${ageLabel}`;
|
|
23005
23041
|
const lines = [
|
|
23006
23042
|
`- ${job.alias} / ${job.taskID} / ${job.agent} / ${status}`,
|
|
23007
23043
|
` Objective: ${job.objective || job.description}`
|
|
23008
23044
|
];
|
|
23009
23045
|
if (job.resultSummary && job.terminalUnreconciled) {
|
|
23010
23046
|
lines.push(` Result: ${singleLine(job.resultSummary)}`);
|
|
23047
|
+
} else if (job.lastStatusError && job.statusUncertain) {
|
|
23048
|
+
lines.push(` Status: ${singleLine(job.lastStatusError)}`);
|
|
23011
23049
|
}
|
|
23012
23050
|
return lines.join(`
|
|
23013
23051
|
`);
|
|
@@ -24094,6 +24132,17 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24094
24132
|
timedOut: status.timedOut,
|
|
24095
24133
|
hasResult: Boolean(status.result)
|
|
24096
24134
|
});
|
|
24135
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24136
|
+
if (isLateCancelledTaskError(existing, status.state)) {
|
|
24137
|
+
log("[task-session-manager] suppressed late cancelled task error", {
|
|
24138
|
+
taskID: status.taskID,
|
|
24139
|
+
alias: existing?.alias,
|
|
24140
|
+
state: existing?.state,
|
|
24141
|
+
terminalState: existing?.terminalState,
|
|
24142
|
+
result: status.result
|
|
24143
|
+
});
|
|
24144
|
+
return existing;
|
|
24145
|
+
}
|
|
24097
24146
|
const updated = backgroundJobBoard.updateStatus({
|
|
24098
24147
|
taskID: status.taskID,
|
|
24099
24148
|
state: status.state,
|
|
@@ -24122,6 +24171,61 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24122
24171
|
}
|
|
24123
24172
|
return updated;
|
|
24124
24173
|
}
|
|
24174
|
+
async function handleTransientTaskStatusOutput(output) {
|
|
24175
|
+
if (typeof output.output !== "string")
|
|
24176
|
+
return false;
|
|
24177
|
+
const status = parseTaskStatusOutput(output.output);
|
|
24178
|
+
if (!status)
|
|
24179
|
+
return false;
|
|
24180
|
+
if (classifyTaskStatusOutput(status) !== "transient_process_error") {
|
|
24181
|
+
return false;
|
|
24182
|
+
}
|
|
24183
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24184
|
+
const liveStatus = existing && existing.state === "running" ? undefined : await getLiveSessionStatus(status.taskID);
|
|
24185
|
+
const recentLiveBusy = !!existing?.lastLiveBusyAt && (!existing.completedAt || existing.lastLiveBusyAt >= existing.completedAt);
|
|
24186
|
+
const isStillRunning = existing?.state === "running" || recentLiveBusy || liveStatus === "busy" || liveStatus === "retry";
|
|
24187
|
+
if (!isStillRunning)
|
|
24188
|
+
return false;
|
|
24189
|
+
const updated = existing?.state === "running" ? backgroundJobBoard.updateStatus({
|
|
24190
|
+
taskID: status.taskID,
|
|
24191
|
+
state: "running",
|
|
24192
|
+
statusUncertain: true,
|
|
24193
|
+
lastStatusError: status.result
|
|
24194
|
+
}) : undefined;
|
|
24195
|
+
log("[task-session-manager] classified transient task_status error", {
|
|
24196
|
+
taskID: status.taskID,
|
|
24197
|
+
alias: existing?.alias,
|
|
24198
|
+
parentSessionID: existing?.parentSessionID,
|
|
24199
|
+
previousState: existing?.state,
|
|
24200
|
+
updatedState: updated?.state,
|
|
24201
|
+
liveStatus,
|
|
24202
|
+
recentLiveBusy
|
|
24203
|
+
});
|
|
24204
|
+
return true;
|
|
24205
|
+
}
|
|
24206
|
+
async function getLiveSessionStatus(sessionID) {
|
|
24207
|
+
try {
|
|
24208
|
+
const response = await _ctx.client.session.status();
|
|
24209
|
+
const data = response.data;
|
|
24210
|
+
if (!isObjectRecord(data))
|
|
24211
|
+
return;
|
|
24212
|
+
const item = data[sessionID];
|
|
24213
|
+
if (item === undefined)
|
|
24214
|
+
return "idle";
|
|
24215
|
+
if (isObjectRecord(item) && typeof item.type === "string") {
|
|
24216
|
+
return item.type;
|
|
24217
|
+
}
|
|
24218
|
+
const directType = data.type;
|
|
24219
|
+
if (typeof directType === "string")
|
|
24220
|
+
return directType;
|
|
24221
|
+
const nestedStatus = data.status;
|
|
24222
|
+
if (!isObjectRecord(nestedStatus))
|
|
24223
|
+
return;
|
|
24224
|
+
return typeof nestedStatus.type === "string" ? nestedStatus.type : undefined;
|
|
24225
|
+
} catch {
|
|
24226
|
+
return;
|
|
24227
|
+
}
|
|
24228
|
+
}
|
|
24125
24229
|
function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
|
|
24126
24230
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
24127
24231
|
return;
|
|
@@ -24134,11 +24238,24 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24134
24238
|
const status = parseTaskStatusOutput(part.text);
|
|
24135
24239
|
if (!status)
|
|
24136
24240
|
return;
|
|
24241
|
+
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
24242
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24243
|
+
if (isFailed && isLateCancelledTaskError(existing, status.state)) {
|
|
24244
|
+
part.text = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
|
|
24245
|
+
log("[task-session-manager] normalized late cancelled injected failure", {
|
|
24246
|
+
taskID: status.taskID,
|
|
24247
|
+
alias: existing?.alias,
|
|
24248
|
+
state: existing?.state,
|
|
24249
|
+
terminalState: existing?.terminalState,
|
|
24250
|
+
result: status.result
|
|
24251
|
+
});
|
|
24252
|
+
rememberProcessedInjectedCompletion(occurrenceId);
|
|
24253
|
+
return existing;
|
|
24254
|
+
}
|
|
24137
24255
|
if (isCompleted && status.state !== "completed")
|
|
24138
24256
|
return;
|
|
24139
24257
|
if (isFailed && status.state !== "error")
|
|
24140
24258
|
return;
|
|
24141
|
-
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
24142
24259
|
if (processedInjectedCompletions.has(occurrenceId))
|
|
24143
24260
|
return;
|
|
24144
24261
|
const updated = updateBackgroundJobFromOutput(part.text);
|
|
@@ -24298,6 +24415,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24298
24415
|
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
24299
24416
|
return;
|
|
24300
24417
|
}
|
|
24418
|
+
normalizeLateCancelledToolStatus(output);
|
|
24419
|
+
if (await handleTransientTaskStatusOutput(output)) {
|
|
24420
|
+
return;
|
|
24421
|
+
}
|
|
24301
24422
|
updateBackgroundJobFromOutput(output.output);
|
|
24302
24423
|
return;
|
|
24303
24424
|
}
|
|
@@ -24421,12 +24542,27 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24421
24542
|
const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
24422
24543
|
const before = sessionId2 ? backgroundJobBoard.get(sessionId2) : undefined;
|
|
24423
24544
|
const updated = sessionId2 ? backgroundJobBoard.markRunningFromLiveSession(sessionId2) : undefined;
|
|
24545
|
+
if (before?.cancellationRequested) {
|
|
24546
|
+
log("[task-session-manager] busy observed after cancel request", {
|
|
24547
|
+
sessionID: sessionId2,
|
|
24548
|
+
previousState: before.state,
|
|
24549
|
+
previousTerminalState: before.terminalState,
|
|
24550
|
+
terminalUnreconciled: before.terminalUnreconciled,
|
|
24551
|
+
resultSummary: before.resultSummary,
|
|
24552
|
+
updatedState: updated?.state,
|
|
24553
|
+
updatedCancellationRequested: updated?.cancellationRequested
|
|
24554
|
+
});
|
|
24555
|
+
}
|
|
24424
24556
|
log("[task-session-manager] busy/status busy observed", {
|
|
24425
24557
|
sessionID: sessionId2,
|
|
24426
24558
|
managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
|
|
24427
24559
|
previousState: before?.state,
|
|
24428
24560
|
previousTerminalState: before?.terminalState,
|
|
24429
|
-
|
|
24561
|
+
previousCancellationRequested: before?.cancellationRequested,
|
|
24562
|
+
previousLastLiveBusyAt: before?.lastLiveBusyAt,
|
|
24563
|
+
updatedState: updated?.state,
|
|
24564
|
+
updatedCancellationRequested: updated?.cancellationRequested,
|
|
24565
|
+
updatedLastLiveBusyAt: updated?.lastLiveBusyAt
|
|
24430
24566
|
});
|
|
24431
24567
|
return;
|
|
24432
24568
|
}
|
|
@@ -24459,715 +24595,45 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24459
24595
|
}
|
|
24460
24596
|
}
|
|
24461
24597
|
};
|
|
24462
|
-
|
|
24463
|
-
|
|
24464
|
-
import { tool } from "@opencode-ai/plugin";
|
|
24465
|
-
|
|
24466
|
-
// src/hooks/todo-continuation/todo-hygiene.ts
|
|
24467
|
-
var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
|
|
24468
|
-
var TODO_FINAL_ACTIVE_REMINDER = "If you are finishing now, do not leave the active todo in_progress. Mark it completed, or move unfinished work back to pending.";
|
|
24469
|
-
var RESET = new Set(["todowrite"]);
|
|
24470
|
-
var IGNORE = new Set(["auto_continue"]);
|
|
24471
|
-
function createTodoHygiene(options) {
|
|
24472
|
-
const pending = new Map;
|
|
24473
|
-
const active = new Set;
|
|
24474
|
-
function clearCycle(sessionID) {
|
|
24475
|
-
pending.delete(sessionID);
|
|
24476
|
-
}
|
|
24477
|
-
function clear(sessionID) {
|
|
24478
|
-
clearCycle(sessionID);
|
|
24479
|
-
active.delete(sessionID);
|
|
24480
|
-
}
|
|
24481
|
-
function isFinalActive(state) {
|
|
24482
|
-
return state.inProgressCount === 1 && state.pendingCount === 0 && state.openCount === 1;
|
|
24483
|
-
}
|
|
24484
|
-
function mark(sessionID, reason) {
|
|
24485
|
-
const reasons = pending.get(sessionID) ?? new Set;
|
|
24486
|
-
reasons.add(reason);
|
|
24487
|
-
pending.set(sessionID, reasons);
|
|
24488
|
-
}
|
|
24489
|
-
function pick(reasons) {
|
|
24490
|
-
if (reasons.has("final_active")) {
|
|
24491
|
-
return TODO_FINAL_ACTIVE_REMINDER;
|
|
24492
|
-
}
|
|
24493
|
-
return TODO_HYGIENE_REMINDER;
|
|
24494
|
-
}
|
|
24495
|
-
return {
|
|
24496
|
-
handleRequestStart(input) {
|
|
24497
|
-
clear(input.sessionID);
|
|
24498
|
-
},
|
|
24499
|
-
async handleToolExecuteAfter(input, _output) {
|
|
24500
|
-
if (!input.sessionID) {
|
|
24501
|
-
return;
|
|
24502
|
-
}
|
|
24503
|
-
const tool = input.tool.toLowerCase();
|
|
24504
|
-
if (IGNORE.has(tool)) {
|
|
24505
|
-
return;
|
|
24506
|
-
}
|
|
24507
|
-
try {
|
|
24508
|
-
if (RESET.has(tool)) {
|
|
24509
|
-
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
24510
|
-
clear(input.sessionID);
|
|
24511
|
-
return;
|
|
24512
|
-
}
|
|
24513
|
-
active.add(input.sessionID);
|
|
24514
|
-
clearCycle(input.sessionID);
|
|
24515
|
-
const state2 = await options.getTodoState(input.sessionID);
|
|
24516
|
-
if (!state2.hasOpenTodos) {
|
|
24517
|
-
active.delete(input.sessionID);
|
|
24518
|
-
options.log?.("Cleared todo hygiene cycle", {
|
|
24519
|
-
sessionID: input.sessionID,
|
|
24520
|
-
tool
|
|
24521
|
-
});
|
|
24522
|
-
return;
|
|
24523
|
-
}
|
|
24524
|
-
if (!isFinalActive(state2)) {
|
|
24525
|
-
options.log?.("Reset todo hygiene cycle", {
|
|
24526
|
-
sessionID: input.sessionID,
|
|
24527
|
-
tool
|
|
24528
|
-
});
|
|
24529
|
-
return;
|
|
24530
|
-
}
|
|
24531
|
-
mark(input.sessionID, "final_active");
|
|
24532
|
-
options.log?.("Armed final-active todo hygiene reminder", {
|
|
24533
|
-
sessionID: input.sessionID,
|
|
24534
|
-
tool
|
|
24535
|
-
});
|
|
24536
|
-
return;
|
|
24537
|
-
}
|
|
24538
|
-
if (!active.has(input.sessionID)) {
|
|
24539
|
-
return;
|
|
24540
|
-
}
|
|
24541
|
-
if (pending.get(input.sessionID)?.has("final_active")) {
|
|
24542
|
-
return;
|
|
24543
|
-
}
|
|
24544
|
-
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
24545
|
-
clear(input.sessionID);
|
|
24546
|
-
return;
|
|
24547
|
-
}
|
|
24548
|
-
const state = await options.getTodoState(input.sessionID);
|
|
24549
|
-
if (!state.hasOpenTodos) {
|
|
24550
|
-
clear(input.sessionID);
|
|
24551
|
-
return;
|
|
24552
|
-
}
|
|
24553
|
-
if (isFinalActive(state)) {
|
|
24554
|
-
mark(input.sessionID, "final_active");
|
|
24555
|
-
} else {
|
|
24556
|
-
mark(input.sessionID, "general");
|
|
24557
|
-
}
|
|
24558
|
-
options.log?.("Armed todo hygiene reminder", {
|
|
24559
|
-
sessionID: input.sessionID,
|
|
24560
|
-
tool,
|
|
24561
|
-
reasons: Array.from(pending.get(input.sessionID) ?? [])
|
|
24562
|
-
});
|
|
24563
|
-
} catch (error) {
|
|
24564
|
-
options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
|
|
24565
|
-
sessionID: input.sessionID,
|
|
24566
|
-
tool,
|
|
24567
|
-
error: error instanceof Error ? error.message : String(error)
|
|
24568
|
-
});
|
|
24569
|
-
}
|
|
24570
|
-
},
|
|
24571
|
-
getPendingReminder(sessionID) {
|
|
24572
|
-
const reasons = pending.get(sessionID);
|
|
24573
|
-
if (!reasons || reasons.size === 0) {
|
|
24574
|
-
return null;
|
|
24575
|
-
}
|
|
24576
|
-
if (options.shouldInject && !options.shouldInject(sessionID)) {
|
|
24577
|
-
clear(sessionID);
|
|
24578
|
-
return null;
|
|
24579
|
-
}
|
|
24580
|
-
const reminder = pick(reasons);
|
|
24581
|
-
options.log?.("Read todo hygiene reminder", {
|
|
24582
|
-
sessionID,
|
|
24583
|
-
reminder,
|
|
24584
|
-
reasons: Array.from(reasons)
|
|
24585
|
-
});
|
|
24586
|
-
return reminder;
|
|
24587
|
-
},
|
|
24588
|
-
handleEvent(event) {
|
|
24589
|
-
if (event.type !== "session.deleted") {
|
|
24590
|
-
return;
|
|
24591
|
-
}
|
|
24592
|
-
const sessionID = event.properties?.sessionID ?? event.properties?.info?.id;
|
|
24593
|
-
if (!sessionID) {
|
|
24594
|
-
return;
|
|
24595
|
-
}
|
|
24596
|
-
clear(sessionID);
|
|
24597
|
-
}
|
|
24598
|
-
};
|
|
24599
|
-
}
|
|
24600
|
-
|
|
24601
|
-
// src/hooks/todo-continuation/index.ts
|
|
24602
|
-
var HOOK_NAME = "todo-continuation";
|
|
24603
|
-
var COMMAND_NAME2 = "auto-continue";
|
|
24604
|
-
var TODO_STATE_TIMEOUT_MS = 500;
|
|
24605
|
-
var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
|
|
24606
|
-
var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
|
|
24607
|
-
var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
|
|
24608
|
-
var SUPPRESS_AFTER_ABORT_MS = 5000;
|
|
24609
|
-
var NOTIFICATION_BUSY_GRACE_MS = 250;
|
|
24610
|
-
var QUESTION_PHRASES = [
|
|
24611
|
-
"would you like",
|
|
24612
|
-
"should i",
|
|
24613
|
-
"do you want",
|
|
24614
|
-
"please review",
|
|
24615
|
-
"let me know",
|
|
24616
|
-
"what do you think",
|
|
24617
|
-
"can you confirm",
|
|
24618
|
-
"would you prefer",
|
|
24619
|
-
"shall i",
|
|
24620
|
-
"any thoughts"
|
|
24621
|
-
];
|
|
24622
|
-
var TERMINAL_TODO_STATUSES = ["completed", "cancelled"];
|
|
24623
|
-
function isQuestion(text) {
|
|
24624
|
-
const lowerText = text.toLowerCase().trim();
|
|
24625
|
-
if (/\?\s*$/.test(lowerText)) {
|
|
24626
|
-
return true;
|
|
24627
|
-
}
|
|
24628
|
-
return QUESTION_PHRASES.some((phrase) => lowerText.includes(phrase));
|
|
24629
|
-
}
|
|
24630
|
-
function cancelPendingTimer(state) {
|
|
24631
|
-
if (state.pendingTimer) {
|
|
24632
|
-
clearTimeout(state.pendingTimer);
|
|
24633
|
-
state.pendingTimer = null;
|
|
24634
|
-
}
|
|
24635
|
-
state.pendingTimerSessionId = null;
|
|
24636
|
-
}
|
|
24637
|
-
function resetState(state) {
|
|
24638
|
-
cancelPendingTimer(state);
|
|
24639
|
-
state.consecutiveContinuations = 0;
|
|
24640
|
-
state.suppressUntil = 0;
|
|
24641
|
-
state.isAutoInjecting = false;
|
|
24642
|
-
state.notifyingSessionIds.clear();
|
|
24643
|
-
state.notificationBusyUntilBySession.clear();
|
|
24644
|
-
}
|
|
24645
|
-
function stripTodoHygieneInstruction(text) {
|
|
24646
|
-
const trimmed = text.trimEnd();
|
|
24647
|
-
if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
|
|
24648
|
-
return trimmed;
|
|
24649
|
-
}
|
|
24650
|
-
const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
|
|
24651
|
-
if (start === -1) {
|
|
24652
|
-
return trimmed;
|
|
24653
|
-
}
|
|
24654
|
-
return trimmed.slice(0, start).trimEnd();
|
|
24655
|
-
}
|
|
24656
|
-
function appendTodoHygieneInstruction(message, reminder) {
|
|
24657
|
-
const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
|
|
24658
|
-
if (!textPart)
|
|
24659
|
-
return;
|
|
24660
|
-
const baseText = stripTodoHygieneInstruction(textPart.text ?? "");
|
|
24661
|
-
const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}
|
|
24662
|
-
${reminder}
|
|
24663
|
-
${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
|
|
24664
|
-
textPart.text = baseText ? `${baseText}
|
|
24665
|
-
|
|
24666
|
-
${instruction}` : instruction;
|
|
24667
|
-
}
|
|
24668
|
-
function stripTodoHygieneInstructionFromMessage(message) {
|
|
24669
|
-
const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
|
|
24670
|
-
if (!textPart)
|
|
24671
|
-
return;
|
|
24672
|
-
textPart.text = stripTodoHygieneInstruction(textPart.text ?? "");
|
|
24673
|
-
}
|
|
24674
|
-
function createTodoContinuationHook(ctx, config) {
|
|
24675
|
-
const maxContinuations = config?.maxContinuations ?? 5;
|
|
24676
|
-
const cooldownMs = config?.cooldownMs ?? 3000;
|
|
24677
|
-
const autoEnable = config?.autoEnable ?? false;
|
|
24678
|
-
const autoEnableThreshold = config?.autoEnableThreshold ?? 4;
|
|
24679
|
-
const backgroundJobBoard = config?.backgroundJobBoard;
|
|
24680
|
-
const requestSignatureBySession = new Map;
|
|
24681
|
-
const state = {
|
|
24682
|
-
enabled: false,
|
|
24683
|
-
consecutiveContinuations: 0,
|
|
24684
|
-
pendingTimer: null,
|
|
24685
|
-
pendingTimerSessionId: null,
|
|
24686
|
-
suppressUntil: 0,
|
|
24687
|
-
orchestratorSessionIds: new Set,
|
|
24688
|
-
sawChatMessage: false,
|
|
24689
|
-
isAutoInjecting: false,
|
|
24690
|
-
notifyingSessionIds: new Set,
|
|
24691
|
-
notificationBusyUntilBySession: new Map
|
|
24692
|
-
};
|
|
24693
|
-
async function fetchTodos(sessionID) {
|
|
24694
|
-
const result = await withTimeout(ctx.client.session.todo({
|
|
24695
|
-
path: { id: sessionID }
|
|
24696
|
-
}), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
|
|
24697
|
-
return result.data;
|
|
24698
|
-
}
|
|
24699
|
-
const hygiene = createTodoHygiene({
|
|
24700
|
-
getTodoState: async (sessionID) => {
|
|
24701
|
-
const todos = await fetchTodos(sessionID);
|
|
24702
|
-
const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
|
|
24703
|
-
return {
|
|
24704
|
-
hasOpenTodos: openTodos.length > 0,
|
|
24705
|
-
openCount: openTodos.length,
|
|
24706
|
-
inProgressCount: openTodos.filter((todo) => todo.status === "in_progress").length,
|
|
24707
|
-
pendingCount: openTodos.filter((todo) => todo.status === "pending").length
|
|
24708
|
-
};
|
|
24709
|
-
},
|
|
24710
|
-
shouldInject: (sessionID) => isOrchestratorSession(sessionID),
|
|
24711
|
-
log: (message, meta) => log(`[${HOOK_NAME}] ${message}`, meta)
|
|
24712
|
-
});
|
|
24713
|
-
function inferSessionID(messages, index) {
|
|
24714
|
-
const direct = messages[index]?.info.sessionID;
|
|
24715
|
-
if (direct) {
|
|
24716
|
-
return direct;
|
|
24717
|
-
}
|
|
24718
|
-
for (let i = index - 1;i >= 0; i--) {
|
|
24719
|
-
const sessionID = messages[i]?.info.sessionID;
|
|
24720
|
-
if (sessionID) {
|
|
24721
|
-
return sessionID;
|
|
24722
|
-
}
|
|
24723
|
-
}
|
|
24724
|
-
for (let i = index + 1;i < messages.length; i++) {
|
|
24725
|
-
const sessionID = messages[i]?.info.sessionID;
|
|
24726
|
-
if (sessionID) {
|
|
24727
|
-
return sessionID;
|
|
24728
|
-
}
|
|
24729
|
-
}
|
|
24730
|
-
if (state.orchestratorSessionIds.size === 1) {
|
|
24731
|
-
return Array.from(state.orchestratorSessionIds)[0];
|
|
24732
|
-
}
|
|
24733
|
-
return;
|
|
24734
|
-
}
|
|
24735
|
-
function isExternalUserMessage(message) {
|
|
24736
|
-
if (message.info.role !== "user") {
|
|
24737
|
-
return false;
|
|
24738
|
-
}
|
|
24739
|
-
const visibleText = message.parts.filter((part) => part.type === "text" && typeof part.text === "string" && !part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)).map((part) => part.text?.trim() ?? "").filter(Boolean).join(`
|
|
24740
|
-
`);
|
|
24741
|
-
const hasNonTextPart = message.parts.some((part) => part.type !== "text");
|
|
24742
|
-
return !(!visibleText && !hasNonTextPart && message.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)));
|
|
24743
|
-
}
|
|
24744
|
-
function getLastExternalUserMessage(messages) {
|
|
24745
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
24746
|
-
const message = messages[i];
|
|
24747
|
-
if (!isExternalUserMessage(message)) {
|
|
24748
|
-
continue;
|
|
24749
|
-
}
|
|
24750
|
-
const sessionID = inferSessionID(messages, i);
|
|
24751
|
-
const partSignature = message.parts.map((part) => {
|
|
24752
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
24753
|
-
const text = stripTodoHygieneInstruction(part.text);
|
|
24754
|
-
return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : text.trim()}`;
|
|
24755
|
-
}
|
|
24756
|
-
return part.type ?? "unknown";
|
|
24757
|
-
}).join("|");
|
|
24758
|
-
const ordinal = messages.slice(0, i + 1).filter((item) => isExternalUserMessage(item)).length;
|
|
24759
|
-
return {
|
|
24760
|
-
sessionID,
|
|
24761
|
-
agent: message.info.agent,
|
|
24762
|
-
message,
|
|
24763
|
-
signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
|
|
24764
|
-
};
|
|
24765
|
-
}
|
|
24766
|
-
return null;
|
|
24767
|
-
}
|
|
24768
|
-
async function handleMessagesTransform(output) {
|
|
24769
|
-
const lastUserMessage = getLastExternalUserMessage(output.messages);
|
|
24770
|
-
if (!lastUserMessage) {
|
|
24771
|
-
return;
|
|
24772
|
-
}
|
|
24773
|
-
if (lastUserMessage.agent && lastUserMessage.agent !== "orchestrator") {
|
|
24774
|
-
return;
|
|
24775
|
-
}
|
|
24776
|
-
if (!lastUserMessage.sessionID) {
|
|
24777
|
-
for (const sessionID of state.orchestratorSessionIds) {
|
|
24778
|
-
requestSignatureBySession.delete(sessionID);
|
|
24779
|
-
hygiene.handleRequestStart({ sessionID });
|
|
24780
|
-
}
|
|
24781
|
-
return;
|
|
24782
|
-
}
|
|
24783
|
-
const knownOrchestrator = isOrchestratorSession(lastUserMessage.sessionID);
|
|
24784
|
-
if (lastUserMessage.agent === "orchestrator") {
|
|
24785
|
-
registerOrchestratorSession(lastUserMessage.sessionID);
|
|
24786
|
-
} else if (!knownOrchestrator) {
|
|
24787
|
-
return;
|
|
24788
|
-
}
|
|
24789
|
-
if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
|
|
24790
|
-
const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
|
|
24791
|
-
const guardrail = backgroundGuardrail(lastUserMessage.sessionID);
|
|
24792
|
-
const combinedReminder = [reminder, guardrail].filter((item) => Boolean(item)).join(" ");
|
|
24793
|
-
if (combinedReminder) {
|
|
24794
|
-
appendTodoHygieneInstruction(lastUserMessage.message, combinedReminder);
|
|
24795
|
-
} else {
|
|
24796
|
-
stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
|
|
24797
|
-
}
|
|
24798
|
-
return;
|
|
24799
|
-
}
|
|
24800
|
-
requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
|
|
24801
|
-
stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
|
|
24802
|
-
hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
|
|
24803
|
-
}
|
|
24804
|
-
function markNotificationStarted(sessionID) {
|
|
24805
|
-
state.notifyingSessionIds.add(sessionID);
|
|
24806
|
-
}
|
|
24807
|
-
function markNotificationFinished(sessionID) {
|
|
24808
|
-
state.notifyingSessionIds.delete(sessionID);
|
|
24809
|
-
state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
|
|
24810
|
-
}
|
|
24811
|
-
function clearNotificationState(sessionID) {
|
|
24812
|
-
state.notifyingSessionIds.delete(sessionID);
|
|
24813
|
-
state.notificationBusyUntilBySession.delete(sessionID);
|
|
24814
|
-
}
|
|
24815
|
-
function isNotificationBusy(sessionID) {
|
|
24816
|
-
if (state.notifyingSessionIds.has(sessionID)) {
|
|
24817
|
-
return true;
|
|
24818
|
-
}
|
|
24819
|
-
const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
|
|
24820
|
-
if (until <= Date.now()) {
|
|
24821
|
-
state.notificationBusyUntilBySession.delete(sessionID);
|
|
24822
|
-
return false;
|
|
24823
|
-
}
|
|
24824
|
-
return true;
|
|
24825
|
-
}
|
|
24826
|
-
function isOrchestratorSession(sessionID) {
|
|
24827
|
-
return state.orchestratorSessionIds.has(sessionID);
|
|
24828
|
-
}
|
|
24829
|
-
function registerOrchestratorSession(sessionID) {
|
|
24830
|
-
state.orchestratorSessionIds.add(sessionID);
|
|
24831
|
-
}
|
|
24832
|
-
function backgroundGuardrail(sessionID) {
|
|
24833
|
-
if (!backgroundJobBoard)
|
|
24834
|
-
return;
|
|
24835
|
-
const hasRunning = backgroundJobBoard.hasRunning(sessionID);
|
|
24836
|
-
const hasTerminal = backgroundJobBoard.hasTerminalUnreconciled(sessionID);
|
|
24837
|
-
if (hasRunning && hasTerminal) {
|
|
24838
|
-
return "Background jobs are still unresolved: call task_status for running jobs and reconcile terminal Background Job Board results before dependent work or finalizing.";
|
|
24839
|
-
}
|
|
24840
|
-
if (hasTerminal) {
|
|
24841
|
-
return "Background jobs have terminal results: reconcile the Background Job Board results before finalizing.";
|
|
24842
|
-
}
|
|
24843
|
-
if (hasRunning) {
|
|
24844
|
-
return "Background jobs are still running: call task_status before dependent work or finalizing.";
|
|
24845
|
-
}
|
|
24846
|
-
return;
|
|
24847
|
-
}
|
|
24848
|
-
function continuationPrompt(sessionID) {
|
|
24849
|
-
const guardrail = backgroundGuardrail(sessionID);
|
|
24850
|
-
if (!guardrail)
|
|
24851
|
-
return CONTINUATION_PROMPT;
|
|
24852
|
-
return `${CONTINUATION_PROMPT} ${guardrail}`;
|
|
24853
|
-
}
|
|
24854
|
-
function handleChatMessage(input) {
|
|
24855
|
-
if (!input.agent) {
|
|
24598
|
+
function normalizeLateCancelledToolStatus(output) {
|
|
24599
|
+
if (typeof output.output !== "string")
|
|
24856
24600
|
return;
|
|
24857
|
-
|
|
24858
|
-
|
|
24859
|
-
if (input.agent === "orchestrator") {
|
|
24860
|
-
registerOrchestratorSession(input.sessionID);
|
|
24861
|
-
}
|
|
24862
|
-
}
|
|
24863
|
-
const autoContinue = tool({
|
|
24864
|
-
description: "Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.",
|
|
24865
|
-
args: { enabled: tool.schema.boolean() },
|
|
24866
|
-
execute: async (args) => {
|
|
24867
|
-
const enabled = args.enabled;
|
|
24868
|
-
state.enabled = enabled;
|
|
24869
|
-
state.consecutiveContinuations = 0;
|
|
24870
|
-
if (enabled) {
|
|
24871
|
-
state.suppressUntil = 0;
|
|
24872
|
-
log(`[${HOOK_NAME}] Auto-continue enabled`, { maxContinuations });
|
|
24873
|
-
return `Auto-continue enabled. Will auto-continue for up to ${maxContinuations} consecutive injections.`;
|
|
24874
|
-
}
|
|
24875
|
-
cancelPendingTimer(state);
|
|
24876
|
-
log(`[${HOOK_NAME}] Auto-continue disabled`);
|
|
24877
|
-
return "Auto-continue disabled.";
|
|
24878
|
-
}
|
|
24879
|
-
});
|
|
24880
|
-
async function handleEvent(input) {
|
|
24881
|
-
const { event } = input;
|
|
24882
|
-
const properties = event.properties ?? {};
|
|
24883
|
-
hygiene.handleEvent({
|
|
24884
|
-
type: event.type,
|
|
24885
|
-
properties: {
|
|
24886
|
-
info: properties.info,
|
|
24887
|
-
sessionID: properties.sessionID
|
|
24888
|
-
}
|
|
24889
|
-
});
|
|
24890
|
-
if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
|
|
24891
|
-
const sessionID = properties.sessionID;
|
|
24892
|
-
if (!sessionID) {
|
|
24893
|
-
return;
|
|
24894
|
-
}
|
|
24895
|
-
log(`[${HOOK_NAME}] Session idle`, { sessionID });
|
|
24896
|
-
if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
|
|
24897
|
-
registerOrchestratorSession(sessionID);
|
|
24898
|
-
log(`[${HOOK_NAME}] Tracked orchestrator session`, {
|
|
24899
|
-
sessionID
|
|
24900
|
-
});
|
|
24901
|
-
}
|
|
24902
|
-
if (!isOrchestratorSession(sessionID)) {
|
|
24903
|
-
log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
|
|
24904
|
-
sessionID
|
|
24905
|
-
});
|
|
24906
|
-
return;
|
|
24907
|
-
}
|
|
24908
|
-
if (autoEnable && !state.enabled) {
|
|
24909
|
-
try {
|
|
24910
|
-
const todos = await fetchTodos(sessionID);
|
|
24911
|
-
const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
|
|
24912
|
-
if (incompleteCount2 >= autoEnableThreshold) {
|
|
24913
|
-
state.enabled = true;
|
|
24914
|
-
state.consecutiveContinuations = 0;
|
|
24915
|
-
state.suppressUntil = 0;
|
|
24916
|
-
log(`[${HOOK_NAME}] Auto-enabled: ${incompleteCount2} incomplete todos >= threshold ${autoEnableThreshold}`, { sessionID });
|
|
24917
|
-
} else {
|
|
24918
|
-
log(`[${HOOK_NAME}] Auto-enable skipped: ${incompleteCount2} incomplete todos < threshold ${autoEnableThreshold}`, { sessionID });
|
|
24919
|
-
}
|
|
24920
|
-
} catch (error) {
|
|
24921
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch todos for auto-enable check`, {
|
|
24922
|
-
sessionID,
|
|
24923
|
-
error: error instanceof Error ? error.message : String(error)
|
|
24924
|
-
});
|
|
24925
|
-
}
|
|
24926
|
-
}
|
|
24927
|
-
if (!state.enabled) {
|
|
24928
|
-
log(`[${HOOK_NAME}] Skipped: auto-continue not enabled`, {
|
|
24929
|
-
sessionID
|
|
24930
|
-
});
|
|
24931
|
-
return;
|
|
24932
|
-
}
|
|
24933
|
-
let hasIncompleteTodos = false;
|
|
24934
|
-
let incompleteCount = 0;
|
|
24935
|
-
try {
|
|
24936
|
-
const todos = await fetchTodos(sessionID);
|
|
24937
|
-
incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
|
|
24938
|
-
hasIncompleteTodos = incompleteCount > 0;
|
|
24939
|
-
log(`[${HOOK_NAME}] Fetched todos`, {
|
|
24940
|
-
sessionID,
|
|
24941
|
-
hasIncompleteTodos,
|
|
24942
|
-
total: todos.length
|
|
24943
|
-
});
|
|
24944
|
-
} catch (error) {
|
|
24945
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch todos`, {
|
|
24946
|
-
sessionID,
|
|
24947
|
-
error: error instanceof Error ? error.message : String(error)
|
|
24948
|
-
});
|
|
24949
|
-
return;
|
|
24950
|
-
}
|
|
24951
|
-
if (!hasIncompleteTodos) {
|
|
24952
|
-
log(`[${HOOK_NAME}] Skipped: no incomplete todos`, { sessionID });
|
|
24953
|
-
return;
|
|
24954
|
-
}
|
|
24955
|
-
let lastAssistantIsQuestion = false;
|
|
24956
|
-
try {
|
|
24957
|
-
const messagesResult = await ctx.client.session.messages({
|
|
24958
|
-
path: { id: sessionID }
|
|
24959
|
-
});
|
|
24960
|
-
const messages = messagesResult.data;
|
|
24961
|
-
const lastAssistantMessage = messages.slice().reverse().find((m) => m.info?.role === "assistant");
|
|
24962
|
-
if (lastAssistantMessage?.parts) {
|
|
24963
|
-
const lastText = lastAssistantMessage.parts.map((p) => p.text ?? "").join(" ");
|
|
24964
|
-
lastAssistantIsQuestion = isQuestion(lastText);
|
|
24965
|
-
}
|
|
24966
|
-
log(`[${HOOK_NAME}] Fetched messages`, {
|
|
24967
|
-
sessionID,
|
|
24968
|
-
lastAssistantIsQuestion
|
|
24969
|
-
});
|
|
24970
|
-
} catch (error) {
|
|
24971
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
|
|
24972
|
-
sessionID,
|
|
24973
|
-
error: error instanceof Error ? error.message : String(error)
|
|
24974
|
-
});
|
|
24975
|
-
return;
|
|
24976
|
-
}
|
|
24977
|
-
if (lastAssistantIsQuestion) {
|
|
24978
|
-
log(`[${HOOK_NAME}] Skipped: last message is question`, {
|
|
24979
|
-
sessionID
|
|
24980
|
-
});
|
|
24981
|
-
return;
|
|
24982
|
-
}
|
|
24983
|
-
if (state.consecutiveContinuations >= maxContinuations) {
|
|
24984
|
-
log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
|
|
24985
|
-
sessionID,
|
|
24986
|
-
consecutive: state.consecutiveContinuations,
|
|
24987
|
-
max: maxContinuations
|
|
24988
|
-
});
|
|
24989
|
-
return;
|
|
24990
|
-
}
|
|
24991
|
-
const now = Date.now();
|
|
24992
|
-
if (now < state.suppressUntil) {
|
|
24993
|
-
log(`[${HOOK_NAME}] Skipped: in suppress window`, {
|
|
24994
|
-
sessionID,
|
|
24995
|
-
suppressUntil: state.suppressUntil
|
|
24996
|
-
});
|
|
24997
|
-
return;
|
|
24998
|
-
}
|
|
24999
|
-
if (state.pendingTimer !== null || state.isAutoInjecting) {
|
|
25000
|
-
log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
|
|
25001
|
-
sessionID
|
|
25002
|
-
});
|
|
25003
|
-
return;
|
|
25004
|
-
}
|
|
25005
|
-
log(`[${HOOK_NAME}] Scheduling continuation`, {
|
|
25006
|
-
sessionID,
|
|
25007
|
-
delayMs: cooldownMs
|
|
25008
|
-
});
|
|
25009
|
-
markNotificationStarted(sessionID);
|
|
25010
|
-
ctx.client.session.prompt({
|
|
25011
|
-
path: { id: sessionID },
|
|
25012
|
-
body: {
|
|
25013
|
-
noReply: true,
|
|
25014
|
-
parts: [
|
|
25015
|
-
{
|
|
25016
|
-
type: "text",
|
|
25017
|
-
text: [
|
|
25018
|
-
`⎔ Auto-continue: ${incompleteCount} incomplete todos remaining — resuming in ${cooldownMs / 1000}s — Esc×2 to cancel`,
|
|
25019
|
-
"",
|
|
25020
|
-
"[system status: continue without acknowledging this notification]"
|
|
25021
|
-
].join(`
|
|
25022
|
-
`)
|
|
25023
|
-
}
|
|
25024
|
-
]
|
|
25025
|
-
}
|
|
25026
|
-
}).catch(() => {}).finally(() => {
|
|
25027
|
-
markNotificationFinished(sessionID);
|
|
25028
|
-
});
|
|
25029
|
-
state.pendingTimerSessionId = sessionID;
|
|
25030
|
-
state.pendingTimer = setTimeout(async () => {
|
|
25031
|
-
state.pendingTimer = null;
|
|
25032
|
-
state.pendingTimerSessionId = null;
|
|
25033
|
-
clearNotificationState(sessionID);
|
|
25034
|
-
if (!state.enabled) {
|
|
25035
|
-
log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
|
|
25036
|
-
sessionID
|
|
25037
|
-
});
|
|
25038
|
-
return;
|
|
25039
|
-
}
|
|
25040
|
-
state.isAutoInjecting = true;
|
|
25041
|
-
try {
|
|
25042
|
-
await ctx.client.session.prompt({
|
|
25043
|
-
path: { id: sessionID },
|
|
25044
|
-
body: {
|
|
25045
|
-
parts: [
|
|
25046
|
-
createInternalAgentTextPart(continuationPrompt(sessionID))
|
|
25047
|
-
]
|
|
25048
|
-
}
|
|
25049
|
-
});
|
|
25050
|
-
state.consecutiveContinuations++;
|
|
25051
|
-
log(`[${HOOK_NAME}] Continuation injected`, {
|
|
25052
|
-
sessionID,
|
|
25053
|
-
consecutive: state.consecutiveContinuations
|
|
25054
|
-
});
|
|
25055
|
-
} catch (error) {
|
|
25056
|
-
log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
|
|
25057
|
-
sessionID,
|
|
25058
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25059
|
-
});
|
|
25060
|
-
} finally {
|
|
25061
|
-
state.isAutoInjecting = false;
|
|
25062
|
-
}
|
|
25063
|
-
}, cooldownMs);
|
|
25064
|
-
} else if (event.type === "session.status") {
|
|
25065
|
-
const status = properties.status;
|
|
25066
|
-
const sessionID = properties.sessionID;
|
|
25067
|
-
if (status?.type === "busy") {
|
|
25068
|
-
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
25069
|
-
const isNotification = isNotificationBusy(sessionID);
|
|
25070
|
-
if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
|
|
25071
|
-
cancelPendingTimer(state);
|
|
25072
|
-
}
|
|
25073
|
-
if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
25074
|
-
state.consecutiveContinuations = 0;
|
|
25075
|
-
log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
|
|
25076
|
-
sessionID
|
|
25077
|
-
});
|
|
25078
|
-
}
|
|
25079
|
-
}
|
|
25080
|
-
} else if (event.type === "session.error") {
|
|
25081
|
-
const error = properties.error;
|
|
25082
|
-
const sessionID = properties.sessionID;
|
|
25083
|
-
const errorName = error?.name;
|
|
25084
|
-
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
25085
|
-
if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
|
|
25086
|
-
state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
|
|
25087
|
-
log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
|
|
25088
|
-
sessionID,
|
|
25089
|
-
errorName
|
|
25090
|
-
});
|
|
25091
|
-
}
|
|
25092
|
-
if (isOrchestrator) {
|
|
25093
|
-
cancelPendingTimer(state);
|
|
25094
|
-
log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
|
|
25095
|
-
sessionID
|
|
25096
|
-
});
|
|
25097
|
-
}
|
|
25098
|
-
} else if (event.type === "session.deleted") {
|
|
25099
|
-
const deletedSessionId = properties.info?.id ?? properties.sessionID;
|
|
25100
|
-
if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
|
|
25101
|
-
requestSignatureBySession.delete(deletedSessionId);
|
|
25102
|
-
if (state.pendingTimerSessionId === deletedSessionId) {
|
|
25103
|
-
cancelPendingTimer(state);
|
|
25104
|
-
log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
|
|
25105
|
-
sessionID: deletedSessionId
|
|
25106
|
-
});
|
|
25107
|
-
}
|
|
25108
|
-
state.orchestratorSessionIds.delete(deletedSessionId);
|
|
25109
|
-
clearNotificationState(deletedSessionId);
|
|
25110
|
-
if (state.orchestratorSessionIds.size === 0) {
|
|
25111
|
-
resetState(state);
|
|
25112
|
-
state.sawChatMessage = false;
|
|
25113
|
-
}
|
|
25114
|
-
log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
|
|
25115
|
-
sessionID: deletedSessionId
|
|
25116
|
-
});
|
|
25117
|
-
}
|
|
25118
|
-
}
|
|
25119
|
-
}
|
|
25120
|
-
async function handleCommandExecuteBefore(input, output) {
|
|
25121
|
-
if (input.command !== COMMAND_NAME2) {
|
|
24601
|
+
const status = parseTaskStatusOutput(output.output);
|
|
24602
|
+
if (!status)
|
|
25122
24603
|
return;
|
|
25123
|
-
|
|
25124
|
-
|
|
25125
|
-
output.parts.length = 0;
|
|
25126
|
-
const arg = input.arguments.trim().toLowerCase();
|
|
25127
|
-
let newEnabled;
|
|
25128
|
-
if (arg === "on") {
|
|
25129
|
-
newEnabled = true;
|
|
25130
|
-
} else if (arg === "off") {
|
|
25131
|
-
newEnabled = false;
|
|
25132
|
-
} else {
|
|
25133
|
-
newEnabled = !state.enabled;
|
|
25134
|
-
}
|
|
25135
|
-
state.enabled = newEnabled;
|
|
25136
|
-
state.consecutiveContinuations = 0;
|
|
25137
|
-
if (!newEnabled) {
|
|
25138
|
-
cancelPendingTimer(state);
|
|
25139
|
-
output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
|
|
25140
|
-
log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME2} command`);
|
|
24604
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24605
|
+
if (!isLateCancelledTaskError(existing, status.state))
|
|
25141
24606
|
return;
|
|
25142
|
-
|
|
25143
|
-
|
|
25144
|
-
|
|
25145
|
-
|
|
24607
|
+
log("[task-session-manager] normalized late cancelled task_status output", {
|
|
24608
|
+
taskID: status.taskID,
|
|
24609
|
+
alias: existing?.alias,
|
|
24610
|
+
state: existing?.state,
|
|
24611
|
+
terminalState: existing?.terminalState,
|
|
24612
|
+
result: status.result
|
|
25146
24613
|
});
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
25150
|
-
hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
|
|
25151
|
-
} catch (error) {
|
|
25152
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
|
|
25153
|
-
sessionID: input.sessionID,
|
|
25154
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25155
|
-
});
|
|
25156
|
-
}
|
|
25157
|
-
if (hasIncompleteTodos) {
|
|
25158
|
-
output.parts.push(createInternalAgentTextPart(`${continuationPrompt(input.sessionID)} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
|
|
25159
|
-
} else {
|
|
25160
|
-
output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
|
|
24614
|
+
output.output = formatCancelledTaskStatusOutput(status.taskID, existing?.resultSummary);
|
|
24615
|
+
if (isObjectRecord(output) && isObjectRecord(output.metadata)) {
|
|
24616
|
+
output.metadata.state = "cancelled";
|
|
25161
24617
|
}
|
|
25162
24618
|
}
|
|
25163
|
-
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
25169
|
-
|
|
25170
|
-
|
|
24619
|
+
}
|
|
24620
|
+
function isLateCancelledTaskError(job, state) {
|
|
24621
|
+
if (state !== "error")
|
|
24622
|
+
return false;
|
|
24623
|
+
if (!job?.cancellationRequested)
|
|
24624
|
+
return false;
|
|
24625
|
+
return job.state === "cancelled" || job.terminalState === "cancelled";
|
|
24626
|
+
}
|
|
24627
|
+
function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
|
|
24628
|
+
return [
|
|
24629
|
+
`task_id: ${taskID}`,
|
|
24630
|
+
"state: cancelled",
|
|
24631
|
+
"",
|
|
24632
|
+
"<task_error>",
|
|
24633
|
+
summary,
|
|
24634
|
+
"</task_error>"
|
|
24635
|
+
].join(`
|
|
24636
|
+
`);
|
|
25171
24637
|
}
|
|
25172
24638
|
// src/interview/manager.ts
|
|
25173
24639
|
import path13 from "node:path";
|
|
@@ -28156,7 +27622,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
28156
27622
|
}
|
|
28157
27623
|
|
|
28158
27624
|
// src/interview/service.ts
|
|
28159
|
-
var
|
|
27625
|
+
var COMMAND_NAME2 = "interview";
|
|
28160
27626
|
var DEFAULT_MAX_QUESTIONS = 2;
|
|
28161
27627
|
function isTruthyEnvFlag(value) {
|
|
28162
27628
|
if (!value) {
|
|
@@ -28403,11 +27869,11 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28403
27869
|
}
|
|
28404
27870
|
function registerCommand(opencodeConfig) {
|
|
28405
27871
|
const configCommand = opencodeConfig.command;
|
|
28406
|
-
if (!configCommand?.[
|
|
27872
|
+
if (!configCommand?.[COMMAND_NAME2]) {
|
|
28407
27873
|
if (!opencodeConfig.command) {
|
|
28408
27874
|
opencodeConfig.command = {};
|
|
28409
27875
|
}
|
|
28410
|
-
opencodeConfig.command[
|
|
27876
|
+
opencodeConfig.command[COMMAND_NAME2] = {
|
|
28411
27877
|
template: "Start an interview and write a live markdown spec",
|
|
28412
27878
|
description: "Open a localhost interview UI linked to the current OpenCode session"
|
|
28413
27879
|
};
|
|
@@ -28481,7 +27947,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28481
27947
|
}
|
|
28482
27948
|
}
|
|
28483
27949
|
async function handleCommandExecuteBefore(input, output) {
|
|
28484
|
-
if (input.command !==
|
|
27950
|
+
if (input.command !== COMMAND_NAME2) {
|
|
28485
27951
|
return;
|
|
28486
27952
|
}
|
|
28487
27953
|
const idea = input.arguments.trim();
|
|
@@ -29951,7 +29417,7 @@ class MultiplexerSessionManager {
|
|
|
29951
29417
|
});
|
|
29952
29418
|
return;
|
|
29953
29419
|
}
|
|
29954
|
-
if (tracked.ownerInstanceId !== this.instanceId) {
|
|
29420
|
+
if (reason !== "deleted" && tracked.ownerInstanceId !== this.instanceId) {
|
|
29955
29421
|
log("[multiplexer-session-manager] close skipped; non-owner instance", {
|
|
29956
29422
|
instanceId: this.instanceId,
|
|
29957
29423
|
ownerInstanceId: tracked.ownerInstanceId,
|
|
@@ -29961,6 +29427,15 @@ class MultiplexerSessionManager {
|
|
|
29961
29427
|
});
|
|
29962
29428
|
return;
|
|
29963
29429
|
}
|
|
29430
|
+
if (reason === "deleted" && tracked.ownerInstanceId !== this.instanceId) {
|
|
29431
|
+
log("[multiplexer-session-manager] closing deleted pane as non-owner", {
|
|
29432
|
+
instanceId: this.instanceId,
|
|
29433
|
+
ownerInstanceId: tracked.ownerInstanceId,
|
|
29434
|
+
sessionId,
|
|
29435
|
+
paneId: tracked.paneId,
|
|
29436
|
+
reason
|
|
29437
|
+
});
|
|
29438
|
+
}
|
|
29964
29439
|
if (reason === "idle" && this.isRunningBackgroundJob(sessionId)) {
|
|
29965
29440
|
log("[multiplexer-session-manager] close skipped; background job running", {
|
|
29966
29441
|
instanceId: this.instanceId,
|
|
@@ -30129,7 +29604,7 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
|
|
|
30129
29604
|
return false;
|
|
30130
29605
|
}
|
|
30131
29606
|
// src/tools/ast-grep/tools.ts
|
|
30132
|
-
import { tool
|
|
29607
|
+
import { tool } from "@opencode-ai/plugin";
|
|
30133
29608
|
|
|
30134
29609
|
// src/tools/ast-grep/cli.ts
|
|
30135
29610
|
import { existsSync as existsSync9 } from "node:fs";
|
|
@@ -30608,14 +30083,14 @@ function showOutputToUser(context, output) {
|
|
|
30608
30083
|
const ctx = context;
|
|
30609
30084
|
ctx.metadata?.({ metadata: { output } });
|
|
30610
30085
|
}
|
|
30611
|
-
var ast_grep_search =
|
|
30086
|
+
var ast_grep_search = tool({
|
|
30612
30087
|
description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + "IMPORTANT: Patterns must be complete AST nodes (valid code). " + "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
|
|
30613
30088
|
args: {
|
|
30614
|
-
pattern:
|
|
30615
|
-
lang:
|
|
30616
|
-
paths:
|
|
30617
|
-
globs:
|
|
30618
|
-
context:
|
|
30089
|
+
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
|
|
30090
|
+
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
|
30091
|
+
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
|
|
30092
|
+
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
|
30093
|
+
context: tool.schema.number().optional().describe("Context lines around match")
|
|
30619
30094
|
},
|
|
30620
30095
|
execute: async (args, context) => {
|
|
30621
30096
|
try {
|
|
@@ -30644,15 +30119,15 @@ ${hint}`;
|
|
|
30644
30119
|
}
|
|
30645
30120
|
}
|
|
30646
30121
|
});
|
|
30647
|
-
var ast_grep_replace =
|
|
30122
|
+
var ast_grep_replace = tool({
|
|
30648
30123
|
description: "Replace code patterns across filesystem with AST-aware rewriting. " + "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " + "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
|
|
30649
30124
|
args: {
|
|
30650
|
-
pattern:
|
|
30651
|
-
rewrite:
|
|
30652
|
-
lang:
|
|
30653
|
-
paths:
|
|
30654
|
-
globs:
|
|
30655
|
-
dryRun:
|
|
30125
|
+
pattern: tool.schema.string().describe("AST pattern to match"),
|
|
30126
|
+
rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
|
30127
|
+
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
|
30128
|
+
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
|
|
30129
|
+
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
|
|
30130
|
+
dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)")
|
|
30656
30131
|
},
|
|
30657
30132
|
execute: async (args, context) => {
|
|
30658
30133
|
try {
|
|
@@ -30676,11 +30151,14 @@ var ast_grep_replace = tool2({
|
|
|
30676
30151
|
});
|
|
30677
30152
|
// src/tools/cancel-task.ts
|
|
30678
30153
|
import {
|
|
30679
|
-
tool as
|
|
30154
|
+
tool as tool2
|
|
30680
30155
|
} from "@opencode-ai/plugin";
|
|
30681
|
-
var z4 =
|
|
30156
|
+
var z4 = tool2.schema;
|
|
30157
|
+
|
|
30158
|
+
class SessionStillRunningError extends Error {
|
|
30159
|
+
}
|
|
30682
30160
|
function createCancelTaskTool(options) {
|
|
30683
|
-
const cancel_task =
|
|
30161
|
+
const cancel_task = tool2({
|
|
30684
30162
|
description: `Cancel a tracked background specialist task.
|
|
30685
30163
|
|
|
30686
30164
|
Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accepts either the native task_id/session ID or the parent-scoped alias shown in the Background Job Board. Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before replacing the lane.`,
|
|
@@ -30702,43 +30180,68 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
30702
30180
|
if (!requested)
|
|
30703
30181
|
throw new Error("cancel_task requires task_id");
|
|
30704
30182
|
const job = options.backgroundJobBoard.resolve(parentSessionID, requested);
|
|
30183
|
+
log("[cancel-task] request received", {
|
|
30184
|
+
parentSessionID,
|
|
30185
|
+
requested,
|
|
30186
|
+
resolvedTaskID: job?.taskID,
|
|
30187
|
+
alias: job?.alias,
|
|
30188
|
+
state: job?.state,
|
|
30189
|
+
terminalState: job?.terminalState,
|
|
30190
|
+
cancellationRequested: job?.cancellationRequested
|
|
30191
|
+
});
|
|
30705
30192
|
if (!job) {
|
|
30706
|
-
|
|
30707
|
-
|
|
30708
|
-
|
|
30709
|
-
|
|
30710
|
-
|
|
30711
|
-
|
|
30712
|
-
|
|
30713
|
-
|
|
30714
|
-
|
|
30715
|
-
|
|
30716
|
-
|
|
30717
|
-
|
|
30718
|
-
|
|
30719
|
-
|
|
30720
|
-
|
|
30721
|
-
|
|
30722
|
-
|
|
30723
|
-
|
|
30724
|
-
|
|
30725
|
-
|
|
30726
|
-
|
|
30193
|
+
if (isSessionID(requested)) {
|
|
30194
|
+
if (requested === parentSessionID) {
|
|
30195
|
+
log("[cancel-task] rejected parent session cancellation", {
|
|
30196
|
+
parentSessionID,
|
|
30197
|
+
taskID: requested
|
|
30198
|
+
});
|
|
30199
|
+
return unknownTaskOutput(requested, "cannot cancel parent session");
|
|
30200
|
+
}
|
|
30201
|
+
const knownJob = options.backgroundJobBoard.get(requested);
|
|
30202
|
+
if (knownJob && knownJob.parentSessionID !== parentSessionID) {
|
|
30203
|
+
log("[cancel-task] rejected unowned tracked raw session", {
|
|
30204
|
+
parentSessionID,
|
|
30205
|
+
taskID: requested,
|
|
30206
|
+
ownerParentSessionID: knownJob.parentSessionID
|
|
30207
|
+
});
|
|
30208
|
+
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
30209
|
+
}
|
|
30210
|
+
const parentID = await getSessionParentID(options.client, requested);
|
|
30211
|
+
if (parentID !== parentSessionID) {
|
|
30212
|
+
log("[cancel-task] rejected raw session without parent ownership", {
|
|
30213
|
+
parentSessionID,
|
|
30214
|
+
taskID: requested,
|
|
30215
|
+
actualParentID: parentID
|
|
30216
|
+
});
|
|
30217
|
+
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
30218
|
+
}
|
|
30219
|
+
log("[cancel-task] falling back to owned raw session abort", {
|
|
30220
|
+
parentSessionID,
|
|
30221
|
+
taskID: requested
|
|
30222
|
+
});
|
|
30223
|
+
return cancelSessionByID(options, requested, args.reason);
|
|
30224
|
+
}
|
|
30225
|
+
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
30727
30226
|
}
|
|
30728
30227
|
try {
|
|
30729
|
-
await
|
|
30228
|
+
await abortAndVerifySession(options, job.taskID);
|
|
30730
30229
|
} catch (error) {
|
|
30731
|
-
const
|
|
30732
|
-
|
|
30733
|
-
|
|
30734
|
-
|
|
30735
|
-
|
|
30736
|
-
|
|
30737
|
-
|
|
30738
|
-
|
|
30230
|
+
const stillRunning = error instanceof SessionStillRunningError;
|
|
30231
|
+
log("[cancel-task] abort failed", {
|
|
30232
|
+
taskID: job.taskID,
|
|
30233
|
+
stillRunning,
|
|
30234
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30235
|
+
});
|
|
30236
|
+
options.backgroundJobBoard.updateStatus({
|
|
30237
|
+
taskID: job.taskID,
|
|
30238
|
+
state: "running",
|
|
30239
|
+
statusUncertain: true,
|
|
30240
|
+
lastStatusError: error instanceof Error ? error.message : String(error)
|
|
30241
|
+
});
|
|
30739
30242
|
return [
|
|
30740
30243
|
`task_id: ${job.taskID}`,
|
|
30741
|
-
|
|
30244
|
+
"state: running",
|
|
30742
30245
|
"",
|
|
30743
30246
|
"<task_error>",
|
|
30744
30247
|
error instanceof Error ? error.message : String(error),
|
|
@@ -30746,7 +30249,14 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
30746
30249
|
].join(`
|
|
30747
30250
|
`);
|
|
30748
30251
|
}
|
|
30749
|
-
const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason);
|
|
30252
|
+
const cancelled = options.backgroundJobBoard.markCancelled(job.taskID, args.reason, Date.now(), { force: true });
|
|
30253
|
+
log("[cancel-task] marked job cancelled after verified abort", {
|
|
30254
|
+
taskID: job.taskID,
|
|
30255
|
+
alias: job.alias,
|
|
30256
|
+
previousState: job.state,
|
|
30257
|
+
state: cancelled?.state,
|
|
30258
|
+
cancellationRequested: cancelled?.cancellationRequested
|
|
30259
|
+
});
|
|
30750
30260
|
return [
|
|
30751
30261
|
`task_id: ${job.taskID}`,
|
|
30752
30262
|
`state: ${cancelled?.state ?? "cancelled"}`,
|
|
@@ -30760,11 +30270,287 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
30760
30270
|
});
|
|
30761
30271
|
return { cancel_task };
|
|
30762
30272
|
}
|
|
30273
|
+
async function cancelSessionByID(options, taskID, reason) {
|
|
30274
|
+
try {
|
|
30275
|
+
await abortAndVerifySession(options, taskID);
|
|
30276
|
+
} catch (error) {
|
|
30277
|
+
const stillRunning = error instanceof SessionStillRunningError;
|
|
30278
|
+
log("[cancel-task] raw session abort failed", {
|
|
30279
|
+
taskID,
|
|
30280
|
+
stillRunning,
|
|
30281
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30282
|
+
});
|
|
30283
|
+
return [
|
|
30284
|
+
`task_id: ${taskID}`,
|
|
30285
|
+
`state: ${stillRunning ? "running" : "error"}`,
|
|
30286
|
+
"",
|
|
30287
|
+
"<task_error>",
|
|
30288
|
+
error instanceof Error ? error.message : String(error),
|
|
30289
|
+
"</task_error>"
|
|
30290
|
+
].join(`
|
|
30291
|
+
`);
|
|
30292
|
+
}
|
|
30293
|
+
return [
|
|
30294
|
+
`task_id: ${taskID}`,
|
|
30295
|
+
"state: cancelled",
|
|
30296
|
+
"",
|
|
30297
|
+
"<task_error>",
|
|
30298
|
+
normalizeCancelReason2(reason),
|
|
30299
|
+
"</task_error>"
|
|
30300
|
+
].join(`
|
|
30301
|
+
`);
|
|
30302
|
+
}
|
|
30303
|
+
async function abortAndVerifySession(options, taskID) {
|
|
30304
|
+
log("[cancel-task] abort attempt starting", { taskID });
|
|
30305
|
+
const abortStartedAt = Date.now();
|
|
30306
|
+
try {
|
|
30307
|
+
await abortSessionWithTimeout(options.client, taskID, options.abortTimeoutMs ?? 1e4);
|
|
30308
|
+
log("[cancel-task] abort call returned", { taskID });
|
|
30309
|
+
} catch (error) {
|
|
30310
|
+
log("[cancel-task] abort call failed", {
|
|
30311
|
+
taskID,
|
|
30312
|
+
error: error instanceof Error ? error.message : String(error),
|
|
30313
|
+
canDelete: canDeleteSession(options.client)
|
|
30314
|
+
});
|
|
30315
|
+
if (!canDeleteSession(options.client))
|
|
30316
|
+
throw error;
|
|
30317
|
+
}
|
|
30318
|
+
if (canDeleteSession(options.client)) {
|
|
30319
|
+
await deleteAndVerifySession(options, taskID, "cancel-task-after-abort");
|
|
30320
|
+
return;
|
|
30321
|
+
}
|
|
30322
|
+
const verifyAbortMs = options.verifyAbortMs ?? 8000;
|
|
30323
|
+
const stableStoppedMs = options.stableStoppedMs ?? 3000;
|
|
30324
|
+
const retryIntervalMs = options.abortRetryIntervalMs ?? 150;
|
|
30325
|
+
const deadline = Date.now() + verifyAbortMs;
|
|
30326
|
+
log("[cancel-task] abort verification starting", {
|
|
30327
|
+
taskID,
|
|
30328
|
+
verifyAbortMs,
|
|
30329
|
+
stableStoppedMs,
|
|
30330
|
+
retryIntervalMs
|
|
30331
|
+
});
|
|
30332
|
+
let attempts = 0;
|
|
30333
|
+
let stableStoppedSince;
|
|
30334
|
+
let lastStatus;
|
|
30335
|
+
while (Date.now() <= deadline) {
|
|
30336
|
+
attempts += 1;
|
|
30337
|
+
const statusSnapshot = await getSessionStatus(options.client, taskID);
|
|
30338
|
+
lastStatus = statusSnapshot.status;
|
|
30339
|
+
log("[cancel-task] abort verification status", {
|
|
30340
|
+
taskID,
|
|
30341
|
+
attempts,
|
|
30342
|
+
status: statusSnapshot.status,
|
|
30343
|
+
statusSource: statusSnapshot.source,
|
|
30344
|
+
statusKeys: statusSnapshot.keys,
|
|
30345
|
+
stableStoppedSince,
|
|
30346
|
+
stableStoppedForMs: stableStoppedSince ? Date.now() - stableStoppedSince : 0,
|
|
30347
|
+
boardState: options.backgroundJobBoard.get(taskID)?.state,
|
|
30348
|
+
boardLastLiveBusyAt: options.backgroundJobBoard.get(taskID)?.lastLiveBusyAt
|
|
30349
|
+
});
|
|
30350
|
+
const boardLastLiveBusyAt = options.backgroundJobBoard.get(taskID)?.lastLiveBusyAt;
|
|
30351
|
+
if (boardLastLiveBusyAt && boardLastLiveBusyAt >= abortStartedAt) {
|
|
30352
|
+
log("[cancel-task] abort verification saw board busy after abort", {
|
|
30353
|
+
taskID,
|
|
30354
|
+
attempts,
|
|
30355
|
+
abortStartedAt,
|
|
30356
|
+
boardLastLiveBusyAt,
|
|
30357
|
+
status: statusSnapshot.status,
|
|
30358
|
+
statusSource: statusSnapshot.source
|
|
30359
|
+
});
|
|
30360
|
+
await deleteAndVerifySession(options, taskID, "board-busy-after-abort");
|
|
30361
|
+
return;
|
|
30362
|
+
}
|
|
30363
|
+
if (statusSnapshot.status === "busy" || statusSnapshot.status === "retry") {
|
|
30364
|
+
if (stableStoppedSince !== undefined) {
|
|
30365
|
+
log("[cancel-task] abort verification saw busy after idle", {
|
|
30366
|
+
taskID,
|
|
30367
|
+
attempts,
|
|
30368
|
+
stableStoppedForMs: Date.now() - stableStoppedSince
|
|
30369
|
+
});
|
|
30370
|
+
await deleteAndVerifySession(options, taskID, "busy-after-idle");
|
|
30371
|
+
return;
|
|
30372
|
+
}
|
|
30373
|
+
stableStoppedSince = undefined;
|
|
30374
|
+
await abortSessionWithTimeout(options.client, taskID, options.abortTimeoutMs ?? 1e4);
|
|
30375
|
+
log("[cancel-task] abort retry returned", {
|
|
30376
|
+
taskID,
|
|
30377
|
+
attempts,
|
|
30378
|
+
status: statusSnapshot.status
|
|
30379
|
+
});
|
|
30380
|
+
await delay(retryIntervalMs);
|
|
30381
|
+
continue;
|
|
30382
|
+
}
|
|
30383
|
+
stableStoppedSince ??= Date.now();
|
|
30384
|
+
if (Date.now() - stableStoppedSince >= stableStoppedMs) {
|
|
30385
|
+
log("[cancel-task] abort verified stopped", {
|
|
30386
|
+
taskID,
|
|
30387
|
+
attempts,
|
|
30388
|
+
status: statusSnapshot.status,
|
|
30389
|
+
stableStoppedMs
|
|
30390
|
+
});
|
|
30391
|
+
return;
|
|
30392
|
+
}
|
|
30393
|
+
await delay(retryIntervalMs);
|
|
30394
|
+
}
|
|
30395
|
+
log("[cancel-task] abort verification timed out", {
|
|
30396
|
+
taskID,
|
|
30397
|
+
attempts,
|
|
30398
|
+
lastStatus,
|
|
30399
|
+
stableStoppedSince
|
|
30400
|
+
});
|
|
30401
|
+
if (lastStatus === "busy" || lastStatus === "retry") {
|
|
30402
|
+
await deleteAndVerifySession(options, taskID, "still-busy-after-abort");
|
|
30403
|
+
return;
|
|
30404
|
+
}
|
|
30405
|
+
throw new SessionStillRunningError(`Session abort returned but task did not stay stopped: ${taskID}`);
|
|
30406
|
+
}
|
|
30407
|
+
async function deleteAndVerifySession(options, taskID, reason) {
|
|
30408
|
+
const session2 = options.client.session;
|
|
30409
|
+
if (!session2.delete) {
|
|
30410
|
+
log("[cancel-task] session delete unavailable", { taskID, reason });
|
|
30411
|
+
throw new SessionStillRunningError(`Session resumed after abort and delete is unavailable: ${taskID}`);
|
|
30412
|
+
}
|
|
30413
|
+
log("[cancel-task] deleting session after unstable abort", {
|
|
30414
|
+
taskID,
|
|
30415
|
+
reason
|
|
30416
|
+
});
|
|
30417
|
+
try {
|
|
30418
|
+
await withTimeout(session2.delete({ path: { id: taskID } }), options.deleteTimeoutMs ?? 1e4, `Session delete timed out after ${options.deleteTimeoutMs ?? 1e4}ms`);
|
|
30419
|
+
log("[cancel-task] session delete returned", { taskID, reason });
|
|
30420
|
+
} catch (error) {
|
|
30421
|
+
log("[cancel-task] session delete failed; verifying live state", {
|
|
30422
|
+
taskID,
|
|
30423
|
+
reason,
|
|
30424
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30425
|
+
});
|
|
30426
|
+
const status = await getSessionStatus(options.client, taskID);
|
|
30427
|
+
log("[cancel-task] delete failure verification status", {
|
|
30428
|
+
taskID,
|
|
30429
|
+
reason,
|
|
30430
|
+
status: status.status,
|
|
30431
|
+
statusSource: status.source,
|
|
30432
|
+
statusKeys: status.keys
|
|
30433
|
+
});
|
|
30434
|
+
if (status.status === "busy" || status.status === "retry") {
|
|
30435
|
+
throw new SessionStillRunningError(`Session delete failed and task is still busy: ${taskID}`);
|
|
30436
|
+
}
|
|
30437
|
+
if (status.status !== "idle")
|
|
30438
|
+
throw error;
|
|
30439
|
+
}
|
|
30440
|
+
const deadline = Date.now() + (options.deleteVerifyMs ?? 1500);
|
|
30441
|
+
const stableStoppedMs = options.deleteStableStoppedMs ?? 300;
|
|
30442
|
+
const retryIntervalMs = options.abortRetryIntervalMs ?? 150;
|
|
30443
|
+
let stableStoppedSince;
|
|
30444
|
+
let attempts = 0;
|
|
30445
|
+
let lastStatus;
|
|
30446
|
+
while (Date.now() <= deadline) {
|
|
30447
|
+
attempts += 1;
|
|
30448
|
+
const status = await getSessionStatus(options.client, taskID);
|
|
30449
|
+
lastStatus = status.status;
|
|
30450
|
+
log("[cancel-task] delete verification status", {
|
|
30451
|
+
taskID,
|
|
30452
|
+
reason,
|
|
30453
|
+
attempts,
|
|
30454
|
+
status: status.status,
|
|
30455
|
+
statusSource: status.source,
|
|
30456
|
+
statusKeys: status.keys,
|
|
30457
|
+
stableStoppedSince
|
|
30458
|
+
});
|
|
30459
|
+
if (status.status === "busy" || status.status === "retry") {
|
|
30460
|
+
stableStoppedSince = undefined;
|
|
30461
|
+
await delay(retryIntervalMs);
|
|
30462
|
+
continue;
|
|
30463
|
+
}
|
|
30464
|
+
stableStoppedSince ??= Date.now();
|
|
30465
|
+
if (Date.now() - stableStoppedSince >= stableStoppedMs)
|
|
30466
|
+
return;
|
|
30467
|
+
await delay(retryIntervalMs);
|
|
30468
|
+
}
|
|
30469
|
+
throw new SessionStillRunningError(`Session delete returned but task did not stay stopped: ${taskID} (${lastStatus ?? "unknown"})`);
|
|
30470
|
+
}
|
|
30471
|
+
function canDeleteSession(client) {
|
|
30472
|
+
const session2 = client.session;
|
|
30473
|
+
return typeof session2.delete === "function";
|
|
30474
|
+
}
|
|
30475
|
+
async function getSessionStatus(client, taskID) {
|
|
30476
|
+
try {
|
|
30477
|
+
const result = await client.session.status();
|
|
30478
|
+
const data = result.data;
|
|
30479
|
+
if (!isObjectRecord2(data)) {
|
|
30480
|
+
return { status: undefined, source: "invalid-data", keys: [] };
|
|
30481
|
+
}
|
|
30482
|
+
const keys = Object.keys(data).slice(0, 20);
|
|
30483
|
+
const item = data[taskID];
|
|
30484
|
+
if (item === undefined) {
|
|
30485
|
+
return { status: "idle", source: "missing-from-map", keys };
|
|
30486
|
+
}
|
|
30487
|
+
if (isObjectRecord2(item) && typeof item.type === "string") {
|
|
30488
|
+
return { status: item.type, source: "task-map-entry", keys };
|
|
30489
|
+
}
|
|
30490
|
+
if (typeof data.type === "string") {
|
|
30491
|
+
return { status: data.type, source: "legacy-data-type", keys };
|
|
30492
|
+
}
|
|
30493
|
+
const nested = data.status;
|
|
30494
|
+
if (isObjectRecord2(nested) && typeof nested.type === "string") {
|
|
30495
|
+
return { status: nested.type, source: "legacy-data-status", keys };
|
|
30496
|
+
}
|
|
30497
|
+
return { status: undefined, source: "unknown-shape", keys };
|
|
30498
|
+
} catch (error) {
|
|
30499
|
+
log("[cancel-task] session status lookup failed", {
|
|
30500
|
+
taskID,
|
|
30501
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30502
|
+
});
|
|
30503
|
+
return { status: undefined, source: "lookup-error", keys: [] };
|
|
30504
|
+
}
|
|
30505
|
+
}
|
|
30506
|
+
function delay(ms) {
|
|
30507
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
30508
|
+
}
|
|
30509
|
+
function isObjectRecord2(value) {
|
|
30510
|
+
return typeof value === "object" && value !== null;
|
|
30511
|
+
}
|
|
30512
|
+
function isSessionID(value) {
|
|
30513
|
+
return /^ses_[\w-]+$/.test(value);
|
|
30514
|
+
}
|
|
30515
|
+
function normalizeCancelReason2(reason) {
|
|
30516
|
+
const normalized = reason?.replace(/\s+/g, " ").trim();
|
|
30517
|
+
return normalized ? `cancelled: ${normalized}` : "cancelled";
|
|
30518
|
+
}
|
|
30519
|
+
async function getSessionParentID(client, taskID) {
|
|
30520
|
+
const session2 = client.session;
|
|
30521
|
+
if (!session2.get)
|
|
30522
|
+
return;
|
|
30523
|
+
try {
|
|
30524
|
+
const response = await session2.get({ path: { id: taskID } });
|
|
30525
|
+
const data = response.data;
|
|
30526
|
+
if (!isObjectRecord2(data))
|
|
30527
|
+
return;
|
|
30528
|
+
const parentID = data.parentID;
|
|
30529
|
+
return typeof parentID === "string" ? parentID : undefined;
|
|
30530
|
+
} catch (error) {
|
|
30531
|
+
log("[cancel-task] session metadata lookup failed", {
|
|
30532
|
+
taskID,
|
|
30533
|
+
error: error instanceof Error ? error.message : String(error)
|
|
30534
|
+
});
|
|
30535
|
+
return;
|
|
30536
|
+
}
|
|
30537
|
+
}
|
|
30538
|
+
function unknownTaskOutput(taskID, message) {
|
|
30539
|
+
return [
|
|
30540
|
+
`task_id: ${taskID}`,
|
|
30541
|
+
"state: unknown",
|
|
30542
|
+
"",
|
|
30543
|
+
"<task_error>",
|
|
30544
|
+
message,
|
|
30545
|
+
"</task_error>"
|
|
30546
|
+
].join(`
|
|
30547
|
+
`);
|
|
30548
|
+
}
|
|
30763
30549
|
// src/tools/council.ts
|
|
30764
30550
|
import {
|
|
30765
|
-
tool as
|
|
30551
|
+
tool as tool3
|
|
30766
30552
|
} from "@opencode-ai/plugin";
|
|
30767
|
-
var z5 =
|
|
30553
|
+
var z5 = tool3.schema;
|
|
30768
30554
|
function formatModelComposition(councillorResults) {
|
|
30769
30555
|
return councillorResults.map((cr) => {
|
|
30770
30556
|
const shortModel = shortModelLabel(cr.model);
|
|
@@ -30772,7 +30558,7 @@ function formatModelComposition(councillorResults) {
|
|
|
30772
30558
|
}).join(", ");
|
|
30773
30559
|
}
|
|
30774
30560
|
function createCouncilTool(_ctx, councilManager) {
|
|
30775
|
-
const council_session =
|
|
30561
|
+
const council_session = tool3({
|
|
30776
30562
|
description: `Launch a multi-LLM council session for consensus-based analysis.
|
|
30777
30563
|
|
|
30778
30564
|
Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
|
|
@@ -30826,6 +30612,9 @@ Returns the councillor responses with a summary footer.`,
|
|
|
30826
30612
|
});
|
|
30827
30613
|
return { council_session };
|
|
30828
30614
|
}
|
|
30615
|
+
// src/tools/preset-manager.ts
|
|
30616
|
+
import * as fs10 from "node:fs";
|
|
30617
|
+
|
|
30829
30618
|
// src/tui-state.ts
|
|
30830
30619
|
import * as fs9 from "node:fs";
|
|
30831
30620
|
import * as os5 from "node:os";
|
|
@@ -30895,11 +30684,11 @@ function recordTuiAgentModel(input) {
|
|
|
30895
30684
|
}
|
|
30896
30685
|
|
|
30897
30686
|
// src/tools/preset-manager.ts
|
|
30898
|
-
var
|
|
30687
|
+
var COMMAND_NAME3 = "preset";
|
|
30899
30688
|
function createPresetManager(ctx, config) {
|
|
30900
30689
|
let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
|
|
30901
30690
|
async function handleCommandExecuteBefore(input, output) {
|
|
30902
|
-
if (input.command !==
|
|
30691
|
+
if (input.command !== COMMAND_NAME3) {
|
|
30903
30692
|
return;
|
|
30904
30693
|
}
|
|
30905
30694
|
output.parts.length = 0;
|
|
@@ -30918,11 +30707,11 @@ function createPresetManager(ctx, config) {
|
|
|
30918
30707
|
}
|
|
30919
30708
|
function registerCommand(opencodeConfig) {
|
|
30920
30709
|
const configCommand = opencodeConfig.command;
|
|
30921
|
-
if (!configCommand?.[
|
|
30710
|
+
if (!configCommand?.[COMMAND_NAME3]) {
|
|
30922
30711
|
if (!opencodeConfig.command) {
|
|
30923
30712
|
opencodeConfig.command = {};
|
|
30924
30713
|
}
|
|
30925
|
-
opencodeConfig.command[
|
|
30714
|
+
opencodeConfig.command[COMMAND_NAME3] = {
|
|
30926
30715
|
template: "List available presets and switch between them",
|
|
30927
30716
|
description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
|
|
30928
30717
|
};
|
|
@@ -30944,64 +30733,47 @@ function createPresetManager(ctx, config) {
|
|
|
30944
30733
|
agentUpdates[resolvedName] = agentConfig;
|
|
30945
30734
|
}
|
|
30946
30735
|
}
|
|
30947
|
-
const currentRuntimePreset = getActiveRuntimePreset();
|
|
30948
|
-
const resetUpdates = {};
|
|
30949
|
-
if (currentRuntimePreset && config.presets?.[currentRuntimePreset]) {
|
|
30950
|
-
const oldPreset = config.presets[currentRuntimePreset];
|
|
30951
|
-
for (const rawName of Object.keys(oldPreset)) {
|
|
30952
|
-
const resolvedOld = AGENT_ALIASES[rawName] ?? rawName;
|
|
30953
|
-
if (resolvedOld in agentUpdates)
|
|
30954
|
-
continue;
|
|
30955
|
-
const baseline = config.agents?.[resolvedOld];
|
|
30956
|
-
if (baseline) {
|
|
30957
|
-
resetUpdates[resolvedOld] = mapOverrideToAgentConfig(baseline);
|
|
30958
|
-
}
|
|
30959
|
-
}
|
|
30960
|
-
}
|
|
30961
30736
|
const hasAgentUpdates = Object.keys(agentUpdates).length > 0;
|
|
30962
|
-
const allUpdates = { ...resetUpdates, ...agentUpdates };
|
|
30963
30737
|
if (!hasAgentUpdates) {
|
|
30964
30738
|
output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
|
|
30965
30739
|
return;
|
|
30966
30740
|
}
|
|
30967
|
-
const previousPreset = activePreset;
|
|
30968
30741
|
setActiveRuntimePresetWithPrevious(presetName);
|
|
30969
30742
|
try {
|
|
30970
|
-
|
|
30971
|
-
|
|
30972
|
-
|
|
30973
|
-
|
|
30974
|
-
|
|
30975
|
-
|
|
30976
|
-
|
|
30977
|
-
|
|
30978
|
-
|
|
30979
|
-
|
|
30980
|
-
|
|
30981
|
-
|
|
30982
|
-
|
|
30983
|
-
|
|
30984
|
-
|
|
30985
|
-
|
|
30986
|
-
|
|
30987
|
-
|
|
30988
|
-
|
|
30989
|
-
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
30995
|
-
if (
|
|
30996
|
-
|
|
30997
|
-
|
|
30998
|
-
|
|
30743
|
+
const { userConfigPath } = findPluginConfigPaths(ctx.directory);
|
|
30744
|
+
if (userConfigPath) {
|
|
30745
|
+
const raw = fs10.readFileSync(userConfigPath, "utf-8");
|
|
30746
|
+
const persisted = JSON.parse(stripJsonComments(raw));
|
|
30747
|
+
persisted.preset = presetName;
|
|
30748
|
+
fs10.writeFileSync(userConfigPath, `${JSON.stringify(persisted, null, 2)}
|
|
30749
|
+
`);
|
|
30750
|
+
}
|
|
30751
|
+
} catch {}
|
|
30752
|
+
const snapshot = readTuiSnapshot();
|
|
30753
|
+
const agentModels = { ...snapshot.agentModels };
|
|
30754
|
+
for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
|
|
30755
|
+
if (typeof agentConfig.model === "string") {
|
|
30756
|
+
agentModels[agentName] = agentConfig.model;
|
|
30757
|
+
}
|
|
30758
|
+
}
|
|
30759
|
+
recordTuiAgentModels({ agentModels });
|
|
30760
|
+
activePreset = presetName;
|
|
30761
|
+
const summaryParts = [];
|
|
30762
|
+
for (const [name, cfg] of Object.entries(agentUpdates)) {
|
|
30763
|
+
const parts = [name];
|
|
30764
|
+
if (cfg.model)
|
|
30765
|
+
parts.push(`model: ${cfg.model}`);
|
|
30766
|
+
if (cfg.variant)
|
|
30767
|
+
parts.push(`variant: ${cfg.variant}`);
|
|
30768
|
+
if (cfg.temperature !== undefined)
|
|
30769
|
+
parts.push(`temp: ${cfg.temperature}`);
|
|
30770
|
+
if (cfg.options)
|
|
30771
|
+
parts.push("options: yes");
|
|
30772
|
+
summaryParts.push(parts.join(" → "));
|
|
30773
|
+
}
|
|
30774
|
+
output.parts.push(createInternalAgentTextPart(`Saved preset "${presetName}". Restart or reload OpenCode to apply it to agent configuration. The current session was not reloaded to avoid interrupting the active conversation.
|
|
30999
30775
|
${summaryParts.join(`
|
|
31000
30776
|
`)}`));
|
|
31001
|
-
} catch (err) {
|
|
31002
|
-
rollbackRuntimePreset(previousPreset);
|
|
31003
|
-
output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
|
|
31004
|
-
}
|
|
31005
30777
|
}
|
|
31006
30778
|
function mapOverrideToAgentConfig(override) {
|
|
31007
30779
|
const agentConfig = {};
|
|
@@ -31091,7 +30863,7 @@ var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs p
|
|
|
31091
30863
|
import os6 from "node:os";
|
|
31092
30864
|
import path18 from "node:path";
|
|
31093
30865
|
import {
|
|
31094
|
-
tool as
|
|
30866
|
+
tool as tool4
|
|
31095
30867
|
} from "@opencode-ai/plugin";
|
|
31096
30868
|
|
|
31097
30869
|
// src/tools/smartfetch/binary.ts
|
|
@@ -32873,10 +32645,10 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
32873
32645
|
}
|
|
32874
32646
|
|
|
32875
32647
|
// src/tools/smartfetch/tool.ts
|
|
32876
|
-
var z6 =
|
|
32648
|
+
var z6 = tool4.schema;
|
|
32877
32649
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
32878
32650
|
const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
|
|
32879
|
-
return
|
|
32651
|
+
return tool4({
|
|
32880
32652
|
description: WEBFETCH_DESCRIPTION,
|
|
32881
32653
|
args: {
|
|
32882
32654
|
url: z6.httpUrl(),
|
|
@@ -33467,7 +33239,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33467
33239
|
let applyPatchHook;
|
|
33468
33240
|
let jsonErrorRecoveryHook;
|
|
33469
33241
|
let foregroundFallback;
|
|
33470
|
-
let todoContinuationHook;
|
|
33471
33242
|
let deepworkCommandHook;
|
|
33472
33243
|
let taskSessionManagerHook;
|
|
33473
33244
|
let backgroundJobBoard;
|
|
@@ -33560,13 +33331,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33560
33331
|
applyPatchHook = createApplyPatchHook(ctx);
|
|
33561
33332
|
jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
33562
33333
|
foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
|
|
33563
|
-
todoContinuationHook = createTodoContinuationHook(ctx, {
|
|
33564
|
-
maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
|
|
33565
|
-
cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
|
|
33566
|
-
autoEnable: config.todoContinuation?.autoEnable ?? false,
|
|
33567
|
-
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
|
|
33568
|
-
backgroundJobBoard
|
|
33569
|
-
});
|
|
33570
33334
|
deepworkCommandHook = createDeepworkCommandHook();
|
|
33571
33335
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
33572
33336
|
maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
|
|
@@ -33583,7 +33347,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33583
33347
|
backgroundJobBoard,
|
|
33584
33348
|
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
33585
33349
|
});
|
|
33586
|
-
toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length +
|
|
33350
|
+
toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + 1 + 2;
|
|
33587
33351
|
} catch (err) {
|
|
33588
33352
|
log("[plugin] FATAL: init failed", String(err));
|
|
33589
33353
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -33627,7 +33391,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33627
33391
|
...councilTools,
|
|
33628
33392
|
...cancelTaskTools,
|
|
33629
33393
|
webfetch,
|
|
33630
|
-
...todoContinuationHook.tool,
|
|
33631
33394
|
ast_grep_search,
|
|
33632
33395
|
ast_grep_replace
|
|
33633
33396
|
},
|
|
@@ -33814,16 +33577,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33814
33577
|
}
|
|
33815
33578
|
agentConfigEntry.permission = agentPermission;
|
|
33816
33579
|
}
|
|
33817
|
-
const configCommand = opencodeConfig.command;
|
|
33818
|
-
if (!configCommand?.["auto-continue"]) {
|
|
33819
|
-
if (!opencodeConfig.command) {
|
|
33820
|
-
opencodeConfig.command = {};
|
|
33821
|
-
}
|
|
33822
|
-
opencodeConfig.command["auto-continue"] = {
|
|
33823
|
-
template: "Call the auto_continue tool with enabled=true",
|
|
33824
|
-
description: "Enable auto-continuation — orchestrator keeps working through incomplete todos"
|
|
33825
|
-
};
|
|
33826
|
-
}
|
|
33827
33580
|
interviewManager.registerCommand(opencodeConfig);
|
|
33828
33581
|
deepworkCommandHook.registerCommand(opencodeConfig);
|
|
33829
33582
|
presetManager.registerCommand(opencodeConfig);
|
|
@@ -33850,7 +33603,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33850
33603
|
await multiplexerSessionManager.onSessionStatus(event);
|
|
33851
33604
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
33852
33605
|
await foregroundFallback.handleEvent(input.event);
|
|
33853
|
-
await todoContinuationHook.handleEvent(input);
|
|
33854
33606
|
await autoUpdateChecker.event(input);
|
|
33855
33607
|
await interviewManager.handleEvent(input);
|
|
33856
33608
|
await taskSessionManagerHook.event(input);
|
|
@@ -33908,7 +33660,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33908
33660
|
}
|
|
33909
33661
|
},
|
|
33910
33662
|
"command.execute.before": async (input, output) => {
|
|
33911
|
-
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
33912
33663
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
33913
33664
|
await presetManager.handleCommandExecuteBefore(input, output);
|
|
33914
33665
|
await deepworkCommandHook.handleCommandExecuteBefore(input, output);
|
|
@@ -33923,10 +33674,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33923
33674
|
if (agent) {
|
|
33924
33675
|
sessionAgentMap.set(input.sessionID, agent);
|
|
33925
33676
|
}
|
|
33926
|
-
todoContinuationHook.handleChatMessage({
|
|
33927
|
-
sessionID: input.sessionID,
|
|
33928
|
-
agent
|
|
33929
|
-
});
|
|
33930
33677
|
},
|
|
33931
33678
|
"experimental.chat.system.transform": async (input, output) => {
|
|
33932
33679
|
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
@@ -33961,9 +33708,6 @@ ${output.system[0]}` : "");
|
|
|
33961
33708
|
disabledAgents,
|
|
33962
33709
|
log
|
|
33963
33710
|
});
|
|
33964
|
-
await todoContinuationHook.handleMessagesTransform({
|
|
33965
|
-
messages: typedOutput.messages
|
|
33966
|
-
});
|
|
33967
33711
|
await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
33968
33712
|
await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
33969
33713
|
await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
@@ -33985,7 +33729,6 @@ ${output.system[0]}` : "");
|
|
|
33985
33729
|
};
|
|
33986
33730
|
await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
|
|
33987
33731
|
await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
|
|
33988
|
-
await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
|
|
33989
33732
|
await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
|
|
33990
33733
|
await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
|
|
33991
33734
|
if (input.tool.toLowerCase() === "task") {
|