@prevalentware/opencode-goal-plugin 0.1.47 → 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 +45 -13
- 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;
|
|
@@ -1647,6 +1649,12 @@ function toolOutputFailed(output) {
|
|
|
1647
1649
|
return true;
|
|
1648
1650
|
return false;
|
|
1649
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
|
+
}
|
|
1650
1658
|
function sessionIDFromEvent(event) {
|
|
1651
1659
|
const direct = event.properties?.sessionID;
|
|
1652
1660
|
if (typeof direct === "string")
|
|
@@ -1772,13 +1780,17 @@ class TaskTracker {
|
|
|
1772
1780
|
if (marker)
|
|
1773
1781
|
this.observeAssistant(sessionID, marker);
|
|
1774
1782
|
}
|
|
1775
|
-
hasBlockingTasks(parentSessionID) {
|
|
1783
|
+
hasBlockingTasks(parentSessionID, maxBlockMs = null) {
|
|
1776
1784
|
this.pruneExpiredSnapshotIdleHolds();
|
|
1785
|
+
const now = Date.now();
|
|
1777
1786
|
for (const task of this.tasks.values()) {
|
|
1778
1787
|
if (task.parentSessionID !== parentSessionID)
|
|
1779
1788
|
continue;
|
|
1780
|
-
if (task.state
|
|
1781
|
-
|
|
1789
|
+
if (task.state !== "running" && !task.terminalUnreconciled)
|
|
1790
|
+
continue;
|
|
1791
|
+
if (taskBlockExpired(task, maxBlockMs, now))
|
|
1792
|
+
continue;
|
|
1793
|
+
return true;
|
|
1782
1794
|
}
|
|
1783
1795
|
for (const hold of this.snapshotIdleHolds.values()) {
|
|
1784
1796
|
if (hold.parentSessionID === parentSessionID)
|
|
@@ -1839,6 +1851,7 @@ class TaskTracker {
|
|
|
1839
1851
|
parentSessionID,
|
|
1840
1852
|
state: "running",
|
|
1841
1853
|
terminalUnreconciled: false,
|
|
1854
|
+
runningSince: existing?.state === "running" ? existing.runningSince ?? Date.now() : Date.now(),
|
|
1842
1855
|
terminalAt: null,
|
|
1843
1856
|
lastAssistantMessageIDAtTerminal: existing?.lastAssistantMessageIDAtTerminal ?? null
|
|
1844
1857
|
});
|
|
@@ -1854,13 +1867,15 @@ class TaskTracker {
|
|
|
1854
1867
|
if (existing && TASK_TERMINAL_STATES.has(existing.state) && !existing.terminalUnreconciled && !options.resetReconciled) {
|
|
1855
1868
|
return;
|
|
1856
1869
|
}
|
|
1870
|
+
const continuesExistingTerminal = existing != null && TASK_TERMINAL_STATES.has(existing.state) && existing.state === state && existing.terminalUnreconciled && !options.resetReconciled;
|
|
1857
1871
|
this.tasks.set(taskID, {
|
|
1858
1872
|
taskID,
|
|
1859
1873
|
parentSessionID: resolvedParentSessionID,
|
|
1860
1874
|
state,
|
|
1861
1875
|
terminalUnreconciled: true,
|
|
1862
|
-
|
|
1863
|
-
|
|
1876
|
+
runningSince: null,
|
|
1877
|
+
terminalAt: continuesExistingTerminal ? existing.terminalAt ?? Date.now() : Date.now(),
|
|
1878
|
+
lastAssistantMessageIDAtTerminal: continuesExistingTerminal ? existing.lastAssistantMessageIDAtTerminal : this.latestAssistantBySession.get(resolvedParentSessionID)?.id ?? null
|
|
1864
1879
|
});
|
|
1865
1880
|
}
|
|
1866
1881
|
markSnapshotIdle(parentSessionID, taskID) {
|
|
@@ -2013,6 +2028,13 @@ async function createGoalFromTool(input, context, services) {
|
|
|
2013
2028
|
function isClosedGoal(goal) {
|
|
2014
2029
|
return goal.status === "complete" || goal.status === "unmet";
|
|
2015
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
|
+
}
|
|
2016
2038
|
function existingGoalResult(goal, requestedObjective, planningOnly) {
|
|
2017
2039
|
const reused = goal.objective === requestedObjective;
|
|
2018
2040
|
return JSON.stringify({
|
|
@@ -2100,6 +2122,7 @@ var server = async ({ client }, options) => {
|
|
|
2100
2122
|
const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
2101
2123
|
const minInterval = nonNegativeIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
2102
2124
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
|
|
2125
|
+
const maxTaskBlockMs = timeoutMillisecondsFromSeconds(options?.max_task_block_seconds ?? DEFAULT_MAX_TASK_BLOCK_SECONDS);
|
|
2103
2126
|
const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
2104
2127
|
const registerCommand = options?.register_command ?? true;
|
|
2105
2128
|
const commandName = commandNameFromOptions(options);
|
|
@@ -2132,7 +2155,7 @@ var server = async ({ client }, options) => {
|
|
|
2132
2155
|
return false;
|
|
2133
2156
|
await taskTracker.refreshLiveChildren(client, sessionID);
|
|
2134
2157
|
return {
|
|
2135
|
-
blocked: taskTracker.hasBlockingTasks(sessionID),
|
|
2158
|
+
blocked: taskTracker.hasBlockingTasks(sessionID, maxTaskBlockMs),
|
|
2136
2159
|
retryAt: taskTracker.nextSnapshotIdleRetryAt(sessionID)
|
|
2137
2160
|
};
|
|
2138
2161
|
}
|
|
@@ -2270,10 +2293,14 @@ var server = async ({ client }, options) => {
|
|
|
2270
2293
|
taskTracker.observeAssistantMessage(sessionID, latestAssistant);
|
|
2271
2294
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
2272
2295
|
if (taskStatus && taskStatus.blocked) {
|
|
2273
|
-
|
|
2274
|
-
if (
|
|
2275
|
-
|
|
2296
|
+
const deferralGoal = await getGoalInternal(sessionID);
|
|
2297
|
+
if (!taskDeferralGoalContinuable(deferralGoal)) {
|
|
2298
|
+
taskDeferredSessions.delete(sessionID);
|
|
2299
|
+
cancelScheduledContinuation(sessionID);
|
|
2300
|
+
return;
|
|
2276
2301
|
}
|
|
2302
|
+
taskDeferredSessions.add(sessionID);
|
|
2303
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt != null ? taskStatus.retryAt - Date.now() : TASK_BLOCK_RETRY_MS, scheduled != null);
|
|
2277
2304
|
return;
|
|
2278
2305
|
}
|
|
2279
2306
|
if (busySessions.has(sessionID))
|
|
@@ -2674,6 +2701,7 @@ async function setupV2(context) {
|
|
|
2674
2701
|
const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
2675
2702
|
const minInterval = nonNegativeIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
2676
2703
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
|
|
2704
|
+
const maxTaskBlockMs = timeoutMillisecondsFromSeconds(options.max_task_block_seconds ?? DEFAULT_MAX_TASK_BLOCK_SECONDS);
|
|
2677
2705
|
const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
2678
2706
|
const registerCommand = options.register_command ?? true;
|
|
2679
2707
|
const commandName = commandNameFromOptions(options);
|
|
@@ -2722,7 +2750,7 @@ async function setupV2(context) {
|
|
|
2722
2750
|
if (!deferWhileTasksActive)
|
|
2723
2751
|
return false;
|
|
2724
2752
|
return {
|
|
2725
|
-
blocked: taskTracker.hasBlockingTasks(sessionID),
|
|
2753
|
+
blocked: taskTracker.hasBlockingTasks(sessionID, maxTaskBlockMs),
|
|
2726
2754
|
retryAt: taskTracker.nextSnapshotIdleRetryAt(sessionID)
|
|
2727
2755
|
};
|
|
2728
2756
|
}
|
|
@@ -2852,10 +2880,14 @@ async function setupV2(context) {
|
|
|
2852
2880
|
}
|
|
2853
2881
|
const taskStatus = taskBlockStatus(sessionID);
|
|
2854
2882
|
if (taskStatus && taskStatus.blocked) {
|
|
2855
|
-
|
|
2856
|
-
if (
|
|
2857
|
-
|
|
2883
|
+
const deferralGoal = await getGoalInternal(sessionID);
|
|
2884
|
+
if (!taskDeferralGoalContinuable(deferralGoal)) {
|
|
2885
|
+
taskDeferredSessions.delete(sessionID);
|
|
2886
|
+
cancelScheduledContinuation(sessionID);
|
|
2887
|
+
return;
|
|
2858
2888
|
}
|
|
2889
|
+
taskDeferredSessions.add(sessionID);
|
|
2890
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt != null ? taskStatus.retryAt - Date.now() : TASK_BLOCK_RETRY_MS, scheduled != null);
|
|
2859
2891
|
return;
|
|
2860
2892
|
}
|
|
2861
2893
|
if (busySessions.has(sessionID))
|
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",
|