@prevalentware/opencode-goal-plugin 0.1.45 → 0.1.46

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 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 OpenCode idle events, including `session.idle` and `session.status` idle notifications. 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, no-progress, or prompt-failure budgets. 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.
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
@@ -2687,6 +2687,7 @@ async function setupV2(context) {
2687
2687
  const planAgents = restrictedAgentSet(options);
2688
2688
  const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
2689
2689
  const activeContinuationsV2 = new Set;
2690
+ const stoppedExecutions = new Set;
2690
2691
  const latestStepBySession = new Map;
2691
2692
  const stepTextBuffers = new Map;
2692
2693
  const stepTokenSums = new Map;
@@ -2833,6 +2834,8 @@ async function setupV2(context) {
2833
2834
  async function runAutoContinue(sessionID, fromTaskDeferral = false, scheduled) {
2834
2835
  if (disposed)
2835
2836
  return;
2837
+ if (stoppedExecutions.has(sessionID))
2838
+ return;
2836
2839
  if (busySessions.has(sessionID))
2837
2840
  return;
2838
2841
  if (activeContinuationsV2.has(sessionID))
@@ -2913,8 +2916,13 @@ async function setupV2(context) {
2913
2916
  if (nativeRetrySessions.has(sessionID))
2914
2917
  return;
2915
2918
  const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
2916
- if (!goal)
2919
+ if (!goal) {
2920
+ const waiting = await getGoalInternal(sessionID);
2921
+ if (waiting?.status === "active" && waiting.pendingAttempt == null && waiting.lastContinuationAt != null && minInterval > 0) {
2922
+ scheduleSettledContinuation(sessionID, continuationDelayFromSnapshot(minInterval, waiting.lastContinuationAt), scheduled != null);
2923
+ }
2917
2924
  return;
2925
+ }
2918
2926
  attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
2919
2927
  if (nativeRetrySessions.has(sessionID)) {
2920
2928
  await rollbackContinuationAttempt(sessionID);
@@ -2955,6 +2963,23 @@ async function setupV2(context) {
2955
2963
  async function handleV2Event(event) {
2956
2964
  const data = event.data;
2957
2965
  const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
2966
+ if (context.location && event.location && (event.location.directory !== context.location.directory || event.location.workspaceID !== context.location.workspaceID)) {
2967
+ if (event.type === "session.created" && sessionID && typeof data.parentID === "string") {
2968
+ taskTracker.observeSessionCreated({ properties: { info: { id: sessionID, parentID: data.parentID } } });
2969
+ } else if (sessionID) {
2970
+ if (event.type === "session.execution.started")
2971
+ taskTracker.observeSessionStatus(sessionID, "busy");
2972
+ if (["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.idle"].includes(event.type)) {
2973
+ taskTracker.observeSessionStatus(sessionID, "idle");
2974
+ }
2975
+ if (event.type === "session.status" && isRecord(data.status) && typeof data.status.type === "string") {
2976
+ taskTracker.observeSessionStatus(sessionID, data.status.type);
2977
+ }
2978
+ if (event.type === "session.deleted")
2979
+ taskTracker.observeSessionDeleted(sessionID);
2980
+ }
2981
+ return;
2982
+ }
2958
2983
  switch (event.type) {
2959
2984
  case "session.created": {
2960
2985
  const parentID = data.parentID;
@@ -2963,10 +2988,13 @@ async function setupV2(context) {
2963
2988
  }
2964
2989
  return;
2965
2990
  }
2991
+ case "session.execution.started":
2992
+ case "session.retry.scheduled":
2966
2993
  case "session.status": {
2967
- const status = data.status;
2994
+ const status = event.type === "session.execution.started" ? { type: "busy" } : event.type === "session.retry.scheduled" ? { type: "retry" } : data.status;
2968
2995
  if (sessionID && isRecord(status) && typeof status.type === "string") {
2969
2996
  if (status.type === "busy") {
2997
+ stoppedExecutions.delete(sessionID);
2970
2998
  busySessions.add(sessionID);
2971
2999
  nativeRetrySessions.delete(sessionID);
2972
3000
  armTurnWatchdog(sessionID);
@@ -2992,6 +3020,7 @@ async function setupV2(context) {
2992
3020
  }
2993
3021
  return;
2994
3022
  }
3023
+ case "session.execution.succeeded":
2995
3024
  case "session.idle": {
2996
3025
  if (sessionID) {
2997
3026
  busySessions.delete(sessionID);
@@ -3007,9 +3036,25 @@ async function setupV2(context) {
3007
3036
  }
3008
3037
  return;
3009
3038
  }
3039
+ case "session.execution.interrupted": {
3040
+ if (!sessionID)
3041
+ return;
3042
+ stoppedExecutions.add(sessionID);
3043
+ busySessions.delete(sessionID);
3044
+ nativeRetrySessions.delete(sessionID);
3045
+ clearTurnWatchdog(sessionID);
3046
+ watchdogRescuedSessions.delete(sessionID);
3047
+ cancelScheduledContinuation(sessionID);
3048
+ taskDeferredSessions.delete(sessionID);
3049
+ taskTracker.observeSessionStatus(sessionID, "idle");
3050
+ return;
3051
+ }
3052
+ case "session.execution.failed":
3010
3053
  case "session.error": {
3011
3054
  if (!sessionID)
3012
3055
  return;
3056
+ if (event.type === "session.execution.failed")
3057
+ nativeRetrySessions.delete(sessionID);
3013
3058
  const inNativeRetry = nativeRetrySessions.has(sessionID);
3014
3059
  busySessions.delete(sessionID);
3015
3060
  clearTurnWatchdog(sessionID);
@@ -3018,6 +3063,13 @@ async function setupV2(context) {
3018
3063
  nativeRetrySessions.delete(sessionID);
3019
3064
  watchdogRescuedSessions.delete(sessionID);
3020
3065
  const errorMessage = transportErrorMessageFromEvent(data);
3066
+ if (event.type === "session.execution.failed" && !isTransportError(errorMessage)) {
3067
+ stoppedExecutions.add(sessionID);
3068
+ cancelScheduledContinuation(sessionID);
3069
+ taskDeferredSessions.delete(sessionID);
3070
+ }
3071
+ if (event.type === "session.execution.failed")
3072
+ taskTracker.observeSessionStatus(sessionID, "idle");
3021
3073
  if (errorMessage && isTransportError(errorMessage)) {
3022
3074
  const goal = await getGoalInternal(sessionID);
3023
3075
  if (goal?.status === "active") {
@@ -3041,6 +3093,7 @@ async function setupV2(context) {
3041
3093
  case "session.deleted": {
3042
3094
  if (!sessionID)
3043
3095
  return;
3096
+ stoppedExecutions.delete(sessionID);
3044
3097
  busySessions.delete(sessionID);
3045
3098
  clearTurnWatchdog(sessionID);
3046
3099
  watchdogRescuedSessions.delete(sessionID);
@@ -3317,6 +3370,7 @@ async function setupV2(context) {
3317
3370
  clearTimeout(watchdog.timer);
3318
3371
  turnWatchdogs.clear();
3319
3372
  activeContinuationsV2.clear();
3373
+ stoppedExecutions.clear();
3320
3374
  nativeRetrySessions.clear();
3321
3375
  locallyDeliveredPendingSessions.clear();
3322
3376
  watchdogRescuedSessions.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
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"