@prevalentware/opencode-goal-plugin 0.1.34 → 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.
- package/dist/server.js +141 -14
- 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,
|
|
@@ -210,15 +218,36 @@ function isMissingStateFile(error) {
|
|
|
210
218
|
function mutableState(state) {
|
|
211
219
|
return JSON.parse(JSON.stringify(state));
|
|
212
220
|
}
|
|
221
|
+
var warnedEmptyStatePaths = new Set;
|
|
222
|
+
function isStatePadding(character) {
|
|
223
|
+
return character === "\x00" || character.trim() === "";
|
|
224
|
+
}
|
|
225
|
+
function parseStateText(raw, file) {
|
|
226
|
+
let start = 0;
|
|
227
|
+
let end = raw.length;
|
|
228
|
+
while (start < end && isStatePadding(raw[start]))
|
|
229
|
+
start += 1;
|
|
230
|
+
while (end > start && isStatePadding(raw[end - 1]))
|
|
231
|
+
end -= 1;
|
|
232
|
+
const content = raw.slice(start, end);
|
|
233
|
+
if (content)
|
|
234
|
+
return JSON.parse(content);
|
|
235
|
+
if (!warnedEmptyStatePaths.has(file)) {
|
|
236
|
+
warnedEmptyStatePaths.add(file);
|
|
237
|
+
console.warn(`[opencode-goal-plugin] Empty or zero-filled state file at ${file}; recovering with empty state.`);
|
|
238
|
+
}
|
|
239
|
+
return emptyState();
|
|
240
|
+
}
|
|
213
241
|
function decodeState(value) {
|
|
214
242
|
return Schema.decodeUnknown(StateSchema)(value).pipe(Effect.map(mutableState), Effect.map(normalizeState), Effect.mapError((cause) => new StateDecodeError({ cause })));
|
|
215
243
|
}
|
|
216
244
|
function readStateEffect() {
|
|
245
|
+
const file = statePath();
|
|
217
246
|
return Effect.tryPromise({
|
|
218
|
-
try: () => readFile(
|
|
247
|
+
try: () => readFile(file, "utf8"),
|
|
219
248
|
catch: (cause) => new StateReadError({ cause })
|
|
220
249
|
}).pipe(Effect.flatMap((raw) => Effect.try({
|
|
221
|
-
try: () =>
|
|
250
|
+
try: () => parseStateText(raw, file),
|
|
222
251
|
catch: (cause) => new StateDecodeError({ cause })
|
|
223
252
|
})), Effect.flatMap(decodeState), Effect.catchAll((error) => error._tag === "StateReadError" && isMissingStateFile(error.cause) ? Effect.succeed(emptyState()) : Effect.fail(error)));
|
|
224
253
|
}
|
|
@@ -294,12 +323,34 @@ function normalizeGoal(goal) {
|
|
|
294
323
|
goal.maxAutoTurns = positiveIntegerOrNull(goal.maxAutoTurns);
|
|
295
324
|
goal.maxDurationSeconds = positiveIntegerOrNull(goal.maxDurationSeconds);
|
|
296
325
|
goal.tokenBudget = positiveIntegerOrNull(goal.tokenBudget);
|
|
326
|
+
goal.usageTrackers = normalizeUsageTrackers(goal.usageTrackers);
|
|
297
327
|
goal.noProgressTokenThreshold = positiveIntegerOrNull(goal.noProgressTokenThreshold) ?? DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD;
|
|
298
328
|
goal.maxNoProgressTurns = positiveIntegerOrNull(goal.maxNoProgressTurns) ?? DEFAULT_MAX_NO_PROGRESS_TURNS;
|
|
299
329
|
goal.budgetWrapupSent = goal.budgetWrapupSent === true;
|
|
300
330
|
goal.stopReason ??= null;
|
|
301
331
|
return goal;
|
|
302
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
|
+
}
|
|
303
354
|
function normalizePendingAttempt(attempt) {
|
|
304
355
|
if (!attempt || typeof attempt !== "object")
|
|
305
356
|
return null;
|
|
@@ -344,6 +395,9 @@ function positiveIntegerOrNull(value) {
|
|
|
344
395
|
function nonNegativeInteger(value, fallback) {
|
|
345
396
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
346
397
|
}
|
|
398
|
+
function nonNegativeIntegerOrNull(value) {
|
|
399
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
400
|
+
}
|
|
347
401
|
function isClosed(status) {
|
|
348
402
|
return status === "complete" || status === "unmet";
|
|
349
403
|
}
|
|
@@ -423,6 +477,7 @@ async function createGoal(sessionID, objective, options) {
|
|
|
423
477
|
status: normalizedOptions.initialStatus,
|
|
424
478
|
tokenBudget: normalizedOptions.tokenBudget,
|
|
425
479
|
tokensUsed: 0,
|
|
480
|
+
usageTrackers: {},
|
|
426
481
|
timeUsedSeconds: 0,
|
|
427
482
|
createdAt: now,
|
|
428
483
|
updatedAt: now,
|
|
@@ -584,14 +639,66 @@ async function clearGoal(sessionID) {
|
|
|
584
639
|
return existed;
|
|
585
640
|
});
|
|
586
641
|
}
|
|
587
|
-
async function accountUsage(sessionID, tokensUsed) {
|
|
642
|
+
async function accountUsage(sessionID, tokensUsed, options) {
|
|
588
643
|
return mutate((state) => {
|
|
589
644
|
const goal = state.goals[sessionID];
|
|
590
645
|
if (!goal)
|
|
591
646
|
return null;
|
|
592
647
|
accountWallClock(goal);
|
|
593
648
|
if (typeof tokensUsed === "number" && Number.isFinite(tokensUsed)) {
|
|
594
|
-
|
|
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
|
+
}
|
|
595
702
|
}
|
|
596
703
|
maybeStopForBudget(goal);
|
|
597
704
|
goal.updatedAt = nowSeconds();
|
|
@@ -1076,7 +1183,7 @@ function commandNameFromOptions(options) {
|
|
|
1076
1183
|
function positiveIntegerOrNull2(value) {
|
|
1077
1184
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
1078
1185
|
}
|
|
1079
|
-
function
|
|
1186
|
+
function nonNegativeIntegerOrNull2(value) {
|
|
1080
1187
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
1081
1188
|
}
|
|
1082
1189
|
function timeoutMillisecondsFromSeconds(value) {
|
|
@@ -1169,9 +1276,9 @@ function outputTokensFromMessage(message) {
|
|
|
1169
1276
|
return outputTokensFromRecord(message.info.tokens);
|
|
1170
1277
|
return;
|
|
1171
1278
|
}
|
|
1172
|
-
function
|
|
1279
|
+
function usageFromMessages(messages) {
|
|
1173
1280
|
const exactTotal = messages.reduce((sum, message) => sum + (exactTokensFromMessage(message) ?? 0), 0);
|
|
1174
|
-
return exactTotal > 0 ? exactTotal : estimateMessages(messages);
|
|
1281
|
+
return exactTotal > 0 ? { tokens: exactTotal, source: "v1.messages.exact" } : { tokens: estimateMessages(messages), source: "v1.messages.estimated" };
|
|
1175
1282
|
}
|
|
1176
1283
|
function taskHeader(output) {
|
|
1177
1284
|
const resultIndex = output.search(/<task_(?:result|error)>/);
|
|
@@ -1675,6 +1782,7 @@ async function createGoalFromTool(input, context, services) {
|
|
|
1675
1782
|
agent: typeof context.agent === "string" ? context.agent : null,
|
|
1676
1783
|
initialStatus: planningOnly ? "paused" : "active"
|
|
1677
1784
|
});
|
|
1785
|
+
await services.initializeUsage?.(context.sessionID);
|
|
1678
1786
|
return JSON.stringify(planningOnly ? { goal, plan_mode_notice: PLAN_MODE_CREATE_NOTICE } : { goal }, null, 2);
|
|
1679
1787
|
}
|
|
1680
1788
|
async function updateGoalObjectiveFromTool(input, context, services) {
|
|
@@ -1737,7 +1845,7 @@ var server = async ({ client }, options) => {
|
|
|
1737
1845
|
const autoContinue = options?.auto_continue ?? true;
|
|
1738
1846
|
const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
|
|
1739
1847
|
const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
1740
|
-
const minInterval =
|
|
1848
|
+
const minInterval = nonNegativeIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
1741
1849
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
|
|
1742
1850
|
const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
1743
1851
|
const registerCommand = options?.register_command ?? true;
|
|
@@ -2146,7 +2254,8 @@ var server = async ({ client }, options) => {
|
|
|
2146
2254
|
const sessionID = "sessionID" in input && typeof input.sessionID === "string" ? input.sessionID : output.messages.find((message) => typeof message.info.sessionID === "string")?.info.sessionID;
|
|
2147
2255
|
if (!sessionID)
|
|
2148
2256
|
return;
|
|
2149
|
-
|
|
2257
|
+
const usage = usageFromMessages(output.messages);
|
|
2258
|
+
await accountUsage(sessionID, usage.tokens, { cumulative: true, source: usage.source });
|
|
2150
2259
|
const observed = await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
|
|
2151
2260
|
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
2152
2261
|
const scheduled = scheduledContinuations.get(sessionID);
|
|
@@ -2277,7 +2386,7 @@ async function setupV2(context) {
|
|
|
2277
2386
|
const autoContinue = options.auto_continue ?? true;
|
|
2278
2387
|
const deferWhileTasksActive = options.defer_while_tasks_active ?? true;
|
|
2279
2388
|
const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
2280
|
-
const minInterval =
|
|
2389
|
+
const minInterval = nonNegativeIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
2281
2390
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
|
|
2282
2391
|
const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
2283
2392
|
const registerCommand = options.register_command ?? true;
|
|
@@ -2293,11 +2402,21 @@ async function setupV2(context) {
|
|
|
2293
2402
|
const toolAttempts = new Map;
|
|
2294
2403
|
const planAgents = restrictedAgentSet(options);
|
|
2295
2404
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
2296
|
-
const goalServices = { options, isPlanAgent };
|
|
2297
2405
|
const activeContinuationsV2 = new Set;
|
|
2298
2406
|
const latestStepBySession = new Map;
|
|
2299
2407
|
const stepTextBuffers = new Map;
|
|
2300
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
|
+
};
|
|
2301
2420
|
const registrations = [];
|
|
2302
2421
|
let disposed = false;
|
|
2303
2422
|
function stepKey(sessionID, messageID2) {
|
|
@@ -2698,7 +2817,11 @@ async function setupV2(context) {
|
|
|
2698
2817
|
if (typeof tokens === "number") {
|
|
2699
2818
|
const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
|
|
2700
2819
|
stepTokenSums.set(sessionID, sum);
|
|
2701
|
-
await accountUsage(sessionID, sum
|
|
2820
|
+
await accountUsage(sessionID, sum, {
|
|
2821
|
+
cumulative: true,
|
|
2822
|
+
source: "v2.steps",
|
|
2823
|
+
initialBaseline: Math.ceil(sum - tokens)
|
|
2824
|
+
});
|
|
2702
2825
|
}
|
|
2703
2826
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
2704
2827
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
@@ -2734,7 +2857,11 @@ async function setupV2(context) {
|
|
|
2734
2857
|
if (typeof tokens === "number") {
|
|
2735
2858
|
const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens;
|
|
2736
2859
|
stepTokenSums.set(sessionID, sum);
|
|
2737
|
-
await accountUsage(sessionID, sum
|
|
2860
|
+
await accountUsage(sessionID, sum, {
|
|
2861
|
+
cumulative: true,
|
|
2862
|
+
source: "v2.steps",
|
|
2863
|
+
initialBaseline: Math.ceil(sum - tokens)
|
|
2864
|
+
});
|
|
2738
2865
|
}
|
|
2739
2866
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
2740
2867
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
@@ -2767,7 +2894,7 @@ async function setupV2(context) {
|
|
|
2767
2894
|
return;
|
|
2768
2895
|
const tokens = tokensFromRecord(data.tokens);
|
|
2769
2896
|
if (typeof tokens === "number")
|
|
2770
|
-
await accountUsage(sessionID, tokens);
|
|
2897
|
+
await accountUsage(sessionID, tokens, { cumulative: true, source: "v2.session" });
|
|
2771
2898
|
return;
|
|
2772
2899
|
}
|
|
2773
2900
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prevalentware/opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
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",
|