@prevalentware/opencode-goal-plugin 0.1.45 → 0.1.47
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 +13 -1
- package/dist/server.js +71 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -160,6 +160,7 @@ Defaults:
|
|
|
160
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
161
|
- `max_auto_turns`: `25`
|
|
162
162
|
- `min_continue_interval_seconds`: `3`
|
|
163
|
+
- Fast V2 executions that finish inside this interval schedule a delayed continuation; they do not require another user message to wake up.
|
|
163
164
|
- `max_turn_time`: unset by default; set a positive number of seconds to retry one active-goal continuation prompt when a model turn remains busy for that long. Each new busy event resets the watchdog. Idle, built-in retry, session deletion, active Task children, and restricted agents suppress the retry. Watchdog retries are independent of `min_continue_interval_seconds` and never consume auto-turn or no-progress budgets, but recognized transport failures still count toward the `max_prompt_failures` ceiling.
|
|
164
165
|
- `max_prompt_failures`: `3`; consecutive transport or no-response continuation failures pause the goal at this ceiling. Prompt delivery alone does not reset the count; substantive assistant or tool progress, a new goal, or an explicit resume does.
|
|
165
166
|
- `default_token_budget`: unset by default; when set, new goals inherit this token budget.
|
|
@@ -253,6 +254,17 @@ bun run build
|
|
|
253
254
|
npm pack --dry-run
|
|
254
255
|
```
|
|
255
256
|
|
|
257
|
+
With `opencode2` installed, run `bun run build && bun run smoke:v2` to exercise the
|
|
258
|
+
native V2 lifecycle, not just mocked events. The smoke test uses a private server,
|
|
259
|
+
isolated home/config/database/goal state, and a deterministic local model with no
|
|
260
|
+
real provider credentials. It invokes `/goal`, requires **two** automatic
|
|
261
|
+
continuations with the default minimum interval, then verifies completion. A
|
|
262
|
+
second loaded location checks that server-wide events do not duplicate delivery.
|
|
263
|
+
The test prints its temporary artifact directory and shuts down its private
|
|
264
|
+
server. Use `OPENCODE_V2_BIN` to select another V2 binary, or run
|
|
265
|
+
`bun run smoke:v2 @prevalentware/opencode-goal-plugin@<version>` to install and
|
|
266
|
+
verify an exact published package in the isolated environment.
|
|
267
|
+
|
|
256
268
|
## Publishing
|
|
257
269
|
|
|
258
270
|
This package is set up for npm Trusted Publishing from GitHub Actions. On every push to `main`, CI runs typecheck, lint, and unit tests in parallel. If they all pass, the publish job computes the next patch version from the latest version on npm, builds the package, and runs `npm publish`.
|
|
@@ -279,6 +291,6 @@ OpenCode plugin modules are target-specific. This package exports separate modul
|
|
|
279
291
|
}
|
|
280
292
|
```
|
|
281
293
|
|
|
282
|
-
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
|
|
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.
|
|
283
295
|
|
|
284
296
|
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
|
@@ -1353,6 +1353,9 @@ function goalCommandDefinitions(commandName) {
|
|
|
1353
1353
|
}
|
|
1354
1354
|
];
|
|
1355
1355
|
}
|
|
1356
|
+
function omitUndefined(value) {
|
|
1357
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1358
|
+
}
|
|
1356
1359
|
function commandNameFromOptions(options) {
|
|
1357
1360
|
const name = options?.command_name?.trim() || DEFAULT_COMMAND_NAME;
|
|
1358
1361
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
|
|
@@ -2687,6 +2690,7 @@ async function setupV2(context) {
|
|
|
2687
2690
|
const planAgents = restrictedAgentSet(options);
|
|
2688
2691
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
2689
2692
|
const activeContinuationsV2 = new Set;
|
|
2693
|
+
const stoppedExecutions = new Set;
|
|
2690
2694
|
const latestStepBySession = new Map;
|
|
2691
2695
|
const stepTextBuffers = new Map;
|
|
2692
2696
|
const stepTokenSums = new Map;
|
|
@@ -2833,6 +2837,8 @@ async function setupV2(context) {
|
|
|
2833
2837
|
async function runAutoContinue(sessionID, fromTaskDeferral = false, scheduled) {
|
|
2834
2838
|
if (disposed)
|
|
2835
2839
|
return;
|
|
2840
|
+
if (stoppedExecutions.has(sessionID))
|
|
2841
|
+
return;
|
|
2836
2842
|
if (busySessions.has(sessionID))
|
|
2837
2843
|
return;
|
|
2838
2844
|
if (activeContinuationsV2.has(sessionID))
|
|
@@ -2913,8 +2919,13 @@ async function setupV2(context) {
|
|
|
2913
2919
|
if (nativeRetrySessions.has(sessionID))
|
|
2914
2920
|
return;
|
|
2915
2921
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
2916
|
-
if (!goal)
|
|
2922
|
+
if (!goal) {
|
|
2923
|
+
const waiting = await getGoalInternal(sessionID);
|
|
2924
|
+
if (waiting?.status === "active" && waiting.pendingAttempt == null && waiting.lastContinuationAt != null && minInterval > 0) {
|
|
2925
|
+
scheduleSettledContinuation(sessionID, continuationDelayFromSnapshot(minInterval, waiting.lastContinuationAt), scheduled != null);
|
|
2926
|
+
}
|
|
2917
2927
|
return;
|
|
2928
|
+
}
|
|
2918
2929
|
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
2919
2930
|
if (nativeRetrySessions.has(sessionID)) {
|
|
2920
2931
|
await rollbackContinuationAttempt(sessionID);
|
|
@@ -2955,6 +2966,23 @@ async function setupV2(context) {
|
|
|
2955
2966
|
async function handleV2Event(event) {
|
|
2956
2967
|
const data = event.data;
|
|
2957
2968
|
const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
|
|
2969
|
+
if (context.location && event.location && (event.location.directory !== context.location.directory || event.location.workspaceID !== context.location.workspaceID)) {
|
|
2970
|
+
if (event.type === "session.created" && sessionID && typeof data.parentID === "string") {
|
|
2971
|
+
taskTracker.observeSessionCreated({ properties: { info: { id: sessionID, parentID: data.parentID } } });
|
|
2972
|
+
} else if (sessionID) {
|
|
2973
|
+
if (event.type === "session.execution.started")
|
|
2974
|
+
taskTracker.observeSessionStatus(sessionID, "busy");
|
|
2975
|
+
if (["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.idle"].includes(event.type)) {
|
|
2976
|
+
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
2977
|
+
}
|
|
2978
|
+
if (event.type === "session.status" && isRecord(data.status) && typeof data.status.type === "string") {
|
|
2979
|
+
taskTracker.observeSessionStatus(sessionID, data.status.type);
|
|
2980
|
+
}
|
|
2981
|
+
if (event.type === "session.deleted")
|
|
2982
|
+
taskTracker.observeSessionDeleted(sessionID);
|
|
2983
|
+
}
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2958
2986
|
switch (event.type) {
|
|
2959
2987
|
case "session.created": {
|
|
2960
2988
|
const parentID = data.parentID;
|
|
@@ -2963,10 +2991,13 @@ async function setupV2(context) {
|
|
|
2963
2991
|
}
|
|
2964
2992
|
return;
|
|
2965
2993
|
}
|
|
2994
|
+
case "session.execution.started":
|
|
2995
|
+
case "session.retry.scheduled":
|
|
2966
2996
|
case "session.status": {
|
|
2967
|
-
const status = data.status;
|
|
2997
|
+
const status = event.type === "session.execution.started" ? { type: "busy" } : event.type === "session.retry.scheduled" ? { type: "retry" } : data.status;
|
|
2968
2998
|
if (sessionID && isRecord(status) && typeof status.type === "string") {
|
|
2969
2999
|
if (status.type === "busy") {
|
|
3000
|
+
stoppedExecutions.delete(sessionID);
|
|
2970
3001
|
busySessions.add(sessionID);
|
|
2971
3002
|
nativeRetrySessions.delete(sessionID);
|
|
2972
3003
|
armTurnWatchdog(sessionID);
|
|
@@ -2992,6 +3023,7 @@ async function setupV2(context) {
|
|
|
2992
3023
|
}
|
|
2993
3024
|
return;
|
|
2994
3025
|
}
|
|
3026
|
+
case "session.execution.succeeded":
|
|
2995
3027
|
case "session.idle": {
|
|
2996
3028
|
if (sessionID) {
|
|
2997
3029
|
busySessions.delete(sessionID);
|
|
@@ -3007,9 +3039,25 @@ async function setupV2(context) {
|
|
|
3007
3039
|
}
|
|
3008
3040
|
return;
|
|
3009
3041
|
}
|
|
3042
|
+
case "session.execution.interrupted": {
|
|
3043
|
+
if (!sessionID)
|
|
3044
|
+
return;
|
|
3045
|
+
stoppedExecutions.add(sessionID);
|
|
3046
|
+
busySessions.delete(sessionID);
|
|
3047
|
+
nativeRetrySessions.delete(sessionID);
|
|
3048
|
+
clearTurnWatchdog(sessionID);
|
|
3049
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
3050
|
+
cancelScheduledContinuation(sessionID);
|
|
3051
|
+
taskDeferredSessions.delete(sessionID);
|
|
3052
|
+
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
case "session.execution.failed":
|
|
3010
3056
|
case "session.error": {
|
|
3011
3057
|
if (!sessionID)
|
|
3012
3058
|
return;
|
|
3059
|
+
if (event.type === "session.execution.failed")
|
|
3060
|
+
nativeRetrySessions.delete(sessionID);
|
|
3013
3061
|
const inNativeRetry = nativeRetrySessions.has(sessionID);
|
|
3014
3062
|
busySessions.delete(sessionID);
|
|
3015
3063
|
clearTurnWatchdog(sessionID);
|
|
@@ -3018,6 +3066,13 @@ async function setupV2(context) {
|
|
|
3018
3066
|
nativeRetrySessions.delete(sessionID);
|
|
3019
3067
|
watchdogRescuedSessions.delete(sessionID);
|
|
3020
3068
|
const errorMessage = transportErrorMessageFromEvent(data);
|
|
3069
|
+
if (event.type === "session.execution.failed" && !isTransportError(errorMessage)) {
|
|
3070
|
+
stoppedExecutions.add(sessionID);
|
|
3071
|
+
cancelScheduledContinuation(sessionID);
|
|
3072
|
+
taskDeferredSessions.delete(sessionID);
|
|
3073
|
+
}
|
|
3074
|
+
if (event.type === "session.execution.failed")
|
|
3075
|
+
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
3021
3076
|
if (errorMessage && isTransportError(errorMessage)) {
|
|
3022
3077
|
const goal = await getGoalInternal(sessionID);
|
|
3023
3078
|
if (goal?.status === "active") {
|
|
@@ -3041,6 +3096,7 @@ async function setupV2(context) {
|
|
|
3041
3096
|
case "session.deleted": {
|
|
3042
3097
|
if (!sessionID)
|
|
3043
3098
|
return;
|
|
3099
|
+
stoppedExecutions.delete(sessionID);
|
|
3044
3100
|
busySessions.delete(sessionID);
|
|
3045
3101
|
clearTurnWatchdog(sessionID);
|
|
3046
3102
|
watchdogRescuedSessions.delete(sessionID);
|
|
@@ -3203,14 +3259,19 @@ async function setupV2(context) {
|
|
|
3203
3259
|
cancelScheduledContinuation(input.sessionID);
|
|
3204
3260
|
clearTurnWatchdog(input.sessionID);
|
|
3205
3261
|
}
|
|
3206
|
-
|
|
3262
|
+
let forwardedPrompt = {};
|
|
3263
|
+
if (command.action === "goal") {
|
|
3264
|
+
const stripMention = ({ mention: _mention, ...attachment }) => attachment;
|
|
3265
|
+
const { files, agents, skills, ...promptFields } = input.prompt;
|
|
3266
|
+
forwardedPrompt = {
|
|
3267
|
+
...omitUndefined(promptFields),
|
|
3268
|
+
...files ? { files: files.map(stripMention) } : {},
|
|
3269
|
+
...agents ? { agents: agents.map(stripMention) } : {},
|
|
3270
|
+
...skills ? { skills: skills.map(stripMention) } : {}
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3207
3273
|
await context.session.prompt({
|
|
3208
|
-
...
|
|
3209
|
-
...input.prompt,
|
|
3210
|
-
files: input.prompt.files?.map(stripMention),
|
|
3211
|
-
agents: input.prompt.agents?.map(stripMention),
|
|
3212
|
-
skills: input.prompt.skills?.map(stripMention)
|
|
3213
|
-
} : {},
|
|
3274
|
+
...forwardedPrompt,
|
|
3214
3275
|
sessionID: input.sessionID,
|
|
3215
3276
|
text: command.template.replaceAll("$ARGUMENTS", () => input.prompt.text.trim()),
|
|
3216
3277
|
delivery: input.delivery
|
|
@@ -3317,6 +3378,7 @@ async function setupV2(context) {
|
|
|
3317
3378
|
clearTimeout(watchdog.timer);
|
|
3318
3379
|
turnWatchdogs.clear();
|
|
3319
3380
|
activeContinuationsV2.clear();
|
|
3381
|
+
stoppedExecutions.clear();
|
|
3320
3382
|
nativeRetrySessions.clear();
|
|
3321
3383
|
locallyDeliveredPendingSessions.clear();
|
|
3322
3384
|
watchdogRescuedSessions.clear();
|
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.47",
|
|
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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"lint": "eslint .",
|
|
50
50
|
"pack:dry-run": "npm pack --dry-run",
|
|
51
51
|
"test": "bun test",
|
|
52
|
+
"smoke:v2": "bun scripts/smoke-v2-lifecycle.ts",
|
|
52
53
|
"test:coverage": "bun test --coverage",
|
|
53
54
|
"typecheck": "tsc --noEmit",
|
|
54
55
|
"prepublishOnly": "bun run test && bun run build"
|