@prevalentware/opencode-goal-plugin 0.1.56 → 0.1.58

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
@@ -326,4 +326,7 @@ The goal sidebar shows the current status, elapsed time, token usage, auto-conti
326
326
 
327
327
  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.
328
328
 
329
- 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.
329
+ Registered `/goal <objective>` and `/resume_goal` commands use the host's public session-wait API to remain pending across automatic continuations until the goal stops, completes, reaches a safety limit, or auto-continuation is disabled. Hosts without that API keep their previous admission behavior. The ACP adapter must also stream each execution owned by the command: an adapter that ends its event consumer after the first execution can still hide later work. Ordinary prompts retain their host-defined turn lifetime.
330
+
331
+
332
+ The Promise-based persistence layer uses Effect 3 under the private `effect-goal-state` npm alias. This keeps it separate from modern OpenCode's shared Effect 4 SDK runtime. It remains an external runtime dependency, and no private Effect values cross the plugin API.
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  import { randomUUID as randomUUID2 } from "crypto";
7
7
  import { mkdir, readFile } from "fs/promises";
8
8
  import { dirname as dirname2 } from "path";
9
- import { Data, Effect, Schema } from "effect";
9
+ import { Data, Effect, Schema } from "effect-goal-state";
10
10
 
11
11
  // src/atomic-write.ts
12
12
  import { randomUUID } from "crypto";
@@ -3744,6 +3744,7 @@ async function setupV2(context) {
3744
3744
  };
3745
3745
  const registrations = [];
3746
3746
  let disposed = false;
3747
+ let eventConsumerStopped = false;
3747
3748
  function stepKey(sessionID, messageID) {
3748
3749
  return `${sessionID}\x00${messageID}`;
3749
3750
  }
@@ -4391,7 +4392,7 @@ async function setupV2(context) {
4391
4392
  draft.add({
4392
4393
  name: command.name,
4393
4394
  description: command.description,
4394
- execute: async (input) => {
4395
+ execute: async (input, execution) => {
4395
4396
  markSessionOwnership(input.sessionID, true);
4396
4397
  if (command.action === "pause") {
4397
4398
  const goal = await getGoal(input.sessionID);
@@ -4414,12 +4415,60 @@ async function setupV2(context) {
4414
4415
  ...skills ? { skills: skills.map(stripMention) } : {}
4415
4416
  };
4416
4417
  }
4417
- await context.session.prompt({
4418
- ...forwardedPrompt,
4419
- sessionID: input.sessionID,
4420
- text: command.template.replaceAll("$ARGUMENTS", () => escapeXmlText2(input.prompt.text.trim())),
4421
- delivery: input.delivery
4422
- });
4418
+ const args = input.prompt.text.trim().toLowerCase();
4419
+ const controlOnly = /^(history|status|show|current|pause|stop|cancel|clear|off|reset|none|edit)(?:\s|$)/.test(args);
4420
+ const pursue = command.action === "resume" || command.action === "goal" && args !== "" && !controlOnly;
4421
+ const signal = execution?.signal ? AbortSignal.any([abortController.signal, execution.signal]) : abortController.signal;
4422
+ let admitted = false;
4423
+ const cancel = () => {
4424
+ if (!pursue || disposed)
4425
+ return;
4426
+ goalServices.stopAutonomy?.(input.sessionID);
4427
+ };
4428
+ execution?.signal?.addEventListener("abort", cancel, { once: true });
4429
+ try {
4430
+ if (execution?.signal?.aborted) {
4431
+ cancel();
4432
+ return;
4433
+ }
4434
+ await context.session.prompt({
4435
+ ...forwardedPrompt,
4436
+ sessionID: input.sessionID,
4437
+ text: command.template.replaceAll("$ARGUMENTS", () => escapeXmlText2(input.prompt.text.trim())),
4438
+ delivery: input.delivery
4439
+ });
4440
+ admitted = true;
4441
+ if (pursue && typeof context.session.wait === "function") {
4442
+ let pursuedGoalID;
4443
+ do {
4444
+ await context.session.wait({ sessionID: input.sessionID }, { signal });
4445
+ if (eventConsumerStopped)
4446
+ throw new Error("goal event stream stopped");
4447
+ const goal = await getGoal(input.sessionID);
4448
+ if (signal.aborted || disposed || stoppedExecutions.has(input.sessionID) || !autoContinue || goal?.status !== "active")
4449
+ break;
4450
+ pursuedGoalID ??= goal.id;
4451
+ if (goal.id !== pursuedGoalID)
4452
+ break;
4453
+ await new Promise((resolve) => setTimeout(resolve, 250));
4454
+ } while (!disposed && !signal.aborted);
4455
+ }
4456
+ } catch (error) {
4457
+ if (!admitted && !disposed && !execution?.signal?.aborted)
4458
+ throw error;
4459
+ if (!disposed && !execution?.signal?.aborted) {
4460
+ goalServices.stopAutonomy?.(input.sessionID);
4461
+ v2ErrorLog("Goal command wait failed; autonomous continuation stopped", error);
4462
+ try {
4463
+ if ((await getGoal(input.sessionID))?.status === "active")
4464
+ await setGoalStatus(input.sessionID, "paused");
4465
+ } catch (stateError) {
4466
+ v2ErrorLog("Failed to pause goal after command failure", stateError);
4467
+ }
4468
+ }
4469
+ } finally {
4470
+ execution?.signal?.removeEventListener("abort", cancel);
4471
+ }
4423
4472
  }
4424
4473
  });
4425
4474
  }
@@ -4451,6 +4500,18 @@ async function setupV2(context) {
4451
4500
  }
4452
4501
  }));
4453
4502
  }
4503
+ try {
4504
+ const hookInterrupt = context.session.hook;
4505
+ registrations.push(await hookInterrupt("interrupt", async ({ sessionID }) => {
4506
+ markSessionOwnership(sessionID, true);
4507
+ goalServices.stopAutonomy?.(sessionID);
4508
+ try {
4509
+ await cancelActiveGoal(sessionID);
4510
+ } catch (error) {
4511
+ v2ErrorLog("Failed to persist explicit session cancellation", error);
4512
+ }
4513
+ }));
4514
+ } catch {}
4454
4515
  registrations.push(await context.tool.transform((draft) => {
4455
4516
  for (const tool of goalToolsV2(goalServices))
4456
4517
  draft.add(tool);
@@ -4552,6 +4613,9 @@ async function setupV2(context) {
4552
4613
  } catch (error) {
4553
4614
  if (!abortController.signal.aborted)
4554
4615
  v2ErrorLog("V2 event consumer stopped", error);
4616
+ } finally {
4617
+ if (!disposed && !abortController.signal.aborted)
4618
+ eventConsumerStopped = true;
4555
4619
  }
4556
4620
  })();
4557
4621
  return async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
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",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "clean": "rm -rf dist",
49
- "build": "bun run clean && bun build ./src/server.ts --outdir ./dist --target bun --external @opencode-ai/plugin --external effect --external zod",
49
+ "build": "bun run clean && bun build ./src/server.ts --outdir ./dist --target bun --external @opencode-ai/plugin --external effect-goal-state --external zod",
50
50
  "ci:version": "bun scripts/resolve-ci-version.ts",
51
51
  "lint": "eslint .",
52
52
  "pack:dry-run": "npm pack --dry-run",
@@ -58,8 +58,8 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@opencode-ai/plugin": "^1.17.1",
61
- "effect": "^3.21.2",
62
- "zod": "^4.1.8"
61
+ "zod": "^4.1.8",
62
+ "effect-goal-state": "npm:effect@^3.21.2"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@opentui/solid": ">=0.5.8",