oh-my-opencode-slim 2.0.0-beta.13 → 2.0.0-beta.15
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/constants.d.ts +1 -1
- package/dist/config/schema.d.ts +0 -13
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/phase-reminder/index.d.ts +1 -1
- package/dist/index.js +170 -989
- package/dist/tools/preset-manager.d.ts +6 -7
- package/dist/tui.js +15 -9
- package/dist/utils/background-job-board.d.ts +0 -1
- package/oh-my-opencode-slim.schema.json +0 -31
- package/package.json +3 -1
- package/src/skills/codemap.md +3 -1
- package/src/skills/deepwork/SKILL.md +3 -3
- 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
|
|
|
@@ -18308,7 +18321,7 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
|
|
|
18308
18321
|
var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
18309
18322
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
18310
18323
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
18311
|
-
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion
|
|
18324
|
+
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
|
|
18312
18325
|
var WRITABLE_FILE_OPERATIONS_RULES = `**File Operations Rules**:
|
|
18313
18326
|
- Prefer dedicated file tools for normal code work: glob/grep/ast_grep_search for discovery, read for file contents, and edit/write/apply_patch for targeted source changes.
|
|
18314
18327
|
- Use bash for execution and automation: git, package managers, tests, builds, scripts, diagnostics, and shell-native filesystem operations.
|
|
@@ -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 {
|
|
@@ -19122,31 +19126,15 @@ ${enabledParallelExamples}
|
|
|
19122
19126
|
|
|
19123
19127
|
Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
|
|
19124
19128
|
|
|
19125
|
-
###
|
|
19126
|
-
-
|
|
19127
|
-
-
|
|
19128
|
-
-
|
|
19129
|
-
-
|
|
19130
|
-
- Continue orchestration while tasks run only when useful: planning, scheduling independent lanes, preparing synthesis, or asking needed user questions.
|
|
19131
|
-
- If no useful independent work remains, stop after a brief status response; do not call \`task_status\` just to wait. OpenCode will resume you when the background completion event arrives.
|
|
19132
|
-
- Use \`task_status(wait: true, timeout_ms: ...)\` only when you actively need a result before a dependent step or final response and no completion event has arrived yet.
|
|
19133
|
-
- If \`task_status(wait: true)\` times out and reports the task still \`running\`, the delegated lane is still owned by that specialist. Do not treat the timeout as failure, cancellation, or permission to do the same work yourself.
|
|
19134
|
-
- For dependent work, either call \`task_status(wait: true)\` again with the same reasonable interval, or stop with a brief waiting status and let the completion event resume you.
|
|
19129
|
+
### Background Task Discipline
|
|
19130
|
+
- Prefer \`task(..., background: true)\` for delegated work that can run independently.
|
|
19131
|
+
- Track each task's specialist, objective, task/session ID, and file/topic ownership.
|
|
19132
|
+
- Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
|
|
19133
|
+
- Before local edits or another writer task, compare against running task scopes.
|
|
19135
19134
|
- Parallel background tasks are allowed only when their write scopes do not conflict.
|
|
19136
|
-
-
|
|
19137
|
-
|
|
19138
|
-
### Background Job Discipline
|
|
19139
|
-
- Every background task owns its declared lane until terminal.
|
|
19140
|
-
- Do not duplicate, undermine, or race a running lane.
|
|
19141
|
-
- A polling timeout is not terminal. The lane remains running until a terminal completion/error/cancel event is observed or the user explicitly cancels it.
|
|
19142
|
-
- After dispatch, classify the next step:
|
|
19143
|
-
1. independent: continue,
|
|
19144
|
-
2. dependent: wait/poll,
|
|
19145
|
-
3. no useful independent work: stop and let hook-driven completion resume.
|
|
19146
|
-
- Before editing files or spawning another writer, compare against running job scopes.
|
|
19135
|
+
- Before final response, reconcile any terminal jobs shown in the Background Job Board.
|
|
19147
19136
|
- Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
|
|
19148
19137
|
- Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
|
|
19149
|
-
- Never finalize work that depends on unresolved background jobs.
|
|
19150
19138
|
|
|
19151
19139
|
### Design Handoff Discipline
|
|
19152
19140
|
- When @designer completes UI/UX work, treat layout, spacing, hierarchy, motion, color, affordances, and component feel as intentional design output.
|
|
@@ -19160,13 +19148,9 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
|
|
|
19160
19148
|
- When too much unrelated, and really needed, start a fresh session with the specialist
|
|
19161
19149
|
- If multiple remembered sessions fit, prefer the most recently used matching session.
|
|
19162
19150
|
- Prefer re-uses over creating new sessions all the time
|
|
19163
|
-
|
|
19164
|
-
|
|
19165
|
-
|
|
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.
|
|
19151
|
+
- When reusing a specialist session, you MUST pass the existing session or alias in the task tool's \`task_id\` argument. Saying "reuse" in prose is not enough.
|
|
19152
|
+
- If the Background Job Board lists \`fix-1 / ses_abc / fixer\`, call task with \`subagent_type: "fixer"\` and \`task_id: "fix-1"\` or \`task_id: "ses_abc"\`.
|
|
19153
|
+
- Do not leave \`task_id\` empty when intending to reuse; omitted or empty \`task_id\` creates a new specialist session.
|
|
19170
19154
|
|
|
19171
19155
|
### Validation routing
|
|
19172
19156
|
- Validation is a workflow stage owned by the Orchestrator, not a separate specialist
|
|
@@ -20039,10 +20023,6 @@ function setActiveRuntimePresetWithPrevious(name) {
|
|
|
20039
20023
|
previousRuntimePreset = activeRuntimePreset;
|
|
20040
20024
|
activeRuntimePreset = name;
|
|
20041
20025
|
}
|
|
20042
|
-
function rollbackRuntimePreset(previous) {
|
|
20043
|
-
activeRuntimePreset = previous;
|
|
20044
|
-
previousRuntimePreset = null;
|
|
20045
|
-
}
|
|
20046
20026
|
|
|
20047
20027
|
// src/utils/logger.ts
|
|
20048
20028
|
import * as fs2 from "node:fs";
|
|
@@ -20055,7 +20035,7 @@ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
20055
20035
|
var logFile = null;
|
|
20056
20036
|
var writeChain = Promise.resolve();
|
|
20057
20037
|
function getLogDir() {
|
|
20058
|
-
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
|
|
20038
|
+
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode/log");
|
|
20059
20039
|
}
|
|
20060
20040
|
function cleanupOldLogs(logDir) {
|
|
20061
20041
|
try {
|
|
@@ -22647,6 +22627,9 @@ var TRANSIENT_PROCESS_ERROR_TEXT = new Set([
|
|
|
22647
22627
|
"Task is not running in this process and has not produced output."
|
|
22648
22628
|
]);
|
|
22649
22629
|
function parseTaskIdFromTaskOutput(output) {
|
|
22630
|
+
const xmlMatch = /<task\s+[^>]*\bid=["']([^"']+)["'][^>]*>/i.exec(output);
|
|
22631
|
+
if (xmlMatch)
|
|
22632
|
+
return xmlMatch[1];
|
|
22650
22633
|
const lines = output.split(/\r?\n/);
|
|
22651
22634
|
for (const line of lines) {
|
|
22652
22635
|
const trimmed = line.trim();
|
|
@@ -22681,20 +22664,10 @@ function parseTaskStatusOutput(output) {
|
|
|
22681
22664
|
result: parseTaskResultFromOutput(output)
|
|
22682
22665
|
};
|
|
22683
22666
|
}
|
|
22684
|
-
function classifyTaskStatusOutput(status) {
|
|
22685
|
-
if (status.timedOut)
|
|
22686
|
-
return "timeout";
|
|
22687
|
-
if (status.state === "running")
|
|
22688
|
-
return "running";
|
|
22689
|
-
if (status.state === "completed" || status.state === "cancelled") {
|
|
22690
|
-
return "terminal";
|
|
22691
|
-
}
|
|
22692
|
-
if (TRANSIENT_PROCESS_ERROR_TEXT.has(status.result ?? "")) {
|
|
22693
|
-
return "transient_process_error";
|
|
22694
|
-
}
|
|
22695
|
-
return "unknown_error";
|
|
22696
|
-
}
|
|
22697
22667
|
function parseTaskStateFromOutput(output) {
|
|
22668
|
+
const xmlMatch = /<task\s+[^>]*\bstate=["'](running|completed|error|cancelled)["'][^>]*>/i.exec(output);
|
|
22669
|
+
if (xmlMatch)
|
|
22670
|
+
return xmlMatch[1].toLowerCase();
|
|
22698
22671
|
for (const line of getTaskHeader(output).split(/\r?\n/)) {
|
|
22699
22672
|
const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
|
|
22700
22673
|
if (match)
|
|
@@ -22830,7 +22803,7 @@ class BackgroundJobBoard {
|
|
|
22830
22803
|
if (!existing)
|
|
22831
22804
|
return;
|
|
22832
22805
|
const isStaleTerminal = TERMINAL_STATES.has(existing.state) || existing.state === "reconciled";
|
|
22833
|
-
if (
|
|
22806
|
+
if (isStaleTerminal) {
|
|
22834
22807
|
const updated2 = {
|
|
22835
22808
|
...existing,
|
|
22836
22809
|
lastLiveBusyAt: now
|
|
@@ -22840,17 +22813,8 @@ class BackgroundJobBoard {
|
|
|
22840
22813
|
}
|
|
22841
22814
|
const updated = {
|
|
22842
22815
|
...existing,
|
|
22843
|
-
state: "running",
|
|
22844
|
-
timedOut: false,
|
|
22845
|
-
statusUncertain: false,
|
|
22846
|
-
cancellationRequested: false,
|
|
22847
|
-
terminalUnreconciled: false,
|
|
22848
22816
|
updatedAt: now,
|
|
22849
|
-
lastLiveBusyAt: now
|
|
22850
|
-
completedAt: undefined,
|
|
22851
|
-
terminalState: undefined,
|
|
22852
|
-
resultSummary: undefined,
|
|
22853
|
-
lastStatusError: undefined
|
|
22817
|
+
lastLiveBusyAt: now
|
|
22854
22818
|
};
|
|
22855
22819
|
this.jobs.set(taskID, updated);
|
|
22856
22820
|
return updated;
|
|
@@ -22909,9 +22873,6 @@ class BackgroundJobBoard {
|
|
|
22909
22873
|
const value = taskIDOrAlias.trim();
|
|
22910
22874
|
return this.list(parentSessionID).find((job) => job.taskID === value || job.alias === value);
|
|
22911
22875
|
}
|
|
22912
|
-
resolveForStatus(parentSessionID, taskIDOrAlias) {
|
|
22913
|
-
return this.resolve(parentSessionID, taskIDOrAlias);
|
|
22914
|
-
}
|
|
22915
22876
|
resolveReusable(parentSessionID, taskIDOrAlias, agent) {
|
|
22916
22877
|
const job = this.resolve(parentSessionID, taskIDOrAlias);
|
|
22917
22878
|
if (!job || !isReusable(job))
|
|
@@ -22967,7 +22928,7 @@ class BackgroundJobBoard {
|
|
|
22967
22928
|
return [
|
|
22968
22929
|
"### Background Job Board",
|
|
22969
22930
|
"SENTINEL: background-job-board-v2",
|
|
22970
|
-
"
|
|
22931
|
+
"Do not poll running jobs. Wait for hook-driven completion, or use cancel_task only for explicit cancellation. Reconcile terminal jobs before final response. Reuse only completed sessions for the same specialist/context; never reuse cancelled or errored sessions.",
|
|
22971
22932
|
"",
|
|
22972
22933
|
"#### Active / Unreconciled",
|
|
22973
22934
|
...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
|
|
@@ -23230,7 +23191,7 @@ function activationPrompt(task2) {
|
|
|
23230
23191
|
"- draft a plan and get `@oracle` review before implementation;",
|
|
23231
23192
|
"- create and review a phased implementation/delegation plan;",
|
|
23232
23193
|
"- execute phase by phase with background specialists where useful;",
|
|
23233
|
-
"-
|
|
23194
|
+
"- wait for hook-driven background completion, reconcile results, validate, and ask `@oracle` to review each phase;",
|
|
23234
23195
|
"- ask `@oracle` to include simplify/readability feedback in phase reviews;",
|
|
23235
23196
|
"- fix actionable review issues before continuing.",
|
|
23236
23197
|
"",
|
|
@@ -24008,6 +23969,7 @@ var BACKGROUND_JOB_BOARD_SENTINEL = "SENTINEL: background-job-board-v2";
|
|
|
24008
23969
|
var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
|
|
24009
23970
|
var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
|
|
24010
23971
|
var MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
|
|
23972
|
+
var RAW_SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_-]+$/;
|
|
24011
23973
|
function djb2Hash(str) {
|
|
24012
23974
|
let hash = 5381;
|
|
24013
23975
|
for (let i = 0;i < str.length; i++) {
|
|
@@ -24042,6 +24004,10 @@ function isObjectRecord(value) {
|
|
|
24042
24004
|
function extractPath(output) {
|
|
24043
24005
|
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
24044
24006
|
}
|
|
24007
|
+
function extractTaskSummary(output) {
|
|
24008
|
+
const summary = /<summary>\s*([\s\S]*?)\s*<\/summary>/i.exec(output)?.[1];
|
|
24009
|
+
return summary?.trim() || undefined;
|
|
24010
|
+
}
|
|
24045
24011
|
function normalizePath(root, file) {
|
|
24046
24012
|
const relative = path9.relative(root, file);
|
|
24047
24013
|
if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
|
|
@@ -24133,7 +24099,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24133
24099
|
const status = parseTaskStatusOutput(output);
|
|
24134
24100
|
if (!status)
|
|
24135
24101
|
return;
|
|
24136
|
-
log("[task-session-manager] parsed task status
|
|
24102
|
+
log("[task-session-manager] parsed task output status", {
|
|
24137
24103
|
taskID: status.taskID,
|
|
24138
24104
|
state: status.state,
|
|
24139
24105
|
timedOut: status.timedOut,
|
|
@@ -24178,73 +24144,23 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24178
24144
|
}
|
|
24179
24145
|
return updated;
|
|
24180
24146
|
}
|
|
24181
|
-
async function handleTransientTaskStatusOutput(output) {
|
|
24182
|
-
if (typeof output.output !== "string")
|
|
24183
|
-
return false;
|
|
24184
|
-
const status = parseTaskStatusOutput(output.output);
|
|
24185
|
-
if (!status)
|
|
24186
|
-
return false;
|
|
24187
|
-
if (classifyTaskStatusOutput(status) !== "transient_process_error") {
|
|
24188
|
-
return false;
|
|
24189
|
-
}
|
|
24190
|
-
const existing = backgroundJobBoard.get(status.taskID);
|
|
24191
|
-
const liveStatus = existing && existing.state === "running" ? undefined : await getLiveSessionStatus(status.taskID);
|
|
24192
|
-
const recentLiveBusy = !!existing?.lastLiveBusyAt && (!existing.completedAt || existing.lastLiveBusyAt >= existing.completedAt);
|
|
24193
|
-
const isStillRunning = existing?.state === "running" || recentLiveBusy || liveStatus === "busy" || liveStatus === "retry";
|
|
24194
|
-
if (!isStillRunning)
|
|
24195
|
-
return false;
|
|
24196
|
-
const updated = existing?.state === "running" ? backgroundJobBoard.updateStatus({
|
|
24197
|
-
taskID: status.taskID,
|
|
24198
|
-
state: "running",
|
|
24199
|
-
statusUncertain: true,
|
|
24200
|
-
lastStatusError: status.result
|
|
24201
|
-
}) : undefined;
|
|
24202
|
-
log("[task-session-manager] classified transient task_status error", {
|
|
24203
|
-
taskID: status.taskID,
|
|
24204
|
-
alias: existing?.alias,
|
|
24205
|
-
parentSessionID: existing?.parentSessionID,
|
|
24206
|
-
previousState: existing?.state,
|
|
24207
|
-
updatedState: updated?.state,
|
|
24208
|
-
liveStatus,
|
|
24209
|
-
recentLiveBusy
|
|
24210
|
-
});
|
|
24211
|
-
return true;
|
|
24212
|
-
}
|
|
24213
|
-
async function getLiveSessionStatus(sessionID) {
|
|
24214
|
-
try {
|
|
24215
|
-
const response = await _ctx.client.session.status();
|
|
24216
|
-
const data = response.data;
|
|
24217
|
-
if (!isObjectRecord(data))
|
|
24218
|
-
return;
|
|
24219
|
-
const item = data[sessionID];
|
|
24220
|
-
if (item === undefined)
|
|
24221
|
-
return "idle";
|
|
24222
|
-
if (isObjectRecord(item) && typeof item.type === "string") {
|
|
24223
|
-
return item.type;
|
|
24224
|
-
}
|
|
24225
|
-
const directType = data.type;
|
|
24226
|
-
if (typeof directType === "string")
|
|
24227
|
-
return directType;
|
|
24228
|
-
const nestedStatus = data.status;
|
|
24229
|
-
if (!isObjectRecord(nestedStatus))
|
|
24230
|
-
return;
|
|
24231
|
-
return typeof nestedStatus.type === "string" ? nestedStatus.type : undefined;
|
|
24232
|
-
} catch {
|
|
24233
|
-
return;
|
|
24234
|
-
}
|
|
24235
|
-
}
|
|
24236
24147
|
function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
|
|
24237
24148
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
24238
24149
|
return;
|
|
24239
24150
|
}
|
|
24240
|
-
|
|
24241
|
-
const isFailed = BACKGROUND_COMPLETION_FAILED.test(part.text);
|
|
24242
|
-
if (part.synthetic !== true || !isCompleted && !isFailed) {
|
|
24151
|
+
if (part.synthetic !== true)
|
|
24243
24152
|
return;
|
|
24244
|
-
}
|
|
24245
24153
|
const status = parseTaskStatusOutput(part.text);
|
|
24246
24154
|
if (!status)
|
|
24247
24155
|
return;
|
|
24156
|
+
if (status.state !== "completed" && status.state !== "error") {
|
|
24157
|
+
return;
|
|
24158
|
+
}
|
|
24159
|
+
const summary = extractTaskSummary(part.text);
|
|
24160
|
+
const isCompleted = summary ? BACKGROUND_COMPLETION_COMPLETED.test(summary) : status.state === "completed";
|
|
24161
|
+
const isFailed = summary ? BACKGROUND_COMPLETION_FAILED.test(summary) : status.state === "error";
|
|
24162
|
+
if (summary && !isCompleted && !isFailed)
|
|
24163
|
+
return;
|
|
24248
24164
|
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
24249
24165
|
const existing = backgroundJobBoard.get(status.taskID);
|
|
24250
24166
|
if (isFailed && isLateCancelledTaskError(existing, status.state)) {
|
|
@@ -24357,23 +24273,13 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24357
24273
|
return {
|
|
24358
24274
|
"tool.execute.before": async (input, output) => {
|
|
24359
24275
|
const toolName = input.tool.toLowerCase();
|
|
24360
|
-
if (toolName !== "task"
|
|
24276
|
+
if (toolName !== "task")
|
|
24361
24277
|
return;
|
|
24362
24278
|
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
24363
24279
|
return;
|
|
24364
24280
|
}
|
|
24365
24281
|
if (!isObjectRecord(output.args))
|
|
24366
24282
|
return;
|
|
24367
|
-
if (toolName === "task_status") {
|
|
24368
|
-
const args2 = output.args;
|
|
24369
|
-
if (typeof args2.task_id !== "string" || args2.task_id.trim() === "") {
|
|
24370
|
-
return;
|
|
24371
|
-
}
|
|
24372
|
-
const resolved = backgroundJobBoard.resolveForStatus(input.sessionID, args2.task_id.trim());
|
|
24373
|
-
if (resolved)
|
|
24374
|
-
args2.task_id = resolved.taskID;
|
|
24375
|
-
return;
|
|
24376
|
-
}
|
|
24377
24283
|
const args = output.args;
|
|
24378
24284
|
if (!isAgentName(args.subagent_type)) {
|
|
24379
24285
|
if (typeof args.task_id === "string" && args.task_id.trim() !== "") {
|
|
@@ -24402,6 +24308,11 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24402
24308
|
const requested = args.task_id.trim();
|
|
24403
24309
|
const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, args.subagent_type);
|
|
24404
24310
|
if (!remembered) {
|
|
24311
|
+
if (RAW_SESSION_ID_PATTERN.test(requested)) {
|
|
24312
|
+
pendingCall.resumedTaskId = requested;
|
|
24313
|
+
rememberPendingCall(pendingCall);
|
|
24314
|
+
return;
|
|
24315
|
+
}
|
|
24405
24316
|
delete args.task_id;
|
|
24406
24317
|
return;
|
|
24407
24318
|
}
|
|
@@ -24418,24 +24329,13 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24418
24329
|
}
|
|
24419
24330
|
return;
|
|
24420
24331
|
}
|
|
24421
|
-
if (input.tool.toLowerCase() === "task_status") {
|
|
24422
|
-
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
24423
|
-
return;
|
|
24424
|
-
}
|
|
24425
|
-
normalizeLateCancelledToolStatus(output);
|
|
24426
|
-
if (await handleTransientTaskStatusOutput(output)) {
|
|
24427
|
-
return;
|
|
24428
|
-
}
|
|
24429
|
-
updateBackgroundJobFromOutput(output.output);
|
|
24430
|
-
return;
|
|
24431
|
-
}
|
|
24432
24332
|
if (input.tool.toLowerCase() !== "task")
|
|
24433
24333
|
return;
|
|
24434
24334
|
const pending = takePendingCall(input.callID, input.sessionID);
|
|
24435
24335
|
if (!pending || typeof output.output !== "string")
|
|
24436
24336
|
return;
|
|
24437
24337
|
const launch = parseTaskLaunchOutput(output.output);
|
|
24438
|
-
if (launch) {
|
|
24338
|
+
if (launch && !launch.result?.match(/Timed out after \d+ms/i)) {
|
|
24439
24339
|
const record = backgroundJobBoard.registerLaunch({
|
|
24440
24340
|
taskID: launch.taskID,
|
|
24441
24341
|
parentSessionID: pending.parentSessionId,
|
|
@@ -24455,6 +24355,39 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24455
24355
|
pendingManagedTaskIds.add(launch.taskID);
|
|
24456
24356
|
return;
|
|
24457
24357
|
}
|
|
24358
|
+
normalizeLateCancelledTaskOutput(output);
|
|
24359
|
+
const status = parseTaskStatusOutput(output.output);
|
|
24360
|
+
if (status) {
|
|
24361
|
+
const existing = backgroundJobBoard.get(status.taskID);
|
|
24362
|
+
const record = existing ?? backgroundJobBoard.registerLaunch({
|
|
24363
|
+
taskID: status.taskID,
|
|
24364
|
+
parentSessionID: pending.parentSessionId,
|
|
24365
|
+
agent: pending.agentType,
|
|
24366
|
+
description: pending.label,
|
|
24367
|
+
objective: pending.label
|
|
24368
|
+
});
|
|
24369
|
+
const updated = backgroundJobBoard.updateStatus({
|
|
24370
|
+
taskID: status.taskID,
|
|
24371
|
+
state: status.state,
|
|
24372
|
+
timedOut: status.timedOut,
|
|
24373
|
+
resultSummary: status.result
|
|
24374
|
+
});
|
|
24375
|
+
log("[task-session-manager] foreground task status registered", {
|
|
24376
|
+
taskID: status.taskID,
|
|
24377
|
+
alias: updated?.alias ?? record.alias,
|
|
24378
|
+
parentSessionID: pending.parentSessionId,
|
|
24379
|
+
agent: pending.agentType,
|
|
24380
|
+
state: updated?.state ?? record.state
|
|
24381
|
+
});
|
|
24382
|
+
if (pending.resumedTaskId && pending.resumedTaskId !== status.taskID) {
|
|
24383
|
+
backgroundJobBoard.drop(pending.resumedTaskId);
|
|
24384
|
+
}
|
|
24385
|
+
pendingManagedTaskIds.delete(status.taskID);
|
|
24386
|
+
const contextFiles2 = contextFilesForPrompt(contextByTask.get(status.taskID));
|
|
24387
|
+
backgroundJobBoard.addContext(status.taskID, contextFiles2);
|
|
24388
|
+
pruneContext();
|
|
24389
|
+
return;
|
|
24390
|
+
}
|
|
24458
24391
|
const taskId = parseTaskIdFromTaskOutput(output.output);
|
|
24459
24392
|
if (!taskId) {
|
|
24460
24393
|
if (pending.resumedTaskId && isMissingRememberedSessionError(output.output)) {
|
|
@@ -24602,7 +24535,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24602
24535
|
}
|
|
24603
24536
|
}
|
|
24604
24537
|
};
|
|
24605
|
-
function
|
|
24538
|
+
function normalizeLateCancelledTaskOutput(output) {
|
|
24606
24539
|
if (typeof output.output !== "string")
|
|
24607
24540
|
return;
|
|
24608
24541
|
const status = parseTaskStatusOutput(output.output);
|
|
@@ -24611,7 +24544,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24611
24544
|
const existing = backgroundJobBoard.get(status.taskID);
|
|
24612
24545
|
if (!isLateCancelledTaskError(existing, status.state))
|
|
24613
24546
|
return;
|
|
24614
|
-
log("[task-session-manager] normalized late cancelled
|
|
24547
|
+
log("[task-session-manager] normalized late cancelled task output", {
|
|
24615
24548
|
taskID: status.taskID,
|
|
24616
24549
|
alias: existing?.alias,
|
|
24617
24550
|
state: existing?.state,
|
|
@@ -24642,715 +24575,6 @@ function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
|
|
|
24642
24575
|
].join(`
|
|
24643
24576
|
`);
|
|
24644
24577
|
}
|
|
24645
|
-
// src/hooks/todo-continuation/index.ts
|
|
24646
|
-
import { tool } from "@opencode-ai/plugin";
|
|
24647
|
-
|
|
24648
|
-
// src/hooks/todo-continuation/todo-hygiene.ts
|
|
24649
|
-
var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
|
|
24650
|
-
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.";
|
|
24651
|
-
var RESET = new Set(["todowrite"]);
|
|
24652
|
-
var IGNORE = new Set(["auto_continue"]);
|
|
24653
|
-
function createTodoHygiene(options) {
|
|
24654
|
-
const pending = new Map;
|
|
24655
|
-
const active = new Set;
|
|
24656
|
-
function clearCycle(sessionID) {
|
|
24657
|
-
pending.delete(sessionID);
|
|
24658
|
-
}
|
|
24659
|
-
function clear(sessionID) {
|
|
24660
|
-
clearCycle(sessionID);
|
|
24661
|
-
active.delete(sessionID);
|
|
24662
|
-
}
|
|
24663
|
-
function isFinalActive(state) {
|
|
24664
|
-
return state.inProgressCount === 1 && state.pendingCount === 0 && state.openCount === 1;
|
|
24665
|
-
}
|
|
24666
|
-
function mark(sessionID, reason) {
|
|
24667
|
-
const reasons = pending.get(sessionID) ?? new Set;
|
|
24668
|
-
reasons.add(reason);
|
|
24669
|
-
pending.set(sessionID, reasons);
|
|
24670
|
-
}
|
|
24671
|
-
function pick(reasons) {
|
|
24672
|
-
if (reasons.has("final_active")) {
|
|
24673
|
-
return TODO_FINAL_ACTIVE_REMINDER;
|
|
24674
|
-
}
|
|
24675
|
-
return TODO_HYGIENE_REMINDER;
|
|
24676
|
-
}
|
|
24677
|
-
return {
|
|
24678
|
-
handleRequestStart(input) {
|
|
24679
|
-
clear(input.sessionID);
|
|
24680
|
-
},
|
|
24681
|
-
async handleToolExecuteAfter(input, _output) {
|
|
24682
|
-
if (!input.sessionID) {
|
|
24683
|
-
return;
|
|
24684
|
-
}
|
|
24685
|
-
const tool = input.tool.toLowerCase();
|
|
24686
|
-
if (IGNORE.has(tool)) {
|
|
24687
|
-
return;
|
|
24688
|
-
}
|
|
24689
|
-
try {
|
|
24690
|
-
if (RESET.has(tool)) {
|
|
24691
|
-
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
24692
|
-
clear(input.sessionID);
|
|
24693
|
-
return;
|
|
24694
|
-
}
|
|
24695
|
-
active.add(input.sessionID);
|
|
24696
|
-
clearCycle(input.sessionID);
|
|
24697
|
-
const state2 = await options.getTodoState(input.sessionID);
|
|
24698
|
-
if (!state2.hasOpenTodos) {
|
|
24699
|
-
active.delete(input.sessionID);
|
|
24700
|
-
options.log?.("Cleared todo hygiene cycle", {
|
|
24701
|
-
sessionID: input.sessionID,
|
|
24702
|
-
tool
|
|
24703
|
-
});
|
|
24704
|
-
return;
|
|
24705
|
-
}
|
|
24706
|
-
if (!isFinalActive(state2)) {
|
|
24707
|
-
options.log?.("Reset todo hygiene cycle", {
|
|
24708
|
-
sessionID: input.sessionID,
|
|
24709
|
-
tool
|
|
24710
|
-
});
|
|
24711
|
-
return;
|
|
24712
|
-
}
|
|
24713
|
-
mark(input.sessionID, "final_active");
|
|
24714
|
-
options.log?.("Armed final-active todo hygiene reminder", {
|
|
24715
|
-
sessionID: input.sessionID,
|
|
24716
|
-
tool
|
|
24717
|
-
});
|
|
24718
|
-
return;
|
|
24719
|
-
}
|
|
24720
|
-
if (!active.has(input.sessionID)) {
|
|
24721
|
-
return;
|
|
24722
|
-
}
|
|
24723
|
-
if (pending.get(input.sessionID)?.has("final_active")) {
|
|
24724
|
-
return;
|
|
24725
|
-
}
|
|
24726
|
-
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
24727
|
-
clear(input.sessionID);
|
|
24728
|
-
return;
|
|
24729
|
-
}
|
|
24730
|
-
const state = await options.getTodoState(input.sessionID);
|
|
24731
|
-
if (!state.hasOpenTodos) {
|
|
24732
|
-
clear(input.sessionID);
|
|
24733
|
-
return;
|
|
24734
|
-
}
|
|
24735
|
-
if (isFinalActive(state)) {
|
|
24736
|
-
mark(input.sessionID, "final_active");
|
|
24737
|
-
} else {
|
|
24738
|
-
mark(input.sessionID, "general");
|
|
24739
|
-
}
|
|
24740
|
-
options.log?.("Armed todo hygiene reminder", {
|
|
24741
|
-
sessionID: input.sessionID,
|
|
24742
|
-
tool,
|
|
24743
|
-
reasons: Array.from(pending.get(input.sessionID) ?? [])
|
|
24744
|
-
});
|
|
24745
|
-
} catch (error) {
|
|
24746
|
-
options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
|
|
24747
|
-
sessionID: input.sessionID,
|
|
24748
|
-
tool,
|
|
24749
|
-
error: error instanceof Error ? error.message : String(error)
|
|
24750
|
-
});
|
|
24751
|
-
}
|
|
24752
|
-
},
|
|
24753
|
-
getPendingReminder(sessionID) {
|
|
24754
|
-
const reasons = pending.get(sessionID);
|
|
24755
|
-
if (!reasons || reasons.size === 0) {
|
|
24756
|
-
return null;
|
|
24757
|
-
}
|
|
24758
|
-
if (options.shouldInject && !options.shouldInject(sessionID)) {
|
|
24759
|
-
clear(sessionID);
|
|
24760
|
-
return null;
|
|
24761
|
-
}
|
|
24762
|
-
const reminder = pick(reasons);
|
|
24763
|
-
options.log?.("Read todo hygiene reminder", {
|
|
24764
|
-
sessionID,
|
|
24765
|
-
reminder,
|
|
24766
|
-
reasons: Array.from(reasons)
|
|
24767
|
-
});
|
|
24768
|
-
return reminder;
|
|
24769
|
-
},
|
|
24770
|
-
handleEvent(event) {
|
|
24771
|
-
if (event.type !== "session.deleted") {
|
|
24772
|
-
return;
|
|
24773
|
-
}
|
|
24774
|
-
const sessionID = event.properties?.sessionID ?? event.properties?.info?.id;
|
|
24775
|
-
if (!sessionID) {
|
|
24776
|
-
return;
|
|
24777
|
-
}
|
|
24778
|
-
clear(sessionID);
|
|
24779
|
-
}
|
|
24780
|
-
};
|
|
24781
|
-
}
|
|
24782
|
-
|
|
24783
|
-
// src/hooks/todo-continuation/index.ts
|
|
24784
|
-
var HOOK_NAME = "todo-continuation";
|
|
24785
|
-
var COMMAND_NAME2 = "auto-continue";
|
|
24786
|
-
var TODO_STATE_TIMEOUT_MS = 500;
|
|
24787
|
-
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.]";
|
|
24788
|
-
var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
|
|
24789
|
-
var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
|
|
24790
|
-
var SUPPRESS_AFTER_ABORT_MS = 5000;
|
|
24791
|
-
var NOTIFICATION_BUSY_GRACE_MS = 250;
|
|
24792
|
-
var QUESTION_PHRASES = [
|
|
24793
|
-
"would you like",
|
|
24794
|
-
"should i",
|
|
24795
|
-
"do you want",
|
|
24796
|
-
"please review",
|
|
24797
|
-
"let me know",
|
|
24798
|
-
"what do you think",
|
|
24799
|
-
"can you confirm",
|
|
24800
|
-
"would you prefer",
|
|
24801
|
-
"shall i",
|
|
24802
|
-
"any thoughts"
|
|
24803
|
-
];
|
|
24804
|
-
var TERMINAL_TODO_STATUSES = ["completed", "cancelled"];
|
|
24805
|
-
function isQuestion(text) {
|
|
24806
|
-
const lowerText = text.toLowerCase().trim();
|
|
24807
|
-
if (/\?\s*$/.test(lowerText)) {
|
|
24808
|
-
return true;
|
|
24809
|
-
}
|
|
24810
|
-
return QUESTION_PHRASES.some((phrase) => lowerText.includes(phrase));
|
|
24811
|
-
}
|
|
24812
|
-
function cancelPendingTimer(state) {
|
|
24813
|
-
if (state.pendingTimer) {
|
|
24814
|
-
clearTimeout(state.pendingTimer);
|
|
24815
|
-
state.pendingTimer = null;
|
|
24816
|
-
}
|
|
24817
|
-
state.pendingTimerSessionId = null;
|
|
24818
|
-
}
|
|
24819
|
-
function resetState(state) {
|
|
24820
|
-
cancelPendingTimer(state);
|
|
24821
|
-
state.consecutiveContinuations = 0;
|
|
24822
|
-
state.suppressUntil = 0;
|
|
24823
|
-
state.isAutoInjecting = false;
|
|
24824
|
-
state.notifyingSessionIds.clear();
|
|
24825
|
-
state.notificationBusyUntilBySession.clear();
|
|
24826
|
-
}
|
|
24827
|
-
function stripTodoHygieneInstruction(text) {
|
|
24828
|
-
const trimmed = text.trimEnd();
|
|
24829
|
-
if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
|
|
24830
|
-
return trimmed;
|
|
24831
|
-
}
|
|
24832
|
-
const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
|
|
24833
|
-
if (start === -1) {
|
|
24834
|
-
return trimmed;
|
|
24835
|
-
}
|
|
24836
|
-
return trimmed.slice(0, start).trimEnd();
|
|
24837
|
-
}
|
|
24838
|
-
function appendTodoHygieneInstruction(message, reminder) {
|
|
24839
|
-
const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
|
|
24840
|
-
if (!textPart)
|
|
24841
|
-
return;
|
|
24842
|
-
const baseText = stripTodoHygieneInstruction(textPart.text ?? "");
|
|
24843
|
-
const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}
|
|
24844
|
-
${reminder}
|
|
24845
|
-
${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
|
|
24846
|
-
textPart.text = baseText ? `${baseText}
|
|
24847
|
-
|
|
24848
|
-
${instruction}` : instruction;
|
|
24849
|
-
}
|
|
24850
|
-
function stripTodoHygieneInstructionFromMessage(message) {
|
|
24851
|
-
const textPart = [...message.parts].reverse().find((part) => part.type === "text" && typeof part.text === "string");
|
|
24852
|
-
if (!textPart)
|
|
24853
|
-
return;
|
|
24854
|
-
textPart.text = stripTodoHygieneInstruction(textPart.text ?? "");
|
|
24855
|
-
}
|
|
24856
|
-
function createTodoContinuationHook(ctx, config) {
|
|
24857
|
-
const maxContinuations = config?.maxContinuations ?? 5;
|
|
24858
|
-
const cooldownMs = config?.cooldownMs ?? 3000;
|
|
24859
|
-
const autoEnable = config?.autoEnable ?? false;
|
|
24860
|
-
const autoEnableThreshold = config?.autoEnableThreshold ?? 4;
|
|
24861
|
-
const backgroundJobBoard = config?.backgroundJobBoard;
|
|
24862
|
-
const requestSignatureBySession = new Map;
|
|
24863
|
-
const state = {
|
|
24864
|
-
enabled: false,
|
|
24865
|
-
consecutiveContinuations: 0,
|
|
24866
|
-
pendingTimer: null,
|
|
24867
|
-
pendingTimerSessionId: null,
|
|
24868
|
-
suppressUntil: 0,
|
|
24869
|
-
orchestratorSessionIds: new Set,
|
|
24870
|
-
sawChatMessage: false,
|
|
24871
|
-
isAutoInjecting: false,
|
|
24872
|
-
notifyingSessionIds: new Set,
|
|
24873
|
-
notificationBusyUntilBySession: new Map
|
|
24874
|
-
};
|
|
24875
|
-
async function fetchTodos(sessionID) {
|
|
24876
|
-
const result = await withTimeout(ctx.client.session.todo({
|
|
24877
|
-
path: { id: sessionID }
|
|
24878
|
-
}), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
|
|
24879
|
-
return result.data;
|
|
24880
|
-
}
|
|
24881
|
-
const hygiene = createTodoHygiene({
|
|
24882
|
-
getTodoState: async (sessionID) => {
|
|
24883
|
-
const todos = await fetchTodos(sessionID);
|
|
24884
|
-
const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
|
|
24885
|
-
return {
|
|
24886
|
-
hasOpenTodos: openTodos.length > 0,
|
|
24887
|
-
openCount: openTodos.length,
|
|
24888
|
-
inProgressCount: openTodos.filter((todo) => todo.status === "in_progress").length,
|
|
24889
|
-
pendingCount: openTodos.filter((todo) => todo.status === "pending").length
|
|
24890
|
-
};
|
|
24891
|
-
},
|
|
24892
|
-
shouldInject: (sessionID) => isOrchestratorSession(sessionID),
|
|
24893
|
-
log: (message, meta) => log(`[${HOOK_NAME}] ${message}`, meta)
|
|
24894
|
-
});
|
|
24895
|
-
function inferSessionID(messages, index) {
|
|
24896
|
-
const direct = messages[index]?.info.sessionID;
|
|
24897
|
-
if (direct) {
|
|
24898
|
-
return direct;
|
|
24899
|
-
}
|
|
24900
|
-
for (let i = index - 1;i >= 0; i--) {
|
|
24901
|
-
const sessionID = messages[i]?.info.sessionID;
|
|
24902
|
-
if (sessionID) {
|
|
24903
|
-
return sessionID;
|
|
24904
|
-
}
|
|
24905
|
-
}
|
|
24906
|
-
for (let i = index + 1;i < messages.length; i++) {
|
|
24907
|
-
const sessionID = messages[i]?.info.sessionID;
|
|
24908
|
-
if (sessionID) {
|
|
24909
|
-
return sessionID;
|
|
24910
|
-
}
|
|
24911
|
-
}
|
|
24912
|
-
if (state.orchestratorSessionIds.size === 1) {
|
|
24913
|
-
return Array.from(state.orchestratorSessionIds)[0];
|
|
24914
|
-
}
|
|
24915
|
-
return;
|
|
24916
|
-
}
|
|
24917
|
-
function isExternalUserMessage(message) {
|
|
24918
|
-
if (message.info.role !== "user") {
|
|
24919
|
-
return false;
|
|
24920
|
-
}
|
|
24921
|
-
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(`
|
|
24922
|
-
`);
|
|
24923
|
-
const hasNonTextPart = message.parts.some((part) => part.type !== "text");
|
|
24924
|
-
return !(!visibleText && !hasNonTextPart && message.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER)));
|
|
24925
|
-
}
|
|
24926
|
-
function getLastExternalUserMessage(messages) {
|
|
24927
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
24928
|
-
const message = messages[i];
|
|
24929
|
-
if (!isExternalUserMessage(message)) {
|
|
24930
|
-
continue;
|
|
24931
|
-
}
|
|
24932
|
-
const sessionID = inferSessionID(messages, i);
|
|
24933
|
-
const partSignature = message.parts.map((part) => {
|
|
24934
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
24935
|
-
const text = stripTodoHygieneInstruction(part.text);
|
|
24936
|
-
return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? "<internal>" : text.trim()}`;
|
|
24937
|
-
}
|
|
24938
|
-
return part.type ?? "unknown";
|
|
24939
|
-
}).join("|");
|
|
24940
|
-
const ordinal = messages.slice(0, i + 1).filter((item) => isExternalUserMessage(item)).length;
|
|
24941
|
-
return {
|
|
24942
|
-
sessionID,
|
|
24943
|
-
agent: message.info.agent,
|
|
24944
|
-
message,
|
|
24945
|
-
signature: message.info.id ? `${message.info.id}:${partSignature}` : `${ordinal}:${partSignature}`
|
|
24946
|
-
};
|
|
24947
|
-
}
|
|
24948
|
-
return null;
|
|
24949
|
-
}
|
|
24950
|
-
async function handleMessagesTransform(output) {
|
|
24951
|
-
const lastUserMessage = getLastExternalUserMessage(output.messages);
|
|
24952
|
-
if (!lastUserMessage) {
|
|
24953
|
-
return;
|
|
24954
|
-
}
|
|
24955
|
-
if (lastUserMessage.agent && lastUserMessage.agent !== "orchestrator") {
|
|
24956
|
-
return;
|
|
24957
|
-
}
|
|
24958
|
-
if (!lastUserMessage.sessionID) {
|
|
24959
|
-
for (const sessionID of state.orchestratorSessionIds) {
|
|
24960
|
-
requestSignatureBySession.delete(sessionID);
|
|
24961
|
-
hygiene.handleRequestStart({ sessionID });
|
|
24962
|
-
}
|
|
24963
|
-
return;
|
|
24964
|
-
}
|
|
24965
|
-
const knownOrchestrator = isOrchestratorSession(lastUserMessage.sessionID);
|
|
24966
|
-
if (lastUserMessage.agent === "orchestrator") {
|
|
24967
|
-
registerOrchestratorSession(lastUserMessage.sessionID);
|
|
24968
|
-
} else if (!knownOrchestrator) {
|
|
24969
|
-
return;
|
|
24970
|
-
}
|
|
24971
|
-
if (requestSignatureBySession.get(lastUserMessage.sessionID) === lastUserMessage.signature) {
|
|
24972
|
-
const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
|
|
24973
|
-
const guardrail = backgroundGuardrail(lastUserMessage.sessionID);
|
|
24974
|
-
const combinedReminder = [reminder, guardrail].filter((item) => Boolean(item)).join(" ");
|
|
24975
|
-
if (combinedReminder) {
|
|
24976
|
-
appendTodoHygieneInstruction(lastUserMessage.message, combinedReminder);
|
|
24977
|
-
} else {
|
|
24978
|
-
stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
|
|
24979
|
-
}
|
|
24980
|
-
return;
|
|
24981
|
-
}
|
|
24982
|
-
requestSignatureBySession.set(lastUserMessage.sessionID, lastUserMessage.signature);
|
|
24983
|
-
stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
|
|
24984
|
-
hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
|
|
24985
|
-
}
|
|
24986
|
-
function markNotificationStarted(sessionID) {
|
|
24987
|
-
state.notifyingSessionIds.add(sessionID);
|
|
24988
|
-
}
|
|
24989
|
-
function markNotificationFinished(sessionID) {
|
|
24990
|
-
state.notifyingSessionIds.delete(sessionID);
|
|
24991
|
-
state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
|
|
24992
|
-
}
|
|
24993
|
-
function clearNotificationState(sessionID) {
|
|
24994
|
-
state.notifyingSessionIds.delete(sessionID);
|
|
24995
|
-
state.notificationBusyUntilBySession.delete(sessionID);
|
|
24996
|
-
}
|
|
24997
|
-
function isNotificationBusy(sessionID) {
|
|
24998
|
-
if (state.notifyingSessionIds.has(sessionID)) {
|
|
24999
|
-
return true;
|
|
25000
|
-
}
|
|
25001
|
-
const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
|
|
25002
|
-
if (until <= Date.now()) {
|
|
25003
|
-
state.notificationBusyUntilBySession.delete(sessionID);
|
|
25004
|
-
return false;
|
|
25005
|
-
}
|
|
25006
|
-
return true;
|
|
25007
|
-
}
|
|
25008
|
-
function isOrchestratorSession(sessionID) {
|
|
25009
|
-
return state.orchestratorSessionIds.has(sessionID);
|
|
25010
|
-
}
|
|
25011
|
-
function registerOrchestratorSession(sessionID) {
|
|
25012
|
-
state.orchestratorSessionIds.add(sessionID);
|
|
25013
|
-
}
|
|
25014
|
-
function backgroundGuardrail(sessionID) {
|
|
25015
|
-
if (!backgroundJobBoard)
|
|
25016
|
-
return;
|
|
25017
|
-
const hasRunning = backgroundJobBoard.hasRunning(sessionID);
|
|
25018
|
-
const hasTerminal = backgroundJobBoard.hasTerminalUnreconciled(sessionID);
|
|
25019
|
-
if (hasRunning && hasTerminal) {
|
|
25020
|
-
return "Background jobs are still unresolved: call task_status for running jobs and reconcile terminal Background Job Board results before dependent work or finalizing.";
|
|
25021
|
-
}
|
|
25022
|
-
if (hasTerminal) {
|
|
25023
|
-
return "Background jobs have terminal results: reconcile the Background Job Board results before finalizing.";
|
|
25024
|
-
}
|
|
25025
|
-
if (hasRunning) {
|
|
25026
|
-
return "Background jobs are still running: call task_status before dependent work or finalizing.";
|
|
25027
|
-
}
|
|
25028
|
-
return;
|
|
25029
|
-
}
|
|
25030
|
-
function continuationPrompt(sessionID) {
|
|
25031
|
-
const guardrail = backgroundGuardrail(sessionID);
|
|
25032
|
-
if (!guardrail)
|
|
25033
|
-
return CONTINUATION_PROMPT;
|
|
25034
|
-
return `${CONTINUATION_PROMPT} ${guardrail}`;
|
|
25035
|
-
}
|
|
25036
|
-
function handleChatMessage(input) {
|
|
25037
|
-
if (!input.agent) {
|
|
25038
|
-
return;
|
|
25039
|
-
}
|
|
25040
|
-
state.sawChatMessage = true;
|
|
25041
|
-
if (input.agent === "orchestrator") {
|
|
25042
|
-
registerOrchestratorSession(input.sessionID);
|
|
25043
|
-
}
|
|
25044
|
-
}
|
|
25045
|
-
const autoContinue = tool({
|
|
25046
|
-
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.",
|
|
25047
|
-
args: { enabled: tool.schema.boolean() },
|
|
25048
|
-
execute: async (args) => {
|
|
25049
|
-
const enabled = args.enabled;
|
|
25050
|
-
state.enabled = enabled;
|
|
25051
|
-
state.consecutiveContinuations = 0;
|
|
25052
|
-
if (enabled) {
|
|
25053
|
-
state.suppressUntil = 0;
|
|
25054
|
-
log(`[${HOOK_NAME}] Auto-continue enabled`, { maxContinuations });
|
|
25055
|
-
return `Auto-continue enabled. Will auto-continue for up to ${maxContinuations} consecutive injections.`;
|
|
25056
|
-
}
|
|
25057
|
-
cancelPendingTimer(state);
|
|
25058
|
-
log(`[${HOOK_NAME}] Auto-continue disabled`);
|
|
25059
|
-
return "Auto-continue disabled.";
|
|
25060
|
-
}
|
|
25061
|
-
});
|
|
25062
|
-
async function handleEvent(input) {
|
|
25063
|
-
const { event } = input;
|
|
25064
|
-
const properties = event.properties ?? {};
|
|
25065
|
-
hygiene.handleEvent({
|
|
25066
|
-
type: event.type,
|
|
25067
|
-
properties: {
|
|
25068
|
-
info: properties.info,
|
|
25069
|
-
sessionID: properties.sessionID
|
|
25070
|
-
}
|
|
25071
|
-
});
|
|
25072
|
-
if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
|
|
25073
|
-
const sessionID = properties.sessionID;
|
|
25074
|
-
if (!sessionID) {
|
|
25075
|
-
return;
|
|
25076
|
-
}
|
|
25077
|
-
log(`[${HOOK_NAME}] Session idle`, { sessionID });
|
|
25078
|
-
if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
|
|
25079
|
-
registerOrchestratorSession(sessionID);
|
|
25080
|
-
log(`[${HOOK_NAME}] Tracked orchestrator session`, {
|
|
25081
|
-
sessionID
|
|
25082
|
-
});
|
|
25083
|
-
}
|
|
25084
|
-
if (!isOrchestratorSession(sessionID)) {
|
|
25085
|
-
log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
|
|
25086
|
-
sessionID
|
|
25087
|
-
});
|
|
25088
|
-
return;
|
|
25089
|
-
}
|
|
25090
|
-
if (autoEnable && !state.enabled) {
|
|
25091
|
-
try {
|
|
25092
|
-
const todos = await fetchTodos(sessionID);
|
|
25093
|
-
const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
|
|
25094
|
-
if (incompleteCount2 >= autoEnableThreshold) {
|
|
25095
|
-
state.enabled = true;
|
|
25096
|
-
state.consecutiveContinuations = 0;
|
|
25097
|
-
state.suppressUntil = 0;
|
|
25098
|
-
log(`[${HOOK_NAME}] Auto-enabled: ${incompleteCount2} incomplete todos >= threshold ${autoEnableThreshold}`, { sessionID });
|
|
25099
|
-
} else {
|
|
25100
|
-
log(`[${HOOK_NAME}] Auto-enable skipped: ${incompleteCount2} incomplete todos < threshold ${autoEnableThreshold}`, { sessionID });
|
|
25101
|
-
}
|
|
25102
|
-
} catch (error) {
|
|
25103
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch todos for auto-enable check`, {
|
|
25104
|
-
sessionID,
|
|
25105
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25106
|
-
});
|
|
25107
|
-
}
|
|
25108
|
-
}
|
|
25109
|
-
if (!state.enabled) {
|
|
25110
|
-
log(`[${HOOK_NAME}] Skipped: auto-continue not enabled`, {
|
|
25111
|
-
sessionID
|
|
25112
|
-
});
|
|
25113
|
-
return;
|
|
25114
|
-
}
|
|
25115
|
-
let hasIncompleteTodos = false;
|
|
25116
|
-
let incompleteCount = 0;
|
|
25117
|
-
try {
|
|
25118
|
-
const todos = await fetchTodos(sessionID);
|
|
25119
|
-
incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
|
|
25120
|
-
hasIncompleteTodos = incompleteCount > 0;
|
|
25121
|
-
log(`[${HOOK_NAME}] Fetched todos`, {
|
|
25122
|
-
sessionID,
|
|
25123
|
-
hasIncompleteTodos,
|
|
25124
|
-
total: todos.length
|
|
25125
|
-
});
|
|
25126
|
-
} catch (error) {
|
|
25127
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch todos`, {
|
|
25128
|
-
sessionID,
|
|
25129
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25130
|
-
});
|
|
25131
|
-
return;
|
|
25132
|
-
}
|
|
25133
|
-
if (!hasIncompleteTodos) {
|
|
25134
|
-
log(`[${HOOK_NAME}] Skipped: no incomplete todos`, { sessionID });
|
|
25135
|
-
return;
|
|
25136
|
-
}
|
|
25137
|
-
let lastAssistantIsQuestion = false;
|
|
25138
|
-
try {
|
|
25139
|
-
const messagesResult = await ctx.client.session.messages({
|
|
25140
|
-
path: { id: sessionID }
|
|
25141
|
-
});
|
|
25142
|
-
const messages = messagesResult.data;
|
|
25143
|
-
const lastAssistantMessage = messages.slice().reverse().find((m) => m.info?.role === "assistant");
|
|
25144
|
-
if (lastAssistantMessage?.parts) {
|
|
25145
|
-
const lastText = lastAssistantMessage.parts.map((p) => p.text ?? "").join(" ");
|
|
25146
|
-
lastAssistantIsQuestion = isQuestion(lastText);
|
|
25147
|
-
}
|
|
25148
|
-
log(`[${HOOK_NAME}] Fetched messages`, {
|
|
25149
|
-
sessionID,
|
|
25150
|
-
lastAssistantIsQuestion
|
|
25151
|
-
});
|
|
25152
|
-
} catch (error) {
|
|
25153
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
|
|
25154
|
-
sessionID,
|
|
25155
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25156
|
-
});
|
|
25157
|
-
return;
|
|
25158
|
-
}
|
|
25159
|
-
if (lastAssistantIsQuestion) {
|
|
25160
|
-
log(`[${HOOK_NAME}] Skipped: last message is question`, {
|
|
25161
|
-
sessionID
|
|
25162
|
-
});
|
|
25163
|
-
return;
|
|
25164
|
-
}
|
|
25165
|
-
if (state.consecutiveContinuations >= maxContinuations) {
|
|
25166
|
-
log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
|
|
25167
|
-
sessionID,
|
|
25168
|
-
consecutive: state.consecutiveContinuations,
|
|
25169
|
-
max: maxContinuations
|
|
25170
|
-
});
|
|
25171
|
-
return;
|
|
25172
|
-
}
|
|
25173
|
-
const now = Date.now();
|
|
25174
|
-
if (now < state.suppressUntil) {
|
|
25175
|
-
log(`[${HOOK_NAME}] Skipped: in suppress window`, {
|
|
25176
|
-
sessionID,
|
|
25177
|
-
suppressUntil: state.suppressUntil
|
|
25178
|
-
});
|
|
25179
|
-
return;
|
|
25180
|
-
}
|
|
25181
|
-
if (state.pendingTimer !== null || state.isAutoInjecting) {
|
|
25182
|
-
log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
|
|
25183
|
-
sessionID
|
|
25184
|
-
});
|
|
25185
|
-
return;
|
|
25186
|
-
}
|
|
25187
|
-
log(`[${HOOK_NAME}] Scheduling continuation`, {
|
|
25188
|
-
sessionID,
|
|
25189
|
-
delayMs: cooldownMs
|
|
25190
|
-
});
|
|
25191
|
-
markNotificationStarted(sessionID);
|
|
25192
|
-
ctx.client.session.prompt({
|
|
25193
|
-
path: { id: sessionID },
|
|
25194
|
-
body: {
|
|
25195
|
-
noReply: true,
|
|
25196
|
-
parts: [
|
|
25197
|
-
{
|
|
25198
|
-
type: "text",
|
|
25199
|
-
text: [
|
|
25200
|
-
`⎔ Auto-continue: ${incompleteCount} incomplete todos remaining — resuming in ${cooldownMs / 1000}s — Esc×2 to cancel`,
|
|
25201
|
-
"",
|
|
25202
|
-
"[system status: continue without acknowledging this notification]"
|
|
25203
|
-
].join(`
|
|
25204
|
-
`)
|
|
25205
|
-
}
|
|
25206
|
-
]
|
|
25207
|
-
}
|
|
25208
|
-
}).catch(() => {}).finally(() => {
|
|
25209
|
-
markNotificationFinished(sessionID);
|
|
25210
|
-
});
|
|
25211
|
-
state.pendingTimerSessionId = sessionID;
|
|
25212
|
-
state.pendingTimer = setTimeout(async () => {
|
|
25213
|
-
state.pendingTimer = null;
|
|
25214
|
-
state.pendingTimerSessionId = null;
|
|
25215
|
-
clearNotificationState(sessionID);
|
|
25216
|
-
if (!state.enabled) {
|
|
25217
|
-
log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
|
|
25218
|
-
sessionID
|
|
25219
|
-
});
|
|
25220
|
-
return;
|
|
25221
|
-
}
|
|
25222
|
-
state.isAutoInjecting = true;
|
|
25223
|
-
try {
|
|
25224
|
-
await ctx.client.session.prompt({
|
|
25225
|
-
path: { id: sessionID },
|
|
25226
|
-
body: {
|
|
25227
|
-
parts: [
|
|
25228
|
-
createInternalAgentTextPart(continuationPrompt(sessionID))
|
|
25229
|
-
]
|
|
25230
|
-
}
|
|
25231
|
-
});
|
|
25232
|
-
state.consecutiveContinuations++;
|
|
25233
|
-
log(`[${HOOK_NAME}] Continuation injected`, {
|
|
25234
|
-
sessionID,
|
|
25235
|
-
consecutive: state.consecutiveContinuations
|
|
25236
|
-
});
|
|
25237
|
-
} catch (error) {
|
|
25238
|
-
log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
|
|
25239
|
-
sessionID,
|
|
25240
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25241
|
-
});
|
|
25242
|
-
} finally {
|
|
25243
|
-
state.isAutoInjecting = false;
|
|
25244
|
-
}
|
|
25245
|
-
}, cooldownMs);
|
|
25246
|
-
} else if (event.type === "session.status") {
|
|
25247
|
-
const status = properties.status;
|
|
25248
|
-
const sessionID = properties.sessionID;
|
|
25249
|
-
if (status?.type === "busy") {
|
|
25250
|
-
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
25251
|
-
const isNotification = isNotificationBusy(sessionID);
|
|
25252
|
-
if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
|
|
25253
|
-
cancelPendingTimer(state);
|
|
25254
|
-
}
|
|
25255
|
-
if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
|
|
25256
|
-
state.consecutiveContinuations = 0;
|
|
25257
|
-
log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
|
|
25258
|
-
sessionID
|
|
25259
|
-
});
|
|
25260
|
-
}
|
|
25261
|
-
}
|
|
25262
|
-
} else if (event.type === "session.error") {
|
|
25263
|
-
const error = properties.error;
|
|
25264
|
-
const sessionID = properties.sessionID;
|
|
25265
|
-
const errorName = error?.name;
|
|
25266
|
-
const isOrchestrator = isOrchestratorSession(sessionID);
|
|
25267
|
-
if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
|
|
25268
|
-
state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
|
|
25269
|
-
log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
|
|
25270
|
-
sessionID,
|
|
25271
|
-
errorName
|
|
25272
|
-
});
|
|
25273
|
-
}
|
|
25274
|
-
if (isOrchestrator) {
|
|
25275
|
-
cancelPendingTimer(state);
|
|
25276
|
-
log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
|
|
25277
|
-
sessionID
|
|
25278
|
-
});
|
|
25279
|
-
}
|
|
25280
|
-
} else if (event.type === "session.deleted") {
|
|
25281
|
-
const deletedSessionId = properties.info?.id ?? properties.sessionID;
|
|
25282
|
-
if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
|
|
25283
|
-
requestSignatureBySession.delete(deletedSessionId);
|
|
25284
|
-
if (state.pendingTimerSessionId === deletedSessionId) {
|
|
25285
|
-
cancelPendingTimer(state);
|
|
25286
|
-
log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
|
|
25287
|
-
sessionID: deletedSessionId
|
|
25288
|
-
});
|
|
25289
|
-
}
|
|
25290
|
-
state.orchestratorSessionIds.delete(deletedSessionId);
|
|
25291
|
-
clearNotificationState(deletedSessionId);
|
|
25292
|
-
if (state.orchestratorSessionIds.size === 0) {
|
|
25293
|
-
resetState(state);
|
|
25294
|
-
state.sawChatMessage = false;
|
|
25295
|
-
}
|
|
25296
|
-
log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
|
|
25297
|
-
sessionID: deletedSessionId
|
|
25298
|
-
});
|
|
25299
|
-
}
|
|
25300
|
-
}
|
|
25301
|
-
}
|
|
25302
|
-
async function handleCommandExecuteBefore(input, output) {
|
|
25303
|
-
if (input.command !== COMMAND_NAME2) {
|
|
25304
|
-
return;
|
|
25305
|
-
}
|
|
25306
|
-
registerOrchestratorSession(input.sessionID);
|
|
25307
|
-
output.parts.length = 0;
|
|
25308
|
-
const arg = input.arguments.trim().toLowerCase();
|
|
25309
|
-
let newEnabled;
|
|
25310
|
-
if (arg === "on") {
|
|
25311
|
-
newEnabled = true;
|
|
25312
|
-
} else if (arg === "off") {
|
|
25313
|
-
newEnabled = false;
|
|
25314
|
-
} else {
|
|
25315
|
-
newEnabled = !state.enabled;
|
|
25316
|
-
}
|
|
25317
|
-
state.enabled = newEnabled;
|
|
25318
|
-
state.consecutiveContinuations = 0;
|
|
25319
|
-
if (!newEnabled) {
|
|
25320
|
-
cancelPendingTimer(state);
|
|
25321
|
-
output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
|
|
25322
|
-
log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME2} command`);
|
|
25323
|
-
return;
|
|
25324
|
-
}
|
|
25325
|
-
state.suppressUntil = 0;
|
|
25326
|
-
log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME2} command`, {
|
|
25327
|
-
maxContinuations
|
|
25328
|
-
});
|
|
25329
|
-
let hasIncompleteTodos = false;
|
|
25330
|
-
try {
|
|
25331
|
-
const todos = await fetchTodos(input.sessionID);
|
|
25332
|
-
hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
|
|
25333
|
-
} catch (error) {
|
|
25334
|
-
log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
|
|
25335
|
-
sessionID: input.sessionID,
|
|
25336
|
-
error: error instanceof Error ? error.message : String(error)
|
|
25337
|
-
});
|
|
25338
|
-
}
|
|
25339
|
-
if (hasIncompleteTodos) {
|
|
25340
|
-
output.parts.push(createInternalAgentTextPart(`${continuationPrompt(input.sessionID)} [Auto-continue enabled: up to ${maxContinuations} continuations.]`));
|
|
25341
|
-
} else {
|
|
25342
|
-
output.parts.push(createInternalAgentTextPart(`[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`));
|
|
25343
|
-
}
|
|
25344
|
-
}
|
|
25345
|
-
return {
|
|
25346
|
-
tool: { auto_continue: autoContinue },
|
|
25347
|
-
handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
|
|
25348
|
-
handleMessagesTransform,
|
|
25349
|
-
handleEvent,
|
|
25350
|
-
handleChatMessage,
|
|
25351
|
-
handleCommandExecuteBefore
|
|
25352
|
-
};
|
|
25353
|
-
}
|
|
25354
24578
|
// src/interview/manager.ts
|
|
25355
24579
|
import path13 from "node:path";
|
|
25356
24580
|
|
|
@@ -28338,7 +27562,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
28338
27562
|
}
|
|
28339
27563
|
|
|
28340
27564
|
// src/interview/service.ts
|
|
28341
|
-
var
|
|
27565
|
+
var COMMAND_NAME2 = "interview";
|
|
28342
27566
|
var DEFAULT_MAX_QUESTIONS = 2;
|
|
28343
27567
|
function isTruthyEnvFlag(value) {
|
|
28344
27568
|
if (!value) {
|
|
@@ -28585,11 +27809,11 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28585
27809
|
}
|
|
28586
27810
|
function registerCommand(opencodeConfig) {
|
|
28587
27811
|
const configCommand = opencodeConfig.command;
|
|
28588
|
-
if (!configCommand?.[
|
|
27812
|
+
if (!configCommand?.[COMMAND_NAME2]) {
|
|
28589
27813
|
if (!opencodeConfig.command) {
|
|
28590
27814
|
opencodeConfig.command = {};
|
|
28591
27815
|
}
|
|
28592
|
-
opencodeConfig.command[
|
|
27816
|
+
opencodeConfig.command[COMMAND_NAME2] = {
|
|
28593
27817
|
template: "Start an interview and write a live markdown spec",
|
|
28594
27818
|
description: "Open a localhost interview UI linked to the current OpenCode session"
|
|
28595
27819
|
};
|
|
@@ -28663,7 +27887,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28663
27887
|
}
|
|
28664
27888
|
}
|
|
28665
27889
|
async function handleCommandExecuteBefore(input, output) {
|
|
28666
|
-
if (input.command !==
|
|
27890
|
+
if (input.command !== COMMAND_NAME2) {
|
|
28667
27891
|
return;
|
|
28668
27892
|
}
|
|
28669
27893
|
const idea = input.arguments.trim();
|
|
@@ -30320,7 +29544,7 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
|
|
|
30320
29544
|
return false;
|
|
30321
29545
|
}
|
|
30322
29546
|
// src/tools/ast-grep/tools.ts
|
|
30323
|
-
import { tool
|
|
29547
|
+
import { tool } from "@opencode-ai/plugin";
|
|
30324
29548
|
|
|
30325
29549
|
// src/tools/ast-grep/cli.ts
|
|
30326
29550
|
import { existsSync as existsSync9 } from "node:fs";
|
|
@@ -30799,14 +30023,14 @@ function showOutputToUser(context, output) {
|
|
|
30799
30023
|
const ctx = context;
|
|
30800
30024
|
ctx.metadata?.({ metadata: { output } });
|
|
30801
30025
|
}
|
|
30802
|
-
var ast_grep_search =
|
|
30026
|
+
var ast_grep_search = tool({
|
|
30803
30027
|
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($$$)'",
|
|
30804
30028
|
args: {
|
|
30805
|
-
pattern:
|
|
30806
|
-
lang:
|
|
30807
|
-
paths:
|
|
30808
|
-
globs:
|
|
30809
|
-
context:
|
|
30029
|
+
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
|
|
30030
|
+
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
|
30031
|
+
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
|
|
30032
|
+
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
|
30033
|
+
context: tool.schema.number().optional().describe("Context lines around match")
|
|
30810
30034
|
},
|
|
30811
30035
|
execute: async (args, context) => {
|
|
30812
30036
|
try {
|
|
@@ -30835,15 +30059,15 @@ ${hint}`;
|
|
|
30835
30059
|
}
|
|
30836
30060
|
}
|
|
30837
30061
|
});
|
|
30838
|
-
var ast_grep_replace =
|
|
30062
|
+
var ast_grep_replace = tool({
|
|
30839
30063
|
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)'",
|
|
30840
30064
|
args: {
|
|
30841
|
-
pattern:
|
|
30842
|
-
rewrite:
|
|
30843
|
-
lang:
|
|
30844
|
-
paths:
|
|
30845
|
-
globs:
|
|
30846
|
-
dryRun:
|
|
30065
|
+
pattern: tool.schema.string().describe("AST pattern to match"),
|
|
30066
|
+
rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
|
30067
|
+
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
|
30068
|
+
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
|
|
30069
|
+
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
|
|
30070
|
+
dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)")
|
|
30847
30071
|
},
|
|
30848
30072
|
execute: async (args, context) => {
|
|
30849
30073
|
try {
|
|
@@ -30867,14 +30091,14 @@ var ast_grep_replace = tool2({
|
|
|
30867
30091
|
});
|
|
30868
30092
|
// src/tools/cancel-task.ts
|
|
30869
30093
|
import {
|
|
30870
|
-
tool as
|
|
30094
|
+
tool as tool2
|
|
30871
30095
|
} from "@opencode-ai/plugin";
|
|
30872
|
-
var z4 =
|
|
30096
|
+
var z4 = tool2.schema;
|
|
30873
30097
|
|
|
30874
30098
|
class SessionStillRunningError extends Error {
|
|
30875
30099
|
}
|
|
30876
30100
|
function createCancelTaskTool(options) {
|
|
30877
|
-
const cancel_task =
|
|
30101
|
+
const cancel_task = tool2({
|
|
30878
30102
|
description: `Cancel a tracked background specialist task.
|
|
30879
30103
|
|
|
30880
30104
|
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.`,
|
|
@@ -31264,9 +30488,9 @@ function unknownTaskOutput(taskID, message) {
|
|
|
31264
30488
|
}
|
|
31265
30489
|
// src/tools/council.ts
|
|
31266
30490
|
import {
|
|
31267
|
-
tool as
|
|
30491
|
+
tool as tool3
|
|
31268
30492
|
} from "@opencode-ai/plugin";
|
|
31269
|
-
var z5 =
|
|
30493
|
+
var z5 = tool3.schema;
|
|
31270
30494
|
function formatModelComposition(councillorResults) {
|
|
31271
30495
|
return councillorResults.map((cr) => {
|
|
31272
30496
|
const shortModel = shortModelLabel(cr.model);
|
|
@@ -31274,7 +30498,7 @@ function formatModelComposition(councillorResults) {
|
|
|
31274
30498
|
}).join(", ");
|
|
31275
30499
|
}
|
|
31276
30500
|
function createCouncilTool(_ctx, councilManager) {
|
|
31277
|
-
const council_session =
|
|
30501
|
+
const council_session = tool3({
|
|
31278
30502
|
description: `Launch a multi-LLM council session for consensus-based analysis.
|
|
31279
30503
|
|
|
31280
30504
|
Sends the prompt to multiple models (councillors) in parallel and returns their formatted responses for you to synthesize.
|
|
@@ -31328,6 +30552,9 @@ Returns the councillor responses with a summary footer.`,
|
|
|
31328
30552
|
});
|
|
31329
30553
|
return { council_session };
|
|
31330
30554
|
}
|
|
30555
|
+
// src/tools/preset-manager.ts
|
|
30556
|
+
import * as fs10 from "node:fs";
|
|
30557
|
+
|
|
31331
30558
|
// src/tui-state.ts
|
|
31332
30559
|
import * as fs9 from "node:fs";
|
|
31333
30560
|
import * as os5 from "node:os";
|
|
@@ -31397,11 +30624,11 @@ function recordTuiAgentModel(input) {
|
|
|
31397
30624
|
}
|
|
31398
30625
|
|
|
31399
30626
|
// src/tools/preset-manager.ts
|
|
31400
|
-
var
|
|
30627
|
+
var COMMAND_NAME3 = "preset";
|
|
31401
30628
|
function createPresetManager(ctx, config) {
|
|
31402
30629
|
let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
|
|
31403
30630
|
async function handleCommandExecuteBefore(input, output) {
|
|
31404
|
-
if (input.command !==
|
|
30631
|
+
if (input.command !== COMMAND_NAME3) {
|
|
31405
30632
|
return;
|
|
31406
30633
|
}
|
|
31407
30634
|
output.parts.length = 0;
|
|
@@ -31420,11 +30647,11 @@ function createPresetManager(ctx, config) {
|
|
|
31420
30647
|
}
|
|
31421
30648
|
function registerCommand(opencodeConfig) {
|
|
31422
30649
|
const configCommand = opencodeConfig.command;
|
|
31423
|
-
if (!configCommand?.[
|
|
30650
|
+
if (!configCommand?.[COMMAND_NAME3]) {
|
|
31424
30651
|
if (!opencodeConfig.command) {
|
|
31425
30652
|
opencodeConfig.command = {};
|
|
31426
30653
|
}
|
|
31427
|
-
opencodeConfig.command[
|
|
30654
|
+
opencodeConfig.command[COMMAND_NAME3] = {
|
|
31428
30655
|
template: "List available presets and switch between them",
|
|
31429
30656
|
description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
|
|
31430
30657
|
};
|
|
@@ -31446,64 +30673,47 @@ function createPresetManager(ctx, config) {
|
|
|
31446
30673
|
agentUpdates[resolvedName] = agentConfig;
|
|
31447
30674
|
}
|
|
31448
30675
|
}
|
|
31449
|
-
const currentRuntimePreset = getActiveRuntimePreset();
|
|
31450
|
-
const resetUpdates = {};
|
|
31451
|
-
if (currentRuntimePreset && config.presets?.[currentRuntimePreset]) {
|
|
31452
|
-
const oldPreset = config.presets[currentRuntimePreset];
|
|
31453
|
-
for (const rawName of Object.keys(oldPreset)) {
|
|
31454
|
-
const resolvedOld = AGENT_ALIASES[rawName] ?? rawName;
|
|
31455
|
-
if (resolvedOld in agentUpdates)
|
|
31456
|
-
continue;
|
|
31457
|
-
const baseline = config.agents?.[resolvedOld];
|
|
31458
|
-
if (baseline) {
|
|
31459
|
-
resetUpdates[resolvedOld] = mapOverrideToAgentConfig(baseline);
|
|
31460
|
-
}
|
|
31461
|
-
}
|
|
31462
|
-
}
|
|
31463
30676
|
const hasAgentUpdates = Object.keys(agentUpdates).length > 0;
|
|
31464
|
-
const allUpdates = { ...resetUpdates, ...agentUpdates };
|
|
31465
30677
|
if (!hasAgentUpdates) {
|
|
31466
30678
|
output.parts.push(createInternalAgentTextPart(`Preset "${presetName}" is empty (no agent overrides defined).`));
|
|
31467
30679
|
return;
|
|
31468
30680
|
}
|
|
31469
|
-
const previousPreset = activePreset;
|
|
31470
30681
|
setActiveRuntimePresetWithPrevious(presetName);
|
|
31471
30682
|
try {
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
31479
|
-
|
|
31480
|
-
|
|
31481
|
-
|
|
31482
|
-
|
|
31483
|
-
|
|
31484
|
-
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
31495
|
-
|
|
31496
|
-
|
|
31497
|
-
if (
|
|
31498
|
-
|
|
31499
|
-
|
|
31500
|
-
|
|
30683
|
+
const { userConfigPath } = findPluginConfigPaths(ctx.directory);
|
|
30684
|
+
if (userConfigPath) {
|
|
30685
|
+
const raw = fs10.readFileSync(userConfigPath, "utf-8");
|
|
30686
|
+
const persisted = JSON.parse(stripJsonComments(raw));
|
|
30687
|
+
persisted.preset = presetName;
|
|
30688
|
+
fs10.writeFileSync(userConfigPath, `${JSON.stringify(persisted, null, 2)}
|
|
30689
|
+
`);
|
|
30690
|
+
}
|
|
30691
|
+
} catch {}
|
|
30692
|
+
const snapshot = readTuiSnapshot();
|
|
30693
|
+
const agentModels = { ...snapshot.agentModels };
|
|
30694
|
+
for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
|
|
30695
|
+
if (typeof agentConfig.model === "string") {
|
|
30696
|
+
agentModels[agentName] = agentConfig.model;
|
|
30697
|
+
}
|
|
30698
|
+
}
|
|
30699
|
+
recordTuiAgentModels({ agentModels });
|
|
30700
|
+
activePreset = presetName;
|
|
30701
|
+
const summaryParts = [];
|
|
30702
|
+
for (const [name, cfg] of Object.entries(agentUpdates)) {
|
|
30703
|
+
const parts = [name];
|
|
30704
|
+
if (cfg.model)
|
|
30705
|
+
parts.push(`model: ${cfg.model}`);
|
|
30706
|
+
if (cfg.variant)
|
|
30707
|
+
parts.push(`variant: ${cfg.variant}`);
|
|
30708
|
+
if (cfg.temperature !== undefined)
|
|
30709
|
+
parts.push(`temp: ${cfg.temperature}`);
|
|
30710
|
+
if (cfg.options)
|
|
30711
|
+
parts.push("options: yes");
|
|
30712
|
+
summaryParts.push(parts.join(" → "));
|
|
30713
|
+
}
|
|
30714
|
+
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.
|
|
31501
30715
|
${summaryParts.join(`
|
|
31502
30716
|
`)}`));
|
|
31503
|
-
} catch (err) {
|
|
31504
|
-
rollbackRuntimePreset(previousPreset);
|
|
31505
|
-
output.parts.push(createInternalAgentTextPart(`Failed to switch preset "${presetName}": ${String(err)}`));
|
|
31506
|
-
}
|
|
31507
30717
|
}
|
|
31508
30718
|
function mapOverrideToAgentConfig(override) {
|
|
31509
30719
|
const agentConfig = {};
|
|
@@ -31593,7 +30803,7 @@ var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs p
|
|
|
31593
30803
|
import os6 from "node:os";
|
|
31594
30804
|
import path18 from "node:path";
|
|
31595
30805
|
import {
|
|
31596
|
-
tool as
|
|
30806
|
+
tool as tool4
|
|
31597
30807
|
} from "@opencode-ai/plugin";
|
|
31598
30808
|
|
|
31599
30809
|
// src/tools/smartfetch/binary.ts
|
|
@@ -33375,10 +32585,10 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
33375
32585
|
}
|
|
33376
32586
|
|
|
33377
32587
|
// src/tools/smartfetch/tool.ts
|
|
33378
|
-
var z6 =
|
|
32588
|
+
var z6 = tool4.schema;
|
|
33379
32589
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
33380
32590
|
const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
|
|
33381
|
-
return
|
|
32591
|
+
return tool4({
|
|
33382
32592
|
description: WEBFETCH_DESCRIPTION,
|
|
33383
32593
|
args: {
|
|
33384
32594
|
url: z6.httpUrl(),
|
|
@@ -33969,7 +33179,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33969
33179
|
let applyPatchHook;
|
|
33970
33180
|
let jsonErrorRecoveryHook;
|
|
33971
33181
|
let foregroundFallback;
|
|
33972
|
-
let todoContinuationHook;
|
|
33973
33182
|
let deepworkCommandHook;
|
|
33974
33183
|
let taskSessionManagerHook;
|
|
33975
33184
|
let backgroundJobBoard;
|
|
@@ -34062,13 +33271,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34062
33271
|
applyPatchHook = createApplyPatchHook(ctx);
|
|
34063
33272
|
jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
34064
33273
|
foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
|
|
34065
|
-
todoContinuationHook = createTodoContinuationHook(ctx, {
|
|
34066
|
-
maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
|
|
34067
|
-
cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
|
|
34068
|
-
autoEnable: config.todoContinuation?.autoEnable ?? false,
|
|
34069
|
-
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
|
|
34070
|
-
backgroundJobBoard
|
|
34071
|
-
});
|
|
34072
33274
|
deepworkCommandHook = createDeepworkCommandHook();
|
|
34073
33275
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
34074
33276
|
maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
|
|
@@ -34085,7 +33287,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34085
33287
|
backgroundJobBoard,
|
|
34086
33288
|
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
34087
33289
|
});
|
|
34088
|
-
toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length +
|
|
33290
|
+
toolCount = Object.keys(councilTools).length + Object.keys(cancelTaskTools).length + 1 + 2;
|
|
34089
33291
|
} catch (err) {
|
|
34090
33292
|
log("[plugin] FATAL: init failed", String(err));
|
|
34091
33293
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -34129,7 +33331,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34129
33331
|
...councilTools,
|
|
34130
33332
|
...cancelTaskTools,
|
|
34131
33333
|
webfetch,
|
|
34132
|
-
...todoContinuationHook.tool,
|
|
34133
33334
|
ast_grep_search,
|
|
34134
33335
|
ast_grep_replace
|
|
34135
33336
|
},
|
|
@@ -34316,16 +33517,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34316
33517
|
}
|
|
34317
33518
|
agentConfigEntry.permission = agentPermission;
|
|
34318
33519
|
}
|
|
34319
|
-
const configCommand = opencodeConfig.command;
|
|
34320
|
-
if (!configCommand?.["auto-continue"]) {
|
|
34321
|
-
if (!opencodeConfig.command) {
|
|
34322
|
-
opencodeConfig.command = {};
|
|
34323
|
-
}
|
|
34324
|
-
opencodeConfig.command["auto-continue"] = {
|
|
34325
|
-
template: "Call the auto_continue tool with enabled=true",
|
|
34326
|
-
description: "Enable auto-continuation — orchestrator keeps working through incomplete todos"
|
|
34327
|
-
};
|
|
34328
|
-
}
|
|
34329
33520
|
interviewManager.registerCommand(opencodeConfig);
|
|
34330
33521
|
deepworkCommandHook.registerCommand(opencodeConfig);
|
|
34331
33522
|
presetManager.registerCommand(opencodeConfig);
|
|
@@ -34352,7 +33543,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34352
33543
|
await multiplexerSessionManager.onSessionStatus(event);
|
|
34353
33544
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
34354
33545
|
await foregroundFallback.handleEvent(input.event);
|
|
34355
|
-
await todoContinuationHook.handleEvent(input);
|
|
34356
33546
|
await autoUpdateChecker.event(input);
|
|
34357
33547
|
await interviewManager.handleEvent(input);
|
|
34358
33548
|
await taskSessionManagerHook.event(input);
|
|
@@ -34410,7 +33600,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34410
33600
|
}
|
|
34411
33601
|
},
|
|
34412
33602
|
"command.execute.before": async (input, output) => {
|
|
34413
|
-
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
34414
33603
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
34415
33604
|
await presetManager.handleCommandExecuteBefore(input, output);
|
|
34416
33605
|
await deepworkCommandHook.handleCommandExecuteBefore(input, output);
|
|
@@ -34425,10 +33614,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34425
33614
|
if (agent) {
|
|
34426
33615
|
sessionAgentMap.set(input.sessionID, agent);
|
|
34427
33616
|
}
|
|
34428
|
-
todoContinuationHook.handleChatMessage({
|
|
34429
|
-
sessionID: input.sessionID,
|
|
34430
|
-
agent
|
|
34431
|
-
});
|
|
34432
33617
|
},
|
|
34433
33618
|
"experimental.chat.system.transform": async (input, output) => {
|
|
34434
33619
|
const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
|
|
@@ -34463,9 +33648,6 @@ ${output.system[0]}` : "");
|
|
|
34463
33648
|
disabledAgents,
|
|
34464
33649
|
log
|
|
34465
33650
|
});
|
|
34466
|
-
await todoContinuationHook.handleMessagesTransform({
|
|
34467
|
-
messages: typedOutput.messages
|
|
34468
|
-
});
|
|
34469
33651
|
await taskSessionManagerHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
34470
33652
|
await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
34471
33653
|
await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
@@ -34487,7 +33669,6 @@ ${output.system[0]}` : "");
|
|
|
34487
33669
|
};
|
|
34488
33670
|
await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
|
|
34489
33671
|
await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
|
|
34490
|
-
await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
|
|
34491
33672
|
await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
|
|
34492
33673
|
await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
|
|
34493
33674
|
if (input.tool.toLowerCase() === "task") {
|