- Implemented in-house, zero-dependency utility modules in `pi-utils` covering DOM manipulation, markdown parsing, templating, browser automation helpers, and terminal buffers.
- Migrated packages across the repository to consume the new internal utilities and `omptype` schema validators instead of external dependencies.
- Removed multiple external runtime and development dependencies including Zod, Marked, LRU cache, Turndown, and Puppeteer browser packages.
- Credential-shaped token redaction now requires explicit opt-in via `configureCredentialRedaction` and is disabled by default, so user-supplied credential-like strings reach providers unmodified unless the host enables redaction.
- Wired the `secrets.enabled` ("Hide Secrets") setting to `configureCredentialRedaction` in the coding-agent so the pattern redaction follows the existing secret-obfuscation toggle.
Brings the per-advisor toggle, status-line glyphs, quota display, and the
failing-advisor stall/abort fix (f4c8143) onto main's rewritten advisor
runtime. Conflict reconciliation kept main's architecture (fingerprint
prefix reconciliation, host-level onTurnError recovery + fallback chains,
terminal-failure classification) and ported the branch semantics onto it:
- #failing latch: waitForCatchup resolves immediately while an advisor is
mid-failure; parked waiters wake the moment a turn fails, before any
async hook or retry sleep.
- Turn-end render containment: a formatter bug restores the cursor/prefix/
dedup snapshot and never propagates into the primary's turn-end callback
(per-advisor try/catch boundary in AgentSession).
- Quota pause: when host recovery declines a usage-limit failure, the
runtime latches quotaExhausted, requeues the batch, and notifies —
cleared only by an explicit reset.
- Hard halt after a permanent rejection or three backlog-drop cycles.
- #recoverAdvisorTurn also marks usage limits for structural errors thrown
before any assistant turn is recorded.
The Duo goal bypasses transformMessages; apply the outbound credential
scrub (#5655) to the rendered ChatML transcript and latest-prompt goal,
and updated the provider test to the redaction contract.
The workflow provider pushed `{ type: "start" }` and then ran six unsigned/untimed REST setup fetches (settings ensure, project discovery, project-id resolve, direct_access, workflow create, available models) plus the cleanup stop PATCH. `streamSimple` special-cases `gitlab-duo-agent` and skips the `register-builtins` `iterateWithIdleTimeout` wrapper, so a stalled setup fetch left the stream with no terminal event.
Added `GITLAB_DUO_WORKFLOW_REST_TIMEOUT_MS = 30_000` and a per-fetch `gitLabDuoWorkflowRestSignal(callerSignal)` helper (`AbortSignal.any([caller, AbortSignal.timeout])`), threaded through each setup helper. Best-effort helpers (settings, discovery, models, project-id resolve) still degrade to undefined/false on timeout; throwing helpers (direct_access, create) surface abort/timeout as a real `error` event through the existing outer catch. `stopGitLabDuoWorkflow` gets a fresh timeout-only signal (matching the existing decoupling from `options.signal` in the finally block) with a swallowing catch so cleanup remains best-effort.
Two regression tests: one drives a caller-signal cancel through a stalled fetch and asserts the stream finalizes with an error rather than hanging; the other structurally asserts every setup endpoint receives an `AbortSignal` on its RequestInit so an absolute deadline is always in place.
Fixes#4227
- Migrated 288 lines of scattered error classification logic from `utils/error-id.ts` into a cohesive `packages/ai/src/error/` module with 13 specialized submodules covering flags, classes, OAuth, providers, rate-limiting, and finalization.
- Replaced 100+ generic `Error` throws across 60+ provider and registry files with semantic `AIError.*` classes (e.g., `AIError.MissingApiKeyError`, `AIError.OAuthError`, `AIError.ProviderResponseError`), improving error diagnostics and retry logic.
- Consolidated error utility imports from `pi-utils` and scattered classification functions into a single `AIError` namespace, reducing coupling and simplifying error handling across all packages.
Register OMP's MCP tools under their bare names (read, bash, ...) instead of the mcp__omp__-prefixed form. The DWS server does not strip prefixes: it binds the model tool schema and matches incoming tool calls under the exact wire name (sanitize_llm_name only replaces illegal characters), verified live (registered name == name the model reports seeing). The prefix only forced the model to learn mcp__omp__read while OMP's own tool docs say read, with no namespacing benefit. Bare registration aligns the name the model sees, the toolset match key, and OMP's docs. The inbound parser still strips a leading mcp__omp__ defensively.
Drop bytes the model never reads from the GitLab Duo Agent goal transcript: omit tool-call and tool-result ids (one call per turn, result rides the next turn, so pair by adjacency), strip the OMP-internal per-call intent (i) field on replay, and stop escaping </> in tool-call JSON (the goal is plain transcript text, not an HTML context). About 7% smaller rendered goal on a real session, no semantic change, live tool dispatch unaffected.
Bound the rendered goal by two byte thresholds and treat oversized goals as context overflow so the session auto-compacts. The DWS/Workhorse transport has no fixed token wall but failure probability rises with goal byte size (<=~1.25MB ok, ~1.4-1.7MB jitter, >=~2MB fails, 4MB gRPC hard cap). Soft threshold 1.25MB = last-guaranteed-success ceiling; hard threshold 2MB = necessary-fail floor. A goal in [soft,hard) is still attempted once and only relabeled as a 'prompt is too long' (OVERFLOW_PATTERNS) error if it actually fails; a goal >=hard is not sent at all, the stream ends proactively with the overflow error (stopping the created workflow) so no quota is spent. The goal body is never truncated; an in-budget error surfaces its raw server message verbatim.
Namespace auto-discovery was already being moved to a per-account cache, but
settings enablement (`ensureGitLabDuoWorkflowSettings`) still lived on the
per-session provider state. That meant independent side-requests like
compaction/handoff could still repeat the namespace settings PUT even though
namespace is account-scoped and never exposed to the model.
Promote the provider's account preparation state to a module-level per-account
map keyed by a non-reversible fingerprint of the credential plus base URL:
- keep the discovered namespace selection there,
- move `settingsEnsured` there too.
Auto-discovery now reuses the cached namespace across sessions/turns and
settings enablement is sent at most once per account. If a cached namespace
later fails in dependent direct_access/workflow-create calls, it is invalidated
and re-discovered once. Explicit namespace/project config still bypasses the
cache and stays authoritative.
Add tests: namespace discovery runs once across two turns for the same account,
re-discovers exactly once after a cached namespace fails, and Duo settings are
enabled once per account rather than once per provider session.
Namespace auto-discovery ran on every turn. The root namespace is a
function of the GitLab credential (account), not of the conversation or
cwd, and the inline ambient flow never exposes namespace to the model, so
re-discovering each request was pure overhead.
Cache the discovered namespace per account, keyed by a non-reversible
fingerprint of the credential plus base URL, in a module-level map that
outlives any single conversation session. On auto-discovery the cached
namespace is the first choice; only if its dependent direct_access /
workflow-create calls later fail (revoked access, deleted group, membership
change) is the cache invalidated and discovery rerun once. Explicit
rootNamespaceId/namespaceId/project configuration (option or env) bypasses
the cache and stays authoritative, preserving the current-credentials
re-discovery contract for stale model metadata.
Add tests: discovery runs once across two turns for the same account; a
cached namespace that fails triggers exactly one re-discovery.
The 250ms action-flush window assumed DWS dispatches a model turn's parallel
tool calls as a back-to-back burst of runMCPTool frames. A bare-request probe
(hold the first action unanswered, watch for a second) proved the opposite:
the inline ambient flow's ToolNode runs a serial `for tool_call: await
tool.ainvoke(...)` loop and each MCP ainvoke blocks in
put_action_and_wait_for_response until the client returns the matching
actionResponse, so only one action is ever in flight per turn and the second
is never dispatched until the first is answered. The batching window therefore
only ever held one frame.
Remove the batching machinery (GITLAB_DUO_WORKFLOW_ACTION_FLUSH_MS, the flush
timer, pendingActionBatch, finishGitLabDuoWorkflowActionBatch) and restore the
serial model: each runMCPTool frame finalizes its own assistant message (one
done, one usage) and commits the single pending action; the action result
settles immediately without closing the socket so the resume path reuses it.
Update the test to assert the per-action contract and the CHANGELOG to describe
the serial wire behavior.
A resumed turn that held pending runMCPTool actions whose requestID could
not be paired with a persisted tool result (id mismatch) silently created a
fresh workflow while leaving the previous one running server-side. The
stranded workflow's LangGraph still treated the tool call as in-flight, so
the model never saw the result and re-issued the same call in a loop until
the user interrupted. The unresolvable-batch path now follows the same
cleanup as a mid-batch steer: stop the stranded workflow server-side and
drop the resumable session before seeding the fresh workflow, whose goal
transcript replays the full history including the unanswered tool result.
Also require the server-assigned requestID on each runMCPTool action frame
instead of synthesizing a random one: a fabricated non-empty id is silently
discarded by the DWS executor outbox (misses the awaiting-futures map),
which would strand the tool call. DWS always assigns a non-empty requestID
(verified against contract.proto and the proto->JSON relay shape), so a
genuinely missing id now fails the turn fast with diagnostics.