Skip to content

fix(cli): a credit limit the person names holds on every paid media call - #7145

Merged
miguel-heygen merged 6 commits into
mainfrom
osscap/run-budget
Nov 4, 2026
Merged

miguel-heygen merged 6 commits into
mainfrom
osscap/run-budget

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

What

A credit limit the person names now holds on every paid media call, not only the calls that remember --max-credits.

Why

Today the limit is a per-call flag, checked in the media-use generate command, so anything else that spends skips it: kit tools that call generate without the flag, transcribe, and a HeyGen voice or avatar video made through resolve. One run stayed under its limit only because the agent edited its own copy of a kit tool.

Related work

None open on this.

How

  • The limit lives in .hyperframes/credit-limit.json as {"credits": N}. The skills tell the agent to write it once when the person names a limit.
  • runJob (packages/cli/src/heygen/v3.ts) is the one function every paid HeyGen job goes through (generate and transcribe). Before sending, it reads the file and what earlier jobs in the project committed (from .hyperframes/heygen-jobs.jsonl, which now records each job's credits, reserved before sending) and refuses a new job that does not fit. A call that cannot be priced ahead (transcribe) is refused under a limit, same as --max-credits already did. A resumed job (same key) is not charged twice; a refused call gives its credits back.
  • A reservation is released only by the call that made it, and only for a refusal that rules the job out (not 401/403/408/409/425/429).
  • The audio engine's HeyGen speech call (synthesizeHeygen) refuses under a limit too.
  • --max-credits stays as a tighter per-call limit.
  • resolve --type voice and avatar video shell out to the heygen CLI, which has no price here, so under a limit they refuse and point at media-use generate, which is priced and counted.
  • The file reader is one small module (credit-limit.mjs) used by both sides.

Not covered: reverse start talks to a different service and is not a media call.

Search first: no existing run-level limit, budget or spend ledger in packages/cli or skills (grep for budget, max-credits, credit before this change found only the per-call flag).

Test plan

  • Unit tests added: v3.test.ts (refuses a call over what is left with no --max-credits, refuses an unpriced call, a resumed job passes, a refused call's credits come back) and credit-limit.test.mjs (file parsing, spend read-back, voice refused before the heygen CLI runs). fails without the fix: v3.test.ts "refuses a call over what is left, before sending, with no --max-credits given" and "refuses a call that cannot be priced ahead" (check in runJob removed); "keeps a lost job's reservation through a resume refused with 422" and "keeps the reservation of a fresh call refused with 429" (release rule changed).
  • Manual testing performed: not run against a live HeyGen account
  • Documentation updated: media-use and style-sampling skills, skills manifest regenerated
  • Comments say why, not what

Ran on a remote Linux box: src/heygen and src/commands/media-use vitest, 6 files, 119 passed; lint, format and skills-manifest checks clean.

Size

Over 100 lines because the limit reader, the check in runJob, the engine guard and their tests have to land together; any one alone leaves a paid path unguarded.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 4 blocker · 1 important · 0 nits · head 89452339

  • blocker (PR body): this is a fix PR, but it has no fails without the fix: line naming a test. "Two of the three new runJob tests fail" doesn't say which tests. Add a fails without the fix: line that names at least one by file and title.

  • blocker: packages/cli/src/heygen/v3.ts:272-273: a resume can wipe out a live job's reservation. creditsSpent keeps the last credits each key logged, and a resumed key skips withinLimit. Any Refused on that resume then logs credits: 0 for the key. That includes the KEEPS_KEY statuses (401/403/408/409/425/429), where the original job may still be running and charging. I reproduced it with limit 100:

    1. k1 for 100 loses its answer (socket hang up), so the job may have started and the reservation stays.
    2. The same command is re-run and gets 429. The ledger total drops to 0.
    3. A new k2 for 100 is sent. That is 200 against a limit of 100.

    Fix: release the reservation only when this call made it and !KEEPS_KEY.has(err.status).

  • blocker (Rule 2): skills/media-use/audio/scripts/lib/tts.mjs:315 (synthesizeHeygen) still POSTs /voices/speech with the HyperFrames credential, and no limit check is on that path. It is reached from the documented audio engine (audio/scripts/audio.mjs:153, references/audio.md) and from heygen-tts.mjs. This PR treats resolve --type voice as a paid HeyGen voice and refuses it under a limit, so this path needs the same refusal. Otherwise the limit still doesn't hold "on every paid media call".

  • blocker (CI): the required Windows lanes fail because preflight lint fails: ✗ media-use/SKILL.md is 5282 B, over its 5120 B budget. The rewritten Credits paragraph needs to lose about 160 B.

  • important: the optional Comments ratchet fails. packages/cli/src/heygen/v3.ts comment share went from 6.4% to 6.8% (+4 lines), and the job reports 3 problems in total.

What I checked
  • Tests at head: v3.test.ts 27/27 and credit-limit.test.mjs 3/3.
  • No double-counting within one process: withinLimit runs after await jobSender(), and its read/check/append is synchronous.
  • Failures that keep the reservation: a client-side timeout or a 5xx comes back as a plain Fix, not Refused, so its reservation correctly stays.
  • Callers: generate.ts:680 and transcribe.ts:80 both pass log.projectDir, so the runJob check covers both.
  • Engine guards: voice-provider.mjs and heygen-video-provider.mjs refuse before running heygen. They fall back to process.cwd() when ctx.projectDir is absent.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 4 resolved, 0 open, 1 new · 89452339 → 0b58b73b

  • blocker (new, required Typecheck): packages/cli/src/heygen/v3.ts:225 still declares withinLimit(...): void, but it now returns true. tsc --noEmit -p packages/cli fails with v3.ts(245,3) TS2322: Type 'boolean' is not assignable to type 'void' and v3.ts(271,9) TS1345: An expression of type 'void' cannot be tested for truthiness. Change the return type to boolean, and have the early exits return false.

The four earlier blockers are all fixed: the body names the tests, a resume keeps its reservation, synthesizeHeygen refuses under a limit, and SKILL.md is 5104 B. Tests at this head: v3.test.ts 30/30, tts.test.mjs plus copy-parity 18/18. The optional Comments check still reports 4 problems.

A limit was only a per-call --max-credits flag that each caller had to remember, so kit tools, voice resolve and transcribe could spend past it. The limit now lives in the project's credit-limit file and runJob, which every paid call goes through, refuses a new job past what is left (unpriced calls included). Voice and avatar video resolved through the heygen CLI are refused under a limit, since they cannot be priced there.
…he audio engine's voice

A resumed job refused with a status that says nothing about the job released its reservation while the job may still be running. Only the call that reserved releases, and only for a refusal that rules the job out. The audio engine's HeyGen speech call now refuses under a limit too.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 1 resolved, 0 open, 1 new (important) · 0b58b73b → db47a539 (rebased)

  • important (optional Fallow audit): packages/cli/src/heygen/v3.ts:249: runJob has cognitive complexity 18, over the threshold of 15, after the reserved release condition was added. Either pull the catch-block bookkeeping into a helper, or use the // fallow-ignore-next-line complexity marker already on call. The optional Comments check is also still red. Neither is a required check.
What I checked
  • Rebase: git range-diff shows the first two commits unchanged apart from the regenerated skills-manifest.json hunks. After those come 1048760b (withinLimit(): boolean, early exits return false) and db47a539 (manifest regenerated, which picks up #7144's style-sampling hash).
  • Typecheck: tsc --noEmit -p packages/cli reports nothing in v3.ts. The only remaining errors come from the unbuilt @hyperframes/inspector in my local sandbox.
  • Tests: src/heygen 118/118, credit-limit.test.mjs 3/3, and tts.test.mjs + copy-parity + core-skills 28/28. skills/media-use/SKILL.md is 5104 B, under its 5120 B budget.
  • CI when I posted: preview-regression (which includes preflight), regression, Player perf and PR captures were green. The CI workflow, including Typecheck, and the Windows lanes were still running.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 1 resolved, 0 open, 0 new · db47a539 → 18bddeb4. runJob now carries the same fallow-ignore-next-line complexity marker as call. The refused-call branch is restructured with the same logic: the reservation is released only when this call made it and the status is outside KEEPS_KEY. .hyperframes/credit-limit.json is added to the citation checker's project-file allowlist. v3.test.ts passes 32/32, with no tsc errors in v3.ts. The optional Comments ratchet is still red: v3.ts is 6.4% against 6.2% at the base, +3 lines. This does not block.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 76da5f6 Nov 4, 2026
69 of 70 checks passed
@miguel-heygen
miguel-heygen deleted the osscap/run-budget branch November 4, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant