@prevalentware/opencode-goal-plugin 0.1.54 → 0.1.55
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 +8 -1
- package/dist/server.js +106 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -315,6 +315,13 @@ OpenCode plugin modules are target-specific. This package exports separate modul
|
|
|
315
315
|
}
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
-
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
|
|
318
|
+
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. A V2 `session.execution.interrupted` event with reason `user`, or a V1 session/assistant `MessageAbortedError`, persists a terminal `cancelled` state for an active goal and invalidates timers and outstanding continuation preparation without charging a prompt failure. A later idle, unrelated prompt, or plugin reload cannot resume that goal; start an explicitly requested new goal with `/goal <objective>` or `/goal replace <objective>`. Cancelling a manual turn preserves paused or limited goals. V2 shutdown, superseded, and other interruptions only suppress local continuation until the host starts another execution; they do not cancel the persisted goal. V1 exposes only `MessageAbortedError`, so it cannot distinguish user cancellation from other host aborts of an active goal. Use `/pause_goal` for a durable, resumable 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 on OpenCode 1, the plugin disables OpenCode's generic synthetic auto-continue while an active goal exists so the goal-specific continuation prompt remains authoritative; on OpenCode 2, compaction runs inside the session's execution flow, so the plugin instead injects the goal snapshot through the `session.compaction` hook where the host provides it.
|
|
319
319
|
|
|
320
320
|
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. It checks the shared goal state file every second so usage and checkpoints stay current during a long run. Closed goals remain visible briefly through the latest tool state as achieved or unmet.
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
### Zed and ACP lifecycle boundaries
|
|
324
|
+
|
|
325
|
+
Cancelling an active goal from an ACP client is durable when OpenCode emits the user-cancellation events described above. The plugin prevents subsequent goal continuations, including callbacks still preparing a prompt when cancellation arrives. OpenCode owns cancellation of in-flight model requests, tools, subprocesses, and already submitted prompts; the plugin cannot guarantee process termination if the host does not abort them or does not publish a cancellation signal. It does not interpret ordinary idle events, provider-error text, or completed tool calls as user cancellation.
|
|
326
|
+
|
|
327
|
+
Goal lifetime and ACP prompt-turn lifetime are separate. The plugin continues an active goal across successful executions, but does not control the ACP adapter's response to `session/prompt`. A host that returns `end_turn` after the initial execution can therefore show an idle client while subsequent goal work runs. In an isolated OpenCode 2.0.21 ACP test, registered `/goal` commands returned `end_turn` while their submitted execution was still running; a normal `session/prompt` remained open and returned `cancelled` after Cancel. Keeping that ACP request open across goal continuations, and exposing structured goal plans as ACP `plan` updates, requires host integration. Request or phase completion alone must not mark a goal complete. Long-running goals remain supported within the configured budgets; this cancellation handling adds no timeout.
|
package/dist/server.js
CHANGED
|
@@ -888,6 +888,16 @@ async function cancelGoal(sessionID, reason = "cancelled") {
|
|
|
888
888
|
return snapshot(goal);
|
|
889
889
|
});
|
|
890
890
|
}
|
|
891
|
+
async function cancelActiveGoal(sessionID) {
|
|
892
|
+
return mutate((state) => {
|
|
893
|
+
const goal = state.goals[sessionID];
|
|
894
|
+
if (!goal)
|
|
895
|
+
return null;
|
|
896
|
+
if (goal.status === "active")
|
|
897
|
+
cancelGoalRecord(goal, "cancelled");
|
|
898
|
+
return snapshot(goal);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
891
901
|
async function clearGoal(sessionID) {
|
|
892
902
|
return mutate((state) => {
|
|
893
903
|
const goal = state.goals[sessionID];
|
|
@@ -1937,6 +1947,27 @@ var NON_TRANSPORT_TERMINAL_PATTERN = /\b(?:abort(?:ed)?|interrupt(?:ed|ion)?)\b/
|
|
|
1937
1947
|
var NON_PROGRESS_TOOLS = new Set(["get_goal", "get_goal_history", "list_all_goals"]);
|
|
1938
1948
|
var TASK_TERMINAL_STATES = new Set(["completed", "error", "cancelled"]);
|
|
1939
1949
|
var activeContinuations = new Set;
|
|
1950
|
+
|
|
1951
|
+
class ContinuationEpochs {
|
|
1952
|
+
values = new Map;
|
|
1953
|
+
current(sessionID) {
|
|
1954
|
+
return this.values.get(sessionID) ?? 0;
|
|
1955
|
+
}
|
|
1956
|
+
invalidate(sessionID) {
|
|
1957
|
+
this.values.set(sessionID, this.current(sessionID) + 1);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function isUserAbortEvent(event) {
|
|
1961
|
+
const properties = event.properties;
|
|
1962
|
+
if (event.type === "session.error") {
|
|
1963
|
+
return isRecord(properties?.error) && properties.error.name === "MessageAbortedError";
|
|
1964
|
+
}
|
|
1965
|
+
const message = properties?.info;
|
|
1966
|
+
return event.type === "message.updated" && isRecord(message) && message.role === "assistant" && isRecord(message.error) && message.error.name === "MessageAbortedError";
|
|
1967
|
+
}
|
|
1968
|
+
function continuationStillReserved(goal, current) {
|
|
1969
|
+
return current?.id === goal.id && current.status === goal.status && (goal.status !== "active" || current.pendingAttempt?.id === goal.pendingAttempt?.id);
|
|
1970
|
+
}
|
|
1940
1971
|
function restrictedAgentSet(options) {
|
|
1941
1972
|
if (options?.allow_goal_execution_from_plan === true)
|
|
1942
1973
|
return new Set;
|
|
@@ -2963,6 +2994,7 @@ var server = async ({ client }, options) => {
|
|
|
2963
2994
|
const toolAttempts = new Map;
|
|
2964
2995
|
const explicitResumeRequests = new Set;
|
|
2965
2996
|
const restartAfterContinuation = new Set;
|
|
2997
|
+
const continuationEpochs = new ContinuationEpochs;
|
|
2966
2998
|
const watchdogRescuedSessions = new Set;
|
|
2967
2999
|
const planAgents = restrictedAgentSet(options);
|
|
2968
3000
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
@@ -2974,6 +3006,7 @@ var server = async ({ client }, options) => {
|
|
|
2974
3006
|
maxObjectiveChars: objectiveChars,
|
|
2975
3007
|
consumeAutoTurnReset: (sessionID) => explicitResumeRequests.delete(sessionID),
|
|
2976
3008
|
stopAutonomy: (sessionID, mode = "stop") => {
|
|
3009
|
+
continuationEpochs.invalidate(sessionID);
|
|
2977
3010
|
cancelScheduledContinuation(sessionID);
|
|
2978
3011
|
if (mode === "stop")
|
|
2979
3012
|
clearTurnWatchdog(sessionID);
|
|
@@ -3027,6 +3060,8 @@ var server = async ({ client }, options) => {
|
|
|
3027
3060
|
turnWatchdogs.set(sessionID, watchdog);
|
|
3028
3061
|
}
|
|
3029
3062
|
async function runTurnWatchdog(sessionID, watchdog) {
|
|
3063
|
+
const epoch = continuationEpochs.current(sessionID);
|
|
3064
|
+
const isCurrent = () => !disposed && epoch === continuationEpochs.current(sessionID);
|
|
3030
3065
|
let claimedContinuation = false;
|
|
3031
3066
|
let claimedGoalID;
|
|
3032
3067
|
try {
|
|
@@ -3062,17 +3097,23 @@ var server = async ({ client }, options) => {
|
|
|
3062
3097
|
claimedContinuation = true;
|
|
3063
3098
|
claimedGoalID = current.id;
|
|
3064
3099
|
watchdogRescuedSessions.add(sessionID);
|
|
3100
|
+
if (!isCurrent())
|
|
3101
|
+
return;
|
|
3065
3102
|
await sendContinuation(client, sessionID, continuationPrompt(current, locale), current.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
3066
|
-
|
|
3103
|
+
if (!isCurrent())
|
|
3104
|
+
return;
|
|
3105
|
+
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures, {
|
|
3067
3106
|
armNoProgress: false,
|
|
3068
3107
|
started: true,
|
|
3069
3108
|
expectedGoalID: claimedGoalID
|
|
3070
3109
|
});
|
|
3071
|
-
|
|
3072
|
-
|
|
3110
|
+
if (isCurrent() && delivered?.pendingAttempt?.delivered) {
|
|
3111
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
3112
|
+
clearTurnWatchdog(sessionID);
|
|
3113
|
+
}
|
|
3073
3114
|
} catch (error) {
|
|
3074
3115
|
try {
|
|
3075
|
-
if (claimedContinuation && isTransportError(error)) {
|
|
3116
|
+
if (claimedContinuation && isCurrent() && isTransportError(error)) {
|
|
3076
3117
|
await recordContinuationResult(sessionID, "failure", maxPromptFailures, { expectedGoalID: claimedGoalID });
|
|
3077
3118
|
}
|
|
3078
3119
|
await client.app?.log?.({
|
|
@@ -3142,16 +3183,24 @@ var server = async ({ client }, options) => {
|
|
|
3142
3183
|
return;
|
|
3143
3184
|
if (activeContinuations.has(sessionID))
|
|
3144
3185
|
return;
|
|
3186
|
+
const epoch = continuationEpochs.current(sessionID);
|
|
3187
|
+
const isCurrent = () => !disposed && epoch === continuationEpochs.current(sessionID);
|
|
3145
3188
|
activeContinuations.add(sessionID);
|
|
3146
3189
|
let attemptReservedAt = Date.now();
|
|
3147
3190
|
let attemptGoalID;
|
|
3148
3191
|
let attemptID;
|
|
3149
3192
|
try {
|
|
3150
3193
|
const latestAssistant = await fetchLatestAssistant(client, sessionID);
|
|
3194
|
+
if (!isCurrent())
|
|
3195
|
+
return;
|
|
3151
3196
|
taskTracker.observeAssistantMessage(sessionID, latestAssistant);
|
|
3152
3197
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
3198
|
+
if (!isCurrent())
|
|
3199
|
+
return;
|
|
3153
3200
|
if (taskStatus && taskStatus.blocked) {
|
|
3154
3201
|
const deferralGoal = await getGoalInternal(sessionID);
|
|
3202
|
+
if (!isCurrent())
|
|
3203
|
+
return;
|
|
3155
3204
|
if (!taskDeferralGoalContinuable(deferralGoal)) {
|
|
3156
3205
|
taskDeferredSessions.delete(sessionID);
|
|
3157
3206
|
cancelScheduledContinuation(sessionID);
|
|
@@ -3161,7 +3210,7 @@ var server = async ({ client }, options) => {
|
|
|
3161
3210
|
scheduleSettledContinuation(sessionID, taskStatus.retryAt != null ? taskStatus.retryAt - Date.now() : TASK_BLOCK_RETRY_MS, scheduled != null || taskStatus.retryAt != null);
|
|
3162
3211
|
return;
|
|
3163
3212
|
}
|
|
3164
|
-
if (busySessions.has(sessionID))
|
|
3213
|
+
if (!isCurrent() || busySessions.has(sessionID))
|
|
3165
3214
|
return;
|
|
3166
3215
|
const observed = await recordAssistantMessage(sessionID, latestAssistant, options ?? {}, true);
|
|
3167
3216
|
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
@@ -3171,7 +3220,7 @@ var server = async ({ client }, options) => {
|
|
|
3171
3220
|
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled)
|
|
3172
3221
|
return;
|
|
3173
3222
|
const current = await getGoalInternal(sessionID);
|
|
3174
|
-
if (!current)
|
|
3223
|
+
if (!isCurrent() || !current)
|
|
3175
3224
|
return;
|
|
3176
3225
|
const latestTurnAgent = agentFromMessage(latestAssistant);
|
|
3177
3226
|
if (isPlanAgent(current.lastPromptAgent) || isPlanAgent(latestTurnAgent)) {
|
|
@@ -3209,7 +3258,7 @@ var server = async ({ client }, options) => {
|
|
|
3209
3258
|
return;
|
|
3210
3259
|
if (!autoContinue)
|
|
3211
3260
|
return;
|
|
3212
|
-
if (nativeRetrySessions.has(sessionID))
|
|
3261
|
+
if (!isCurrent() || nativeRetrySessions.has(sessionID))
|
|
3213
3262
|
return;
|
|
3214
3263
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
3215
3264
|
if (!goal)
|
|
@@ -3217,7 +3266,8 @@ var server = async ({ client }, options) => {
|
|
|
3217
3266
|
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
3218
3267
|
attemptGoalID = goal.id;
|
|
3219
3268
|
attemptID = goal.pendingAttempt?.id;
|
|
3220
|
-
|
|
3269
|
+
const beforeDelivery = await getGoalInternal(sessionID);
|
|
3270
|
+
if (!isCurrent() || !continuationStillReserved(goal, beforeDelivery) || busySessions.has(sessionID) || nativeRetrySessions.has(sessionID)) {
|
|
3221
3271
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3222
3272
|
return;
|
|
3223
3273
|
}
|
|
@@ -3226,19 +3276,20 @@ var server = async ({ client }, options) => {
|
|
|
3226
3276
|
return;
|
|
3227
3277
|
}
|
|
3228
3278
|
await sendContinuation(client, sessionID, goal.status === "active" ? continuationPrompt(goal, locale) : limitPrompt(goal, locale), goal.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
3229
|
-
if (
|
|
3279
|
+
if (!isCurrent()) {
|
|
3230
3280
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3231
3281
|
return;
|
|
3232
3282
|
}
|
|
3233
3283
|
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures, {
|
|
3234
3284
|
expectedGoalID: attemptGoalID
|
|
3235
3285
|
});
|
|
3236
|
-
|
|
3286
|
+
if (isCurrent() && delivered?.pendingAttempt?.delivered)
|
|
3287
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
3237
3288
|
if (!delivered?.pendingAttempt?.delivered) {
|
|
3238
3289
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3239
3290
|
}
|
|
3240
3291
|
} catch (error) {
|
|
3241
|
-
if (
|
|
3292
|
+
if (!isCurrent()) {
|
|
3242
3293
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3243
3294
|
return;
|
|
3244
3295
|
}
|
|
@@ -3503,6 +3554,17 @@ var server = async ({ client }, options) => {
|
|
|
3503
3554
|
async event({ event }) {
|
|
3504
3555
|
const sessionID = sessionIDFromEvent(event);
|
|
3505
3556
|
const eventType = event.type;
|
|
3557
|
+
if (sessionID && isUserAbortEvent(event)) {
|
|
3558
|
+
explicitResumeRequests.delete(sessionID);
|
|
3559
|
+
goalServices.stopAutonomy?.(sessionID);
|
|
3560
|
+
busySessions.delete(sessionID);
|
|
3561
|
+
nativeRetrySessions.delete(sessionID);
|
|
3562
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
3563
|
+
clearToolAttemptsForSession(toolAttempts, sessionID);
|
|
3564
|
+
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
3565
|
+
await cancelActiveGoal(sessionID);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3506
3568
|
if (eventType === "session.created") {
|
|
3507
3569
|
taskTracker.observeSessionCreated(event);
|
|
3508
3570
|
}
|
|
@@ -3571,6 +3633,7 @@ var server = async ({ client }, options) => {
|
|
|
3571
3633
|
}
|
|
3572
3634
|
}
|
|
3573
3635
|
if (sessionID && eventType === "session.deleted") {
|
|
3636
|
+
continuationEpochs.invalidate(sessionID);
|
|
3574
3637
|
explicitResumeRequests.delete(sessionID);
|
|
3575
3638
|
busySessions.delete(sessionID);
|
|
3576
3639
|
clearTurnWatchdog(sessionID);
|
|
@@ -3635,6 +3698,7 @@ async function setupV2(context) {
|
|
|
3635
3698
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
3636
3699
|
const activeContinuationsV2 = new Set;
|
|
3637
3700
|
const restartAfterContinuation = new Set;
|
|
3701
|
+
const continuationEpochs = new ContinuationEpochs;
|
|
3638
3702
|
const stoppedExecutions = new Set;
|
|
3639
3703
|
const latestStepBySession = new Map;
|
|
3640
3704
|
const stepTextBuffers = new Map;
|
|
@@ -3654,6 +3718,7 @@ async function setupV2(context) {
|
|
|
3654
3718
|
}
|
|
3655
3719
|
},
|
|
3656
3720
|
stopAutonomy: (sessionID, mode = "stop") => {
|
|
3721
|
+
continuationEpochs.invalidate(sessionID);
|
|
3657
3722
|
cancelScheduledContinuation(sessionID);
|
|
3658
3723
|
if (mode === "stop")
|
|
3659
3724
|
clearTurnWatchdog(sessionID);
|
|
@@ -3712,6 +3777,8 @@ async function setupV2(context) {
|
|
|
3712
3777
|
turnWatchdogs.set(sessionID, watchdog);
|
|
3713
3778
|
}
|
|
3714
3779
|
async function runTurnWatchdog(sessionID, watchdog) {
|
|
3780
|
+
const epoch = continuationEpochs.current(sessionID);
|
|
3781
|
+
const isCurrent = () => !disposed && epoch === continuationEpochs.current(sessionID);
|
|
3715
3782
|
let claimedContinuation = false;
|
|
3716
3783
|
let claimedGoalID;
|
|
3717
3784
|
try {
|
|
@@ -3743,17 +3810,23 @@ async function setupV2(context) {
|
|
|
3743
3810
|
claimedContinuation = true;
|
|
3744
3811
|
claimedGoalID = current.id;
|
|
3745
3812
|
watchdogRescuedSessions.add(sessionID);
|
|
3813
|
+
if (!isCurrent())
|
|
3814
|
+
return;
|
|
3746
3815
|
await sendContinuation(sessionID, continuationPrompt(current, locale), current.lastPromptAgent ?? latestStep?.agent ?? null);
|
|
3747
|
-
|
|
3816
|
+
if (!isCurrent())
|
|
3817
|
+
return;
|
|
3818
|
+
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures, {
|
|
3748
3819
|
armNoProgress: false,
|
|
3749
3820
|
started: true,
|
|
3750
3821
|
expectedGoalID: claimedGoalID
|
|
3751
3822
|
});
|
|
3752
|
-
|
|
3753
|
-
|
|
3823
|
+
if (isCurrent() && delivered?.pendingAttempt?.delivered) {
|
|
3824
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
3825
|
+
clearTurnWatchdog(sessionID);
|
|
3826
|
+
}
|
|
3754
3827
|
} catch (error) {
|
|
3755
3828
|
try {
|
|
3756
|
-
if (claimedContinuation && isTransportError(error)) {
|
|
3829
|
+
if (claimedContinuation && isCurrent() && isTransportError(error)) {
|
|
3757
3830
|
await recordContinuationResult(sessionID, "failure", maxPromptFailures, { expectedGoalID: claimedGoalID });
|
|
3758
3831
|
}
|
|
3759
3832
|
v2ErrorLog("Turn watchdog retry failed", error);
|
|
@@ -3818,8 +3891,10 @@ async function setupV2(context) {
|
|
|
3818
3891
|
return;
|
|
3819
3892
|
if (activeContinuationsV2.has(sessionID))
|
|
3820
3893
|
return;
|
|
3894
|
+
const epoch = continuationEpochs.current(sessionID);
|
|
3895
|
+
const isCurrent = () => !disposed && epoch === continuationEpochs.current(sessionID);
|
|
3821
3896
|
await taskRecoveryComplete;
|
|
3822
|
-
if (
|
|
3897
|
+
if (!isCurrent() || stoppedExecutions.has(sessionID) || busySessions.has(sessionID))
|
|
3823
3898
|
return;
|
|
3824
3899
|
activeContinuationsV2.add(sessionID);
|
|
3825
3900
|
let attemptReservedAt = Date.now();
|
|
@@ -3833,6 +3908,8 @@ async function setupV2(context) {
|
|
|
3833
3908
|
const taskStatus = taskBlockStatus(sessionID);
|
|
3834
3909
|
if (taskStatus && taskStatus.blocked) {
|
|
3835
3910
|
const deferralGoal = await getGoalInternal(sessionID);
|
|
3911
|
+
if (!isCurrent())
|
|
3912
|
+
return;
|
|
3836
3913
|
if (!taskDeferralGoalContinuable(deferralGoal)) {
|
|
3837
3914
|
taskDeferredSessions.delete(sessionID);
|
|
3838
3915
|
cancelScheduledContinuation(sessionID);
|
|
@@ -3864,7 +3941,7 @@ async function setupV2(context) {
|
|
|
3864
3941
|
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled)
|
|
3865
3942
|
return;
|
|
3866
3943
|
const current = await getGoalInternal(sessionID);
|
|
3867
|
-
if (!current)
|
|
3944
|
+
if (!isCurrent() || !current)
|
|
3868
3945
|
return;
|
|
3869
3946
|
const latestTurnAgent = latestStep?.agent;
|
|
3870
3947
|
if (isPlanAgent(current.lastPromptAgent) || isPlanAgent(latestTurnAgent)) {
|
|
@@ -3902,7 +3979,7 @@ async function setupV2(context) {
|
|
|
3902
3979
|
return;
|
|
3903
3980
|
if (!autoContinue)
|
|
3904
3981
|
return;
|
|
3905
|
-
if (nativeRetrySessions.has(sessionID))
|
|
3982
|
+
if (!isCurrent() || nativeRetrySessions.has(sessionID))
|
|
3906
3983
|
return;
|
|
3907
3984
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
3908
3985
|
if (!goal) {
|
|
@@ -3915,7 +3992,8 @@ async function setupV2(context) {
|
|
|
3915
3992
|
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
3916
3993
|
attemptGoalID = goal.id;
|
|
3917
3994
|
attemptID = goal.pendingAttempt?.id;
|
|
3918
|
-
|
|
3995
|
+
const beforeDelivery = await getGoalInternal(sessionID);
|
|
3996
|
+
if (!isCurrent() || !continuationStillReserved(goal, beforeDelivery) || busySessions.has(sessionID) || nativeRetrySessions.has(sessionID)) {
|
|
3919
3997
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3920
3998
|
return;
|
|
3921
3999
|
}
|
|
@@ -3924,19 +4002,20 @@ async function setupV2(context) {
|
|
|
3924
4002
|
return;
|
|
3925
4003
|
}
|
|
3926
4004
|
await sendContinuation(sessionID, goal.status === "active" ? continuationPrompt(goal, locale) : limitPrompt(goal, locale), goal.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
3927
|
-
if (
|
|
4005
|
+
if (!isCurrent()) {
|
|
3928
4006
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3929
4007
|
return;
|
|
3930
4008
|
}
|
|
3931
4009
|
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures, {
|
|
3932
4010
|
expectedGoalID: attemptGoalID
|
|
3933
4011
|
});
|
|
3934
|
-
|
|
4012
|
+
if (isCurrent() && delivered?.pendingAttempt?.delivered)
|
|
4013
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
3935
4014
|
if (!delivered?.pendingAttempt?.delivered) {
|
|
3936
4015
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3937
4016
|
}
|
|
3938
4017
|
} catch (error) {
|
|
3939
|
-
if (
|
|
4018
|
+
if (!isCurrent()) {
|
|
3940
4019
|
await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID });
|
|
3941
4020
|
return;
|
|
3942
4021
|
}
|
|
@@ -4101,9 +4180,11 @@ async function setupV2(context) {
|
|
|
4101
4180
|
nativeRetrySessions.delete(sessionID);
|
|
4102
4181
|
clearTurnWatchdog(sessionID);
|
|
4103
4182
|
watchdogRescuedSessions.delete(sessionID);
|
|
4104
|
-
|
|
4105
|
-
|
|
4183
|
+
goalServices.stopAutonomy?.(sessionID);
|
|
4184
|
+
clearToolAttemptsForSession(toolAttempts, sessionID);
|
|
4106
4185
|
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
4186
|
+
if (data.reason === "user")
|
|
4187
|
+
await cancelActiveGoal(sessionID);
|
|
4107
4188
|
return;
|
|
4108
4189
|
}
|
|
4109
4190
|
case "session.execution.failed": {
|
|
@@ -4144,6 +4225,7 @@ async function setupV2(context) {
|
|
|
4144
4225
|
case "session.deleted": {
|
|
4145
4226
|
if (!sessionID)
|
|
4146
4227
|
return;
|
|
4228
|
+
continuationEpochs.invalidate(sessionID);
|
|
4147
4229
|
explicitResumeRequests.delete(sessionID);
|
|
4148
4230
|
stoppedExecutions.delete(sessionID);
|
|
4149
4231
|
sessionOwnership.delete(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.55",
|
|
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",
|