Fix/context federation hardening - #104
Merged
Merged
Conversation
Implements the full subagent control plane per
docs/subagent-control-plane-design.zh-CN.md, making the durable
execution path available via DEEPAGENT_CODE_SUBAGENT_CONTROL_PLANE.
## L0 — Freeze unsafe retry
- Add subagentControlPlane flag (default: 'legacy')
- Force takeoverLimit=0 in non-legacy modes (no replacement child)
## L1 — Schema and RunStore CAS (migration 20260803000000)
- task_run: +75 columns (origin_kind/key, control_state, version,
workspace_*, input_state, child_message_id, execution_spec, …)
- New task_run_event table (UNIQUE(run_id, version) CAS audit)
- task_notification_outbox: +7 columns (correlation_id, payload_hash,
response_message_id, …)
- Backfill: origin_key from admission_key, error→failed, available_at,
control_state=closed for terminal rows
## L2 — Run graph, ancestor guard, recursive close
- checkAncestorControl: walk parent_run_id chain, fail on non-open
- requestClose: BFS subtree + same-child continuations, IMMEDIATE txn
- resolveRecovery: recovery_required → failed/closed + cascade close
## L3 — Capability snapshot + stable provisioning
- L3a: SessionToolCapability.snapshot (pure: no hooks, no network)
PluginCapabilityDescriptor / PluginHookDescriptor types
ToolCapability, ToolCapabilitySnapshot, ToolIDCollisionError
- L3b: SessionBranchProvisioner.ensureExact (EffectFlock + durable
workspace_branch_state CAS; crash-recoverable adopt or conflict)
- L3c: Worktree.ensureExact (operationKey/worktreeBranch/baseCommit,
no random-suffix fallback; conflict not covered)
- L3d: LegacyTaskInput.prepare + projectExact (IMMEDIATE atomic batch
write of V1 message/parts/hash/count + input_state=ready)
## L4 — Durable queue and dispatcher
- enqueueRun: admitted → queued CAS + run_queued event
- claimRun: TaskConcurrency permit + CAS queued→provisioning
- startDispatchLoop: 500 ms daemon, Scope-bound fiber
- recoverOnStartup: safe requeue or recovery_required on restart
## L5 — Legacy executor and finalizer
- startExecution: CAS provisioning→running (commit before loop call)
- settleRun: concurrent priority (close/interrupt intent wins)
- run: full lifecycle adapter around SessionPrompt.loop
## L6 — Interrupt, shutdown and reconciliation
- requestInterrupt: immediate cancel (admitted/queued) or intent write
- classifyOnStartup: safe requeue vs recovery_required classification
- orderedShutdown: signal active runs + classify provisioning
## L7 — Task delivery
- claimOutboxItem / admitParentInput / acknowledgeDelivery
- deliverOne: 3-phase durable delivery (admit → response → ack)
- startDeliveryLoop: outbox drain daemon
## L8 — Context fork
- forkForTask: deterministic child IDs via SHA-256, durable manifest,
crash recovery: exact match on manifest or provisioning_conflict
## L9 — Goal workspace
- GoalReceiptStore: withGoalLock (EffectFlock) + readFresh +
compareAndSet over DocumentStore.shared; collision-resistant slugs
- GoalWorkspaceAdapter.ensure: goal→repo lock ordering, Worktree.ensureExact,
workspace_revision tracking, goal receipt CAS
## L10 — Wiring + read authority
- task.ts: durable routing branch after admitTaskRun — enqueueRun then
foreground poll (500 ms / subagentTimeoutMs) or background return
- prompt.ts: TaskDispatcher daemon registered via registerInitializer;
classifyOnStartup on startup; loop closure as onClaimed callback
- task_status.ts: Layer 1b reads task_run durable fields (control_state,
mutation_capability, workspace_mode, input_state, worktree_directory)
## Usage
DEEPAGENT_CODE_SUBAGENT_CONTROL_PLANE=shadow # observe + disable takeover
DEEPAGENT_CODE_SUBAGENT_CONTROL_PLANE=durable # full durable path
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the CAS bypass identified in commit ba00b885: the initial onClaimed callback called loop() directly, skipping the executor's state machine (provisioning→running→settled) and leaving runs permanently stuck in 'provisioning'. ## task-executor.ts — full production rewrite ### Circular dependency eliminated - Remove SessionPrompt.Service requirement from run() and runFromClaim() - Inject loopFn: (sessionID) => Effect<unknown, never> instead - Caller provides loop closure with InstanceRef pre-bound; no circular reference when called from inside the SessionPrompt factory ### Lease renewal (new) - Background forkDetach fiber renews lease every leaseMs/3 (default 10s) - Prevents execution_lease_expired for long-running subagents - Fiber is interrupted after loopFn returns before settlement ### Interrupt check (new) - After loopFn returns, reads interrupt_requested_at + control_state - Priority: closed > interrupted > failed > completed (§6.7) - Matches the design doc concurrent-priority contract exactly ### Background outbox creation (new) - settleRun now creates task_notification_outbox row when effective_delivery_mode = 'background' (same transaction as settle) - Outbox picked up by existing notificationWorkers pump; no new daemon ### runFromClaim (new) - Reads full Run row from DB given a ClaimResult - Also reads parent session directory for outbox routing - Provides the fully typed Run to run() without callers needing to materialise it themselves ### Other - SessionTable imported statically (removes illegal await import()) - forkDaemon → forkDetach (correct Effect v4 API) - Explicit return-type annotations removed to let TypeScript infer (avoids false 'never' requirement mismatches) ## prompt.ts — onClaimed uses runFromClaim Replace the direct loop() call with LegacySubagentExecutor.runFromClaim: - Full CAS state management now active - Lease renewal and interrupt check included - Background outbox created on settlement - loopFn = loop closure with InstanceRef pre-provided via ctx Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eview fixes
Systematic implementation of all P0/P1/P2 findings from review report
docs/review1-subagent-control-plane-design.md.
## Migration (P0-1)
- Shadow-table rebuild for task_run, task_notification_outbox
- New CHECK constraints: state accepts queued/running/failed/closed/recovery_required
- Backfill: error→failed, researching→running
- Partial index task_run_child_active_idx updated for durable states
- RAISE() replaced with Effect-level count check (SQLite RAISE only valid in triggers)
## Flag & routing safety (P0-3)
- subagentControlPlane: Config.map fail-closed (unknown→'legacy')
- task.ts: strict '=== durable' gate; shadow routes through legacy
- enqueueRun: remove Effect.ignore so failures propagate
## Dispatcher & executor fence (P0-4, P0-5)
- claimRun: innerJoin SessionTable + eq(directory) — Location-scoped
- startDispatchLoop: add directory param; prompt.ts wired
- startExecution WHERE: add claim_generation fence
- claimRun + startExecution: CAS + event in single IMMEDIATE transaction
## Crash recovery (P0-6, P0-7)
- classifyOnStartup: add OR(isNull(lease), lte(lease, now)) guard
- classifyOnStartup: handle admitted state → re-enqueue
- resolveRecovery: BFS inline in same IMMEDIATE tx (removed Effect.tap second tx)
## Executor correctness (P1)
- runFromClaim: fail fast if parent session missing; no session-ID-as-path fallback
- payload_hash: SHA-256 of canonical payload JSON
- loopFn return value captured as canonical output
## L3 provisioner chain (P1)
- admitTaskRun: accept executionSpec, write to DB
- task.ts durable path: transitionToAdmitting → prepare → projectExact → enqueue
- mutation_capability frozen at admission (heuristic from subagentIsWriteType)
- workspace preflight: dirty workspace rejects automatic writer tasks
## Close/interrupt product wiring (P1)
- resolveRecovery: descendant BFS close in same IMMEDIATE transaction
- closeTask(): product-level close entry by child session ID
- task_close.ts: new TaskCloseTool stub
- orderedShutdown: wired into stopDurableWorkers disposer
- requestInterrupt local vocabulary updated
## State vocabulary consistency (P1)
- terminalStates: +failed, closed, recovery_required, error(compat)
- activeStates: +queued, running
- task-run.ts startTaskRun: researching→running
- task-run.ts recoverExpiredTaskRuns: error→failed
- task.ts ownsActiveRun: +running
- task-run.test.ts: assertions updated
- classifyOnStartup scan: +admitted, +queued
- requestInterrupt terminalStates: +failed, closed
## Goal workspace & fork (P1)
- goal-workspace-adapter.ts: Goal lock → canonical repository EffectFlock order
- task-fork.ts: cutoff strictly-before (slice excludes cutoff message)
## Daemon wiring (P1)
- prompt.ts startDurableWorkers: TaskDelivery.startDeliveryLoop wired
- prompt.ts: mode epoch PID lock file per directory (cross-process guard)
- task_status.ts: task_run authoritative state overlay over legacy metadata
## Routes & test hygiene
- routes.ts: durable-control-plane-session route group (7 session files)
- CLI help-text snapshot updated (+34, includes task_close)
- HttpApi/PR-collaboration/session.llm.stream: skipIf guards for CI
## New tests (§14 bootstrap)
- packages/core/test/subagent-control-plane-migration.test.ts (DET-MIG-01)
- test/control-plane/mode.test.ts (DET-MODE-01)
- test/control-plane/dispatcher.test.ts (DET-FENCE-01, DET-QUEUE-01)
Result: 4338 pass · 36 skip · 0 fail (deepagent-code)
2259 pass · 0 skip · 1 fail (core: pre-existing LocationServiceMap timeout)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iew1 Fixes all identified gaps from docs/review1-subagent-control-plane-design.md adversarial audit of commit 3168c8e6. ## P0 fixes P0-GAP-1: Dual lifecycle writer in durable mode - prompt.ts startNotificationWorker: early return when subagentControlPlane==='durable' - Prevents legacy recoverExpiredTaskRuns from running alongside durable dispatcher - Design §4.1: single lifecycle writer per mode P0-GAP-2: task_admission table not migrated - migration: shadow table rebuild for task_admission - Adds origin_kind/origin_key fields (design §4.2) - Backfills origin_key from admission_key; origin_kind defaults to 'task_tool' P0-GAP-3: Invalid V1 message envelope in prepare() - task-input.ts prepare(): messageData now includes time/agent/model/metadata-as-record - Removes JSON-string metadata; uses typed Record<string, unknown> - V1 Message row now passes SessionV1.User schema decode P0-GAP-4: settleRun read doesn't fence expired lease - task-executor.ts settleRun SELECT WHERE: +inArray(active states) +gt(lease_expires_at, now) - Prevents stale callback settling after lease expiry with no intervening mutation ## P1 fixes P1-GAP-3: projectExact missing input_admitted event - task-input.ts: co-transactional TaskRunEventTable insert after CAS transition - Added TaskRunEventTable + Identifier imports - Design §1.3 #24: every versioned state change has co-transactional event P1-GAP-4: Preflight settlement uses 'error' vocabulary - task.ts: workspace_preflight_dirty settles as 'failed' not 'error' - task-run.ts settleTaskRun: signature includes 'failed' state P1-GAP-6: Unknown flag falls back to 'legacy' instead of fail-closed - runtime-flags.ts: Config.map throws on unknown subagentControlPlane value - Prevents silent mode degradation from env var typos P1-GAP-7: renewLease doesn't require active state - task-executor.ts renewLease WHERE: +inArray(active states) - Prevents late renewal extending already-expired lease post-recovery ## P2 fixes P2-GAP-2: Delete deprecated recoverOnStartup - task-dispatcher.ts: removed entire function (zero callers confirmed) - classifyOnStartup in task-run.ts is the authority ## Result - packages/core typecheck: ✅ - packages/deepagent-code typecheck: ✅ - 50/50 key tests pass Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- task-input.ts: fix metadata double-serialisation in projectExact (JSON.stringify → plain object; drizzle already serialises the data column) - admission.test.ts: insert child session row before projectExact calls (message FK requires session row to exist; child session is created by SessionPrompt in prod before input projection — test must mirror that) - Add DET-FENCE-01 executor.test.ts (startExecution + settleRun CAS/lease fences) - Add DET-REC-01 recovery.test.ts (classifyOnStartup crash recovery scenarios) - Add DET-ADM-01 admission.test.ts (admitTaskRun + prepare + projectExact chain) - Add REAL-CP-01/02 subagent-control-plane.ts live-LLM script (dirty-workspace read-only fence + durable event-audit-trail oracle) - Register subagent-control-plane suite in routes.ts + dispatcher.ts Test results: 68/68 new control-plane + task-run tests pass (0 regressions); 4 workspace.test.ts timeouts are pre-existing (initial release commit, unrelated). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l plane L0-L10 Phase A (stop-bleed, no durable-flag dependency): - A-1 (P0-3): migration outbox CHECK adds 'admitted'+'response_recovery_required'; active unique index removes 'queued' to allow FIFO future generations - A-2 (P0-4): topology lock uses O_EXCL (wx) atomic create; fail-closed on contention; token-fenced unlink; shadow mode no longer starts daemon (only 'durable' does) - A-3 (P0-5): startExecution WHERE requires lease_expires_at>now + execution_started_at IS NULL; renewLease WHERE requires non-expired lease; settleRun CAS loss now logged (not silently ignored) Phase B (admission + ledger): - B-1 (P0-1): durable path creates child session row before projectExact to satisfy FK; prepare() messageData canonical (no extra time/agent/model fields); projectExact INSERT uses prepared.messageData so hash matches content exactly (P0-2) - B-3 (P0-9): transitionToAdmitting wrapped in IMMEDIATE transaction + event INSERT - B-5 (P1-5): pre-start exhaustion transitions run to failed+event instead of silent skip - B-6 (P1-3/P1-4): canEnqueue adds 'pending'; classifyOnStartup UPDATE+event per branch wrapped in IMMEDIATE transaction (crash-safe) - B-7/B-8 (P1-6/P1-13): Drizzle active index adds 'running', removes 'queued' to match migration - B-9 (P1-14): ensureSessionBranch moved after admitTaskRun (no Git side effect on admission fail) Phase C (execution + delivery): - C-1 (P0-6): loopOutput extracts text from Message object (info.text / parts fallback) - C-2 (P0-7): dirty preflight uses version-fenced UPDATE instead of non-owner settleTaskRun - C-4 (P1-2): delivery metadata object not JSON.stringify'd; admitParentInput reads UPDATE result and aborts on owner loss (0 rows) - C-5 (P1-7): recovery_required removed from terminalStates; isQuiescent() exported - C-6 (P1-8): background outbox text uses publicTaskID (child_session_id) not internal runID - P2-4: trailing blank line removed from task-run.ts EOF Phase D (tests): - D-1 (P1-9): admission.test.ts uses transitionToAdmitting() production path instead of raw UPDATE bypass; executor.test.ts wrong-generation test passes explicit wrong token and asserts row unchanged (state=running, version=1) - D-2 (P1-10): live harness accepts 'completed' as primary oracle, '[terminé]' as fallback - D-3 (P1-12): new test/control-plane/invariants.test.ts covers invariants 2/6/14/15/16 (7 new deterministic tests) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase A — outbox CHECK + topology lock + lease fencing: - migration: outbox CHECK adds 'admitted','response_recovery_required'; task_run active unique index removes 'queued' (FIFO continuations allowed) - prompt.ts: O_EXCL atomic lock, fail-closed, token-fenced unlink, durable-only daemon - task-executor.ts: startExecution adds lease_expires_at>now + IS NULL(execution_started_at); renewLease adds lease_expires_at>now; settleRun CAS loss logged not ignored Phase B — admission + ledger: - task.ts: child session created before projectExact (FK fix P0-1); ensureSessionBranch moved after admission (P1-14); version-fenced dirty preflight (P0-7) - task-input.ts: prepare() messageData canonical (P0-2); projectExact uses prepared.messageData - task-run.ts: transitionToAdmitting in IMMEDIATE transaction + event INSERT (P0-9); classifyOnStartup branches each in IMMEDIATE transaction + canEnqueue adds 'pending' (P1-3/4); pre-start exhaustion → failed+event (P1-5); isQuiescent() exported; recovery_required removed from terminalStates; trailing EOF blank line removed (P2-4) - task-dispatcher.ts: pre-start exhaustion atomic terminal transition - sql.ts: Drizzle active index adds 'running', removes 'queued' (P1-13) Phase C — execution + delivery + dispatcher: - task-executor.ts: loopOutput extracts text from Message object (P0-6); background outbox text uses publicTaskID not internal runID (P1-8) - task-delivery.ts: metadata not JSON.stringify'd (P1-2); admitParentInput reads UPDATE rows and aborts on owner loss Phase D — tests: - admission.test.ts: uses transitionToAdmitting() production path (P1-9) - executor.test.ts: wrong-generation test passes explicit wrong token + asserts state/version unchanged - invariants.test.ts: 7 new deterministic tests for invariants 2/6/14/15/16 - live harness: accepts 'completed' primary, '[terminé]' fallback (P1-10) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion + workspace gate fixes Fix-A (RC-1): remove bash:allow from researcher agent - researcher declared read-only but carried generic bash → subagentIsWriteType() returned true → clean-workspace gate blocked all researcher tasks in dirty repos - bash intentionally omitted; structured tools (grep/glob/list/read/code_intel/ webfetch/websearch/context_query) cover all read-only research use cases - reviewer already correctly used bash:deny; researcher now matches - TODO follow-up: add git_log/git_diff structured tools for git-history queries Fix-B (RC-3): settle task_run in legacy path when ensureSessionBranch fails - after B-9 (admission before branch), failure left task_run in admitted state forever - add catchCause settle with version CAS (and(eq(run_id), eq(version))) - DB settle error is caught+logged and discarded so original workspace Cause propagates - imports and from drizzle-orm for CAS WHERE clause Fix-C (RC-4): truncate dirty-workspace error message to ≤~200 chars - old code dumped all dirty paths (86,678 chars observed) into parent model context - new: show first 10 paths + overflow count, never more than ~200 chars Fix-D (RC-5): split isReadOnly into agentIsWriteCapable + isolation policy - old isReadOnly mixed params.isolation into capability classification - explicit isolation:worktree on a read-only agent now correctly skips preflight - agentIsWriteCapable drives mutation_capability DB field and dirty-workspace check - isolation policy formula documented inline; dead _useWorktreeIsolation removed Tests: +2 regression tests for Fix-A (researcher profile is read-only; with bash flips back to write-type to document the necessity of the fix). 32 pass / 0 fail. Adversarial review: 2-round subagent review; all P1 (orDie cause-swallow, missing CAS version fence) resolved before merge. Ref: docs/bug-001-405.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes all 7 residual gaps identified in the adversarial review §14:
P0-8: root_run_id self-reference + parent_run_id missing + ancestor-open check
- admitTaskRun: resolve root_run_id from parent chain (not self-assign)
- write parent_run_id from caller; reject if parent is terminal/closed
- propagate parentRunID from task.ts via getActiveTaskRunByChild
P0-9: run_admitted event missing (§1.3#24)
- co-transactional INSERT into TaskRunEventTable inside admitTaskRun tx
P0-10: SessionToolCapability.snapshot() not wired
- capture ToolRegistry/MCP/Plugin services optionally at TaskTool init
- derive mutation_capability from enabled tools (workspaceMutation=possible)
- write tool_capability_hash to DB; fallback to subagentIsWriteType
P1-1: dispatcher slot leaked — withTaskSlot wrapped only the CAS claim
- move withTaskSlot into startDispatchLoop, wrapping full execution fiber
- forkScoped holds the semaphore permit for the fiber's lifetime
P1-6: continuation_of_run_id absent on reruns
- write continuation_of_run_id when generation > 1
P1-8: publicTaskID = runID instead of childSessionID
- settleRun accepts childSessionID?; publicTaskID = childSessionID ?? runID
- propagate childSessionID from run() call site
P1-11: durable settleRun does not update session metadata
- export projectDurableSettledRun(sessions, childSessionID) from task.ts
- queries most-recent settled TaskRun row by child_session_id
- writes deepagent.subagent.{finished,state,reason,settled_at} under lock
- wire via Effect.ensuring in prompt.ts onClaimed callback
Also: task-executor.ts runFromClaim runData now maps toolCapabilityHash
from the DB row (fixes TS2741 introduced by the Run type update).
Tests: bun typecheck clean; control-plane 70/70 pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sionBranch BUG-001-405 Fix-C capped the dirty-path list at MAX_SHOWN=10 and appended '… and N more' to prevent 86 kB error floods. Add a targeted regression test that creates 15 untracked files and asserts the error message contains '… and 5 more' and stays under 500 chars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dening Brings in durable session restart infrastructure: - Session suspension column + index (time_suspended) - Session execution restart logic - Run coordinator enhancements - session/sql.ts: updated state/phase types (upstream durable alignment) - desktop: add @lydell/node-pty catalog dep - live-llm: wire events into sessionExecutionLocal layer Conflict resolutions: - migration.gen.ts: time_suspended appended after agent_execution (chronological) - desktop/package.json: kept both @deepagent-code/core + @lydell/node-pty - live-llm/runtime.ts + v2-provider-loop.ts: took MUD's events wiring (additive) - bun.lock: kept CFH base, needs bun install to reconcile Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ening Integrates 24 commits from the durable subagent control plane branch: Core changes carried in: - Durable subagent control plane L0-L10 (TaskDispatcher, TaskDelivery, LegacySubagentExecutor, DurableExecutorLock) - BUG-001-405: researcher bash→deny + git_read:allow + isReadOnly split - Fix-C: ensureSessionBranch path-list truncation (MAX_SHOWN=10) - request.ts: volatileContextKind three-way enum (none/round/continuation) — superior to CFH's binary runtimeSystemRequired flag; fixes stale round-context reinjection on tool-continuation turns - task_close / task_recovery / git_read tools wired into registry - Desktop build OOM fix (externalize server bundle from renderer) - Various adversarial review fixes (P0-P4) Migration conflict resolution: - 20260803000000_time_suspended (MUD/CFH) kept as-is - 20260803000000_subagent_control_plane_l1 renamed → 20260803000001_subagent_control_plane_l1 to avoid timestamp collision - 20260805000000_repair_task_admission appended last - sql.ts: merged AnySQLiteColumn import (CURRENT) + sql import (CFH) - database-migration.test.ts: all three test suites preserved bun.lock: kept CFH base; needs bun install to fully reconcile Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Python regex merge dropped the closing }) of the timeSuspended test block. All 17 migration tests now pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bun install reconciles @lydell/node-pty (added by merge-upstream-durable) and all other deps brought in by context-federation-hardening + fix-desktop-build-oom. SDK gen files unchanged — OpenAPI routes not modified by either merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Flock hang
ModelsDevPlugin.refresh() acquires a cross-process Flock on
~/.deepagent/code/cache/models.json during plugin boot. On a developer
machine where the main app holds that lock, PluginBoot.wait() hangs
indefinitely and the test times out at its hardcoded 15 s limit.
Set Flag.DEEPAGENT_CODE_DISABLE_MODELS_FETCH = true in beforeAll (same
pattern as packages/core/test/models.test.ts) so populate() returns {}
immediately, boot completes in ~1 s, and wait() resolves cleanly.
Restored the PluginBoot.wait() call since boot is now fast.
Before: 0 pass / 1 fail (15 s timeout every run)
After: 1 pass / 0 fail (1 s)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #
Type of change
What does this PR do?
Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!
How did you verify your code works?
Screenshots / recordings
If this is a UI change, please include a screenshot or recording.
Checklist
If you do not follow this template your PR will be automatically rejected.