@prevalentware/opencode-goal-plugin 0.1.18 → 0.1.19

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 +3 -1
  2. package/dist/server.js +393 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -82,6 +82,7 @@ Server options can be configured in `opencode.json`:
82
82
  "@prevalentware/opencode-goal-plugin",
83
83
  {
84
84
  "auto_continue": true,
85
+ "defer_while_tasks_active": true,
85
86
  "max_auto_turns": 25,
86
87
  "min_continue_interval_seconds": 3,
87
88
  "max_prompt_failures": 3,
@@ -98,6 +99,7 @@ Server options can be configured in `opencode.json`:
98
99
  Defaults:
99
100
 
100
101
  - `auto_continue`: `true`
102
+ - `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.
101
103
  - `max_auto_turns`: `25`
102
104
  - `min_continue_interval_seconds`: `3`
103
105
  - `max_prompt_failures`: `3`
@@ -194,6 +196,6 @@ OpenCode plugin modules are target-specific. This package exports separate modul
194
196
  }
195
197
  ```
196
198
 
197
- 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. During compaction, the plugin disables OpenCode's generic synthetic auto-continue while an active goal exists so the goal-specific continuation prompt remains authoritative.
199
+ 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. 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.
198
200
 
199
201
  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
@@ -701,6 +701,9 @@ var DEFAULT_CONTINUE_INTERVAL_SECONDS = 3;
701
701
  var DEFAULT_MAX_PROMPT_FAILURES = 3;
702
702
  var DEFAULT_COMMAND_NAME = "goal";
703
703
  var GOAL_SYSTEM_MARKER = "OpenCode goal mode";
704
+ var TASK_SETTLE_DELAY_MS = 25;
705
+ var SNAPSHOT_IDLE_HOLD_MS = 250;
706
+ var TASK_TERMINAL_STATES = new Set(["completed", "error", "cancelled"]);
704
707
  var activeContinuations = new Set;
705
708
  function goalCommandTemplate(commandName) {
706
709
  return `OpenCode goal mode command "/${commandName}" was invoked.
@@ -757,6 +760,16 @@ function textFromMessage(message) {
757
760
  return (message.parts ?? []).map(textFromPart).filter(Boolean).join(`
758
761
  `).trim();
759
762
  }
763
+ function isRecord(value) {
764
+ return typeof value === "object" && value !== null;
765
+ }
766
+ function sessionIDFromMessage(message) {
767
+ if (typeof message.sessionID === "string")
768
+ return message.sessionID;
769
+ if (isRecord(message.info) && typeof message.info.sessionID === "string")
770
+ return message.info.sessionID;
771
+ return;
772
+ }
760
773
  function estimateMessages(messages) {
761
774
  return messages.reduce((sum, message) => sum + estimateTokensFromText(textFromMessage(message)), 0);
762
775
  }
@@ -810,6 +823,52 @@ function tokensFromMessages(messages) {
810
823
  const exactTotal = messages.reduce((sum, message) => sum + (exactTokensFromMessage(message) ?? 0), 0);
811
824
  return exactTotal > 0 ? exactTotal : estimateMessages(messages);
812
825
  }
826
+ function taskHeader(output) {
827
+ const resultIndex = output.search(/<task_(?:result|error)>/);
828
+ return resultIndex === -1 ? output : output.slice(0, resultIndex);
829
+ }
830
+ function parseTaskID(output) {
831
+ const xmlMatch = /<task\s+[^>]*\bid=["']([^"']+)["'][^>]*>/i.exec(output);
832
+ if (xmlMatch?.[1])
833
+ return xmlMatch[1];
834
+ for (const line of output.split(/\r?\n/)) {
835
+ const match = /^task_id:\s*([^\s()]+)(?:\s*\(.*)?$/i.exec(line.trim());
836
+ if (match?.[1])
837
+ return match[1];
838
+ }
839
+ return;
840
+ }
841
+ function parseTaskState(output) {
842
+ const xmlMatch = /<task\s+[^>]*\bstate=["'](running|completed|error|cancelled)["'][^>]*>/i.exec(output);
843
+ if (xmlMatch?.[1])
844
+ return xmlMatch[1].toLowerCase();
845
+ for (const line of taskHeader(output).split(/\r?\n/)) {
846
+ const match = /^state:\s*(running|completed|error|cancelled)\s*$/i.exec(line.trim());
847
+ if (match?.[1])
848
+ return match[1].toLowerCase();
849
+ }
850
+ return;
851
+ }
852
+ function parseTaskStatus(output) {
853
+ if (typeof output !== "string")
854
+ return;
855
+ const taskID = parseTaskID(output);
856
+ const state = parseTaskState(output);
857
+ return taskID && state ? { taskID, state } : undefined;
858
+ }
859
+ function messageCompletedAt(message) {
860
+ const time = isRecord(message.time) ? message.time : isRecord(message.info) && isRecord(message.info.time) ? message.info.time : undefined;
861
+ const completed = time?.completed;
862
+ return typeof completed === "number" && Number.isFinite(completed) ? completed : null;
863
+ }
864
+ function assistantMarker(message) {
865
+ if (messageRole(message) !== "assistant")
866
+ return;
867
+ return {
868
+ id: messageID(message) ?? null,
869
+ completedAt: messageCompletedAt(message)
870
+ };
871
+ }
813
872
  async function sendContinuation(client, sessionID, prompt) {
814
873
  await client.session.promptAsync({
815
874
  path: { id: sessionID },
@@ -861,6 +920,235 @@ async function fetchLatestAssistant(client, sessionID) {
861
920
  const data = Array.isArray(result.data) ? result.data : [];
862
921
  return latestAssistantMessage(data);
863
922
  }
923
+
924
+ class TaskTracker {
925
+ tasks = new Map;
926
+ pendingTaskCalls = new Map;
927
+ latestAssistantBySession = new Map;
928
+ snapshotIdleHolds = new Map;
929
+ settledSnapshotIdleTasks = new Set;
930
+ noteTaskCall(input) {
931
+ if (typeof input.tool !== "string" || input.tool.toLowerCase() !== "task")
932
+ return;
933
+ if (typeof input.sessionID !== "string")
934
+ return;
935
+ if (typeof input.callID === "string")
936
+ this.pendingTaskCalls.set(input.callID, input.sessionID);
937
+ }
938
+ noteTaskOutput(input, output) {
939
+ if (typeof input.tool !== "string" || input.tool.toLowerCase() !== "task")
940
+ return;
941
+ const parentSessionID = typeof input.callID === "string" ? this.pendingTaskCalls.get(input.callID) ?? input.sessionID : input.sessionID;
942
+ if (typeof input.callID === "string")
943
+ this.pendingTaskCalls.delete(input.callID);
944
+ if (typeof parentSessionID !== "string")
945
+ return;
946
+ const status = parseTaskStatus(output.output);
947
+ if (!status)
948
+ return;
949
+ if (status.state === "running") {
950
+ this.markRunning(parentSessionID, status.taskID);
951
+ return;
952
+ }
953
+ this.markTerminal(status.taskID, status.state, parentSessionID, { resetReconciled: true });
954
+ }
955
+ observeSessionCreated(event) {
956
+ const info = event.properties?.info;
957
+ if (!isRecord(info) || typeof info.id !== "string" || typeof info.parentID !== "string")
958
+ return;
959
+ this.markRunning(info.parentID, info.id);
960
+ }
961
+ observeSessionStatus(sessionID, status) {
962
+ const task = this.tasks.get(sessionID);
963
+ if (!task)
964
+ return;
965
+ if (status === "busy") {
966
+ this.markRunning(task.parentSessionID, sessionID);
967
+ return;
968
+ }
969
+ if (status === "idle")
970
+ this.markTerminal(sessionID, "completed", task.parentSessionID);
971
+ }
972
+ observeSessionDeleted(sessionID) {
973
+ this.tasks.delete(sessionID);
974
+ for (const task of this.tasks.values()) {
975
+ if (task.parentSessionID === sessionID)
976
+ this.tasks.delete(task.taskID);
977
+ }
978
+ this.latestAssistantBySession.delete(sessionID);
979
+ this.clearSnapshotIdleForSession(sessionID);
980
+ }
981
+ observeMessages(messages) {
982
+ for (const message of messages) {
983
+ const sessionID = sessionIDFromMessage(message);
984
+ if (!sessionID)
985
+ continue;
986
+ const marker = assistantMarker(message);
987
+ if (marker) {
988
+ this.observeAssistant(sessionID, marker);
989
+ continue;
990
+ }
991
+ for (const part of message.parts ?? []) {
992
+ const status = parseTaskStatus(textFromPart(part));
993
+ if (!status)
994
+ continue;
995
+ if (status.state === "running")
996
+ this.markRunning(sessionID, status.taskID);
997
+ else
998
+ this.markTerminal(status.taskID, status.state, sessionID, { resetReconciled: true });
999
+ }
1000
+ }
1001
+ }
1002
+ observeAssistantMessage(sessionID, message) {
1003
+ const marker = message ? assistantMarker(message) : undefined;
1004
+ if (marker)
1005
+ this.observeAssistant(sessionID, marker);
1006
+ }
1007
+ hasBlockingTasks(parentSessionID) {
1008
+ this.pruneExpiredSnapshotIdleHolds();
1009
+ for (const task of this.tasks.values()) {
1010
+ if (task.parentSessionID !== parentSessionID)
1011
+ continue;
1012
+ if (task.state === "running" || task.terminalUnreconciled)
1013
+ return true;
1014
+ }
1015
+ for (const hold of this.snapshotIdleHolds.values()) {
1016
+ if (hold.parentSessionID === parentSessionID)
1017
+ return true;
1018
+ }
1019
+ return false;
1020
+ }
1021
+ nextSnapshotIdleRetryAt(parentSessionID) {
1022
+ this.pruneExpiredSnapshotIdleHolds();
1023
+ let next = null;
1024
+ for (const hold of this.snapshotIdleHolds.values()) {
1025
+ if (hold.parentSessionID !== parentSessionID)
1026
+ continue;
1027
+ next = next == null ? hold.expiresAt : Math.min(next, hold.expiresAt);
1028
+ }
1029
+ return next;
1030
+ }
1031
+ async refreshLiveChildren(client, parentSessionID) {
1032
+ const session = client.session;
1033
+ if (!session.children || !session.status)
1034
+ return;
1035
+ let childIDs;
1036
+ try {
1037
+ const result = await session.children({ path: { id: parentSessionID } });
1038
+ const data = Array.isArray(result) ? result : Array.isArray(result.data) ? result.data : [];
1039
+ childIDs = data.flatMap((child) => isRecord(child) && typeof child.id === "string" ? [child.id] : []);
1040
+ } catch {
1041
+ return;
1042
+ }
1043
+ if (childIDs.length === 0)
1044
+ return;
1045
+ let statuses;
1046
+ try {
1047
+ const result = await session.status();
1048
+ statuses = isRecord(result) && isRecord(result.data) ? result.data : isRecord(result) ? result : {};
1049
+ } catch {
1050
+ return;
1051
+ }
1052
+ for (const childID of childIDs) {
1053
+ const status = statuses[childID];
1054
+ const statusType = isRecord(status) && typeof status.type === "string" ? status.type : undefined;
1055
+ if (statusType === "busy")
1056
+ this.markRunning(parentSessionID, childID);
1057
+ else if (statusType === "idle") {
1058
+ if (this.tasks.has(childID))
1059
+ this.markTerminal(childID, "completed", parentSessionID);
1060
+ else
1061
+ this.markSnapshotIdle(parentSessionID, childID);
1062
+ }
1063
+ }
1064
+ }
1065
+ markRunning(parentSessionID, taskID) {
1066
+ const existing = this.tasks.get(taskID);
1067
+ this.clearSnapshotIdle(parentSessionID, taskID);
1068
+ this.tasks.set(taskID, {
1069
+ taskID,
1070
+ parentSessionID,
1071
+ state: "running",
1072
+ terminalUnreconciled: false,
1073
+ terminalAt: null,
1074
+ lastAssistantMessageIDAtTerminal: existing?.lastAssistantMessageIDAtTerminal ?? null
1075
+ });
1076
+ }
1077
+ markTerminal(taskID, state, parentSessionID, options = {}) {
1078
+ if (!TASK_TERMINAL_STATES.has(state))
1079
+ return;
1080
+ const existing = this.tasks.get(taskID);
1081
+ const resolvedParentSessionID = existing?.parentSessionID ?? parentSessionID;
1082
+ if (!resolvedParentSessionID)
1083
+ return;
1084
+ this.clearSnapshotIdle(resolvedParentSessionID, taskID);
1085
+ if (existing && TASK_TERMINAL_STATES.has(existing.state) && !existing.terminalUnreconciled && !options.resetReconciled) {
1086
+ return;
1087
+ }
1088
+ this.tasks.set(taskID, {
1089
+ taskID,
1090
+ parentSessionID: resolvedParentSessionID,
1091
+ state,
1092
+ terminalUnreconciled: true,
1093
+ terminalAt: Date.now(),
1094
+ lastAssistantMessageIDAtTerminal: this.latestAssistantBySession.get(resolvedParentSessionID)?.id ?? null
1095
+ });
1096
+ }
1097
+ markSnapshotIdle(parentSessionID, taskID) {
1098
+ const key = this.snapshotIdleKey(parentSessionID, taskID);
1099
+ if (this.settledSnapshotIdleTasks.has(key) || this.snapshotIdleHolds.has(key))
1100
+ return;
1101
+ this.snapshotIdleHolds.set(key, {
1102
+ taskID,
1103
+ parentSessionID,
1104
+ expiresAt: Date.now() + SNAPSHOT_IDLE_HOLD_MS
1105
+ });
1106
+ }
1107
+ clearSnapshotIdle(parentSessionID, taskID) {
1108
+ const key = this.snapshotIdleKey(parentSessionID, taskID);
1109
+ this.snapshotIdleHolds.delete(key);
1110
+ this.settledSnapshotIdleTasks.delete(key);
1111
+ }
1112
+ clearSnapshotIdleForSession(sessionID) {
1113
+ for (const [key, hold] of this.snapshotIdleHolds) {
1114
+ if (hold.taskID === sessionID || hold.parentSessionID === sessionID)
1115
+ this.snapshotIdleHolds.delete(key);
1116
+ }
1117
+ for (const key of this.settledSnapshotIdleTasks) {
1118
+ if (key.startsWith(`${sessionID}\x00`) || key.endsWith(`\x00${sessionID}`)) {
1119
+ this.settledSnapshotIdleTasks.delete(key);
1120
+ }
1121
+ }
1122
+ }
1123
+ pruneExpiredSnapshotIdleHolds(now = Date.now()) {
1124
+ for (const [key, hold] of this.snapshotIdleHolds) {
1125
+ if (hold.expiresAt > now)
1126
+ continue;
1127
+ this.snapshotIdleHolds.delete(key);
1128
+ this.settledSnapshotIdleTasks.add(key);
1129
+ }
1130
+ }
1131
+ snapshotIdleKey(parentSessionID, taskID) {
1132
+ return `${parentSessionID}\x00${taskID}`;
1133
+ }
1134
+ observeAssistant(sessionID, marker) {
1135
+ this.latestAssistantBySession.set(sessionID, marker);
1136
+ for (const task of this.tasks.values()) {
1137
+ if (task.parentSessionID !== sessionID || !task.terminalUnreconciled)
1138
+ continue;
1139
+ if (this.assistantReconcilesTask(task, marker)) {
1140
+ this.tasks.set(task.taskID, { ...task, terminalUnreconciled: false });
1141
+ }
1142
+ }
1143
+ }
1144
+ assistantReconcilesTask(task, marker) {
1145
+ if (marker.id && task.lastAssistantMessageIDAtTerminal && marker.id !== task.lastAssistantMessageIDAtTerminal)
1146
+ return true;
1147
+ if (marker.completedAt != null && task.terminalAt != null && marker.completedAt >= task.terminalAt)
1148
+ return true;
1149
+ return false;
1150
+ }
1151
+ }
864
1152
  async function recordAssistantMessage(sessionID, message, options) {
865
1153
  if (!message)
866
1154
  return;
@@ -887,12 +1175,86 @@ ${reminder}`;
887
1175
  }
888
1176
  var server = async ({ client }, options) => {
889
1177
  const autoContinue = options?.auto_continue ?? true;
1178
+ const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
890
1179
  const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
891
1180
  const minInterval = positiveIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
892
1181
  const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
893
1182
  const registerCommand = options?.register_command ?? true;
894
1183
  const commandName = commandNameFromOptions(options);
1184
+ const taskTracker = new TaskTracker;
1185
+ const taskDeferredSessions = new Set;
1186
+ const scheduledContinuations = new Map;
1187
+ const busySessions = new Set;
1188
+ async function taskBlockStatus(sessionID) {
1189
+ if (!deferWhileTasksActive)
1190
+ return false;
1191
+ await taskTracker.refreshLiveChildren(client, sessionID);
1192
+ return {
1193
+ blocked: taskTracker.hasBlockingTasks(sessionID),
1194
+ retryAt: taskTracker.nextSnapshotIdleRetryAt(sessionID)
1195
+ };
1196
+ }
1197
+ function scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS) {
1198
+ if (scheduledContinuations.has(sessionID))
1199
+ return;
1200
+ const timer = setTimeout(() => {
1201
+ scheduledContinuations.delete(sessionID);
1202
+ runAutoContinue(sessionID, true);
1203
+ }, Math.max(0, delayMs));
1204
+ const maybeUnref = timer;
1205
+ if (typeof maybeUnref.unref === "function")
1206
+ maybeUnref.unref();
1207
+ scheduledContinuations.set(sessionID, timer);
1208
+ }
1209
+ async function runAutoContinue(sessionID, fromTaskDeferral = false) {
1210
+ if (busySessions.has(sessionID))
1211
+ return;
1212
+ if (activeContinuations.has(sessionID))
1213
+ return;
1214
+ activeContinuations.add(sessionID);
1215
+ try {
1216
+ const latestAssistant = await fetchLatestAssistant(client, sessionID);
1217
+ taskTracker.observeAssistantMessage(sessionID, latestAssistant);
1218
+ const taskStatus = await taskBlockStatus(sessionID);
1219
+ if (taskStatus && taskStatus.blocked) {
1220
+ taskDeferredSessions.add(sessionID);
1221
+ if (taskStatus.retryAt != null)
1222
+ scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now());
1223
+ return;
1224
+ }
1225
+ if (busySessions.has(sessionID))
1226
+ return;
1227
+ await recordAssistantMessage(sessionID, latestAssistant, options ?? {});
1228
+ if (!fromTaskDeferral && taskDeferredSessions.has(sessionID)) {
1229
+ scheduleSettledContinuation(sessionID);
1230
+ return;
1231
+ }
1232
+ taskDeferredSessions.delete(sessionID);
1233
+ const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
1234
+ if (!goal)
1235
+ return;
1236
+ await sendContinuation(client, sessionID, goal.status === "active" ? continuationPrompt(goal) : limitPrompt(goal));
1237
+ await recordContinuationResult(sessionID, "success", maxPromptFailures);
1238
+ } catch (error) {
1239
+ await recordContinuationResult(sessionID, "failure", maxPromptFailures);
1240
+ await client.app?.log?.({
1241
+ body: {
1242
+ service: "opencode-goal-plugin",
1243
+ level: "error",
1244
+ message: "Auto-continue failed",
1245
+ extra: { error: error instanceof Error ? error.message : String(error) }
1246
+ }
1247
+ });
1248
+ } finally {
1249
+ activeContinuations.delete(sessionID);
1250
+ }
1251
+ }
895
1252
  return {
1253
+ async dispose() {
1254
+ for (const timer of scheduledContinuations.values())
1255
+ clearTimeout(timer);
1256
+ scheduledContinuations.clear();
1257
+ },
896
1258
  async config(config) {
897
1259
  if (!registerCommand)
898
1260
  return;
@@ -1005,7 +1367,14 @@ var server = async ({ client }, options) => {
1005
1367
  }
1006
1368
  }
1007
1369
  },
1370
+ async "tool.execute.before"(input) {
1371
+ taskTracker.noteTaskCall(input);
1372
+ },
1373
+ async "tool.execute.after"(input, output) {
1374
+ taskTracker.noteTaskOutput(input, output);
1375
+ },
1008
1376
  async "experimental.chat.messages.transform"(input, output) {
1377
+ taskTracker.observeMessages(output.messages);
1009
1378
  const sessionID = "sessionID" in input && typeof input.sessionID === "string" ? input.sessionID : output.messages.find((message) => typeof message.info.sessionID === "string")?.info.sessionID;
1010
1379
  if (!sessionID)
1011
1380
  return;
@@ -1030,38 +1399,39 @@ var server = async ({ client }, options) => {
1030
1399
  },
1031
1400
  async event({ event }) {
1032
1401
  const sessionID = sessionIDFromEvent(event);
1402
+ const eventType = event.type;
1403
+ if (eventType === "session.created") {
1404
+ taskTracker.observeSessionCreated(event);
1405
+ }
1406
+ if (sessionID && eventType === "session.status") {
1407
+ const status = event.properties?.status;
1408
+ if (isRecord(status) && typeof status.type === "string") {
1409
+ if (status.type === "busy")
1410
+ busySessions.add(sessionID);
1411
+ if (status.type === "idle")
1412
+ busySessions.delete(sessionID);
1413
+ taskTracker.observeSessionStatus(sessionID, status.type);
1414
+ }
1415
+ }
1416
+ if (sessionID && eventType === "session.idle") {
1417
+ busySessions.delete(sessionID);
1418
+ taskTracker.observeSessionStatus(sessionID, "idle");
1419
+ }
1420
+ if (sessionID && eventType === "session.deleted") {
1421
+ busySessions.delete(sessionID);
1422
+ taskTracker.observeSessionDeleted(sessionID);
1423
+ }
1033
1424
  if (sessionID && event.type === "message.updated") {
1034
1425
  const props = event.properties ?? {};
1035
1426
  const message = [props.info, props.message].find((value) => value && typeof value === "object");
1427
+ taskTracker.observeAssistantMessage(sessionID, message);
1036
1428
  await recordAssistantMessage(sessionID, message, options ?? {});
1037
1429
  }
1038
1430
  if (!autoContinue || !isIdleEvent(event))
1039
1431
  return;
1040
1432
  if (!sessionID)
1041
1433
  return;
1042
- if (activeContinuations.has(sessionID))
1043
- return;
1044
- activeContinuations.add(sessionID);
1045
- try {
1046
- await recordAssistantMessage(sessionID, await fetchLatestAssistant(client, sessionID), options ?? {});
1047
- const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
1048
- if (!goal)
1049
- return;
1050
- await sendContinuation(client, sessionID, goal.status === "active" ? continuationPrompt(goal) : limitPrompt(goal));
1051
- await recordContinuationResult(sessionID, "success", maxPromptFailures);
1052
- } catch (error) {
1053
- await recordContinuationResult(sessionID, "failure", maxPromptFailures);
1054
- await client.app?.log?.({
1055
- body: {
1056
- service: "opencode-goal-plugin",
1057
- level: "error",
1058
- message: "Auto-continue failed",
1059
- extra: { error: error instanceof Error ? error.message : String(error) }
1060
- }
1061
- });
1062
- } finally {
1063
- activeContinuations.delete(sessionID);
1064
- }
1434
+ await runAutoContinue(sessionID);
1065
1435
  }
1066
1436
  };
1067
1437
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
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",