@link-assistant/hive-mind 1.74.8 → 1.74.10
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/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/src/codex.lib.mjs +11 -2
- package/src/solve.auto-continue.lib.mjs +1 -1
- package/src/solve.auto-merge.lib.mjs +1 -1
- package/src/solve.mjs +1 -1
- package/src/solve.validation.lib.mjs +48 -7
- package/src/usage-limit.lib.mjs +35 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.74.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 88adc75: Fix the auto-resume wait calculation for weekly `--tool codex` usage limits (Issue #1869, phase 2). After the display parser was fixed to keep the full reset date, the separate auto-resume parser in `solve.validation.lib.mjs` still crashed with `Invalid time format: Jun 11, 2026, 12:27 AM` and, even when it parsed, discarded the date and scheduled for today/tomorrow — so auto-resume woke up far too early. `calculateWaitTime` now delegates to the robust date-aware `parseResetTime` from `usage-limit.lib.mjs` (honoring explicit year, weekly date, and timezone) and returns the real time-until-reset, and all three call sites now forward the timezone. This consolidates onto a single reset-time parser.
|
|
8
|
+
|
|
9
|
+
## 1.74.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- c4070e1: Fix incorrect usage-limit reset time for `--tool codex`. Codex reports weekly limits as a full calendar date (e.g. "try again at Jun 11th, 2026 12:27 AM"), but the reset-time parser dropped the month/day/year and kept only the time, making a multi-day weekly reset look like a same-day 5-hour reset. This both mis-informed users and made auto-resume fire far too early. `extractResetTime` now parses ordinal days and explicit years (keyword-independent), `parseResetTime` honors an explicit year, and Codex now traces the raw limit message and parsed reset under verbose mode.
|
|
14
|
+
</content>
|
|
15
|
+
|
|
3
16
|
## 1.74.8
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
package/src/codex.lib.mjs
CHANGED
|
@@ -1009,9 +1009,15 @@ export const executeCodexCommand = async params => {
|
|
|
1009
1009
|
await log(`⚠️ Ignoring non-fatal Codex item error event(s): ${ignoredMessages}`, { level: 'warning', verbose: true });
|
|
1010
1010
|
}
|
|
1011
1011
|
if (codexErrorSummary.hasError) {
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1012
|
+
const limitSource = codexErrorSummary.message || lastMessage;
|
|
1013
|
+
const limitInfo = detectUsageLimit(limitSource);
|
|
1014
|
+
const retryableError = classifyRetryableError(limitSource);
|
|
1014
1015
|
if (limitInfo.isUsageLimit) {
|
|
1016
|
+
// Issue #1869: Trace the raw limit text and what we parsed out of it so
|
|
1017
|
+
// a mis-parsed reset (e.g. a weekly reset read as a 5-hour reset) can be
|
|
1018
|
+
// diagnosed from the log without guessing at the original message.
|
|
1019
|
+
await log(`🔍 Codex usage limit detected. Raw message: ${JSON.stringify(limitSource)}`, { verbose: true });
|
|
1020
|
+
await log(`🔍 Parsed reset time: ${JSON.stringify(limitInfo.resetTime)}, timezone: ${JSON.stringify(limitInfo.timezone)}`, { verbose: true });
|
|
1015
1021
|
limitReached = true;
|
|
1016
1022
|
limitResetTime = limitInfo.resetTime;
|
|
1017
1023
|
|
|
@@ -1098,6 +1104,9 @@ export const executeCodexCommand = async params => {
|
|
|
1098
1104
|
// Check for usage limit errors first (more specific)
|
|
1099
1105
|
const limitInfo = detectUsageLimit(lastMessage);
|
|
1100
1106
|
if (limitInfo.isUsageLimit) {
|
|
1107
|
+
// Issue #1869: Trace raw limit text + parsed reset for diagnosability.
|
|
1108
|
+
await log(`🔍 Codex usage limit detected (exit ${exitCode}). Raw message: ${JSON.stringify(lastMessage)}`, { verbose: true });
|
|
1109
|
+
await log(`🔍 Parsed reset time: ${JSON.stringify(limitInfo.resetTime)}, timezone: ${JSON.stringify(limitInfo.timezone)}`, { verbose: true });
|
|
1101
1110
|
limitReached = true;
|
|
1102
1111
|
limitResetTime = limitInfo.resetTime;
|
|
1103
1112
|
|
|
@@ -92,7 +92,7 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
92
92
|
const nextAutoResumeIteration = currentAutoResumeIteration + 1;
|
|
93
93
|
const resetTime = global.limitResetTime;
|
|
94
94
|
const timezone = global.limitTimezone || null;
|
|
95
|
-
const baseWaitMs = calculateWaitTime(resetTime);
|
|
95
|
+
const baseWaitMs = calculateWaitTime(resetTime, timezone);
|
|
96
96
|
|
|
97
97
|
// Add buffer time after limit reset to account for server time differences
|
|
98
98
|
// Default: 10 minutes (configurable via HIVE_MIND_LIMIT_RESET_BUFFER_MS)
|
|
@@ -735,7 +735,7 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
735
735
|
limitResumeCount++;
|
|
736
736
|
const resumeSessionId = toolResult.sessionId;
|
|
737
737
|
const resetTime = toolResult.limitResetTime;
|
|
738
|
-
const baseWaitMs = resetTime ? calculateWaitTime(resetTime) : 0;
|
|
738
|
+
const baseWaitMs = resetTime ? calculateWaitTime(resetTime, toolResult.limitTimezone || null) : 0;
|
|
739
739
|
const bufferMs = limitReset.bufferMs;
|
|
740
740
|
const jitterMs = Math.floor(Math.random() * limitReset.jitterMs);
|
|
741
741
|
const waitMs = baseWaitMs + bufferMs + jitterMs;
|
package/src/solve.mjs
CHANGED
|
@@ -1029,7 +1029,7 @@ try {
|
|
|
1029
1029
|
// Calculate wait time in d:h:m:s format
|
|
1030
1030
|
const validation = await import('./solve.validation.lib.mjs');
|
|
1031
1031
|
const { calculateWaitTime } = validation;
|
|
1032
|
-
const waitMs = calculateWaitTime(global.limitResetTime);
|
|
1032
|
+
const waitMs = calculateWaitTime(global.limitResetTime, global.limitTimezone || null);
|
|
1033
1033
|
|
|
1034
1034
|
const formatWaitTime = ms => {
|
|
1035
1035
|
const seconds = Math.floor(ms / 1000);
|
|
@@ -43,6 +43,13 @@ const claudeLib = await import('./claude.lib.mjs');
|
|
|
43
43
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
44
44
|
const { reportError } = sentryLib;
|
|
45
45
|
|
|
46
|
+
// Import the robust usage-limit reset-time parser.
|
|
47
|
+
// This returns a full dayjs date (honoring an explicit year and timezone) so the
|
|
48
|
+
// auto-resume wait calculation can respect weekly limits that are days out, rather
|
|
49
|
+
// than collapsing every reset to "today/tomorrow at HH:MM" (Issue #1869).
|
|
50
|
+
const usageLimitLib = await import('./usage-limit.lib.mjs');
|
|
51
|
+
const { parseResetTime: parseResetTimeToDate } = usageLimitLib;
|
|
52
|
+
|
|
46
53
|
const { validateClaudeConnection } = claudeLib;
|
|
47
54
|
|
|
48
55
|
// Wrapper function for disk space check using imported module
|
|
@@ -394,13 +401,23 @@ export const parseUrlComponents = issueUrl => {
|
|
|
394
401
|
};
|
|
395
402
|
};
|
|
396
403
|
|
|
397
|
-
// Helper function to parse time string
|
|
404
|
+
// Helper function to parse a reset time string into hour/minute components.
|
|
405
|
+
//
|
|
406
|
+
// Accepts:
|
|
407
|
+
// - Time-only forms: "5:30am", "11:45pm", "12:16 PM", "07:05 Am", "5am", "5 AM"
|
|
408
|
+
// - Date+time forms: "Apr 17, 4:00 AM" (date portion ignored)
|
|
409
|
+
// - Date+year+time forms: "Jun 11, 2026, 12:27 AM" (date+year ignored — Issue #1869)
|
|
410
|
+
//
|
|
411
|
+
// NOTE: This helper only extracts the time-of-day. For computing the actual wait
|
|
412
|
+
// duration use calculateWaitTime(), which preserves the full date so weekly limits
|
|
413
|
+
// (which can be days away) are honored instead of being collapsed to today/tomorrow.
|
|
398
414
|
export const parseResetTime = timeStr => {
|
|
399
|
-
// Normalize and parse time formats like:
|
|
400
|
-
// "5:30am", "11:45pm", "12:16 PM", "07:05 Am", "5am", "5 AM"
|
|
401
|
-
// Also accepts date+time forms like "Apr 17, 4:00 AM" and ignores the date portion.
|
|
402
415
|
const normalized = (timeStr || '').toString().trim();
|
|
403
|
-
|
|
416
|
+
// Strip an optional leading "Month Day," and an optional "Year," so the
|
|
417
|
+
// remaining string is just the time-of-day. The year group (Issue #1869) makes
|
|
418
|
+
// Codex weekly-limit strings like "Jun 11, 2026, 12:27 AM" parse instead of
|
|
419
|
+
// throwing "Invalid time format".
|
|
420
|
+
const timePortion = normalized.replace(/^(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?,\s+(?:\d{4},\s+)?/i, '');
|
|
404
421
|
|
|
405
422
|
// Accept both HH:MM am/pm and HH am/pm
|
|
406
423
|
let match = timePortion.match(/^(\d{1,2})(?::(\d{2}))?\s*([ap]m)$/i);
|
|
@@ -423,8 +440,32 @@ export const parseResetTime = timeStr => {
|
|
|
423
440
|
return { hour, minute };
|
|
424
441
|
};
|
|
425
442
|
|
|
426
|
-
// Calculate milliseconds until the
|
|
427
|
-
|
|
443
|
+
// Calculate milliseconds until the limit reset.
|
|
444
|
+
//
|
|
445
|
+
// Issue #1869: This MUST respect the full reset date (including an explicit year),
|
|
446
|
+
// not just the time-of-day. A weekly Codex limit reports "Jun 11th, 2026 12:27 AM"
|
|
447
|
+
// which can be days in the future; the previous implementation only looked at the
|
|
448
|
+
// hour/minute and scheduled for today/tomorrow, so auto-resume woke up far too early
|
|
449
|
+
// and burned an auto-resume iteration without the limit actually having reset.
|
|
450
|
+
//
|
|
451
|
+
// We delegate to the robust usage-limit parser, which returns a full dayjs date that
|
|
452
|
+
// already handles: explicit year, weekly date+time, time-only (rolls forward to the
|
|
453
|
+
// next occurrence), and an optional IANA timezone. We then return the real diff.
|
|
454
|
+
//
|
|
455
|
+
// @param {string} resetTime - Reset time string (time-only, date+time, or date+year+time)
|
|
456
|
+
// @param {string|null} timezone - Optional IANA timezone (e.g. "Europe/Berlin")
|
|
457
|
+
// @returns {number} - Milliseconds until reset (never negative)
|
|
458
|
+
export const calculateWaitTime = (resetTime, timezone = null) => {
|
|
459
|
+
const resetDate = parseResetTimeToDate(resetTime, timezone);
|
|
460
|
+
|
|
461
|
+
if (resetDate && resetDate.isValid()) {
|
|
462
|
+
const diffMs = resetDate.valueOf() - Date.now();
|
|
463
|
+
return diffMs > 0 ? diffMs : 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Fallback: the robust parser could not interpret the string. Fall back to the
|
|
467
|
+
// legacy time-of-day behavior (today/tomorrow) so we still wait a sensible amount
|
|
468
|
+
// rather than throwing — parseResetTime throws for genuinely unparseable input.
|
|
428
469
|
const { hour, minute } = parseResetTime(resetTime);
|
|
429
470
|
|
|
430
471
|
const now = new Date();
|
package/src/usage-limit.lib.mjs
CHANGED
|
@@ -121,19 +121,33 @@ export function extractResetTime(message) {
|
|
|
121
121
|
// Normalize whitespace for easier matching
|
|
122
122
|
const normalized = message.replace(/\s+/g, ' ');
|
|
123
123
|
|
|
124
|
-
// Pattern 0: Weekly limit with
|
|
125
|
-
// This pattern must come
|
|
124
|
+
// Pattern 0: Weekly/long-window limit with an explicit calendar date.
|
|
125
|
+
// This pattern must come FIRST so a date+time is never truncated to a bare
|
|
126
|
+
// time by the time-only patterns below (Issue #1869): Codex reports weekly
|
|
127
|
+
// limits as "try again at Jun 11th, 2026 12:27 AM" — keeping only "12:27 AM"
|
|
128
|
+
// makes a 2-days-out weekly reset look like a same-day 5-hour reset, which
|
|
129
|
+
// both mis-informs the user and triggers a far-too-early auto-resume.
|
|
130
|
+
//
|
|
131
|
+
// Handled shapes (keyword prefix like "resets"/"try again at" is optional —
|
|
132
|
+
// the month+day+time structure is specific enough on its own):
|
|
133
|
+
// - "resets Jan 15, 8am" (no year, Claude weekly)
|
|
134
|
+
// - "resets January 20, 10:30am"
|
|
135
|
+
// - "try again at Jun 11th, 2026 12:27 AM" (ordinal day + year, Codex weekly)
|
|
126
136
|
const monthPattern = '(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)';
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
// (month) (day)[ordinal] [, year] [,] (hour)[:minute] (am|pm)
|
|
138
|
+
const dateWithTimeRegex = new RegExp(`(${monthPattern})\\s+(\\d{1,2})(?:st|nd|rd|th)?(?:,?\\s+(\\d{4}))?,?\\s+([0-9]{1,2})(?::([0-9]{2}))?\\s*([ap]m)`, 'i');
|
|
139
|
+
const dateWithTime = normalized.match(dateWithTimeRegex);
|
|
140
|
+
if (dateWithTime) {
|
|
141
|
+
const month = dateWithTime[1];
|
|
142
|
+
const day = dateWithTime[2];
|
|
143
|
+
const year = dateWithTime[3]; // optional
|
|
144
|
+
const hour = dateWithTime[4];
|
|
145
|
+
const minute = dateWithTime[5] || '00';
|
|
146
|
+
const ampm = dateWithTime[6].toUpperCase();
|
|
147
|
+
// Return formatted date+time string for weekly limits, preserving the year
|
|
148
|
+
// when present so the reset is anchored to the correct day rather than
|
|
149
|
+
// being assumed to be "this year / next occurrence".
|
|
150
|
+
return year ? `${month} ${day}, ${year}, ${hour}:${minute} ${ampm}` : `${month} ${day}, ${hour}:${minute} ${ampm}`;
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
// Pattern 1: "try again at 12:16 PM"
|
|
@@ -244,8 +258,10 @@ export function parseResetTime(timeStr, tz = null) {
|
|
|
244
258
|
const normalized = timeStr.replace(/\bSept\b/gi, 'Sep');
|
|
245
259
|
|
|
246
260
|
// Try date+time formats using dayjs custom parse
|
|
247
|
-
// dayjs uses: MMM=Jan, MMMM=January, D=day, h=12-hour, mm=minutes, A=AM/PM
|
|
248
|
-
|
|
261
|
+
// dayjs uses: MMM=Jan, MMMM=January, D=day, YYYY=year, h=12-hour, mm=minutes, A=AM/PM
|
|
262
|
+
// Year-bearing formats are tried first so an explicit year (Codex weekly
|
|
263
|
+
// limits, Issue #1869) is honored instead of being dropped to the current year.
|
|
264
|
+
const dateTimeFormats = ['MMM D, YYYY, h:mm A', 'MMMM D, YYYY, h:mm A', 'MMM D, h:mm A', 'MMMM D, h:mm A'];
|
|
249
265
|
|
|
250
266
|
for (const format of dateTimeFormats) {
|
|
251
267
|
let parsed;
|
|
@@ -261,9 +277,11 @@ export function parseResetTime(timeStr, tz = null) {
|
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
if (parsed.isValid()) {
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
280
|
+
// When the format omits the year, dayjs defaults to the current year, so a
|
|
281
|
+
// past date should roll forward to next year. When an explicit year is
|
|
282
|
+
// present (Issue #1869), trust it verbatim and never bump it.
|
|
283
|
+
const hasExplicitYear = format.includes('YYYY');
|
|
284
|
+
if (!hasExplicitYear && parsed.isBefore(now)) {
|
|
267
285
|
parsed = parsed.add(1, 'year');
|
|
268
286
|
}
|
|
269
287
|
return parsed;
|