tokenmaxxing 1.32.0 → 1.33.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -80,11 +80,16 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
80
80
  const seatUuid =
81
81
  (ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.accountUuid === ctx.liveAccount) ? ctx.liveAccount : null) ??
82
82
  ctx.accounts.activeAccountUuid;
83
+ const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
84
+ const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
85
+ const seat = ctx.accounts.accounts.find((a) => a.accountUuid === seatUuid);
86
+ if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
87
+ const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
83
88
  const active =
84
89
  windows.length > 0
85
- ? `${col.green("◆")} ${windows.join(" ")}`
90
+ ? `${seatMarker} ${windows.join(" ")}`
86
91
  : seatUuid != null
87
- ? `${col.green("◆")} ?`
92
+ ? `${seatMarker} ?`
88
93
  : "";
89
94
 
90
95
  const parked = sortBy(
@@ -92,7 +97,8 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
92
97
  [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
93
98
  );
94
99
  const poolSeg = (a: Account): string => {
95
- const marker = a.needsReauth ? col.red("✗") : col.cyan("◇");
100
+ const marker = a.needsReauth ? col.red("✗") : walled(a) ? paint(100)("◇") : col.cyan("◇");
101
+ if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
96
102
  const week = a.lastUsage?.sevenDay;
97
103
  if (week == null) return `${marker} ?`;
98
104
  const weekUsed = used(week);
@@ -3,14 +3,13 @@ import { z } from "zod";
3
3
  import { paths } from "../lib/paths.ts";
4
4
  import { writeFileAtomic } from "../lib/atomic.ts";
5
5
  import { readOAuthAccount } from "../lib/claudejson.ts";
6
- import { enforcedWindowMs, evaluateAndMaybeSwap, postSwapProof, recordEnforcedLimit } from "../lib/decide.ts";
7
- import { loadConfig, loadLastSwapAt } from "../lib/state.ts";
6
+ import { evaluateAndMaybeSwap } from "../lib/decide.ts";
7
+ import { withLock } from "../lib/lock.ts";
8
+ import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
8
9
  import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
9
10
  import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
10
11
  import { log } from "../lib/log.ts";
11
12
 
12
- export const RETRIGGER_PROMPT = "Continue where the previous turn left off; it was interrupted by a usage limit and tokenmaxxing switched accounts.";
13
-
14
13
  const StopFailureStdin = z.looseObject({
15
14
  session_id: z.uuid().optional().catch(undefined),
16
15
  transcript_path: z.string().optional().catch(undefined),
@@ -30,7 +29,8 @@ async function readStdin(): Promise<string> {
30
29
  export async function runStopFailureHook(): Promise<number> {
31
30
  if (process.env.TOKENMAXXING_PROBE) return 0;
32
31
 
33
- const account = readOAuthAccount()?.accountUuid ?? null;
32
+ const entry = await withLock(paths.lockFile, () => ({ swapClock: loadLastSwapAt(), account: readOAuthAccount()?.accountUuid ?? null }));
33
+ const account = entry.account;
34
34
  const now = Date.now();
35
35
  const raw = await readStdin();
36
36
  const parsed = StopFailureStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
@@ -44,29 +44,27 @@ export async function runStopFailureHook(): Promise<number> {
44
44
  const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
45
45
 
46
46
  try {
47
+ const lastSwapAt = loadLastSwapAt();
48
+ if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
49
+ log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
50
+ return 0;
51
+ }
47
52
  const cfg = loadConfig();
48
- const found = stdin.transcript_path
53
+ const row = stdin.transcript_path
49
54
  ? findEnforcedRow({ rows: readTranscriptTail(stdin.transcript_path), lastAssistantMessage: stdin.last_assistant_message, now })
50
55
  : null;
51
- const limit = found ? classifyEnforcedLimit(found.row, cfg.policy.switchModels) : null;
56
+ const limit = row ? classifyEnforcedLimit(row, cfg.policy.switchModels) : null;
52
57
 
53
58
  let enforced: EnforcedLimit | null = null;
54
- if (limit && found && account) {
55
- if (postSwapProof({ swapAt: loadLastSwapAt(), launchedAt, errorAt: found.errorAt, now })) {
56
- const stamp = await recordEnforcedLimit({ limit, account, now });
57
- log("stopfailure.enforced", { kind: limit.kind, family: limit.kind === "model" ? limit.family : undefined, outcome: stamp.outcome, resetsAt: stamp.resetsAt, subagent: !mainLoop });
58
- if (stamp.outcome !== "account-moved") {
59
- enforced = { account, family: limit.kind === "model" ? limit.family : null, resetsAt: stamp.resetsAt, windowMs: enforcedWindowMs(limit) };
60
- }
61
- } else {
62
- log("stopfailure.unproven", { kind: limit.kind });
63
- }
59
+ if (limit && account) {
60
+ enforced = { account, kind: limit.kind, family: limit.kind === "model" ? limit.family : null, resetsAt: limit.resetsAt, blind: !mainLoop && limit.kind !== "model" };
61
+ log("stopfailure.enforced", { kind: limit.kind, family: enforced.family ?? undefined, resetsAt: limit.resetsAt, subagent: !mainLoop });
64
62
  } else {
65
63
  log("stopfailure.unclassified", {
66
- row: found != null,
67
- type: found?.row.quotaLimits?.rateLimitType,
68
- transient: found?.row.apiErrorIsTransient,
69
- body: found ? parseErrorBody(found.row.errorDetails)?.error?.type : undefined,
64
+ row: row != null,
65
+ type: row?.quotaLimits?.rateLimitType,
66
+ transient: row?.apiErrorIsTransient,
67
+ body: row ? parseErrorBody(row.errorDetails)?.error?.type : undefined,
70
68
  });
71
69
  }
72
70
 
@@ -78,7 +76,6 @@ export async function runStopFailureHook(): Promise<number> {
78
76
  ts: Date.now(),
79
77
  waitUntil: decision.waitUntil ?? now,
80
78
  sessionId: stdinSid ?? pinnedSid,
81
- prompt: RETRIGGER_PROMPT,
82
79
  ...(launchedAt != null ? { launchedAt } : {}),
83
80
  });
84
81
  writeFileAtomic(marker, JSON.stringify(payload));
@@ -292,7 +292,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
292
292
  if (await countdownWait(m.account, m.waitUntil)) overriddenUntil = m.waitUntil;
293
293
  } else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: switched to ${m.account} - resuming...\x1b[0m\n`);
294
294
  saveSessionFlags(m.sessionId, persistable, process.cwd());
295
- launchArgs = ["--resume", m.sessionId, ...persistable, ...(m.prompt ? ["--", m.prompt] : [])];
295
+ launchArgs = ["--resume", m.sessionId, ...persistable];
296
296
  continue;
297
297
  }
298
298
  log("supervisor.exit", { sid, respawns, code: child.exitCode, signal: child.signalCode });
package/src/lib/decide.ts CHANGED
@@ -2,11 +2,11 @@ import { maxBy } from "es-toolkit";
2
2
  import { z } from "zod";
3
3
  import { withLock } from "./lock.ts";
4
4
  import { paths } from "./paths.ts";
5
- import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsage, loadUsageSnapshot, saveAccounts, saveDepletedWait, writeUsage } from "./state.ts";
5
+ import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsageSnapshot, saveAccounts, saveDepletedWait, writeUsage } from "./state.ts";
6
6
  import { readOAuthAccount } from "./claudejson.ts";
7
7
  import { isSkippableSwapError, performSwap } from "./swap.ts";
8
- import { nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
9
- import { familyTokens, gatedFamilies, keepRows, probeUsage, type EnforcedClass } from "./usage.ts";
8
+ import { isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
9
+ import { familyTokens, gatedFamilies, keepRows, probeUsage } from "./usage.ts";
10
10
  import { log } from "./log.ts";
11
11
  import { AccountSchema, UsageStateSchema, type Account, type Config, type EnforcedLimit, type Thresholds, type UsageState, type UsageWindow } from "./types.ts";
12
12
 
@@ -40,7 +40,9 @@ function capForFamily(account: Account, family: string, now: number): UsageWindo
40
40
  }
41
41
 
42
42
  function isOver(u: UsageState | null, account: Account | undefined, bars: Thresholds, cfg: Config, now: number): boolean {
43
- if (!u || !account || u.account !== account.accountUuid) return false;
43
+ if (!account) return false;
44
+ if (account.enforcedUntil != null && account.enforcedUntil > now) return true;
45
+ if (!u || u.account !== account.accountUuid) return false;
44
46
  if (
45
47
  liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= bars.session ||
46
48
  liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now }) >= bars.weekly
@@ -75,14 +77,15 @@ function freshest(u: UsageState | null, uAt: number | null, account: Account | u
75
77
  return { ...u, ts: uAt };
76
78
  }
77
79
 
78
- async function loadFreshSnapshots(cfg: Config, account: string | null, now: number): Promise<Snapshots> {
80
+ async function loadFreshSnapshots(cfg: Config, account: string | null, now: number, probe: boolean): Promise<Snapshots> {
79
81
  const snap = loadUsageSnapshot();
80
82
  let u = snap?.state ?? null;
81
83
  let uAt = snap?.at ?? null;
82
84
  const ttl = cfg.policy.usagePollTtlMs;
83
85
  const stored = loadAccounts().accounts.find((a) => a.accountUuid === account);
84
86
  const probeAttempted = stored?.lastProbeAt != null && now - stored.lastProbeAt <= ttl;
85
- if (account && stored && !probeAttempted && (!usageFresh(u, uAt, account, ttl, now) || needsPerModel(u, cfg))) {
87
+ const walled = stored?.enforcedUntil != null && stored.enforcedUntil > now;
88
+ if (probe && account && stored && !walled && !probeAttempted && (!usageFresh(u, uAt, account, ttl, now) || needsPerModel(u, cfg))) {
86
89
  const startedAt = Date.now();
87
90
  const full = await probeUsage();
88
91
  const ts = Date.now();
@@ -121,21 +124,20 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
121
124
 
122
125
  export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = false, enforced: EnforcedLimit | null = null): Promise<SwapDecision> {
123
126
  const activeAccount = readOAuthAccount()?.accountUuid ?? null;
124
- const enforced0 = enforced && enforced.account === activeAccount ? enforced : null;
125
127
 
126
128
  const lastSwapAt = loadLastSwapAt();
127
- if (!enforced0 && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
129
+ if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
128
130
  return depletedReplay(now) ?? { swapped: false, account: null, reason: "post-swap-cooldown" };
129
131
  }
130
132
 
131
133
  const cfg = loadConfig();
132
134
  const bars = thresholdBars(cfg);
133
135
 
134
- const { u: teeUsage, uAt } = await loadFreshSnapshots(cfg, activeAccount, now);
136
+ const { u: teeUsage, uAt } = await loadFreshSnapshots(cfg, activeAccount, now, enforced == null);
135
137
  const stored = loadAccounts().accounts.find((a) => a.accountUuid === activeAccount);
136
138
  const usage = freshest(teeUsage, uAt, stored);
137
139
 
138
- if (!enforced0 && !isOver(usage, stored, bars, cfg, now)) {
140
+ if (!enforced && !isOver(usage, stored, bars, cfg, now)) {
139
141
  const measured = usage != null && activeAccount != null && usage.account === activeAccount;
140
142
  if (!measured) {
141
143
  const replay = depletedReplay(now);
@@ -147,11 +149,20 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
147
149
  return withLock(paths.lockFile, async () => {
148
150
  const idx = loadAccounts();
149
151
  const account2 = readOAuthAccount()?.accountUuid ?? null;
150
- const enforced2 = enforced0 && enforced0.account === account2 ? enforced0 : null;
151
152
  const tee = loadUsageSnapshot();
152
153
  const active = account2 ? idx.accounts.find((a) => a.accountUuid === account2) : undefined;
153
154
  const u2 = tee ? freshest(tee.state, tee.at, active) : usage;
154
155
 
156
+ const origin = enforced ? idx.accounts.find((a) => a.accountUuid === enforced.account) : undefined;
157
+ const prior = origin?.enforcedUntil != null && origin.enforcedUntil > now;
158
+ if (enforced && origin) {
159
+ origin.enforcedUntil = Math.max(origin.enforcedUntil ?? 0, enforcedWall(enforced, origin, now));
160
+ origin.lastProbeAt = now;
161
+ saveAccounts(idx);
162
+ log("usage.enforced_limit", { kind: enforced.kind, family: enforced.family ?? undefined, resetsAt: origin.enforcedUntil, blind: prior || enforced.blind, live: origin === active });
163
+ }
164
+ const enforced2 = enforced && origin && origin === active ? enforced : null;
165
+
155
166
  if (account2 != null && !active) {
156
167
  return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
157
168
  }
@@ -162,10 +173,14 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
162
173
  saveAccounts(idx);
163
174
  }
164
175
 
165
- const gated = gatedFamilies(u2?.model ?? null, cfg.policy.switchModels);
166
- const switchFamilies = enforced2?.family && !gated.includes(enforced2.family) ? [...gated, enforced2.family] : gated;
176
+ const walled = active?.enforcedUntil != null && active.enforcedUntil > now;
177
+ const blindScreen = (enforced != null && (prior || enforced.blind)) || (walled && !enforced2);
178
+ const gated = blindScreen ? cfg.policy.switchModels : gatedFamilies(u2?.model ?? null, cfg.policy.switchModels);
179
+ const family = enforced?.family ?? null;
180
+ const switchFamilies = family != null && !gated.includes(family) ? [...gated, family] : gated;
167
181
 
168
- if (!enforced2 && !isOver(u2, active, bars, cfg, now)) {
182
+ const seatExhausted = active != null && isExhausted(active, { now, thresholds: bars, currentAccountUuid: account2, switchFamilies });
183
+ if (!enforced2 && !isOver(u2, active, bars, cfg, now) && !(enforced && seatExhausted)) {
169
184
  return depletedReplay(now) ?? { swapped: false, account: null, reason: "raced-already-swapped" };
170
185
  }
171
186
 
@@ -201,8 +216,7 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
201
216
  const fresh = loadAccounts();
202
217
  const current = seatOf(fresh);
203
218
  const ctx = { now, thresholds: bars, currentAccountUuid: current?.accountUuid ?? null, switchFamilies };
204
- const enforcedUntil = enforced2 && current && current.accountUuid === enforced2.account ? (enforced2.resetsAt ?? now + enforced2.windowMs) : 0;
205
- const currentAt = current ? Math.max(usableAt(current, ctx), enforcedUntil) : Number.POSITIVE_INFINITY;
219
+ const currentAt = current ? usableAt(current, ctx) : Number.POSITIVE_INFINITY;
206
220
  const other = pickEarliestReset(usable(fresh.accounts), ctx);
207
221
 
208
222
  let target: Account | null = null;
@@ -245,63 +259,19 @@ function depletedReplay(now: number): SwapDecision | null {
245
259
  return { swapped: false, account, reason: "depleted-wait", waitUntil: rec.waitUntil };
246
260
  }
247
261
 
248
- export function enforcedWindowMs(limit: EnforcedClass): number {
249
- return limit.kind === "session" ? FIVE_HOURS_MS : WEEK_MS;
250
- }
251
-
252
- export function postSwapProof(input: { swapAt: number | null; launchedAt: number | null; errorAt: number | null; now: number }): boolean {
253
- const { swapAt, launchedAt, errorAt, now } = input;
254
- if (swapAt == null) return true;
255
- if (launchedAt != null && launchedAt > swapAt) return true;
256
- return (errorAt ?? now) - swapAt >= POST_SWAP_COOLDOWN_MS;
257
- }
258
-
259
- const StampSchema = z.object({ outcome: z.enum(["stamped", "account-moved", "no-carrier"]), resetsAt: z.number().nullable() });
260
- export type Stamp = z.infer<typeof StampSchema>;
261
-
262
- export async function recordEnforcedLimit(input: { limit: EnforcedClass; account: string; now: number }): Promise<Stamp> {
263
- const { limit, account: accountUuid, now } = input;
264
- return withLock(paths.lockFile, () => {
265
- if ((readOAuthAccount()?.accountUuid ?? null) !== accountUuid) return { outcome: "account-moved", resetsAt: limit.resetsAt };
266
- const prior = loadUsage();
267
- const priorSame = prior && prior.account === accountUuid ? prior : null;
268
- const idx = loadAccounts();
269
- const account = idx.accounts.find((a) => a.accountUuid === accountUuid);
270
- if (limit.kind === "model") {
271
- const rows = account?.lastUsage?.perModel ?? {};
272
- const knownReset = Object.entries(rows).filter(([k]) => familyTokens(k).includes(limit.family)).map(([, w]) => w.resetsAt).find((r): r is number => r != null) ?? null;
273
- const weeklyReset = priorSame?.sevenDay.resetsAt ?? account?.lastUsage?.sevenDay.resetsAt ?? null;
274
- const resetsAt = limit.resetsAt ?? nextWeeklyReset(knownReset ?? weeklyReset, now);
275
- if (!account) return { outcome: "no-carrier", resetsAt };
276
- if (account.lastUsage) {
277
- const others = Object.fromEntries(Object.entries(rows).filter(([k]) => !familyTokens(k).includes(limit.family)));
278
- account.lastUsage = { ...account.lastUsage, perModel: { ...others, [limit.family]: { usedPercentage: 100, resetsAt } }, ...(resetsAt == null ? { rowsAt: now } : {}) };
279
- } else {
280
- account.enforcedUntil = resetsAt ?? now + WEEK_MS;
281
- }
282
- account.lastProbeAt = now;
283
- saveAccounts(idx);
284
- log("usage.enforced_limit", { kind: limit.kind, family: limit.family, resetsAt });
285
- return { outcome: "stamped", resetsAt };
286
- }
287
- if (account) {
288
- account.lastProbeAt = now;
289
- if (limit.resetsAt != null) account.enforcedUntil = limit.resetsAt;
290
- saveAccounts(idx);
291
- }
292
- const carrier = priorSame ?? (account?.lastUsage ? { ...account.lastUsage, model: null } : null);
293
- if (!carrier) return { outcome: "no-carrier", resetsAt: limit.resetsAt };
294
- const window: UsageWindow = { usedPercentage: 100, resetsAt: limit.resetsAt };
295
- writeUsage({
296
- ...(priorSame ?? {}),
297
- ...(priorSame == null && account?.lastUsageAt != null ? { sampledAt: account.lastUsageAt } : {}),
298
- fiveHour: limit.kind === "session" ? window : carrier.fiveHour,
299
- sevenDay: limit.kind === "weekly" ? window : carrier.sevenDay,
300
- account: accountUuid,
301
- ts: now,
302
- model: carrier.model,
303
- }, { stamp: true });
304
- log("usage.enforced_limit", { kind: limit.kind, resetsAt: limit.resetsAt });
305
- return { outcome: "stamped", resetsAt: limit.resetsAt };
306
- });
262
+ function enforcedWall(limit: EnforcedLimit, account: Account, now: number): number {
263
+ const cached = account.lastUsage;
264
+ const { family } = limit;
265
+ const familyReset =
266
+ family == null
267
+ ? null
268
+ : Object.entries(cached?.perModel ?? {})
269
+ .filter(([k]) => familyTokens(k).includes(family))
270
+ .map(([, w]) => w.resetsAt)
271
+ .find((r): r is number => r != null) ?? null;
272
+ const cachedReset =
273
+ limit.kind === "session"
274
+ ? cached?.fiveHour.resetsAt != null && cached.fiveHour.resetsAt > now ? cached.fiveHour.resetsAt : null
275
+ : nextWeeklyReset(familyReset ?? cached?.sevenDay.resetsAt ?? null, now);
276
+ return limit.resetsAt ?? cachedReset ?? now + (limit.kind === "session" ? FIVE_HOURS_MS : WEEK_MS);
307
277
  }
package/src/lib/types.ts CHANGED
@@ -82,9 +82,10 @@ export type AccountsIndex = z.infer<typeof AccountsIndexSchema>;
82
82
 
83
83
  export const EnforcedLimitSchema = z.object({
84
84
  account: z.string(),
85
+ kind: z.enum(["session", "weekly", "model"]),
85
86
  family: z.string().nullable(),
86
87
  resetsAt: z.number().nullable(),
87
- windowMs: z.number(),
88
+ blind: z.boolean(),
88
89
  });
89
90
  export type EnforcedLimit = z.infer<typeof EnforcedLimitSchema>;
90
91
 
@@ -128,7 +129,6 @@ export const RespawnMarkerSchema = z.object({
128
129
  ts: z.number(),
129
130
  waitUntil: z.number(),
130
131
  sessionId: z.string(),
131
- prompt: z.string().optional(),
132
132
  launchedAt: z.number().optional(),
133
133
  });
134
134
 
package/src/lib/usage.ts CHANGED
@@ -169,13 +169,7 @@ export function transcriptRowText(row: TranscriptRow): string {
169
169
 
170
170
  const ROW_RECENCY_MS = 60_000;
171
171
 
172
- const EnforcedRowSchema = z.object({
173
- row: TranscriptRowSchema,
174
- errorAt: z.number().nullable(),
175
- });
176
- export type EnforcedRow = z.infer<typeof EnforcedRowSchema>;
177
-
178
- export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMessage: string | undefined; now: number }): EnforcedRow | null {
172
+ export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMessage: string | undefined; now: number }): TranscriptRow | null {
179
173
  const { rows, lastAssistantMessage, now } = input;
180
174
  for (let i = rows.length - 1; i >= 0; i--) {
181
175
  const row = rows[i]!;
@@ -183,8 +177,7 @@ export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMes
183
177
  const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
184
178
  const byContent = lastAssistantMessage != null && lastAssistantMessage !== "" && transcriptRowText(row) === lastAssistantMessage;
185
179
  const byRecency = Number.isFinite(ts) && Math.abs(now - ts) <= ROW_RECENCY_MS;
186
- if (!byContent && !byRecency) continue;
187
- return { row, errorAt: Number.isFinite(ts) ? ts : null };
180
+ if (byContent || byRecency) return row;
188
181
  }
189
182
  return null;
190
183
  }