@prevalentware/opencode-goal-plugin 0.1.46 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/server.js +60 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ In OpenCode 1, server options use the package-and-options tuple in `opencode.jso
|
|
|
122
122
|
"max_auto_turns": 25,
|
|
123
123
|
"min_continue_interval_seconds": 3,
|
|
124
124
|
"max_turn_time": 300,
|
|
125
|
+
"max_task_block_seconds": 900,
|
|
125
126
|
"max_prompt_failures": 3,
|
|
126
127
|
"default_token_budget": 200000,
|
|
127
128
|
"max_goal_duration_seconds": 1800,
|
|
@@ -157,7 +158,8 @@ In OpenCode 2, use the plugin object form instead:
|
|
|
157
158
|
Defaults:
|
|
158
159
|
|
|
159
160
|
- `auto_continue`: `true`
|
|
160
|
-
- `defer_while_tasks_active`: `true`; when enabled, goal auto-continuation waits for active OpenCode Task child sessions and their orchestrator reconciliation before sending the next goal prompt.
|
|
161
|
+
- `defer_while_tasks_active`: `true`; when enabled, goal auto-continuation waits for active OpenCode Task child sessions and their orchestrator reconciliation before sending the next goal prompt. A deferral re-checks child sessions on a short timer, so a goal deferred by a task never depends on a further idle event to resume.
|
|
162
|
+
- `max_task_block_seconds`: `900`; wall-clock ceiling on how long a single Task child session may defer goal continuation. A child that stays listed but never reports a terminal state, or a terminal child whose result is never reconciled, stops blocking once the ceiling passes. Set a smaller value for shorter subagents, `0` to remove the ceiling, or disable deferral entirely with `defer_while_tasks_active: false`.
|
|
161
163
|
- `max_auto_turns`: `25`
|
|
162
164
|
- `min_continue_interval_seconds`: `3`
|
|
163
165
|
- Fast V2 executions that finish inside this interval schedule a delayed continuation; they do not require another user message to wake up.
|
|
@@ -291,6 +293,6 @@ OpenCode plugin modules are target-specific. This package exports separate modul
|
|
|
291
293
|
}
|
|
292
294
|
```
|
|
293
295
|
|
|
294
|
-
Codex goal mode has deeper runtime integration for thread lifecycle control. This plugin implements the same workflow using OpenCode plugin hooks. Token usage is read from OpenCode step-finish usage when available and falls back to message token metadata or text estimation when exact usage is unavailable. Continuation is driven by V2 `session.execution.succeeded` events and legacy `session.idle` / `session.status` idle notifications, never by intermediate model-step completion. V2 execution starts arm busy tracking; native `session.retry.scheduled` events cancel plugin recovery while OpenCode retries. Terminal execution transport failures use bounded recovery, while interruptions (user, shutdown, or superseded) cancel local timers without starting another turn or charging a prompt failure. Interrupted or non-transport-failed executions remain suppressed until the host starts a new execution; this does not change the persisted goal status. Use `/pause_goal` for a durable pause. Each V2 plugin instance handles goal events only for its own location while still observing cross-location child task lifecycles. The optional `max_turn_time` watchdog can retry one goal continuation prompt when a model turn remains busy, without consuming the goal's auto-turn or no-progress budgets; recognized transport failures do count toward the prompt-failure ceiling. By default, continuation is deferred while OpenCode Task child sessions are active or their terminal result still needs an orchestrator turn. During compaction, the plugin disables OpenCode's generic synthetic auto-continue while an active goal exists so the goal-specific continuation prompt remains authoritative.
|
|
296
|
+
Codex goal mode has deeper runtime integration for thread lifecycle control. This plugin implements the same workflow using OpenCode plugin hooks. Token usage is read from OpenCode step-finish usage when available and falls back to message token metadata or text estimation when exact usage is unavailable. Continuation is driven by V2 `session.execution.succeeded` events and legacy `session.idle` / `session.status` idle notifications, never by intermediate model-step completion. V2 execution starts arm busy tracking; native `session.retry.scheduled` events cancel plugin recovery while OpenCode retries. Terminal execution transport failures use bounded recovery, while interruptions (user, shutdown, or superseded) cancel local timers without starting another turn or charging a prompt failure. Interrupted or non-transport-failed executions remain suppressed until the host starts a new execution; this does not change the persisted goal status. Use `/pause_goal` for a durable pause. Each V2 plugin instance handles goal events only for its own location while still observing cross-location child task lifecycles. The optional `max_turn_time` watchdog can retry one goal continuation prompt when a model turn remains busy, without consuming the goal's auto-turn or no-progress budgets; recognized transport failures do count toward the prompt-failure ceiling. By default, continuation is deferred while OpenCode Task child sessions are active or their terminal result still needs an orchestrator turn, bounded by the `max_task_block_seconds` ceiling so an unobservable child cannot stall a goal indefinitely. During compaction, the plugin disables OpenCode's generic synthetic auto-continue while an active goal exists so the goal-specific continuation prompt remains authoritative.
|
|
295
297
|
|
|
296
298
|
The goal sidebar shows the current status, elapsed time, token usage, auto-continue count, latest checkpoint, latest status message, stop reason, and objective when a goal is active, paused, or safety-limited. Closed goals remain visible briefly through the latest tool state as achieved or unmet.
|
package/dist/server.js
CHANGED
|
@@ -1254,6 +1254,8 @@ var DEFAULT_COMMAND_NAME = "goal";
|
|
|
1254
1254
|
var DEFAULT_RESTRICTED_AGENTS = ["plan"];
|
|
1255
1255
|
var TASK_SETTLE_DELAY_MS = 25;
|
|
1256
1256
|
var SNAPSHOT_IDLE_HOLD_MS = 250;
|
|
1257
|
+
var DEFAULT_MAX_TASK_BLOCK_SECONDS = 900;
|
|
1258
|
+
var TASK_BLOCK_RETRY_MS = 1000;
|
|
1257
1259
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1258
1260
|
var STALE_PENDING_MS = 30000;
|
|
1259
1261
|
var RETRY_SETTLE_MS = 25;
|
|
@@ -1353,6 +1355,9 @@ function goalCommandDefinitions(commandName) {
|
|
|
1353
1355
|
}
|
|
1354
1356
|
];
|
|
1355
1357
|
}
|
|
1358
|
+
function omitUndefined(value) {
|
|
1359
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1360
|
+
}
|
|
1356
1361
|
function commandNameFromOptions(options) {
|
|
1357
1362
|
const name = options?.command_name?.trim() || DEFAULT_COMMAND_NAME;
|
|
1358
1363
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
|
|
@@ -1644,6 +1649,12 @@ function toolOutputFailed(output) {
|
|
|
1644
1649
|
return true;
|
|
1645
1650
|
return false;
|
|
1646
1651
|
}
|
|
1652
|
+
function taskBlockExpired(task, maxBlockMs, now) {
|
|
1653
|
+
if (maxBlockMs == null)
|
|
1654
|
+
return false;
|
|
1655
|
+
const blockingSince = task.state === "running" ? task.runningSince : task.terminalAt;
|
|
1656
|
+
return blockingSince != null && now - blockingSince >= maxBlockMs;
|
|
1657
|
+
}
|
|
1647
1658
|
function sessionIDFromEvent(event) {
|
|
1648
1659
|
const direct = event.properties?.sessionID;
|
|
1649
1660
|
if (typeof direct === "string")
|
|
@@ -1769,13 +1780,17 @@ class TaskTracker {
|
|
|
1769
1780
|
if (marker)
|
|
1770
1781
|
this.observeAssistant(sessionID, marker);
|
|
1771
1782
|
}
|
|
1772
|
-
hasBlockingTasks(parentSessionID) {
|
|
1783
|
+
hasBlockingTasks(parentSessionID, maxBlockMs = null) {
|
|
1773
1784
|
this.pruneExpiredSnapshotIdleHolds();
|
|
1785
|
+
const now = Date.now();
|
|
1774
1786
|
for (const task of this.tasks.values()) {
|
|
1775
1787
|
if (task.parentSessionID !== parentSessionID)
|
|
1776
1788
|
continue;
|
|
1777
|
-
if (task.state
|
|
1778
|
-
|
|
1789
|
+
if (task.state !== "running" && !task.terminalUnreconciled)
|
|
1790
|
+
continue;
|
|
1791
|
+
if (taskBlockExpired(task, maxBlockMs, now))
|
|
1792
|
+
continue;
|
|
1793
|
+
return true;
|
|
1779
1794
|
}
|
|
1780
1795
|
for (const hold of this.snapshotIdleHolds.values()) {
|
|
1781
1796
|
if (hold.parentSessionID === parentSessionID)
|
|
@@ -1836,6 +1851,7 @@ class TaskTracker {
|
|
|
1836
1851
|
parentSessionID,
|
|
1837
1852
|
state: "running",
|
|
1838
1853
|
terminalUnreconciled: false,
|
|
1854
|
+
runningSince: existing?.state === "running" ? existing.runningSince ?? Date.now() : Date.now(),
|
|
1839
1855
|
terminalAt: null,
|
|
1840
1856
|
lastAssistantMessageIDAtTerminal: existing?.lastAssistantMessageIDAtTerminal ?? null
|
|
1841
1857
|
});
|
|
@@ -1851,13 +1867,15 @@ class TaskTracker {
|
|
|
1851
1867
|
if (existing && TASK_TERMINAL_STATES.has(existing.state) && !existing.terminalUnreconciled && !options.resetReconciled) {
|
|
1852
1868
|
return;
|
|
1853
1869
|
}
|
|
1870
|
+
const continuesExistingTerminal = existing != null && TASK_TERMINAL_STATES.has(existing.state) && existing.state === state && existing.terminalUnreconciled && !options.resetReconciled;
|
|
1854
1871
|
this.tasks.set(taskID, {
|
|
1855
1872
|
taskID,
|
|
1856
1873
|
parentSessionID: resolvedParentSessionID,
|
|
1857
1874
|
state,
|
|
1858
1875
|
terminalUnreconciled: true,
|
|
1859
|
-
|
|
1860
|
-
|
|
1876
|
+
runningSince: null,
|
|
1877
|
+
terminalAt: continuesExistingTerminal ? existing.terminalAt ?? Date.now() : Date.now(),
|
|
1878
|
+
lastAssistantMessageIDAtTerminal: continuesExistingTerminal ? existing.lastAssistantMessageIDAtTerminal : this.latestAssistantBySession.get(resolvedParentSessionID)?.id ?? null
|
|
1861
1879
|
});
|
|
1862
1880
|
}
|
|
1863
1881
|
markSnapshotIdle(parentSessionID, taskID) {
|
|
@@ -2010,6 +2028,13 @@ async function createGoalFromTool(input, context, services) {
|
|
|
2010
2028
|
function isClosedGoal(goal) {
|
|
2011
2029
|
return goal.status === "complete" || goal.status === "unmet";
|
|
2012
2030
|
}
|
|
2031
|
+
function taskDeferralGoalContinuable(goal) {
|
|
2032
|
+
if (!goal)
|
|
2033
|
+
return false;
|
|
2034
|
+
if (goal.status === "budgetLimited" || goal.status === "usageLimited")
|
|
2035
|
+
return !goal.budgetWrapupSent;
|
|
2036
|
+
return goal.status === "active";
|
|
2037
|
+
}
|
|
2013
2038
|
function existingGoalResult(goal, requestedObjective, planningOnly) {
|
|
2014
2039
|
const reused = goal.objective === requestedObjective;
|
|
2015
2040
|
return JSON.stringify({
|
|
@@ -2097,6 +2122,7 @@ var server = async ({ client }, options) => {
|
|
|
2097
2122
|
const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
2098
2123
|
const minInterval = nonNegativeIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
2099
2124
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
|
|
2125
|
+
const maxTaskBlockMs = timeoutMillisecondsFromSeconds(options?.max_task_block_seconds ?? DEFAULT_MAX_TASK_BLOCK_SECONDS);
|
|
2100
2126
|
const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
2101
2127
|
const registerCommand = options?.register_command ?? true;
|
|
2102
2128
|
const commandName = commandNameFromOptions(options);
|
|
@@ -2129,7 +2155,7 @@ var server = async ({ client }, options) => {
|
|
|
2129
2155
|
return false;
|
|
2130
2156
|
await taskTracker.refreshLiveChildren(client, sessionID);
|
|
2131
2157
|
return {
|
|
2132
|
-
blocked: taskTracker.hasBlockingTasks(sessionID),
|
|
2158
|
+
blocked: taskTracker.hasBlockingTasks(sessionID, maxTaskBlockMs),
|
|
2133
2159
|
retryAt: taskTracker.nextSnapshotIdleRetryAt(sessionID)
|
|
2134
2160
|
};
|
|
2135
2161
|
}
|
|
@@ -2267,10 +2293,14 @@ var server = async ({ client }, options) => {
|
|
|
2267
2293
|
taskTracker.observeAssistantMessage(sessionID, latestAssistant);
|
|
2268
2294
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
2269
2295
|
if (taskStatus && taskStatus.blocked) {
|
|
2270
|
-
|
|
2271
|
-
if (
|
|
2272
|
-
|
|
2296
|
+
const deferralGoal = await getGoalInternal(sessionID);
|
|
2297
|
+
if (!taskDeferralGoalContinuable(deferralGoal)) {
|
|
2298
|
+
taskDeferredSessions.delete(sessionID);
|
|
2299
|
+
cancelScheduledContinuation(sessionID);
|
|
2300
|
+
return;
|
|
2273
2301
|
}
|
|
2302
|
+
taskDeferredSessions.add(sessionID);
|
|
2303
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt != null ? taskStatus.retryAt - Date.now() : TASK_BLOCK_RETRY_MS, scheduled != null);
|
|
2274
2304
|
return;
|
|
2275
2305
|
}
|
|
2276
2306
|
if (busySessions.has(sessionID))
|
|
@@ -2671,6 +2701,7 @@ async function setupV2(context) {
|
|
|
2671
2701
|
const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
2672
2702
|
const minInterval = nonNegativeIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
2673
2703
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
|
|
2704
|
+
const maxTaskBlockMs = timeoutMillisecondsFromSeconds(options.max_task_block_seconds ?? DEFAULT_MAX_TASK_BLOCK_SECONDS);
|
|
2674
2705
|
const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
2675
2706
|
const registerCommand = options.register_command ?? true;
|
|
2676
2707
|
const commandName = commandNameFromOptions(options);
|
|
@@ -2719,7 +2750,7 @@ async function setupV2(context) {
|
|
|
2719
2750
|
if (!deferWhileTasksActive)
|
|
2720
2751
|
return false;
|
|
2721
2752
|
return {
|
|
2722
|
-
blocked: taskTracker.hasBlockingTasks(sessionID),
|
|
2753
|
+
blocked: taskTracker.hasBlockingTasks(sessionID, maxTaskBlockMs),
|
|
2723
2754
|
retryAt: taskTracker.nextSnapshotIdleRetryAt(sessionID)
|
|
2724
2755
|
};
|
|
2725
2756
|
}
|
|
@@ -2849,10 +2880,14 @@ async function setupV2(context) {
|
|
|
2849
2880
|
}
|
|
2850
2881
|
const taskStatus = taskBlockStatus(sessionID);
|
|
2851
2882
|
if (taskStatus && taskStatus.blocked) {
|
|
2852
|
-
|
|
2853
|
-
if (
|
|
2854
|
-
|
|
2883
|
+
const deferralGoal = await getGoalInternal(sessionID);
|
|
2884
|
+
if (!taskDeferralGoalContinuable(deferralGoal)) {
|
|
2885
|
+
taskDeferredSessions.delete(sessionID);
|
|
2886
|
+
cancelScheduledContinuation(sessionID);
|
|
2887
|
+
return;
|
|
2855
2888
|
}
|
|
2889
|
+
taskDeferredSessions.add(sessionID);
|
|
2890
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt != null ? taskStatus.retryAt - Date.now() : TASK_BLOCK_RETRY_MS, scheduled != null);
|
|
2856
2891
|
return;
|
|
2857
2892
|
}
|
|
2858
2893
|
if (busySessions.has(sessionID))
|
|
@@ -3256,14 +3291,19 @@ async function setupV2(context) {
|
|
|
3256
3291
|
cancelScheduledContinuation(input.sessionID);
|
|
3257
3292
|
clearTurnWatchdog(input.sessionID);
|
|
3258
3293
|
}
|
|
3259
|
-
|
|
3294
|
+
let forwardedPrompt = {};
|
|
3295
|
+
if (command.action === "goal") {
|
|
3296
|
+
const stripMention = ({ mention: _mention, ...attachment }) => attachment;
|
|
3297
|
+
const { files, agents, skills, ...promptFields } = input.prompt;
|
|
3298
|
+
forwardedPrompt = {
|
|
3299
|
+
...omitUndefined(promptFields),
|
|
3300
|
+
...files ? { files: files.map(stripMention) } : {},
|
|
3301
|
+
...agents ? { agents: agents.map(stripMention) } : {},
|
|
3302
|
+
...skills ? { skills: skills.map(stripMention) } : {}
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3260
3305
|
await context.session.prompt({
|
|
3261
|
-
...
|
|
3262
|
-
...input.prompt,
|
|
3263
|
-
files: input.prompt.files?.map(stripMention),
|
|
3264
|
-
agents: input.prompt.agents?.map(stripMention),
|
|
3265
|
-
skills: input.prompt.skills?.map(stripMention)
|
|
3266
|
-
} : {},
|
|
3306
|
+
...forwardedPrompt,
|
|
3267
3307
|
sessionID: input.sessionID,
|
|
3268
3308
|
text: command.template.replaceAll("$ARGUMENTS", () => input.prompt.text.trim()),
|
|
3269
3309
|
delivery: input.delivery
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prevalentware/opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "OpenCode goal plugin that adds Codex-style long-running goal mode, /goal commands, persistence, and TUI status for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|