@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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/server.js +106 -24
  3. 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, 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 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.
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
- await recordContinuationResult(sessionID, "success", maxPromptFailures, {
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
- locallyDeliveredPendingSessions.add(sessionID);
3072
- clearTurnWatchdog(sessionID);
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
- if (nativeRetrySessions.has(sessionID)) {
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 (disposed) {
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
- locallyDeliveredPendingSessions.add(sessionID);
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 (disposed) {
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
- await recordContinuationResult(sessionID, "success", maxPromptFailures, {
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
- locallyDeliveredPendingSessions.add(sessionID);
3753
- clearTurnWatchdog(sessionID);
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 (disposed || stoppedExecutions.has(sessionID) || busySessions.has(sessionID))
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
- if (nativeRetrySessions.has(sessionID)) {
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 (disposed) {
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
- locallyDeliveredPendingSessions.add(sessionID);
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 (disposed) {
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
- cancelScheduledContinuation(sessionID);
4105
- taskDeferredSessions.delete(sessionID);
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.54",
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",