@prevalentware/opencode-goal-plugin 0.1.35 → 0.1.36

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 (2) hide show
  1. package/dist/server.js +118 -12
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -152,12 +152,20 @@ var PendingAttemptSchema = Schema.Struct({
152
152
  armNoProgress: Schema.Boolean,
153
153
  previousLastContinuationAt: Schema.NullOr(Schema.Number)
154
154
  });
155
+ var UsageTrackerSchema = Schema.Struct({
156
+ baseline: Schema.optionalWith(Schema.Unknown, { default: () => null }),
157
+ lastObserved: Schema.optionalWith(Schema.Unknown, { default: () => null }),
158
+ baseTokens: Schema.optionalWith(Schema.Unknown, { default: () => null }),
159
+ pendingBaseline: Schema.optionalWith(Schema.Unknown, { default: () => null }),
160
+ pendingBaseTokens: Schema.optionalWith(Schema.Unknown, { default: () => null })
161
+ });
155
162
  var GoalSchema = Schema.Struct({
156
163
  sessionID: Schema.String,
157
164
  objective: Schema.String,
158
165
  status: Schema.Literal("active", "paused", "budgetLimited", "usageLimited", "complete", "unmet"),
159
166
  tokenBudget: NullableNumber,
160
167
  tokensUsed: Schema.Number,
168
+ usageTrackers: Schema.optionalWith(Schema.Record({ key: Schema.String, value: UsageTrackerSchema }), { default: () => ({}) }),
161
169
  timeUsedSeconds: Schema.Number,
162
170
  createdAt: Schema.Number,
163
171
  updatedAt: Schema.Number,
@@ -315,12 +323,34 @@ function normalizeGoal(goal) {
315
323
  goal.maxAutoTurns = positiveIntegerOrNull(goal.maxAutoTurns);
316
324
  goal.maxDurationSeconds = positiveIntegerOrNull(goal.maxDurationSeconds);
317
325
  goal.tokenBudget = positiveIntegerOrNull(goal.tokenBudget);
326
+ goal.usageTrackers = normalizeUsageTrackers(goal.usageTrackers);
318
327
  goal.noProgressTokenThreshold = positiveIntegerOrNull(goal.noProgressTokenThreshold) ?? DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD;
319
328
  goal.maxNoProgressTurns = positiveIntegerOrNull(goal.maxNoProgressTurns) ?? DEFAULT_MAX_NO_PROGRESS_TURNS;
320
329
  goal.budgetWrapupSent = goal.budgetWrapupSent === true;
321
330
  goal.stopReason ??= null;
322
331
  return goal;
323
332
  }
333
+ function normalizeUsageTrackers(trackers) {
334
+ const normalized = {};
335
+ for (const [source, rawTracker] of Object.entries(trackers ?? {})) {
336
+ const tracker = rawTracker;
337
+ const baseline = nonNegativeIntegerOrNull(tracker?.baseline);
338
+ const lastObserved = nonNegativeIntegerOrNull(tracker?.lastObserved);
339
+ const baseTokens = nonNegativeIntegerOrNull(tracker?.baseTokens);
340
+ if (source && baseline != null && lastObserved != null && baseTokens != null && lastObserved >= baseline) {
341
+ const pendingBaseline = nonNegativeIntegerOrNull(tracker.pendingBaseline);
342
+ const pendingBaseTokens = nonNegativeIntegerOrNull(tracker.pendingBaseTokens);
343
+ normalized[source] = {
344
+ baseline,
345
+ lastObserved,
346
+ baseTokens,
347
+ pendingBaseline,
348
+ pendingBaseTokens: pendingBaseline == null ? null : pendingBaseTokens
349
+ };
350
+ }
351
+ }
352
+ return normalized;
353
+ }
324
354
  function normalizePendingAttempt(attempt) {
325
355
  if (!attempt || typeof attempt !== "object")
326
356
  return null;
@@ -365,6 +395,9 @@ function positiveIntegerOrNull(value) {
365
395
  function nonNegativeInteger(value, fallback) {
366
396
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
367
397
  }
398
+ function nonNegativeIntegerOrNull(value) {
399
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
400
+ }
368
401
  function isClosed(status) {
369
402
  return status === "complete" || status === "unmet";
370
403
  }
@@ -444,6 +477,7 @@ async function createGoal(sessionID, objective, options) {
444
477
  status: normalizedOptions.initialStatus,
445
478
  tokenBudget: normalizedOptions.tokenBudget,
446
479
  tokensUsed: 0,
480
+ usageTrackers: {},
447
481
  timeUsedSeconds: 0,
448
482
  createdAt: now,
449
483
  updatedAt: now,
@@ -605,14 +639,66 @@ async function clearGoal(sessionID) {
605
639
  return existed;
606
640
  });
607
641
  }
608
- async function accountUsage(sessionID, tokensUsed) {
642
+ async function accountUsage(sessionID, tokensUsed, options) {
609
643
  return mutate((state) => {
610
644
  const goal = state.goals[sessionID];
611
645
  if (!goal)
612
646
  return null;
613
647
  accountWallClock(goal);
614
648
  if (typeof tokensUsed === "number" && Number.isFinite(tokensUsed)) {
615
- goal.tokensUsed = Math.max(goal.tokensUsed, Math.max(0, Math.ceil(tokensUsed)));
649
+ const observed = Math.max(0, Math.ceil(tokensUsed));
650
+ if (options?.cumulative === true) {
651
+ const source = options.source?.trim() || "default";
652
+ let tracker = goal.usageTrackers[source];
653
+ if (!tracker) {
654
+ const initialBaseline = nonNegativeIntegerOrNull(options.initialBaseline);
655
+ tracker = initialBaseline != null && initialBaseline <= observed ? {
656
+ baseline: initialBaseline,
657
+ lastObserved: observed,
658
+ baseTokens: goal.tokensUsed,
659
+ pendingBaseline: null,
660
+ pendingBaseTokens: null
661
+ } : {
662
+ baseline: observed,
663
+ lastObserved: observed,
664
+ baseTokens: goal.tokensUsed,
665
+ pendingBaseline: null,
666
+ pendingBaseTokens: null
667
+ };
668
+ goal.usageTrackers[source] = tracker;
669
+ } else if (observed < tracker.lastObserved) {
670
+ const initialBaseline = nonNegativeIntegerOrNull(options.initialBaseline);
671
+ if (initialBaseline != null && initialBaseline <= observed) {
672
+ tracker = {
673
+ baseline: initialBaseline,
674
+ lastObserved: observed,
675
+ baseTokens: goal.tokensUsed,
676
+ pendingBaseline: null,
677
+ pendingBaseTokens: null
678
+ };
679
+ goal.usageTrackers[source] = tracker;
680
+ } else if (tracker.pendingBaseline == null || observed < tracker.pendingBaseline) {
681
+ tracker.pendingBaseline = observed;
682
+ tracker.pendingBaseTokens = goal.tokensUsed;
683
+ } else {
684
+ tracker = {
685
+ baseline: tracker.pendingBaseline,
686
+ lastObserved: observed,
687
+ baseTokens: tracker.pendingBaseTokens ?? goal.tokensUsed,
688
+ pendingBaseline: null,
689
+ pendingBaseTokens: null
690
+ };
691
+ goal.usageTrackers[source] = tracker;
692
+ }
693
+ } else {
694
+ tracker.lastObserved = observed;
695
+ tracker.pendingBaseline = null;
696
+ tracker.pendingBaseTokens = null;
697
+ }
698
+ goal.tokensUsed = Math.max(goal.tokensUsed, tracker.baseTokens + observed - tracker.baseline);
699
+ } else {
700
+ goal.tokensUsed = Math.max(goal.tokensUsed, observed);
701
+ }
616
702
  }
617
703
  maybeStopForBudget(goal);
618
704
  goal.updatedAt = nowSeconds();
@@ -1097,7 +1183,7 @@ function commandNameFromOptions(options) {
1097
1183
  function positiveIntegerOrNull2(value) {
1098
1184
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1099
1185
  }
1100
- function nonNegativeIntegerOrNull(value) {
1186
+ function nonNegativeIntegerOrNull2(value) {
1101
1187
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
1102
1188
  }
1103
1189
  function timeoutMillisecondsFromSeconds(value) {
@@ -1190,9 +1276,9 @@ function outputTokensFromMessage(message) {
1190
1276
  return outputTokensFromRecord(message.info.tokens);
1191
1277
  return;
1192
1278
  }
1193
- function tokensFromMessages(messages) {
1279
+ function usageFromMessages(messages) {
1194
1280
  const exactTotal = messages.reduce((sum, message) => sum + (exactTokensFromMessage(message) ?? 0), 0);
1195
- return exactTotal > 0 ? exactTotal : estimateMessages(messages);
1281
+ return exactTotal > 0 ? { tokens: exactTotal, source: "v1.messages.exact" } : { tokens: estimateMessages(messages), source: "v1.messages.estimated" };
1196
1282
  }
1197
1283
  function taskHeader(output) {
1198
1284
  const resultIndex = output.search(/<task_(?:result|error)>/);
@@ -1696,6 +1782,7 @@ async function createGoalFromTool(input, context, services) {
1696
1782
  agent: typeof context.agent === "string" ? context.agent : null,
1697
1783
  initialStatus: planningOnly ? "paused" : "active"
1698
1784
  });
1785
+ await services.initializeUsage?.(context.sessionID);
1699
1786
  return JSON.stringify(planningOnly ? { goal, plan_mode_notice: PLAN_MODE_CREATE_NOTICE } : { goal }, null, 2);
1700
1787
  }
1701
1788
  async function updateGoalObjectiveFromTool(input, context, services) {
@@ -1758,7 +1845,7 @@ var server = async ({ client }, options) => {
1758
1845
  const autoContinue = options?.auto_continue ?? true;
1759
1846
  const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
1760
1847
  const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
1761
- const minInterval = nonNegativeIntegerOrNull(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
1848
+ const minInterval = nonNegativeIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
1762
1849
  const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
1763
1850
  const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
1764
1851
  const registerCommand = options?.register_command ?? true;
@@ -2167,7 +2254,8 @@ var server = async ({ client }, options) => {
2167
2254
  const sessionID = "sessionID" in input && typeof input.sessionID === "string" ? input.sessionID : output.messages.find((message) => typeof message.info.sessionID === "string")?.info.sessionID;
2168
2255
  if (!sessionID)
2169
2256
  return;
2170
- await accountUsage(sessionID, tokensFromMessages(output.messages));
2257
+ const usage = usageFromMessages(output.messages);
2258
+ await accountUsage(sessionID, usage.tokens, { cumulative: true, source: usage.source });
2171
2259
  const observed = await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
2172
2260
  await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
2173
2261
  const scheduled = scheduledContinuations.get(sessionID);
@@ -2298,7 +2386,7 @@ async function setupV2(context) {
2298
2386
  const autoContinue = options.auto_continue ?? true;
2299
2387
  const deferWhileTasksActive = options.defer_while_tasks_active ?? true;
2300
2388
  const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
2301
- const minInterval = nonNegativeIntegerOrNull(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
2389
+ const minInterval = nonNegativeIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
2302
2390
  const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
2303
2391
  const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
2304
2392
  const registerCommand = options.register_command ?? true;
@@ -2314,11 +2402,21 @@ async function setupV2(context) {
2314
2402
  const toolAttempts = new Map;
2315
2403
  const planAgents = restrictedAgentSet(options);
2316
2404
  const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
2317
- const goalServices = { options, isPlanAgent };
2318
2405
  const activeContinuationsV2 = new Set;
2319
2406
  const latestStepBySession = new Map;
2320
2407
  const stepTextBuffers = new Map;
2321
2408
  const stepTokenSums = new Map;
2409
+ const goalServices = {
2410
+ options,
2411
+ isPlanAgent,
2412
+ initializeUsage: async (sessionID) => {
2413
+ try {
2414
+ await accountUsage(sessionID, stepTokenSums.get(sessionID) ?? 0, { cumulative: true, source: "v2.steps" });
2415
+ } catch (error) {
2416
+ v2ErrorLog("Failed to initialize goal usage accounting", error);
2417
+ }
2418
+ }
2419
+ };
2322
2420
  const registrations = [];
2323
2421
  let disposed = false;
2324
2422
  function stepKey(sessionID, messageID2) {
@@ -2719,7 +2817,11 @@ async function setupV2(context) {
2719
2817
  if (typeof tokens === "number") {
2720
2818
  const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
2721
2819
  stepTokenSums.set(sessionID, sum);
2722
- await accountUsage(sessionID, sum);
2820
+ await accountUsage(sessionID, sum, {
2821
+ cumulative: true,
2822
+ source: "v2.steps",
2823
+ initialBaseline: Math.ceil(sum - tokens)
2824
+ });
2723
2825
  }
2724
2826
  const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
2725
2827
  stepTextBuffers.delete(stepKey(sessionID, messageID2));
@@ -2755,7 +2857,11 @@ async function setupV2(context) {
2755
2857
  if (typeof tokens === "number") {
2756
2858
  const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
2757
2859
  stepTokenSums.set(sessionID, sum);
2758
- await accountUsage(sessionID, sum);
2860
+ await accountUsage(sessionID, sum, {
2861
+ cumulative: true,
2862
+ source: "v2.steps",
2863
+ initialBaseline: Math.ceil(sum - tokens)
2864
+ });
2759
2865
  }
2760
2866
  const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
2761
2867
  stepTextBuffers.delete(stepKey(sessionID, messageID2));
@@ -2788,7 +2894,7 @@ async function setupV2(context) {
2788
2894
  return;
2789
2895
  const tokens = tokensFromRecord(data.tokens);
2790
2896
  if (typeof tokens === "number")
2791
- await accountUsage(sessionID, tokens);
2897
+ await accountUsage(sessionID, tokens, { cumulative: true, source: "v2.session" });
2792
2898
  return;
2793
2899
  }
2794
2900
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
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",