product-console-companion 0.7.4 → 0.7.6
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/cli.js +48 -10
- package/dist/investigation-reader.js +7 -6
- package/dist/usage-limit.js +130 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { anthropicTextModel } from './text-model.js';
|
|
|
14
14
|
import { MappingFailure, runSurfaceMapping, SURFACE_SEED_MODEL } from './surface-mapper.js';
|
|
15
15
|
import { assertSnapshotReadBoundary, createSandbox } from './sandbox.js';
|
|
16
16
|
import { runGitRangeTask } from './git-range.js';
|
|
17
|
+
import { isUsageLimitError, UsageLimitPause } from './usage-limit.js';
|
|
17
18
|
const DEFAULT_CODEX_MODEL = 'gpt-6-astra';
|
|
18
19
|
const SURFACE_MAPPING_UNAVAILABLE_STATUSES = new Set([401, 403, 404, 405]);
|
|
19
20
|
let expectedExitReason = null;
|
|
@@ -545,6 +546,7 @@ export async function runOneTask(runtime, client, repoPath, task, connection) {
|
|
|
545
546
|
console.log(runtime.backend === 'codex'
|
|
546
547
|
? `${done}; token usage: ${formatCodexUsage(runtime.takeCodexUsage())}; dollar budget is not enforced (Codex reports tokens and has no cost cap)`
|
|
547
548
|
: done);
|
|
549
|
+
return null;
|
|
548
550
|
}
|
|
549
551
|
catch (err) {
|
|
550
552
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -560,6 +562,7 @@ export async function runOneTask(runtime, client, repoPath, task, connection) {
|
|
|
560
562
|
catch (reportErr) {
|
|
561
563
|
console.error(' could not even report the error back:', reportErr instanceof Error ? reportErr.message : reportErr);
|
|
562
564
|
}
|
|
565
|
+
return runtime.backend === 'cli' && isUsageLimitError(err) ? message : null;
|
|
563
566
|
}
|
|
564
567
|
finally {
|
|
565
568
|
clearInterval(heartbeat);
|
|
@@ -591,9 +594,25 @@ async function runOneInvestigation(anthropic, client, connections, runtimes, req
|
|
|
591
594
|
const runtime = request.requestedSources
|
|
592
595
|
.map((source) => runtimes.get(source.connectionId))
|
|
593
596
|
.find((candidate) => !!candidate);
|
|
594
|
-
const
|
|
597
|
+
const baseCallModel = runtime
|
|
595
598
|
? structuredModelCallerForRuntime(runtime, anthropic)
|
|
596
599
|
: (modelRequest) => runStructuredModelCall(anthropic, modelRequest);
|
|
600
|
+
let usageLimitMessage = null;
|
|
601
|
+
const callModel = backend === 'cli'
|
|
602
|
+
? async (modelRequest) => {
|
|
603
|
+
if (usageLimitMessage)
|
|
604
|
+
throw new Error(usageLimitMessage);
|
|
605
|
+
try {
|
|
606
|
+
return await baseCallModel(modelRequest);
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
if (isUsageLimitError(error)) {
|
|
610
|
+
usageLimitMessage = error instanceof Error ? error.message : String(error);
|
|
611
|
+
}
|
|
612
|
+
throw error;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
: baseCallModel;
|
|
597
616
|
const validationRejectedObligationIds = new Set();
|
|
598
617
|
try {
|
|
599
618
|
const result = await runAndReportInvestigation(request, connections, callModel, async (envelope) => {
|
|
@@ -622,6 +641,7 @@ async function runOneInvestigation(anthropic, client, connections, runtimes, req
|
|
|
622
641
|
catch (error) {
|
|
623
642
|
console.error(` RESULT NOT DELIVERED for ${request.investigationId} attempt ${request.attempt}; no automatic retry will be made and the claim must expire:`, error instanceof Error ? error.message : error);
|
|
624
643
|
}
|
|
644
|
+
return usageLimitMessage;
|
|
625
645
|
}
|
|
626
646
|
async function runOneSurfaceMapping(anthropic, client, connection, runtime, job) {
|
|
627
647
|
console.log(`[${new Date().toISOString()}] claimed surface mapping ${job.jobId} attempt ${job.attempt} via ${runtime.backend}`);
|
|
@@ -643,13 +663,17 @@ async function runOneSurfaceMapping(anthropic, client, connection, runtime, job)
|
|
|
643
663
|
const status = surfaceMappingUnavailableStatus(error);
|
|
644
664
|
if (status !== null) {
|
|
645
665
|
console.error(` automatic mapping result could not be reported (HTTP ${status}); abandoning the job so its lease can expire.`);
|
|
646
|
-
return;
|
|
666
|
+
return null;
|
|
647
667
|
}
|
|
648
668
|
throw error;
|
|
649
669
|
}
|
|
650
670
|
console.log(` automatic map ${acknowledgment.mapId} delivered (${manifest.expansion.files.length} files, expansion ${manifest.expansion.status})`);
|
|
671
|
+
return null;
|
|
651
672
|
}
|
|
652
673
|
catch (error) {
|
|
674
|
+
const usageLimitMessage = runtime.backend === 'cli' && isUsageLimitError(error)
|
|
675
|
+
? (error instanceof Error ? error.message : String(error))
|
|
676
|
+
: null;
|
|
653
677
|
const report = error instanceof MappingFailure
|
|
654
678
|
? { kind: error.kind, code: error.code, detail: error.message }
|
|
655
679
|
: {
|
|
@@ -665,10 +689,11 @@ async function runOneSurfaceMapping(anthropic, client, connection, runtime, job)
|
|
|
665
689
|
const status = surfaceMappingUnavailableStatus(reportError);
|
|
666
690
|
if (status !== null) {
|
|
667
691
|
console.error(` automatic mapping error could not be reported (HTTP ${status}); abandoning the job so its lease can expire.`);
|
|
668
|
-
return;
|
|
692
|
+
return usageLimitMessage;
|
|
669
693
|
}
|
|
670
694
|
console.error(' could not report the mapping error:', reportError instanceof Error ? reportError.message : reportError);
|
|
671
695
|
}
|
|
696
|
+
return usageLimitMessage;
|
|
672
697
|
}
|
|
673
698
|
}
|
|
674
699
|
async function cmdRun(options = {}) {
|
|
@@ -812,6 +837,7 @@ async function cmdRun(options = {}) {
|
|
|
812
837
|
let nextInvestigationPollAt = 0;
|
|
813
838
|
let surfaceMappingPollingEnabled = true;
|
|
814
839
|
const readinessReporting = { enabled: true, loggedErrors: new Set() };
|
|
840
|
+
const usageLimitPause = new UsageLimitPause();
|
|
815
841
|
while (!stopping) {
|
|
816
842
|
try {
|
|
817
843
|
let claimedAny = false;
|
|
@@ -846,15 +872,25 @@ async function cmdRun(options = {}) {
|
|
|
846
872
|
expectedExitReason = 'the server rejected its token; pair a new token and reconnect';
|
|
847
873
|
return;
|
|
848
874
|
}
|
|
875
|
+
let usageClaimLimit = usageLimitPause.claimLimit();
|
|
876
|
+
const mayClaim = () => usageClaimLimit === null || usageClaimLimit > 0;
|
|
877
|
+
const recordCompletedClaim = (usageLimitMessage) => {
|
|
878
|
+
if (usageClaimLimit !== null)
|
|
879
|
+
usageClaimLimit--;
|
|
880
|
+
if (usageLimitMessage) {
|
|
881
|
+
usageLimitPause.pause(usageLimitMessage);
|
|
882
|
+
usageClaimLimit = 0;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
849
885
|
for (const c of readyConnections) {
|
|
850
|
-
if (stopping)
|
|
886
|
+
if (stopping || !mayClaim())
|
|
851
887
|
break;
|
|
852
888
|
if (surfaceMappingPollingEnabled) {
|
|
853
889
|
try {
|
|
854
890
|
const mapping = await client.claimNextSurfaceMapping(c.connectionId);
|
|
855
891
|
if (mapping.job) {
|
|
856
892
|
claimedAny = true;
|
|
857
|
-
await runOneSurfaceMapping(anthropic, client, c, runtimes.get(c.connectionId), mapping.job);
|
|
893
|
+
recordCompletedClaim(await runOneSurfaceMapping(anthropic, client, c, runtimes.get(c.connectionId), mapping.job));
|
|
858
894
|
}
|
|
859
895
|
}
|
|
860
896
|
catch (err) {
|
|
@@ -868,6 +904,8 @@ async function cmdRun(options = {}) {
|
|
|
868
904
|
}
|
|
869
905
|
}
|
|
870
906
|
}
|
|
907
|
+
if (!mayClaim())
|
|
908
|
+
break;
|
|
871
909
|
let claim;
|
|
872
910
|
try {
|
|
873
911
|
claim = await client.claimNext(c.connectionId);
|
|
@@ -884,9 +922,9 @@ async function cmdRun(options = {}) {
|
|
|
884
922
|
if (!claim.task)
|
|
885
923
|
continue;
|
|
886
924
|
claimedAny = true;
|
|
887
|
-
await runOneTask(runtimes.get(c.connectionId), client, c.repoPath, claim.task, c);
|
|
925
|
+
recordCompletedClaim(await runOneTask(runtimes.get(c.connectionId), client, c.repoPath, claim.task, c));
|
|
888
926
|
}
|
|
889
|
-
if (!stopping && workspaceRuntime) {
|
|
927
|
+
if (!stopping && workspaceRuntime && mayClaim()) {
|
|
890
928
|
let claim;
|
|
891
929
|
try {
|
|
892
930
|
claim = await client.claimNext();
|
|
@@ -903,10 +941,10 @@ async function cmdRun(options = {}) {
|
|
|
903
941
|
if (claim.task) {
|
|
904
942
|
claimedAny = true;
|
|
905
943
|
// Connection-less task kinds do not read a repo; cwd is a harmless placeholder.
|
|
906
|
-
await runOneTask(workspaceRuntime, client, process.cwd(), claim.task);
|
|
944
|
+
recordCompletedClaim(await runOneTask(workspaceRuntime, client, process.cwd(), claim.task));
|
|
907
945
|
}
|
|
908
946
|
}
|
|
909
|
-
if (!stopping && readyConnections.length > 0
|
|
947
|
+
if (!stopping && mayClaim() && readyConnections.length > 0
|
|
910
948
|
&& readyConnections.length === activeConnections.length
|
|
911
949
|
&& activeConnections.length === config.connections.length
|
|
912
950
|
&& Date.now() >= nextInvestigationPollAt) {
|
|
@@ -916,7 +954,7 @@ async function cmdRun(options = {}) {
|
|
|
916
954
|
nextInvestigationPollAt = 0;
|
|
917
955
|
if (investigation) {
|
|
918
956
|
claimedAny = true;
|
|
919
|
-
await runOneInvestigation(anthropic, client, activeConnections, runtimes, investigation);
|
|
957
|
+
recordCompletedClaim(await runOneInvestigation(anthropic, client, activeConnections, runtimes, investigation));
|
|
920
958
|
}
|
|
921
959
|
}
|
|
922
960
|
catch (err) {
|
|
@@ -7,6 +7,7 @@ import { listRepoFiles } from './local-scan.js';
|
|
|
7
7
|
import { obtainRequestedSources } from './observed-sources.js';
|
|
8
8
|
import { scopeCovers, validateReaderResult } from './reader-result-contract.js';
|
|
9
9
|
import { gitErrorMessage } from './sandbox.js';
|
|
10
|
+
import { isUsageLimitError } from './usage-limit.js';
|
|
10
11
|
export const MAX_INVESTIGATION_CANDIDATE_FILES = 24;
|
|
11
12
|
export const MAX_EVIDENCE_FILE_BYTES = 512 * 1024;
|
|
12
13
|
/**
|
|
@@ -103,7 +104,10 @@ const assessedScopeSchema = {
|
|
|
103
104
|
additionalProperties: false,
|
|
104
105
|
required: ['breadth'],
|
|
105
106
|
properties: {
|
|
106
|
-
breadth: {
|
|
107
|
+
breadth: {
|
|
108
|
+
enum: ['element', 'named_paths', 'module_surface'],
|
|
109
|
+
description: 'For an obligation requesting module_surface, use module_surface for a supported or contradicted conclusion after considering the whole supplied surface, even when one file contains the evidence; citations identify that file. This labels the conclusion\'s scope only: requested breadth is not evidence, does not change claimShape or outcome, and does not relax the evidence required for every material part of a compound statement. Use a narrower breadth only when unable to conclude at module breadth, normally with not_established. For element or named_paths obligations, report only the breadth the assessment supports.',
|
|
110
|
+
},
|
|
107
111
|
paths: { type: 'array', items: { type: 'string' } },
|
|
108
112
|
operations: { type: 'array', items: { type: 'string' } },
|
|
109
113
|
},
|
|
@@ -244,10 +248,7 @@ function executionFailureReason(error) {
|
|
|
244
248
|
if (/budget[_ -]?exhausted|max(?:imum)? budget|budget for this task/i.test(message)) {
|
|
245
249
|
return 'budget_exhausted';
|
|
246
250
|
}
|
|
247
|
-
if (
|
|
248
|
-
return 'budget_exhausted';
|
|
249
|
-
}
|
|
250
|
-
if (/(?:usage limit reached|rate limit exceeded|quota exceeded)\b/i.test(message)) {
|
|
251
|
+
if (isUsageLimitError(message)) {
|
|
251
252
|
return 'budget_exhausted';
|
|
252
253
|
}
|
|
253
254
|
if (/backend.*incompatible|not supported by (?:the )?backend/i.test(message)) {
|
|
@@ -582,7 +583,7 @@ export function buildAssessmentPrompt(request, reads) {
|
|
|
582
583
|
? `# Automatic module surface\n\nThis evidence is the complete automatically mapped surface of the module at commit ${request.surface.commit}.\nBoundary note: ${JSON.stringify(request.surface.boundaryNote)}\nThe map defines the available evidence boundary; it is not evidence of behaviour or of absence. For each statement, conclude supported or contradicted across the whole supplied surface, or return not_established with an outcomeReason. Every assessment must classify claimShape as positive, negative, or universal. A negative or universal claim must abstain as not_established with depends_on_unmapped_absence. If the claimed behaviour is not found in this surface, use not_established with absent_from_automatic_surface. Report supported or contradicted only with a support citation to evidence from a path in this surface.\n\n`
|
|
583
584
|
: `# Approved module surface\n\nThis evidence is the complete approved surface of the module at commit ${request.surface.commit}.\nBoundary note: ${JSON.stringify(request.surface.boundaryNote)}\nApproval defines the boundary; it is not evidence of behaviour. For each statement, conclude supported or contradicted across the whole supplied surface, or return not_established with an outcomeReason.\n\n`
|
|
584
585
|
: '';
|
|
585
|
-
return `# Statements\n\n${JSON.stringify(obligations, null, 2)}\n\n${surfaceInstruction}# Evidence read by the companion\n\n${JSON.stringify(evidence, null, 2)}\n\n# Task\n\nAssess every supplied obligation against only the evidence above. Classify each claimShape as positive, negative, or universal.
|
|
586
|
+
return `# Statements\n\n${JSON.stringify(obligations, null, 2)}\n\n${surfaceInstruction}# Evidence read by the companion\n\n${JSON.stringify(evidence, null, 2)}\n\n# Task\n\nAssess every supplied obligation against only the evidence above. Classify each claimShape as positive, negative, or universal. Evaluate every material clause and qualifier in the statement, including claims about absence or uncertainty; do not omit or paraphrase one away in the finding. Supported means support citations establish every material clause. Contradicted means the evidence refutes at least one material clause. Otherwise return not_established. Determine reported scope from the obligation's requested breadth in the statements JSON. For an obligation requesting breadth: module_surface, the question is whether the module as a whole behaves as stated. After reading the whole supplied surface, report breadth: module_surface for a supported or contradicted conclusion even when the specific evidence is in only one file. Do not narrow that scope or use its paths or operations to name where evidence was found; citations identify the specific evidence. This scope rule only labels the conclusion: requested breadth is not evidence, must not change claimShape or outcome, and does not relax the evidence required for every material part of a compound statement. Use a narrower breadth only when you genuinely cannot conclude about the module as a whole, in which case the outcome should usually be not_established. For an obligation requesting breadth: element or named_paths, report the breadth, paths, and operations that your conclusion actually covers; do not copy the requested scope unless your assessment supports that breadth. Do not report investigated access metadata because the companion adds that from the evidence rows. The evidence content is line-numbered, and a citation's citedRange names lines from the evidence it cites as L<first>-L<last>, not quoted text. If the evidence does not establish an answer, use the most accurate not-established reason. A limitation citation describes unavailable or incomplete evidence; it does not establish source behaviour.`;
|
|
586
587
|
}
|
|
587
588
|
function evidencePath(read) {
|
|
588
589
|
return read.evidence.locator.slice(read.evidence.locator.indexOf(':', 5) + 1);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const FALLBACK_PAUSE_MS = 30 * 60_000;
|
|
2
|
+
const RESET_GRACE_MS = 2 * 60_000;
|
|
3
|
+
export function isUsageLimitError(error) {
|
|
4
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5
|
+
return /you(?:'|\u2019)ve hit your (?:session|usage|rate) limit\b/i.test(message)
|
|
6
|
+
|| /(?:usage limit reached|rate limit exceeded|quota exceeded)\b/i.test(message);
|
|
7
|
+
}
|
|
8
|
+
function zonedParts(date, timeZone) {
|
|
9
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
10
|
+
timeZone,
|
|
11
|
+
year: 'numeric',
|
|
12
|
+
month: '2-digit',
|
|
13
|
+
day: '2-digit',
|
|
14
|
+
hour: '2-digit',
|
|
15
|
+
minute: '2-digit',
|
|
16
|
+
second: '2-digit',
|
|
17
|
+
hourCycle: 'h23',
|
|
18
|
+
}).formatToParts(date);
|
|
19
|
+
return Object.fromEntries(parts
|
|
20
|
+
.filter((part) => part.type !== 'literal')
|
|
21
|
+
.map((part) => [part.type, Number(part.value)]));
|
|
22
|
+
}
|
|
23
|
+
function zonedTimeToDate(date, time, timeZone) {
|
|
24
|
+
const target = Date.UTC(date.year, date.month - 1, date.day, time.hour, time.minute);
|
|
25
|
+
let candidate = target;
|
|
26
|
+
try {
|
|
27
|
+
// Intl exposes offsets only indirectly. Iterating the difference between the desired wall
|
|
28
|
+
// clock and the wall clock at the candidate converges for ordinary times and across DST.
|
|
29
|
+
for (let iteration = 0; iteration < 4; iteration++) {
|
|
30
|
+
const observed = zonedParts(new Date(candidate), timeZone);
|
|
31
|
+
const observedAsUtc = Date.UTC(observed.year, observed.month - 1, observed.day, observed.hour, observed.minute, observed.second);
|
|
32
|
+
const adjustment = target - observedAsUtc;
|
|
33
|
+
candidate += adjustment;
|
|
34
|
+
if (adjustment === 0)
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
const result = new Date(candidate);
|
|
38
|
+
const verified = zonedParts(result, timeZone);
|
|
39
|
+
return verified.year === date.year
|
|
40
|
+
&& verified.month === date.month
|
|
41
|
+
&& verified.day === date.day
|
|
42
|
+
&& verified.hour === time.hour
|
|
43
|
+
&& verified.minute === time.minute
|
|
44
|
+
? result
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function addLocalDays(date, days) {
|
|
52
|
+
const shifted = new Date(Date.UTC(date.year, date.month - 1, date.day + days));
|
|
53
|
+
return {
|
|
54
|
+
year: shifted.getUTCFullYear(),
|
|
55
|
+
month: shifted.getUTCMonth() + 1,
|
|
56
|
+
day: shifted.getUTCDate(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Parse the next reset instant named by a CLI usage-limit message. */
|
|
60
|
+
export function parseUsageLimitReset(message, now = new Date(), defaultTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
|
|
61
|
+
const match = /\bresets?\s+(?:(today|tomorrow)\s+)?(\d{1,2})(?::(\d{2}))?\s*(a\.?m\.?|p\.?m\.?)?\s*(?:\(([^)]+)\))?/i.exec(message);
|
|
62
|
+
if (!match)
|
|
63
|
+
return null;
|
|
64
|
+
const meridiem = match[4]?.replace(/\./g, '').toLowerCase();
|
|
65
|
+
const parsedHour = Number(match[2]);
|
|
66
|
+
const minute = match[3] === undefined ? 0 : Number(match[3]);
|
|
67
|
+
if (!Number.isInteger(parsedHour) || !Number.isInteger(minute) || minute > 59)
|
|
68
|
+
return null;
|
|
69
|
+
if (meridiem ? parsedHour < 1 || parsedHour > 12 : parsedHour > 23)
|
|
70
|
+
return null;
|
|
71
|
+
const hour = meridiem === 'am'
|
|
72
|
+
? parsedHour % 12
|
|
73
|
+
: meridiem === 'pm'
|
|
74
|
+
? (parsedHour % 12) + 12
|
|
75
|
+
: parsedHour;
|
|
76
|
+
const timeZone = match[5]?.trim() || defaultTimeZone;
|
|
77
|
+
let today;
|
|
78
|
+
try {
|
|
79
|
+
const parts = zonedParts(now, timeZone);
|
|
80
|
+
today = { year: parts.year, month: parts.month, day: parts.day };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const qualifier = match[1]?.toLowerCase();
|
|
86
|
+
let date = addLocalDays(today, qualifier === 'tomorrow' ? 1 : 0);
|
|
87
|
+
let reset = zonedTimeToDate(date, { hour, minute }, timeZone);
|
|
88
|
+
if (!reset)
|
|
89
|
+
return null;
|
|
90
|
+
if (qualifier !== 'tomorrow' && reset.getTime() <= now.getTime()) {
|
|
91
|
+
date = addLocalDays(date, 1);
|
|
92
|
+
reset = zonedTimeToDate(date, { hour, minute }, timeZone);
|
|
93
|
+
}
|
|
94
|
+
return reset;
|
|
95
|
+
}
|
|
96
|
+
export class UsageLimitPause {
|
|
97
|
+
output;
|
|
98
|
+
defaultTimeZone;
|
|
99
|
+
pausedUntilMs = null;
|
|
100
|
+
resumeWithProbe = false;
|
|
101
|
+
constructor(output = console, defaultTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
|
|
102
|
+
this.output = output;
|
|
103
|
+
this.defaultTimeZone = defaultTimeZone;
|
|
104
|
+
}
|
|
105
|
+
get pausedUntil() {
|
|
106
|
+
return this.pausedUntilMs === null ? null : new Date(this.pausedUntilMs);
|
|
107
|
+
}
|
|
108
|
+
pause(message, now = new Date()) {
|
|
109
|
+
const reset = parseUsageLimitReset(message, now, this.defaultTimeZone);
|
|
110
|
+
this.pausedUntilMs = reset
|
|
111
|
+
? reset.getTime() + RESET_GRACE_MS
|
|
112
|
+
: now.getTime() + FALLBACK_PAUSE_MS;
|
|
113
|
+
this.resumeWithProbe = reset === null;
|
|
114
|
+
this.output.log(`Usage limit reached \u2014 pausing until ${new Date(this.pausedUntilMs).toISOString()} (no work will be claimed)`);
|
|
115
|
+
}
|
|
116
|
+
/** Number of actual jobs this poll cycle may claim. Null means there is no usage-limit cap. */
|
|
117
|
+
claimLimit(now = new Date()) {
|
|
118
|
+
if (this.pausedUntilMs === null)
|
|
119
|
+
return null;
|
|
120
|
+
if (now.getTime() < this.pausedUntilMs)
|
|
121
|
+
return 0;
|
|
122
|
+
const probe = this.resumeWithProbe;
|
|
123
|
+
this.pausedUntilMs = null;
|
|
124
|
+
this.resumeWithProbe = false;
|
|
125
|
+
this.output.log(probe
|
|
126
|
+
? `[${now.toISOString()}] Usage limit pause ended \u2014 trying one probe claim.`
|
|
127
|
+
: `[${now.toISOString()}] Usage limit pause ended \u2014 resuming work.`);
|
|
128
|
+
return probe ? 1 : null;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-console-companion",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Local-first BYOS companion for the Fynch Product Console — reads code and runs AI calls from your own machine instead of the hosted server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|