fix(chat): budget ask-mode history by tokens - #55
Conversation
Upgrade mdast-util-to-hast to address CVE-2025-66400 / GHSA-4fh9-h7wg-q85m, a medium severity unsanitized class attribute issue. The package is reachable through react-markdown in MarkdownRenderer, which renders chat markdown content, and 13.2.1 contains the upstream fix. Co-authored-by: Herik Webb <herikwebb@users.noreply.github.com>
Two automation changes that make an automated fork-review -> upstream-promotion flow reliable: - scripts/review-pr.sh now asks the model for an explicit trailing `VERDICT: APPROVE` / `VERDICT: CHANGES_REQUESTED`, strips it from the posted comment, and exits non-zero on anything but APPROVE (fail-closed on a missing or unrecognized verdict). "Reviewer signed off" now equals a green `review` check instead of string-matching the comment body. The diff range is also switched from two-dot to three-dot (merge-base) so the reviewer sees exactly what the PR introduces, not commits the base has moved ahead by. - check-release.yml's check_release job is guarded with `if: github.repository == 'plmbr/notebook-intelligence'`. jupyter-releaser's check-npm asserts package.json's repository.url matches the running repo, so it fails on every fork PR with no bearing on the change; it now runs only on the upstream repository.
- review-pr.sh honors an `override-review-gate` label: when present, the review still runs and posts its comment but the check exits 0 despite a CHANGES_REQUESTED verdict. An LLM reviewer occasionally flags correct or intentional code, and a fail-closed gate would otherwise block such PRs (and any automated promotion keyed on the check) with no recourse. Label name is overridable via REVIEW_GATE_OVERRIDE_LABEL. - Guard the three-dot diff range with a merge-base check, falling back to a two-dot range on a shallow clone instead of erroring before the review runs. - check_release now skips via `!github.event.repository.fork` rather than a hard-coded upstream name, so the intent (don't run release validation on forks) is self-documenting and survives a rename.
Opens a cross-fork PR from a fork branch into plmbr/notebook-intelligence using a classic PAT (secrets.UPSTREAM_PAT, public_repo scope) rather than the built-in GITHUB_TOKEN, which cannot target a repo we do not own. Manual (workflow_dispatch) by default and idempotent (skips if an open PR from the branch already exists). Inert until UPSTREAM_PAT is configured.
Add environment: upstream-promotion to the promote job so the UPSTREAM_PAT is only exposed after a manual approval, provided the environment is configured with required reviewers.
The promote workflow wrote a generic one-line body that gave upstream maintainers no context. Derive the PR title from the commit subject and the body from the commit message body (leading blank lines stripped), and refresh an already-open PR via gh pr edit instead of skipping, so a re-run repairs an existing promotion PR's description.
gh pr list --head does not reliably match a cross-fork head on the base repo, so a re-run fell through to gh pr create and failed with 'already exists'. Query the REST pulls head filter (user:branch) directly and gh pr edit the matched PR.
gh pr edit issues GraphQL that requires read:org and read:discussion scopes, so it failed under a least-privilege repo/public_repo PAT. Switch both the create and edit paths to REST pulls (POST/PATCH), which need only repo scope, so the token stays minimally scoped.
The pr-review job runs gpt-5.5 via the /v1/responses API, whose max_output_tokens budget also covers the model's hidden reasoning tokens. A large diff could consume the whole 3000-token budget on reasoning and return an 'incomplete' response with no review text, which the script treated as a hard error (exit 1, no comment posted) -- turning a tooling shortfall into a red check that blocked the PR for a non-code reason. Treat an empty/inconclusive response as a non-blocking pass that posts an explanatory comment (verdict APPROVE) and record the incomplete reason, and raise the default output budget to 8000 so the reviewer usually has room to finish. A genuine CHANGES_REQUESTED verdict still fails the gate as before.
Addresses review feedback: the first cut approved on any empty response, which would let a genuine tooling failure (schema change, content filter, service anomaly) become a green 'reviewer signed off' gate and promote an unreviewed change. Keep the fail-closed semantics used for unrecognized verdicts -- an empty response now yields CHANGES_REQUESTED -- but post an explanatory comment (with the API status/reason) instead of crashing with no output, and point at the existing override-review-gate label for an intentional bypass. The raised default max_output_tokens still makes an empty response rare in the first place.
Wire promote-upstream to a workflow_run trigger on the 'PR Review' workflow completing with success, so a branch is promoted automatically once its review loop ends on APPROVE (or an override label); manual dispatch is kept. The branch is resolved from the triggering run. Promotion is always clean: reduce the branch to its own changes (diff from its fork-point via merge-base), drop fork-only paths (pr-review.yml, promote-upstream.yml, check-release.yml, review-pr.sh, AGENTS.md, yarn.lock), and replay just those files onto the upstream tip as a fresh promote/<branch>. A fork-only branch promotes nothing. This keeps fork automation and the fork's base out of upstream PRs regardless of whether a fix branch was cut from the fork's main or the upstream tip.
Bumps the default OPENAI_MODEL to gpt-5.6 and adds an OPENAI_REASONING_EFFORT setting (default high) passed through to the OpenAI Responses API call.
Sol is the gpt-5.6 family's flagship, maximum-capability tier, which is what the review gate needs for deep multi-step reasoning over diffs.
review-claude runs in parallel with the existing GPT job and uses claude-opus-5 with adaptive thinking at high reasoning effort. It mirrors review-pr.sh's diff-building, override-label, and fail-closed verdict logic against the Anthropic Messages API instead of OpenAI's. Since both jobs live in the same "PR Review" workflow and promotion only fires on a successful full-workflow run, either job returning CHANGES_REQUESTED (without the override label) now blocks promotion — both reviewers must approve. Also excludes the new script from cross-fork promotion, matching the existing GPT reviewer script.
The Claude review job on PR #52 failed with a urllib TimeoutError: the request was still in progress, not stuck -- claude-opus-5 with adaptive thinking at high reasoning effort can take several minutes on a large diff before the first response byte arrives, and 120s wasn't enough headroom. Raised to 600s (matching the Anthropic SDKs' own default) and made it configurable via CLAUDE_REQUEST_TIMEOUT_SECONDS. Applied the same fix to the GPT script since it runs the same high-reasoning-effort pattern against the same 120s cap and would hit the identical failure once its OpenAI credits are restored.
PR #52's review-claude job returned stop_reason "max_tokens" with no review text: adaptive thinking at high effort on an 8-file diff consumed the entire 16000-token budget on reasoning before any output block was written, so the response came back empty and the check correctly failed closed. Raising the budget gives thinking + review text enough headroom to both complete on a normal-sized diff.
Merge the latest fork reviewer workflows and scripts onto the current upstream-based main branch. Keep the fork-only review and promotion automation while retaining the newer upstream product tree. Signed-off-by: Herik Webb <herikwebb@users.noreply.github.com>
Revert cb415aa relative to the fork original main parent, restoring the exact pre-merge tree. Signed-off-by: Herik Webb <herikwebb@users.noreply.github.com>
Automated PR Review (Claude)Model: Changed files: Review result: ReviewOverall the extraction of the token-budget helpers out of 1.
|
Automated PR Review (Claude)Model: Changed files: Review result: Review — Ask-mode chat history budgetingOverall the new Findings1.
|
Automated PR Review (Claude)Model: Changed files: Review result: ReviewOverall the refactor is well-structured (shared budgeting module, fail-open wrapper, lazy tokenizer). A few things look worth addressing before merge. 1.
|
Automated PR Review (Claude)Model: Changed files: Review result: ReviewOverall this is a well-structured, well-tested addition: the budgeter fails open, the tokenizer load moved off the import path, and the new test file covers most of the pruning matrix. A few things I'd like resolved or explicitly confirmed before merge. 1. PEP 604 annotations may break the supported Python floor —
|
Automated PR Review (Claude)Model: Changed files: Review result: SummaryThe new 1. Medium — Images are silently dropped from the mandatory user message with no signal to the model
text_parts = []
for item in content:
... collect only {"type": "text"} / str parts ...
if not text_parts:
text_parts.append(_IMAGE_OMISSION_TEXT)When the newest user message is multimodal and has at least one text part, every Suggest always appending Related, same branch: when 2. Low — A hung tokenizer download pins
|
Bring the fork baseline up to date with the merged terminal-safe dispatch, bounded Claude tool-result, and inline-prompt changes while retaining the fork-only review and promotion automation. # Conflicts: # docs/admin-guide.md # notebook_intelligence/acp_agent.py # notebook_intelligence/api.py # notebook_intelligence/claude.py # notebook_intelligence/extension.py # tests/test_websocket_handler_integration.py
Apply the shared input budget at final ask-mode dispatch and built-in generation boundaries. Preserve system instructions and the newest request, prioritize current-turn context, retain recent complete turns, and reuse the same strict truncation helpers for WebSocket attachments.
Keep assistant tool calls, matching tool outputs, and the final assistant response as one atomic history turn. Leave under-budget provider message sequences unchanged and document that agent tool loops remain outside this ask-mode change.
Preserve the original request when estimation encounters malformed history or tokenizer failures so response finalization still occurs. Truncate an oversized newest prompt only as a last resort and warm tokenization outside the request path.
Load the tokenizer lazily in a daemon thread with bounded retries and use linear-time truncation rather than repeated full-prefix encoding. Exercise the real ask-mode and WebSocket budgeting call sites under fallback conditions.
Fall back to UTF-8 estimates when tokenizer warm-up cannot start, account for fixed message metadata during truncation, and retain a bounded tail for over-budget histories without a user request. Add a cheap upper-bound screen to avoid unnecessary exact tokenization.
Add model-visible notices whenever mandatory text, images, or system guidance must be truncated or removed. Prefer the newest current-turn context and recover from stalled tokenizer loads without silently changing the request contract.
Skip pruning when compatible providers expose only a guessed context size, use conservative UTF-8 and image screening estimates, and count all message metadata. Preserve bounded system guidance and the newest message even for histories without a user role.
Keep mandatory image omissions visible, budget structured message metadata consistently, and select current-turn attachments newest-first. Remove the unrelated MCP dependency edit so the change remains scoped to chat budgeting.
Treat injected workspace rules and untrusted fenced context as indivisible budget units so truncation cannot weaken instruction or prompt-injection delimiters. Reserve bounded space for required guidance and validate the behavior through ask-mode integration tests.
Fail open when model context-window lookup raises, distinguish configured from fallback GitHub Copilot windows, and repeat tokenizer-fallback diagnostics at a bounded interval. Protect all fenced current-turn context from partial truncation.
21342a0 to
7bdb553
Compare
|
Closing this fork staging PR because the finalized change was promoted upstream in plmbr#412 and merged on 2026-08-31. The upstream version includes additional integration fixes and tests, while this fork PR is now conflicted after the base diverged. |
Summary
Validation
.venv/bin/python -m pytest -q tests/test_chat_history_budget.py tests/test_base_chat_participant_integration.py tests/test_websocket_handler_integration.py tests/test_github_copilot_llm_provider.py tests/test_litellm_compatible_llm_provider.py tests/test_openai_compatible_llm_provider.py(122 passed).venv/bin/python -m pytest -q(1,613 passed; one existing asyncio subprocess cleanup warning)git diff --check origin/main...HEAD