@prevalentware/opencode-goal-plugin 0.1.50 → 0.1.51

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
@@ -178,7 +178,7 @@ Defaults:
178
178
  - `auto_continue`: `true`
179
179
  - `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. A deferral re-checks child sessions on a short timer, so a goal deferred by a task never depends on a further idle event to resume.
180
180
  - `max_task_block_seconds`: `900`; wall-clock ceiling on how long a single Task child session may defer goal continuation. A child that stays listed but never reports a terminal state, or a terminal child whose result is never reconciled, stops blocking once the ceiling passes. Set a smaller value for shorter subagents, `0` to remove the ceiling, or disable deferral entirely with `defer_while_tasks_active: false`.
181
- - `max_auto_turns`: `25`
181
+ - `max_auto_turns`: `25`; explicitly resuming a goal with `/goal resume` or `/resume_goal` after it reaches this limit starts a fresh auto-turn window. Token usage and elapsed-time usage are preserved.
182
182
  - `min_continue_interval_seconds`: `3`
183
183
  - Fast V2 executions that finish inside this interval schedule a delayed continuation; they do not require another user message to wake up.
184
184
  - `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.
package/dist/server.js CHANGED
@@ -132,6 +132,7 @@ var MAX_LISTED_GOALS = 50;
132
132
  var CHECKPOINT_CHAR_LIMIT = 280;
133
133
  var DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD = 50;
134
134
  var DEFAULT_MAX_NO_PROGRESS_TURNS = 2;
135
+ var MAX_AUTO_CONTINUES_STOP_REASON_PREFIX = "max auto-continues reached (";
135
136
  var PLAN_MODE_STOP_REASON = "plan mode";
136
137
  var PLAN_MODE_BLOCKER = "Goal execution is paused while the session is in Plan mode. Switch to Build mode and resume the goal to continue.";
137
138
  var NullableString = Schema.NullOr(Schema.String);
@@ -689,7 +690,7 @@ async function pauseGoalForPlanMode(sessionID) {
689
690
  return snapshot(goal);
690
691
  });
691
692
  }
692
- async function setGoalStatus(sessionID, status, agent) {
693
+ async function setGoalStatus(sessionID, status, agent, options) {
693
694
  const agentValue = typeof agent === "string" && agent.trim() ? agent.trim() : null;
694
695
  return mutate((state) => {
695
696
  const goal = state.goals[sessionID];
@@ -701,10 +702,12 @@ async function setGoalStatus(sessionID, status, agent) {
701
702
  return snapshot(goal);
702
703
  if (status === "paused" && goal.status !== "active")
703
704
  return snapshot(goal);
705
+ const resumesAutoTurnLimit = options?.resetAutoTurnLimit === true && status === "active" && goal.status === "usageLimited" && goal.stopReason?.startsWith(MAX_AUTO_CONTINUES_STOP_REASON_PREFIX) === true;
704
706
  accountWallClock(goal);
705
707
  goal.status = status;
706
708
  goal.updatedAt = nowSeconds();
707
709
  goal.lastAccountedAt = status === "active" ? goal.updatedAt : null;
710
+ goal.autoTurns = resumesAutoTurnLimit ? 0 : goal.autoTurns;
708
711
  goal.continuationFailures = status === "active" ? 0 : goal.continuationFailures;
709
712
  goal.pendingAttempt = status === "active" ? null : goal.pendingAttempt;
710
713
  goal.noProgressTurns = status === "active" ? 0 : goal.noProgressTurns;
@@ -1053,7 +1056,7 @@ function maybeStopForUsageLimit(goal, defaultMaxAutoTurns, now = nowSeconds()) {
1053
1056
  if (effectiveMaxAutoTurns > 0 && goal.autoTurns >= effectiveMaxAutoTurns) {
1054
1057
  goal.status = "usageLimited";
1055
1058
  goal.lastAccountedAt = null;
1056
- goal.stopReason = `max auto-continues reached (${effectiveMaxAutoTurns})`;
1059
+ goal.stopReason = `${MAX_AUTO_CONTINUES_STOP_REASON_PREFIX}${effectiveMaxAutoTurns})`;
1057
1060
  goal.lastStatus = `${goal.stopReason}; wrap-up required.`;
1058
1061
  pushHistory(goal, "limited", goal.lastStatus);
1059
1062
  return true;
@@ -1617,7 +1620,7 @@ ${budgetLines(goal, locale)}
1617
1620
  \u72B6\u6001\uFF1A${presentGoalStatus(goal.status, locale)}
1618
1621
  \u505C\u6B62\u539F\u56E0\uFF1A${presentGoalStopReason(goal.stopReason ?? "goal limit reached", locale)}
1619
1622
 
1620
- \u4E0D\u8981\u4E3A\u6B64\u76EE\u6807\u5F00\u59CB\u65B0\u7684\u5B9E\u8D28\u6027\u5DE5\u4F5C\u3002\u5C3D\u5FEB\u7ED3\u675F\u672C\u8F6E\uFF1A\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587\u603B\u7ED3\u6709\u6548\u8FDB\u5C55\uFF0C\u6307\u51FA\u5269\u4F59\u5DE5\u4F5C\u6216\u963B\u585E\u9879\uFF0C\u5E76\u7ED9\u7528\u6237\u4E00\u4E2A\u6E05\u6670\u7684\u4E0B\u4E00\u6B65\u3002\u9664\u975E\u76EE\u6807\u786E\u5B9E\u5DF2\u7ECF\u5B8C\u6210\uFF0C\u5426\u5219\u4E0D\u8981\u8C03\u7528 update_goal\u3002`;
1623
+ \u4E0D\u8981\u4E3A\u6B64\u76EE\u6807\u5F00\u59CB\u65B0\u7684\u5B9E\u8D28\u6027\u5DE5\u4F5C\u3002\u4E0D\u8981\u8C03\u7528 update_goal_status \u6765\u7EE7\u7EED\u76EE\u6807\uFF1B\u53EA\u6709\u7528\u6237\u660E\u786E\u53D1\u51FA\u7EE7\u7EED\u547D\u4EE4\u540E\u624D\u80FD\u7EE7\u7EED\u3002\u5C3D\u5FEB\u7ED3\u675F\u672C\u8F6E\uFF1A\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587\u603B\u7ED3\u6709\u6548\u8FDB\u5C55\uFF0C\u6307\u51FA\u5269\u4F59\u5DE5\u4F5C\u6216\u963B\u585E\u9879\uFF0C\u5E76\u7ED9\u7528\u6237\u4E00\u4E2A\u6E05\u6670\u7684\u4E0B\u4E00\u6B65\u3002\u9664\u975E\u76EE\u6807\u786E\u5B9E\u5DF2\u7ECF\u5B8C\u6210\uFF0C\u5426\u5219\u4E0D\u8981\u8C03\u7528 update_goal\u3002`;
1621
1624
  }
1622
1625
  return `The active session goal has reached a safety limit.
1623
1626
 
@@ -1633,7 +1636,7 @@ ${budgetLines(goal, locale)}
1633
1636
  Status: ${goal.status}
1634
1637
  Stop reason: ${goal.stopReason ?? "goal limit reached"}
1635
1638
 
1636
- Do not start new substantive work for this goal. Wrap up this turn soon: summarize useful progress, identify remaining work or blockers, and leave the user with a clear next step. Do not call update_goal unless the goal is actually complete.`;
1639
+ Do not start new substantive work for this goal. Do not call update_goal_status to resume it; only an explicit user resume command may continue the goal. Wrap up this turn soon: summarize useful progress, identify remaining work or blockers, and leave the user with a clear next step. Do not call update_goal unless the goal is actually complete.`;
1637
1640
  }
1638
1641
  function systemReminder(locale = "en") {
1639
1642
  if (locale === "zh-CN") {
@@ -1890,6 +1893,10 @@ Ignore any command arguments. Call get_goal first, then handle only this resume
1890
1893
 
1891
1894
  Do not create, edit, clear, complete, or mark a goal unmet.`;
1892
1895
  }
1896
+ function isExplicitResumePrompt(text, commandName, locale, messages) {
1897
+ const value = text.trim();
1898
+ return value === goalStatusCommandTemplate("resume_goal", locale) || value === goalCommandTemplate(commandName, locale).replace("$ARGUMENTS", "resume") || value === messages.tui.resumePrompt;
1899
+ }
1893
1900
  function goalCommandDefinitions(commandName, locale = "en") {
1894
1901
  const messages = messagesFor(locale);
1895
1902
  return [
@@ -2662,10 +2669,11 @@ async function closeGoalFromTool(input, context, services) {
2662
2669
  return JSON.stringify({ goal, unmet_report: report }, null, 2);
2663
2670
  }
2664
2671
  async function updateGoalStatusFromTool(input, context, services) {
2672
+ const resetAutoTurnLimit = input.status === "active" && services.consumeAutoTurnReset(context.sessionID);
2665
2673
  if (input.status === "active" && services.isPlanAgent(context.agent)) {
2666
2674
  throw new Error(services.messages.notices.cannotResumeInPlan);
2667
2675
  }
2668
- const goal = await setGoalStatus(context.sessionID, input.status, typeof context.agent === "string" ? context.agent : null);
2676
+ const goal = await setGoalStatus(context.sessionID, input.status, typeof context.agent === "string" ? context.agent : null, { resetAutoTurnLimit });
2669
2677
  return JSON.stringify({ goal }, null, 2);
2670
2678
  }
2671
2679
  function v2ObjectSchema(properties, required = []) {
@@ -2737,10 +2745,18 @@ var server = async ({ client }, options) => {
2737
2745
  const nativeRetrySessions = new Set;
2738
2746
  const locallyDeliveredPendingSessions = new Set;
2739
2747
  const toolAttempts = new Map;
2748
+ const explicitResumeRequests = new Set;
2740
2749
  const watchdogRescuedSessions = new Set;
2741
2750
  const planAgents = restrictedAgentSet(options);
2742
2751
  const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
2743
- const goalServices = { options: options ?? {}, locale, messages, isPlanAgent, maxObjectiveChars: objectiveChars };
2752
+ const goalServices = {
2753
+ options: options ?? {},
2754
+ locale,
2755
+ messages,
2756
+ isPlanAgent,
2757
+ maxObjectiveChars: objectiveChars,
2758
+ consumeAutoTurnReset: (sessionID) => explicitResumeRequests.delete(sessionID)
2759
+ };
2744
2760
  const stopStateRecoveryReporting = onStateRecovery(statePath(), async ({ stateFile, quarantineFile, outcome, error }) => {
2745
2761
  await client.app?.log?.({
2746
2762
  body: {
@@ -3014,6 +3030,7 @@ var server = async ({ client }, options) => {
3014
3030
  locallyDeliveredPendingSessions.clear();
3015
3031
  nativeRetrySessions.clear();
3016
3032
  toolAttempts.clear();
3033
+ explicitResumeRequests.clear();
3017
3034
  },
3018
3035
  async config(config) {
3019
3036
  if (!registerCommand)
@@ -3116,7 +3133,10 @@ var server = async ({ client }, options) => {
3116
3133
  },
3117
3134
  async "command.execute.before"(input, output) {
3118
3135
  if (input.command === commandName) {
3119
- escapeGoalCommandArguments(output, goalCommandTemplate(commandName, locale), input.arguments);
3136
+ const sanitized = escapeGoalCommandArguments(output, goalCommandTemplate(commandName, locale), input.arguments);
3137
+ if (sanitized && input.arguments.trim().toLowerCase() === "resume") {
3138
+ explicitResumeRequests.add(input.sessionID);
3139
+ }
3120
3140
  return;
3121
3141
  }
3122
3142
  if (input.command !== "pause_goal" && input.command !== "resume_goal")
@@ -3124,6 +3144,8 @@ var server = async ({ client }, options) => {
3124
3144
  const template = goalStatusCommandTemplate(input.command, locale);
3125
3145
  if (!sanitizeGoalStatusCommandParts(output, template))
3126
3146
  return;
3147
+ if (input.command === "resume_goal")
3148
+ explicitResumeRequests.add(input.sessionID);
3127
3149
  if (input.command !== "pause_goal")
3128
3150
  return;
3129
3151
  const goal = await getGoal(input.sessionID);
@@ -3164,7 +3186,13 @@ var server = async ({ client }, options) => {
3164
3186
  async "chat.message"(input, output) {
3165
3187
  const sessionID = typeof input?.sessionID === "string" ? input.sessionID : output.message?.sessionID;
3166
3188
  const agent = typeof input?.agent === "string" && input.agent.trim() ? input.agent : output.message?.agent;
3167
- if (typeof sessionID !== "string" || typeof agent !== "string" || !agent.trim())
3189
+ if (typeof sessionID !== "string")
3190
+ return;
3191
+ explicitResumeRequests.delete(sessionID);
3192
+ if (output.parts?.some((part) => isExplicitResumePrompt(textFromPart(part), commandName, locale, messages))) {
3193
+ explicitResumeRequests.add(sessionID);
3194
+ }
3195
+ if (typeof agent !== "string" || !agent.trim())
3168
3196
  return;
3169
3197
  await recordPromptAgent(sessionID, agent);
3170
3198
  },
@@ -3215,6 +3243,7 @@ var server = async ({ client }, options) => {
3215
3243
  if (status.type === "busy")
3216
3244
  await markPendingContinuationStarted(sessionID);
3217
3245
  if (status.type === "idle") {
3246
+ explicitResumeRequests.delete(sessionID);
3218
3247
  busySessions.delete(sessionID);
3219
3248
  nativeRetrySessions.delete(sessionID);
3220
3249
  clearTurnWatchdog(sessionID);
@@ -3229,6 +3258,7 @@ var server = async ({ client }, options) => {
3229
3258
  }
3230
3259
  }
3231
3260
  if (sessionID && eventType === "session.idle") {
3261
+ explicitResumeRequests.delete(sessionID);
3232
3262
  busySessions.delete(sessionID);
3233
3263
  nativeRetrySessions.delete(sessionID);
3234
3264
  clearTurnWatchdog(sessionID);
@@ -3236,6 +3266,7 @@ var server = async ({ client }, options) => {
3236
3266
  taskTracker.observeSessionStatus(sessionID, "idle");
3237
3267
  }
3238
3268
  if (sessionID && eventType === "session.error") {
3269
+ explicitResumeRequests.delete(sessionID);
3239
3270
  const inNativeRetry = nativeRetrySessions.has(sessionID);
3240
3271
  busySessions.delete(sessionID);
3241
3272
  clearTurnWatchdog(sessionID);
@@ -3265,6 +3296,7 @@ var server = async ({ client }, options) => {
3265
3296
  }
3266
3297
  }
3267
3298
  if (sessionID && eventType === "session.deleted") {
3299
+ explicitResumeRequests.delete(sessionID);
3268
3300
  busySessions.delete(sessionID);
3269
3301
  clearTurnWatchdog(sessionID);
3270
3302
  watchdogRescuedSessions.delete(sessionID);
@@ -3323,6 +3355,7 @@ async function setupV2(context) {
3323
3355
  const locallyDeliveredPendingSessions = new Set;
3324
3356
  const watchdogRescuedSessions = new Set;
3325
3357
  const toolAttempts = new Map;
3358
+ const explicitResumeRequests = new Set;
3326
3359
  const planAgents = restrictedAgentSet(options);
3327
3360
  const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
3328
3361
  const activeContinuationsV2 = new Set;
@@ -3336,6 +3369,7 @@ async function setupV2(context) {
3336
3369
  messages,
3337
3370
  maxObjectiveChars: objectiveChars,
3338
3371
  isPlanAgent,
3372
+ consumeAutoTurnReset: (sessionID) => explicitResumeRequests.delete(sessionID),
3339
3373
  initializeUsage: async (sessionID) => {
3340
3374
  try {
3341
3375
  await accountUsage(sessionID, stepTokenSums.get(sessionID) ?? 0, { cumulative: true, source: "v2.steps" });
@@ -3706,6 +3740,7 @@ async function setupV2(context) {
3706
3740
  await markPendingContinuationStarted(sessionID);
3707
3741
  }
3708
3742
  if (status.type === "idle") {
3743
+ explicitResumeRequests.delete(sessionID);
3709
3744
  busySessions.delete(sessionID);
3710
3745
  nativeRetrySessions.delete(sessionID);
3711
3746
  clearTurnWatchdog(sessionID);
@@ -3728,6 +3763,7 @@ async function setupV2(context) {
3728
3763
  case "session.execution.succeeded":
3729
3764
  case "session.idle": {
3730
3765
  if (sessionID) {
3766
+ explicitResumeRequests.delete(sessionID);
3731
3767
  busySessions.delete(sessionID);
3732
3768
  nativeRetrySessions.delete(sessionID);
3733
3769
  clearTurnWatchdog(sessionID);
@@ -3744,6 +3780,7 @@ async function setupV2(context) {
3744
3780
  case "session.execution.interrupted": {
3745
3781
  if (!sessionID)
3746
3782
  return;
3783
+ explicitResumeRequests.delete(sessionID);
3747
3784
  stoppedExecutions.add(sessionID);
3748
3785
  busySessions.delete(sessionID);
3749
3786
  nativeRetrySessions.delete(sessionID);
@@ -3757,6 +3794,7 @@ async function setupV2(context) {
3757
3794
  case "session.execution.failed": {
3758
3795
  if (!sessionID)
3759
3796
  return;
3797
+ explicitResumeRequests.delete(sessionID);
3760
3798
  nativeRetrySessions.delete(sessionID);
3761
3799
  busySessions.delete(sessionID);
3762
3800
  clearTurnWatchdog(sessionID);
@@ -3791,6 +3829,7 @@ async function setupV2(context) {
3791
3829
  case "session.deleted": {
3792
3830
  if (!sessionID)
3793
3831
  return;
3832
+ explicitResumeRequests.delete(sessionID);
3794
3833
  stoppedExecutions.delete(sessionID);
3795
3834
  sessionOwnership.delete(sessionID);
3796
3835
  busySessions.delete(sessionID);
@@ -3956,6 +3995,9 @@ async function setupV2(context) {
3956
3995
  cancelScheduledContinuation(input.sessionID);
3957
3996
  clearTurnWatchdog(input.sessionID);
3958
3997
  }
3998
+ if (command.action === "resume" || command.action === "goal" && input.prompt.text.trim().toLowerCase() === "resume") {
3999
+ explicitResumeRequests.add(input.sessionID);
4000
+ }
3959
4001
  let forwardedPrompt = {};
3960
4002
  if (command.action === "goal") {
3961
4003
  const stripMention = ({ mention: _mention, ...attachment }) => attachment;
@@ -3977,25 +4019,31 @@ async function setupV2(context) {
3977
4019
  });
3978
4020
  }
3979
4021
  }));
4022
+ }
4023
+ if (registerCommand) {
3980
4024
  registrations.push(await context.session.hook("prompt", async (input) => {
3981
4025
  if (typeof input.sessionID === "string")
3982
4026
  markSessionOwnership(input.sessionID, true);
4027
+ explicitResumeRequests.delete(input.sessionID);
3983
4028
  const pauseTemplate = goalStatusCommandTemplate("pause_goal", locale);
3984
4029
  const resumeTemplate = goalStatusCommandTemplate("resume_goal", locale);
3985
4030
  const template = input.prompt.text.startsWith(pauseTemplate) ? pauseTemplate : input.prompt.text.startsWith(resumeTemplate) ? resumeTemplate : null;
3986
- if (!template)
3987
- return;
3988
- input.prompt.text = template;
3989
- delete input.prompt.files;
3990
- delete input.prompt.agents;
3991
- delete input.prompt.skills;
3992
- if (template !== pauseTemplate)
3993
- return;
3994
- const goal = await getGoal(input.sessionID);
3995
- if (goal?.status === "active")
3996
- await setGoalStatus(input.sessionID, "paused");
3997
- cancelScheduledContinuation(input.sessionID);
3998
- clearTurnWatchdog(input.sessionID);
4031
+ if (template) {
4032
+ input.prompt.text = template;
4033
+ delete input.prompt.files;
4034
+ delete input.prompt.agents;
4035
+ delete input.prompt.skills;
4036
+ if (template === pauseTemplate) {
4037
+ const goal = await getGoal(input.sessionID);
4038
+ if (goal?.status === "active")
4039
+ await setGoalStatus(input.sessionID, "paused");
4040
+ cancelScheduledContinuation(input.sessionID);
4041
+ clearTurnWatchdog(input.sessionID);
4042
+ }
4043
+ }
4044
+ if (isExplicitResumePrompt(input.prompt.text, commandName, locale, messages)) {
4045
+ explicitResumeRequests.add(input.sessionID);
4046
+ }
3999
4047
  }));
4000
4048
  }
4001
4049
  registrations.push(await context.tool.transform((draft) => {
@@ -4114,6 +4162,7 @@ async function setupV2(context) {
4114
4162
  locallyDeliveredPendingSessions.clear();
4115
4163
  watchdogRescuedSessions.clear();
4116
4164
  toolAttempts.clear();
4165
+ explicitResumeRequests.clear();
4117
4166
  for (const registration of registrations)
4118
4167
  await registration.dispose();
4119
4168
  const termination = Promise.allSettled([consumer, eventIterator?.return?.()]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
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",