Skip to content

feat(paths): saves and settings in a per-user data folder, caches and logs in the game folder's cache/ and logs/ - #2317

Open
LNDF wants to merge 10 commits into
NotRexed:mainfrom
LNDF:structure
Open

LNDF wants to merge 10 commits into
NotRexed:mainfrom
LNDF:structure

Conversation

@LNDF

@LNDF LNDF commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Saves, settings, logs and several GB of shader caches were all written loose into the game folder (on Windows even the saves), mixed in with what the build made. This sorts them: your saves and settings go to a per-user data folder that survives rebuilding or deleting the game folder, and the caches and logs go into two clearly named folders of the game folder.

What changed

Where files go

What Before Now
Saves (save/, mouse_sens.txt, …) <game>\user (Windows), ~/.local/share/hookshot (Linux) data folder: %LOCALAPPDATA%\Hookshot / $XDG_DATA_HOME/hookshot
hookshot_options.ini, hookshot_env.txt, hookshot_update_skip.txt game folder data folder (hookshot_env.txt beside the game is still read when the data folder has none)
Shader and pipeline caches, hookshot_driver_faults.txt game folder <game>/cache/ (safe to delete; one per game folder, so a release and a nightly don't invalidate each other)
Generated button prompts <game>/prompts/ <game>/cache/prompts/
hookshot_log*.txt, hookshot_startup.log, hookshot_frames.csv game folder <game>/logs/
language.txt, hookshot_shaders.bin, shipped lists game folder unchanged (the build's own)

Exceptions:

  • --user <folder> sets the data folder; a run given an explicit image.bin uses ./user.
  • Portable install (.hookshot_builder_home in the game folder, feat(builder): add relocatable Windows portable installations #2367): the data folder is <game>\user, so saves go with the drive. Only the settings move into it; nothing goes to %LOCALAPPDATA%.
  • A second game folder whose saves clash with the first's (a nightly beside a release) keeps its saves and settings in its own <game>\user.

Code

  • New host/hos_data.c/.h: picks the data folder once at start (before hookshot_env.txt and the log), makes cache/ and logs/, and moves an older build's files there on the first start. Replaces Linux's saves_dir() in hos_main.c.
    • Saves move all or nothing: an entry that can't move brings back the others (a partial original left by a copy is removed first).
    • Links (fix(saves): the Linux saves' move leaves a linked folder and its saves alone #2282's handling, carried over): on Linux a link in the saves is made again in the data folder to the same folder, never renamed or copied through; a user that is a link itself is used as it is. On Windows a link or junction keeps the saves where they are.
    • Saves in both places (a downgrade, then the upgrade again): this folder's are used, nothing is merged, and saves-in-two-places.txt says where both are and how to switch.
    • A copied user that can't be fully deleted is renamed user.moved.
    • files-moved.txt says what moved; the log also says when a settings file beside the game is not read.
  • Host readers of the settings (nn_applet, nn_hid, nn_audio, discord_rpc, prompts, hos_main, hos_update) use hos_data_path().
  • Renderer: MkwVkHost.data_dir (MKWVK_API_VERSION 19); vk_paths.h gets data_path(), cache_path(), log_path(), used for the options file, the program cache, pipeline cache, notes, state list, prepared record, driver faults and frame trace.
  • The Vulkan pipeline cache is renamed mkw_vk_pipelines*.bin → hookshot_vk_pipelines*.bin; existing ones are renamed as they move, so nothing is rebuilt.
  • make_game.py seeds the recorded pipeline list into cache/hookshot_pipelines.bin, as main seeds the game folder's.
  • scripts/oot_saves.py finds Windows saves in %LOCALAPPDATA%\Hookshot without --hookshot.
  • Docs: readme (where files go, updating, uninstalling, log location), CONTRIBUTING, development, versioning, saves.md, the game-problem issue form.

Split out at review: the Linux builder's DLSS SDK move is #2485 (merged); its Windows counterpart is #2528.

Testing

  • python -I -B tests/run_all.py on Linux: same results as main. fix(saves): the Linux saves' move leaves a linked folder and its saves alone #2282's tests/test_linux_saves_move.py runs all its cases on hos_data.c. New tests/test_data_dir.py builds hos_data.c and covers folder choice, the migration, the bin/ layout, a second game folder, a relative link on the same drive, the portable rule, the "not read" log, and copies across file systems (rollback, links, read-only originals).
  • hookshot_vk.so compiled and linked in an Ubuntu 24.04 container; Linux AppImage built.
  • Not yet done: a Windows build, and a run of the game (updating a folder built by main). The Windows save move across drives is the main thing to check.

🤖 Generated with Claude Code

LNDF and others added 4 commits November 11, 2026 14:17
The shader and pipeline caches are made for one build and one GPU and run to
several GB, so they stay with the game, on the drive the player chose for it,
instead of the data folder (%LOCALAPPDATA% is on C:). Each game folder keeps
its own, so a release and a nightly side by side no longer undo each other's.
Saves and settings stay in the data folder.

- <game>/cache/: hookshot_programs.bin/.idx, hookshot_pipelines.bin, the Vulkan
  pipeline cache and the other caches. Safe to delete: the next start prepares
  the shaders again.
- <game>/logs/: hookshot_log*.txt, the Linux builder's hookshot_startup.log and
  the RX_VK_FRAME_TRACE trace.
- The Vulkan pipeline cache is renamed from mkw_vk_pipelines*.bin to
  hookshot_vk_pipelines*.bin; the first start moves an old one into cache/
  under the new name, so it is not built again.
- hos_data.c makes cache/ and logs/ at every start and moves loose caches and
  logs into them; the renderer uses vk_paths.h cache_path()/log_path().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of the data folder change found ways the one-time move could lose
or hide saves on Windows (where it copies across drives), a crash loop with
the shipped pipeline notes, and per-PC files still in the wrong place.

- Saves move all or nothing: an entry that cannot go brings back those that
  went. A link or junction is never copied through or deleted into. A copy
  whose original cannot be deleted leaves <game>/user renamed user.moved, so
  the next start does not go back to the leftover; the log, files-moved.txt
  and the readme say so.
- The shipped pipeline notes are taken once per stamp (cache/
  hookshot_pipelines_shipped.taken): notes the precompile dropped after two
  crashed starts are not taken back at the next start.
- The generated button prompts go to <game>/cache/prompts (a cache, and one
  per game folder); nothing is written when that folder cannot be made.
- language.txt is read only from the game folder again: it is the build's.
- files-moved.txt says only what moved.
- The precompile's crash count (hookshot_pipelines.bin.running) moves into
  cache/ too, and stale mkw_vk_pipelines*.tmp files are deleted.
- Docs: the issue form, development.md and versioning.md name the new places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The readme, saves.md, design.md and the game-problem issue form describe the
current layout only; older docs keep the history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LNDF

LNDF commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Once we update to a major versioon, we can remove the migration code

Conflicts: host/hos_main.c and host/nn_hid.c, two additions side by side;
both kept (the data folder's log line, then the control bridge's start).
MKWVK_API_VERSION 17: upstream's version 16 added the control bridge's
renderer functions and this branch's version 16 added MkwVkHost.data_dir;
the merged interface is neither, so it is a version of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Arkex-ai

Copy link
Copy Markdown
Contributor

Heads-up on an integration point with #2367 (portable Windows Builder): it keeps the Builder home, settings/work, updater and game-to-Builder association relocatable. This PR changes the game's save/settings location, so we should agree the behavior when a portable-built game starts: shared per-user data by default versus an explicit game-local --user profile. I don't want to change the default save policy implicitly or migrate existing saves twice. Could we coordinate that contract and the migration tests before either branch absorbs the other's path changes? The launcher proposals #2373 and #2403 also need to consume the same decision.

Conflicts resolved:
- MKWVK_API_VERSION 18: upstream's version 17 is prompts_live (NotRexed#2366); the
  data folder's MkwVkHost.data_dir follows it as version 18.
- host/hos_update.c: "Skip this version" stays in the data folder; upstream's
  call order (NotRexed#2283, the check after the saves' place is known) is kept, with
  hos_data_init having chosen that place first. tests/test_linux_update_skip.py
  builds the start with hos_data.c instead of the removed saves_dir.
- host/prompts.c: upstream's live prompts and mod size tables (NotRexed#2366, NotRexed#2270),
  with the generated files (and the new rsizetable.mod.zs) in cache/prompts;
  made_dir makes cache/ too.
- builder/linux/play-diagnostics.sh: upstream's socket stderr (NotRexed#2279), with
  hookshot_startup.log in logs/.
- The program cache moved to vk_progcache.cpp (NotRexed#2323): its path is cache/.
- hookshot_driver_faults.txt (NotRexed#1823) goes to cache/ and is moved there.
- env_on/env_value_off (NotRexed#2042), env_override (vk_menu.cpp), the prepare
  watchdog (vk_main.cpp), audio and docs merged with the data folder paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LNDF

LNDF commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up on an integration point with #2367 (portable Windows Builder): it keeps the Builder home, settings/work, updater and game-to-Builder association relocatable. This PR changes the game's save/settings location, so we should agree the behavior when a portable-built game starts: shared per-user data by default versus an explicit game-local --user profile. I don't want to change the default save policy implicitly or migrate existing saves twice. Could we coordinate that contract and the migration tests before either branch absorbs the other's path changes? The launcher proposals #2373 and #2403 also need to consume the same decision.

<@324633150510727178> about the comment on #2317 and the portable/launcher PRs. here's what I'm thinking:

  • normal installs: saves + settings go to the per-user data folder (%LOCALAPPDATA%\Hookshot / ~/.local/share/hookshot), caches and logs stay in the game folder (cache/, logs/)
  • portable (feat(builder): add relocatable Windows portable installations #2367): the data folder just stays inside the game folder, so everything moves with the SSD. probably \user since that's where windows saves already are, so nothing has to be migrated. the game can tell it's portable from the .hookshot_builder_home marker
  • the migration to the per-user folder only happens on normal installs, so saves never get moved twice
  • launcher (feat(builder): the launcher, one window to play, build and update (Windows and Linux) #2373): anything it writes like hookshot_env.txt goes to the data folder, wherever that ends up, not the game folder

if that sounds ok I'll add the portable part to my PR so you can build on it

@Joey-Magnus Joey-Magnus 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.

Separating the player's files from the build's is the right direction, and the save move is careful in the important places: the first pass checks before anything moves, a failure rolls back, delete_tree never enters a link, and user.moved stops a later start from reading leftovers as saves. MkwVkHost.data_dir is added at the end with MKWVK_API_VERSION bumped, and paths stay UTF-8 (GetEnvironmentVariableW + CP_UTF8, under the exe's UTF-8 manifest). I merged the branch with current main (no conflicts) and ran the checks it touches: test_data_dir, test_env_file, test_linux_update_skip, test_oot_saves, test_pipeline_play_cache, test_pipeline_notes_open, test_menu_env_on, test_mods_size_table, test_make_game_ngx, test_utf8_paths, test_prompts_live, the driver-fault and precompile checks and a few more. All pass. I focused on the save and settings migration, since that's where a mistake costs players data. Findings:

blocking: the Windows save move hasn't been run. This PR moves every Windows player's saves for the first time, from <game>\user to %LOCALAPPDATA%\Hookshot, usually across drives, so through copy_tree + delete_tree. Windows is most of the player base, and you list it yourself as not yet done. Please run at least: game folder on a second drive; a save file held open or read-only (to exercise rollback and user.moved); a non-ASCII user name; and a second start afterwards. Also, only the title check has run on 0981ace7. The full game-free suite and the Windows Vulkan build still have to go green on this head.

blocking host/hos_data.c:163-164: move_entry tries MoveFileExA before tree_has_link. On the same drive, an entry that is a symbolic link is therefore renamed as it stands, and a relative one (user/save -> ../../sync/save) names nothing from ~/.local/share/hookshot: the player sees no saves and can't write any. This is the bug #2282 fixes in the code this PR replaces. Checking tree_has_link(from) before the rename (keep the saves where they are, as you already do for the copy) or carrying over #2282's re-linking would close it. Whichever of the two PRs lands second needs to carry the other's handling and its test (test_linux_saves_move.py cuts saves_dir out of hos_main.c, which this PR deletes).

should fix host/hos_data.c:294-305: when a game folder's saves can't move (the usual case for a second game folder, such as a nightly beside a release, whose user/save collides with the first one's), hos_user_dir stays <game>/user, but data_dir is still the shared folder. That folder's hookshot_options.ini also collides, so move_file leaves it loose in the game folder, where nothing reads it any more. That game then runs with its own saves but the other folder's settings, and its own settings are silently dropped. Please either keep saves and settings together (when the saves stay, data_dir stays too) or log clearly that the game folder's hookshot_options.ini is ignored. The same applies to hookshot_env.txt: once the data folder has one, a file put beside the game, which is what issues and Discord have told players to do until now, is ignored without a word (hos_main.c env reading). A log line when both exist would save support time.

should fix: the scope is large for one PR (CONTRIBUTING, "one topic"). The saves/settings move, the cache/ and logs/ split with the pipeline-cache rename, the shipped-notes semantics (seed_pipelines no longer overwrites the notes, notes_adopt_shipped plus .taken), and the Linux builder's NGX SDK relocation can each break separately and would be much easier to review, test and revert apart. At minimum I'd take the shipped-notes change and the NGX SDK move out into their own PRs.

question: the portable-builder contract with #2367/#2373/#2403 (Arkex-ai's comment, your reply) is still open. Merging this first makes "per-user data folder" the default for portable builds too, until the follow-up lands. Is that acceptable to them, or should the portable check (.hookshot_builder_home) be in this PR?

minor host/hos_data.c:168,211: if an entry's copy succeeded but its original couldn't be fully deleted (left), and a later entry fails, the rollback finds the partial original still there (exists(to)) and keeps both. The game then runs from <game>/user with that entry incomplete, while the complete copy sits in the data folder. Nothing is lost, but the player sees missing saves. The rollback could delete the partial original before moving the complete copy back.

minor: the PR description says MKWVK_API_VERSION 16, but the code is 18, and #2261 also takes 18 for MkwVkHost.wheel, so whichever merges second has to go to 19. Also, nx2/make_game.py:1021 says the notes are in "the data folder's shaders/hookshot_pipelines.bin", but pipe_notes_path() uses cache/hookshot_pipelines.bin.

…aves, portable data folder

- A link in the saves (a symbolic link or a Windows junction), or a folder
  holding one, is never moved: on the same drive it was renamed into the
  data folder, where a relative link names nothing. The saves stay with it.
- When a game folder's saves cannot move (a second game folder beside the
  first), the data folder stays with them in <game>/user, so it keeps its own
  saves and settings instead of the first folder's settings.
- The log says when a hookshot_options.ini or hookshot_env.txt beside the
  game is not read.
- Rollback: what a copy left of an original is deleted before the whole copy
  is moved back.
- A portable install (.hookshot_builder_home, NotRexed#2367) keeps its data folder in
  <game>/user; only its settings move into it.
- The shipped pipeline notes are no longer a change of their own: make_game.py
  seeds cache/hookshot_pipelines.bin as main seeds the game folder's.
- The Linux builder's DLSS SDK move is taken out into its own PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LNDF

LNDF commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Pushed 469aff4a:

  • Links (blocking): a link or a folder holding one is now never moved, rename included, so the saves stay with it. Added a test with a relative user/save -> ../../synced-saves on the same drive. Whichever of this and fix(saves): the Linux saves' move leaves a linked folder and its saves alone #2282 lands second carries the other's test over.
  • Second game folder: when the saves can't move, the data folder stays with them in <game>\user, so the folder keeps its own saves and settings. The log now also says when a hookshot_options.ini or hookshot_env.txt beside the game isn't read.
  • Rollback: a partial original left by a copy is deleted before the full copy comes back. No test for this one: it needs a delete that half-fails and then a later entry that fails, in that order, which depends on the folder's listing order.
  • Portable: added here. With .hookshot_builder_home in the game folder, the data folder is <game>\user, only the settings move into it, and nothing goes to %LOCALAPPDATA%. Same proposal I sent Arkex-ai.
  • Scope: the Linux DLSS SDK move is now fix(builder): Linux keeps NVIDIA's DLSS SDK in its cache folder, and the license accepted in its settings #2485. The shipped-notes change is gone instead of split: with caches in the game folder, make_game.py seeds cache/hookshot_pipelines.bin the same way main seeds the game folder.
  • Minor: description updated (API version 18, noted that feat: an optional item wheel for the R/L item row, Twilight Princess style #2261 will need 19 if it lands second), and the make_game.py docstring fixed.

Still open: the Windows run (second drive, held/read-only save, non-ASCII user name, second start). I'll post results when I have them.

@Joey-Magnus Joey-Magnus 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.

The earlier points are addressed in 469aff4a. Two things remain:

  • Downgrade then upgrade. Verified in host/hos_data.c move_saves/migrate: a player who goes back to an older build in the same folder gets a fresh <game>\user\save. On the next upgrade that collides, so all saves stay in <game>\user and the game shows the new ones, while the migrated saves sit unseen in %LOCALAPPDATA%\Hookshot. Nothing is deleted, but files-moved.txt isn't written and only the log mentions it. This needs handling or a release note.
  • The blocking Windows run is still missing: second drive, save held open or read-only, non-ASCII user name, second start.

Coordination:

  • The conflict is only docs/saves.md.
  • MKWVK_API_VERSION 18 is also claimed by #2261, #2459, #2478 and #2479.
  • #2282 should land first, and this PR should carry its link handling into hos_data.c.
  • #2338's hookshot_mirror_skip.txt is written loose in the game folder, and belongs under cache/ once this lands.

hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…the license accepted in its settings (#2485)

On Linux, an update can silently drop DLSS: the builder uses the DLSS
SDK's `LICENSE.txt` as its record that you accepted NVIDIA's license, so
when that downloaded file is gone, the next update builds without DLSS
and deletes the game's runtime. The SDK (58 MB) also sits in the game's
data folder rather than the builder's own.

Split out of #2317 at review.

## What changed
- The SDK goes to the builder's cache folder,
`~/.cache/hookshot-builder/ngx-sdk`, beside `make_game.py`'s build
files. Windows already keeps it in `%LOCALAPPDATA%\HookshotBuilder`.
- License acceptance is recorded in `~/.config/hookshot/builder.ini`
(`nvidia_license=1`) when you tick DLSS. `folder_has_dlss()` reads that
instead of the SDK's `LICENSE.txt`, so an emptied cache means the SDK is
downloaded again, and DLSS is kept.
- An SDK in the old place (`~/.local/share/hookshot/builder/ngx-sdk`) is
moved once when the builder starts, and its license is recorded.
- readme: the Linux DLSS paragraph names the new place.

## Testing
- `tests/test_make_game_ngx.py`: the new folder, the settings record, an
emptied cache keeping DLSS, the one-time move, including an SDK in both
places.
- `python -I -B tests/run_all.py` on Linux: nothing new fails (the three
failures there also fail on `main`).
- Not run in the AppImage builder yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
blessedplummer pushed a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 11, 2026
…ed, and its trials decide on frame time

hckrman101's review of NotRexed#2338, all points taken:

- Issue 1795's in-place hazard, per pool. A refused pool binds guest memory the recording
  executes, and the game rewrites its constants for the next frame before the GPU gets there -
  on the Deck that was the giant HUD heart, slabs and stretched geometry. The UB snapshot that
  fix provides for mirror-less devices now covers the policy's mirror-less pools too
  (dgpu_ub_in_place: a binding now says whether it is guest memory a recording will execute);
  a trial-won pool's mirror was synced at the submit and keeps the in-place bind.
- The verdict compares the frame the player gets: present-to-present (present_time_total_ns,
  one clock read a present, never reset), not the recordings' GPU timestamps - the mirror's
  cost is mostly CPU and in the upload command buffer those never time, and a GPU-only verdict
  kept mirrors while the frame was CPU-bound, which is the case this policy wins. The windows
  stay 120/12/120. The GPU-only total is gone.
- The claims and the code agree on parallel replays, with no race: the trials are gated on
  RX_VK_PARALLEL itself (par_active only covers the segments' recording and is false again by
  the time the pick runs), so the policy runs trial-less pure direct under it, and the fast
  path's counters (dgpu_read, dgpu_binds) only run when no parallel recording is in progress -
  they are lock-free there and fed the 2x bind-volume check with lost counts before.
- No more abort-forever: the trials need a mirror that can come on (with mirrors off - an iGPU -
  there are none at all: direct is right on unified memory), and a trial whose mirror never
  comes on (a full mirror budget) is given up three times at most before the pool sits out for
  the session.
- The skip list says the device and driver its verdicts were measured on; a file from another
  device or driver, or an older build's without the header, is ignored and its pools trial
  again. (Placement note for upstream's NotRexed#2317 at the path's single site: its per-user cache dir
  would take the file when it lands.)
- The check runs on any C++ compiler ({cxx}, as the neighbouring tests do).

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
…ves in two places, API 19

- NotRexed#2282 (the Linux saves' move leaves a linked folder alone) is carried into
  host/hos_data.c: on Linux a link in the saves is made again in the data
  folder to the same folder (never renamed or copied through), a link to an
  entry that moves too follows it, and a user/ that is a link itself is used
  as it is. On Windows a link or junction keeps the saves where they are.
  tests/test_linux_saves_move.py runs its cases on hos_data.c.
- Saves in both <game>/user and the data folder (a downgrade, then the upgrade
  again): this folder's are used, nothing is merged, and
  saves-in-two-places.txt and the log say where both are and how to switch.
- MKWVK_API_VERSION 19: main's 18 is NotRexed#1990's pad inputs.
- Conflicts in nn_hid.c, docs/saves.md and docs/development.md: main's text
  with the data folder's places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
blessedplummer pushed a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 11, 2026
…ed, and its trials decide on frame time

hckrman101's review of NotRexed#2338, all points taken:

- Issue 1795's in-place hazard, per pool. A refused pool binds guest memory the recording
  executes, and the game rewrites its constants for the next frame before the GPU gets there -
  on the Deck that was the giant HUD heart, slabs and stretched geometry. The UB snapshot that
  fix provides for mirror-less devices now covers the policy's mirror-less pools too
  (dgpu_ub_in_place: a binding now says whether it is guest memory a recording will execute);
  a trial-won pool's mirror was synced at the submit and keeps the in-place bind.
- The verdict compares the frame the player gets: present-to-present (present_time_total_ns,
  one clock read a present, never reset), not the recordings' GPU timestamps - the mirror's
  cost is mostly CPU and in the upload command buffer those never time, and a GPU-only verdict
  kept mirrors while the frame was CPU-bound, which is the case this policy wins. The windows
  stay 120/12/120. The GPU-only total is gone.
- The claims and the code agree on parallel replays, with no race: the trials are gated on
  RX_VK_PARALLEL itself (par_active only covers the segments' recording and is false again by
  the time the pick runs), so the policy runs trial-less pure direct under it, and the fast
  path's counters (dgpu_read, dgpu_binds) only run when no parallel recording is in progress -
  they are lock-free there and fed the 2x bind-volume check with lost counts before.
- No more abort-forever: the trials need a mirror that can come on (with mirrors off - an iGPU -
  there are none at all: direct is right on unified memory), and a trial whose mirror never
  comes on (a full mirror budget) is given up three times at most before the pool sits out for
  the session.
- The skip list says the device and driver its verdicts were measured on; a file from another
  device or driver, or an older build's without the header, is ignored and its pools trial
  again. (Placement note for upstream's NotRexed#2317 at the path's single site: its per-user cache dir
  would take the file when it lands.)
- The check runs on any C++ compiler ({cxx}, as the neighbouring tests do).

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
@LNDF

LNDF commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Pushed 2b32b1c3 (merged with main):

About the Windows run: that one will have to wait a bit. I don't have a good Windows machine or a second drive to test with right now. I'll post the results (second drive, held/read-only save, non-ASCII user name, second start) once I can, or if someone with that setup can try it before, even better.

@hckrman101

Copy link
Copy Markdown
Collaborator

This now conflicts with main in docs/development.md. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.

- docs/development.md: main's new RX_VK_GM_LOOP row, with the driver faults
  file in the game folder's cache/.
- nx2/pipeline_share.py (NotRexed#2447, the builders' Save my pipeline list): the list
  is read from cache/, the log from logs/, and hookshot_options.ini from where
  the game keeps it (the game folder's user/, a loose one in the game folder,
  then the per-user data folder), each falling back to the old place.
  tests/test_pipeline_share.py covers that layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LNDF

LNDF commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (26793b60); docs/development.md was the only conflict. The merge also brought #2447's Save my pipeline list: nx2/pipeline_share.py now reads the list from cache/, the log from logs/ and the options from the data folder (old places as fallback), with a test.

blessedplummer pushed a commit to blessedplummer/Hookshot-optimi that referenced this pull request Nov 11, 2026
…ed, and its trials decide on frame time

hckrman101's review of NotRexed#2338, all points taken:

- Issue 1795's in-place hazard, per pool. A refused pool binds guest memory the recording
  executes, and the game rewrites its constants for the next frame before the GPU gets there -
  on the Deck that was the giant HUD heart, slabs and stretched geometry. The UB snapshot that
  fix provides for mirror-less devices now covers the policy's mirror-less pools too
  (dgpu_ub_in_place: a binding now says whether it is guest memory a recording will execute);
  a trial-won pool's mirror was synced at the submit and keeps the in-place bind.
- The verdict compares the frame the player gets: present-to-present (present_time_total_ns,
  one clock read a present, never reset), not the recordings' GPU timestamps - the mirror's
  cost is mostly CPU and in the upload command buffer those never time, and a GPU-only verdict
  kept mirrors while the frame was CPU-bound, which is the case this policy wins. The windows
  stay 120/12/120. The GPU-only total is gone.
- The claims and the code agree on parallel replays, with no race: the trials are gated on
  RX_VK_PARALLEL itself (par_active only covers the segments' recording and is false again by
  the time the pick runs), so the policy runs trial-less pure direct under it, and the fast
  path's counters (dgpu_read, dgpu_binds) only run when no parallel recording is in progress -
  they are lock-free there and fed the 2x bind-volume check with lost counts before.
- No more abort-forever: the trials need a mirror that can come on (with mirrors off - an iGPU -
  there are none at all: direct is right on unified memory), and a trial whose mirror never
  comes on (a full mirror budget) is given up three times at most before the pool sits out for
  the session.
- The skip list says the device and driver its verdicts were measured on; a file from another
  device or driver, or an older build's without the header, is ignored and its pools trial
  again. (Placement note for upstream's NotRexed#2317 at the path's single site: its per-user cache dir
  would take the file when it lands.)
- The check runs on any C++ compiler ({cxx}, as the neighbouring tests do).

Co-authored-by: GLM-5.3-Flash <glm-5.3-flash@opencode.noreply.github.com>
hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
…ess (#2347)

**What this is for:** a player who meets a problem presses one row in
the Insert menu, **Gameplay → Bug report**, and gets a folder with
everything the game problem form asks for: the form's answers in
`report.txt` (version, the map he is in, graphics card and driver
version, CPU, RAM and system, the options changed and the switches set),
this run's log and the run before's (the crashed one after a restart)
with his home folder and user name taken out, and a picture of the game.
Maintainers get reports with the log and the hardware from the start
instead of asking for them. **What is measured:** on Linux (Omarchy, an
Arch Linux; RTX 4090, NVIDIA 615.78.08), on `main` `ba6eef70`: the new
test passes with g++ 16 and clang 23 and fails on `main`; its cases for
the review's points (NUL bytes in a log, a picture that comes late,
which switch values are written, the reason a folder cannot be made);
`tests/run_all.py`; headless runs of this head that pressed the row (one
pressed it in Kokiri Forest, one after a `kill -9` and a restart: both
reports written, read back by a script against the logs they came from
(no file holds the home folder or the user name; each log copy is the
original line for line apart from those two), the after-crash report's
`hookshot_log.1.txt` is the killed start's whole log, and its picture
came through #926's `shot_ask`); the length GitHub takes for the link,
signed out. **Not measured:** Windows at run time (compiled and linked
only), a windowed run, the link opened in a browser (signed out or in),
AMD and Intel drivers' strings, a Steam Deck.

Refs #2346 (closed by the maintainer: tracked in this pull request).

## Problem

- The game problem form (`.github/ISSUE_TEMPLATE/1-game-problem.yml`)
requires the version, where in the game, the graphics card **and driver
version**, CPU, RAM and operating system, and `hookshot_log.txt` (and
`hookshot_log.1.txt` "if you started the game again after the problem").
The player has to find each one by hand.
- `hookshot_log.txt` answers only part of it: its first line has the
version, but the driver is only Vulkan's packed number (`vk: device 0:
NVIDIA GeForce RTX 4090, Vulkan 1.4, driver 0x99d38200, type 2`), there
is no line for the system, the CPU or the options changed, and nothing
logged where the player was (the host reads `Sequence_CurrentBanc` only
for the debug tools).
- After a crash a player starts the game again, and the crashed run's
log becomes `hookshot_log.1.txt` (`host/hos_main.c`, the rotation added
because players sent the new run's log and the crash was gone). It still
has to be found and attached.
- The logs carry the home folder and the user name in their paths (`fs:
mounted content: -> <home>/...`, `vk: pipeline cache <home>/...`), which
a player may not want to post.
- So reports arrive without the log or the hardware, and
`docs/workflow.md` scores such a report at confidence 0.4 and asks for
the log first. This gathers what CONTRIBUTING.md already asks ("Include
the version, your hardware, where you were and `hookshot_log.txt`";
"Report the version you actually ran: ... the first line of
`hookshot_log.txt`"). It changes nothing about where reports go.

## What changed

- `gpu/vk/vk_bug_report.h`, `vk_bug_report.cpp` (new, 59 + 602 lines):
the packer, without Vulkan and without the menu, so the test compiles it
alone. It writes `bug-reports/<YYYY-MM-DD_HH-MM-SS>/` in the game folder
on a thread of its own:
- `report.txt`: the form's questions under the form's own `### `
headings, so `scripts/hardware_report.py`'s `section()` reads it as an
issue: **Hookshot version** (`HOOKSHOT_VERSION`, as the log's first
line); **Where in the game** (the last `map:` line of this run's log,
and the run before's, and a request to add what you were doing);
**Graphics card and driver version** (the device and
`VkPhysicalDeviceDriverProperties`' `driverName` + `driverInfo`, e.g.
`NVIDIA GeForce RTX 4090, NVIDIA 615.78.08`); **CPU, RAM and operating
system** (the CPU's brand string and threads, RAM, `os-release`, desktop
and session type on Linux; `RtlGetVersion` and Wine on Windows);
**Changes from the defaults** (each Insert-menu row not at its default
as the menu shows it; a microphone chosen by its name is not named; then
the switches set, below); **hookshot_log.txt** (what is attached, the
run before's version and date, each log's first `FATAL`, `CRASH` or
`device lost` line); **Screenshots or video**; then a link that opens
the form with version, where, GPU, system and settings filled in (GitHub
fills issue-form fields from query parameters named by the fields' ids).
- **The switches.** CONTRIBUTING.md lists `hookshot_env.txt` among the
files never to share, so the report never copies it, and of the switches
set (the names `hookshot_env.txt` takes: `RX_`, `HOS_`, `HOOKSHOT_`,
`EDEN_`, `NVN_`, `SDL_`; and what hooks other code in:
`VK_INSTANCE_LAYERS`, `MANGOHUD`, `ENABLE_VKBASALT`, `LD_PRELOAD` ...)
it writes each name, and the value only when it cannot hold anything
private: a number of at most 12 characters, a size such as `1920x1080`,
or on/off, true/false, yes/no, and never for a name with `TOKEN`, `KEY`,
`ROOM`, `SECRET`, `PASS`, `SERVER` or `USER` in it. Every other value
reads `(value left out)`: paths, addresses, scripts. Other variables are
not listed at all.
- **The link** stays within 2000 characters and carries no switch value:
the options changed and the switches' names, or, when the names do not
fit, how many switches are set. A player who is not signed in is sent to
GitHub's sign-in page with the whole link inside it, and that page
failed for links from about 3500 characters of escaped text (table
below).
- `hookshot_log.txt` (this run's log up to the press) and
`hookshot_log.1.txt` (the run before's; when this run logs elsewhere,
e.g. from a terminal, the game folder's `hookshot_log.txt`). Read by
length (`fread`, split on newlines), so NUL bytes, which a run ended by
a power cut or a hard kill can leave, are copied as they are. Each line:
the home folder (a whole folder; on Windows either slash and any case)
becomes `~` or `%USERPROFILE%`, the user's name as a folder of any path
`<user>`. A log over 10 MB keeps its first MB and its last 9 MB (GitHub
takes files up to 25 MB).
- `picture.png`: 1280x720, the game's next frame, without the menu
(below). If none comes within 3 s, the report goes without it and says
so; a picture that comes later is deleted again.
- A folder that cannot be made is reported with the system's reason
(`GetLastError()` on Windows, `errno` elsewhere).
- `gpu/vk/vk_menu.cpp` (+89): the row before Restart and Quit
(`kBugReportRow = kRestartRow - 1`), its help (what goes in, that no
saves or game files go in and nothing is sent), its value (Press to save
/ Saving... / Saved / Not saved), Save in both prompt lines (the full
menu's and the compact bar's), the press, `changed_rows_text()` (rows
against a default-constructed `Options`), `bug_report_save()`, and the
picture's ask, `bug_report_picture()`. It follows Save my pipeline
list's pattern (#2447): its value in `row_value_cell`, its line under
the help in `draw_options` once pressed (where the folder is and how to
send it: File Explorer on Windows, a file manager on Linux; on a Steam
Deck, Desktop Mode), and room for that line's longest form in
`measure_help`. The game opens no browser or file manager: Game Mode on
a Deck has neither.
- `gpu/vk/vk_replay.cpp` (+2): `shot_ask_next()`, which calls #926's
`shot_ask(path, -1, done, ctx)` from outside its anonymous namespace
(declared in `vk_internal.h`). The report's picture is the control
bridge's kind of shot: an image and a buffer of its own (1280x720, made
at the first ask), copied from `src`, the game's frame, which the menu
is never drawn into (`menu_record_image` draws only into the
screenshots' image), read back once the GPU is past it and written on a
thread. No other line of `do_present` changes.
- `gpu/vk/vk_main.cpp` (+11), `vk_internal.h` (+6):
`device_report_text()`, and the declaration above.
- `host/hos_main.c` (+12): `map: <Sequence_CurrentBanc> (child|adult)`
when the map or the age changes, in `oot_inventory_refresh` (on Link's
update, at most every 500 ms, where the age is already read for the
Debug tab). It is where the report's place comes from, and it puts the
place into every log. The names are the game's own map names, which the
debug tools' teleport lines already print.
- `readme.md`: the row in the menu table, a paragraph on what it saves
and which values it leaves out, a pointer from "If the problem
continues".
- `tests/test_bug_report.py` (new), and `tests/test_menu_tune.py`: its
check of the compact bar's prompt line now reads the line with the Bug
report in it (the check is otherwise unchanged).
- Unchanged: nothing is sent anywhere; no saves, no game files, not
`hookshot_options.ini` or `hookshot_env.txt` themselves; no new option
or switch; the logs' own rotation; the screenshot path.

## How to try it

Insert menu (Insert, F1 or Esc, or the menu button) → **Gameplay** →
**Bug report** → Enter or A.
The row reads Saved and its help names the folder:
`bug-reports/<date>_<time>` in the game folder, with `report.txt`,
`hookshot_log.txt`, `hookshot_log.1.txt` and `picture.png`.

## How it was tested

**2026-11-11, after the two reviews, head `a75f3ca5` on `main`
`ba6eef70`.** Rebased twice today: onto #926, then onto #2042, #2378,
#2447 and the rest. The conflicts, each kept on both sides: #926's
`shot_ask_record` beside the old readback (which is now gone); #2042's
Debug rows where the Bug report row goes before Restart; #2378's
`row_value_cell()` and #2447's share row, where the row's value cell and
prompt went (the row now follows the share row's pattern, above); a
forward declaration in `host/hos_main.c`. #2042's rules hold for the
row: it has no value, so `preset_exempt` does not apply
(`tests/test_preset_exempt.py` reads only rows with one), and the branch
reads no on/off switch. #2042 also took `TOTK_` out of the names
`hookshot_env.txt` takes; the report follows (the test compares the two
lists).

Environment: Omarchy (an Arch Linux), kernel 7.2.15-1-omarchy, RTX 4090
with NVIDIA 615.78.08, i9-13900K, 94 GB, g++ 16.2.1 and clang 23.1.3.

- The new test on the branch (`python -I -B tests/test_bug_report.py`,
with `--cxx g++` and with `--cxx clang++`; the same line from both):
  ```
bug report: ok (report.txt as the issue form, both logs redacted, the
picture, the long log, the terminal start, switch values, NUL bytes, the
folder's reason)
  ```
Against `main`'s source (the test copied into an export of `ba6eef70`;
with both compilers): it fails at its first step, the menu row:
  ```
  at = menu.index('{TAB_GAMEPLAY, "Bug report", nullptr')
  ValueError: substring not found
  ```
And the review's NUL case against the old log copy (`fgets` and
`line.back()`, put back on its own):
  ```
bug report: the copy of a log with NUL bytes differs: b'[hos host]
hookshot v2.1.0-rc.3-39-gbdfe09c (source 763271ad)\n[hos Main] fs:
mounted content: -> ~/Games\n[hos Main] map: KokiriForest_Child
(child)\n[hos Main] half a line'
  ```
It compiles `vk_bug_report.cpp` alone with a driver and writes reports
from a made-up game folder inside a made-up home folder
(`.../home/alice/Games/Hookshot/`), with paths of the home and of
`/run/media/alice/...` in the logs, map lines and a `CRASH:` line in the
run before's. It checks: the folder holds exactly the report, both logs
and the picture; no file names the home folder, the user as a folder or
the game folder's path (`alice123` and `Malice` stay);
`hardware_report.py`'s `section`, `classify` and `os_of` read the
version, `NVIDIA` / `RTX 40` and Linux from `report.txt`; the place is
the last map, and the run before's; the switches: values kept
(`HOS_HEADLESS=1`, `60`, `off`, `3840x2160`, `-1`, `1.5`) and left out
(a path, a drive path, a menu script, an IP address, a `host:port`, a
13-digit number, and numbers under ROOM, KEY, SERVER, USER and PASSWORD
names), none of those values anywhere in `report.txt`, `PATH` and a
token not listed, no `%3D` in the link; the run before's crash line; the
link's fields and its length (with 98 switches: within 2000, the options
and the count in it, every switch in `report.txt`); the menu's way (a
thread, one report at a time); no frame means no picture, said so; a
picture that comes after the report went without it is deleted; a start
from a terminal; two reports in one second; a 22 MB log cut to its first
and last part with its middle's map still found; a log with a NUL in a
line and 4 KB of NULs at its end copied byte for byte, the map after the
NUL found; a folder that cannot be made, with the system's reason; the
redaction's Windows cases (either slash, any case); this PC's system
line. And the wiring by text: the row and what its help promises, Save
in both prompt lines, the press, the picture asked of `shot_ask_next`
and copied from the game's frame without the menu, the host's `map:`
line, that the switches it lists are the names `hookshot_env.txt` takes
(`env_line_allowed`'s), the form's labels and ids. Eight runs at once at
load 280 all pass (the first version of the menu's case read the
report's state too late under load in the full suite; it now holds the
report until it has looked).
- The checks the review named and those that read the menu's rows, on
this head (with g++ and clang++ where they compile C++):
`test_capture_dimensions.py`, `test_capture_slot_ownership.py`,
`test_menu_env_overrides.py`, `test_menu_keys.py`,
`test_pipeline_share.py`, `test_menu_tune.py`,
`test_stick_dead_zone.py`, `test_preset_exempt.py` (62 rows),
`test_menu_env_on.py`, `test_env_on.py`, `test_fps_stats.py`,
`test_gpu_choice.py`, `test_menu_restart_slots.py`,
`test_mouse_settings.py`: all pass. `vk_bug_report.cpp` alone with
`-Wall -Wextra -Wpedantic -Werror`: no warning from either compiler.
- `python -I -B tests/run_all.py` on Linux, on this head:
  ```
  440 passed, 1 failed, 20 skipped in 600 s (linux, 32 at once)
  ```
The one failure is `test_recorder_sync.py`, which ran out of its 600 s
at load 250-320 (other work ran beside it) and touches nothing of this
branch. An earlier run of the whole suite on this branch failed
`test_bug_report.py` once under that load: its menu case read the
report's state too late (fixed in the test, above).
- Merged into the newest `main` (`fce53453`, nine commits later; it
merges cleanly): `test_bug_report.py` (both compilers),
`test_menu_tune.py`, `test_pipeline_share.py`, `test_menu_keys.py`,
`test_menu_env_overrides.py`, `test_preset_exempt.py` (63 rows),
`test_menu_restart_slots.py`, both capture tests: pass. `test_env_on.py`
fails there and on `fce53453` alone (`HOS_FPS_LOG
(host/oot_high_fps.c:15)`, from #2470), not from this branch.
- Windows: `vk_bug_report.cpp` with a small driver compiles and links
for x86_64-w64-mingw32 (clang 23 with llvm-mingw 20261104's headers and
libraries, `-Wall -Wextra -Werror`: a PE32+ console executable). Not
run.
- In the game, on a build of this head's code
(`v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc)`; the head differs only in
two test files), headless, RTX 4090, `RX_VK_PRECOMPILE=0` (shaders
compiled when first used), load 200 to 320 (other work ran beside it),
the save loaded into Kokiri Forest and the menu script pressing
**Gameplay → Bug report**:

  | Run | What | Harness result | The report |
  |---|---|---|---|
| br-menu2 | the row pressed 55-63 s after the first frame | exit 124
(its 120 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 |
`bug-reports/2026-11-11_16-58-06`: report.txt and both logs, **no
picture**, and it says so. Rightly: the game showed no frame from 39 s
to 107 s (the save loading, shaders compiled as they came, at load 300),
took the scripted keys when frames came again, and stopped once more at
109 s |
| br-crash3 | a first start killed with `kill -9` after 110 s, then a
second start; the row pressed 63 s after its first frame | exit 124 (its
260 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 |
`bug-reports/2026-11-11_17-13-48`: report.txt, both logs
(`hookshot_log.1.txt` is the killed start's whole log), picture.png
(present 1772) |
| br-menu3 | the row pressed 130-138 s after the first frame, in play |
exit 124 (its 200 s), fatal 0, crash 0, device lost 0, kernel driver
lines 0 | `bug-reports/2026-11-11_17-25-32`: report.txt, both logs,
picture.png (present 3063, 138 s) |
  The after-crash run's log lines (the picture's path shortened):
  ```
  [hos BaseProcExecutor5] map: KokiriForest_Child (child)
  [hos Presentation Thread] menu: bug report asked for
[hos host] vk: present 1772 written to
~/Games/.../bug-reports/2026-11-11_17-13-48/picture.png (the control
bridge)
[hos host] menu: bug report written to bug-reports/2026-11-11_17-13-48:
report.txt, hookshot_log.txt (245 KB), hookshot_log.1.txt (117 KB),
picture.png
  ```
Its folder read back against the game folder's logs (`check_report.py`:
each copy compared line by line with its original, with the home folder
and the user name replaced as the game does):
  ```
files: hookshot_log.1.txt (119514 bytes), hookshot_log.txt (250199
bytes), picture.png (2765798 bytes), report.txt (3212 bytes)
no file holds the home folder or the user name (any case, anywhere) [the
checker's line, with the two values left out here]
hookshot_log.txt: 1827 of the original's 2991 lines, 12 of them with the
home or the user replaced, nothing else changed
hookshot_log.1.txt: 991 of the original's 991 lines, 11 of them with the
home or the user replaced, nothing else changed
hardware_report.py: vendor and class ('NVIDIA', 'RTX 40'), OS Linux /
SteamOS
  ok
  ```
br-menu2's and br-menu3's folders pass the same check (1387 of 1571 and
3230 of 3230 lines; 2861 of 3679 and 3230 of 3230 lines; only the home
folder and the user name replaced). A picture's pixel data is not read
as text: br-menu3's picture.png has the bytes `4d 4f 4f 4b 49` among its
pixels (row 176), which an any-case byte search reads as a name; the PNG
holds only IHDR, IDAT and IEND chunks. The host name is in no text file
of either.
- Pictures (headless, 1280x720; the first two are the harness's shots
with the menu drawn in, the third the report's own file):

![1: the Bug report row selected, Press to save. 2: after Enter, Saved,
with the folder under the help. 3: that report's picture.png: the game's
frame without the
menu](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/a943230353c26f026eef40ea371a06b754cc084f/hookshot-bugreport-shotask.jpg)

1. br-menu3, the row before the press: **Bug report** selected, **Press
to save**, its help (what goes in; no saves or game files; nothing is
sent), the prompt bar **Save Enter**.
2. After Enter: **Saved**, and the paragraph under the help names the
folder (`bug-reports/2026-11-11_17-25-32`) and how to send it.
3. That report's `picture.png`, taken through `shot_ask` at present
3063: child Link and Navi on the path in Kokiri Forest, hearts and
minimap; no menu, not dimmed.

![4: after a kill -9 and a restart, Saved, with its folder under the
help. 5: that report's picture.png: the game's frame without the
menu](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/a943230353c26f026eef40ea371a06b754cc084f/hookshot-bugreport-aftercrash.jpg)

4. br-crash3, after the `kill -9` and the restart: **Saved**, the folder
`bug-reports/2026-11-11_17-13-48` under the help.
5. That report's `picture.png` (present 1772): Kokiri Forest from low
down, facing the bright sky, hearts and minimap; no menu.

- The `report.txt` the after-crash run wrote, as a player would attach
it (the switches are the test harness's: see which values are written
and which are left out):

<details><summary>The report.txt that run wrote</summary>

```text
Hookshot bug report, made by the Insert menu's Bug report on 2026-11-11 at 17:13.

To send it: open https://github.com/NotRexed/Hookshot/issues/new/choose and choose Game problem (or the link at the end of this file, which fills in the answers below), copy the answers below into the fields of the same names, say what happened, and drag the files of this folder, this one too, into the issue.

### Hookshot version

v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc)

### Where in the game

KokiriForest_Child (child)
(The map this run's log names last: add what you were doing there.)

### Graphics card and driver version

NVIDIA GeForce RTX 4090, NVIDIA 615.78.08

### CPU, RAM and operating system

13th Gen Intel(R) Core(TM) i9-13900K (32 threads), 94 GB, Omarchy (Linux 7.2.15-1-omarchy), Hyprland (wayland)

### Changes from the defaults

Every Insert menu option is at its default.
Switches set (the environment, hookshot_env.txt; a value only when it is a number or on/off, and never for a name with TOKEN, KEY, ROOM, SECRET, PASS, SERVER or USER in it):
HOOKSHOT_NO_UPDATE_CHECK=1
HOS_AUDIO=0
HOS_DISCORD=0
HOS_ENV_FILE=(value left out)
HOS_HEADLESS=1
HOS_INPUT_BACKGROUND=0
HOS_INPUT_EXCLUSIVE=1
HOS_INPUT_FRAME_CLOCK=1
HOS_INPUT_SCRIPT=(value left out)
HOS_PRESENT_TRACE=(value left out)
RX_CPM=(value left out)
RX_VK_DEVICE=0
RX_VK_FRAME_LOG=(value left out)
RX_VK_MENU_SCRIPT=(value left out)
RX_VK_PRECOMPILE=0
RX_VK_SHOTS=(value left out)
RX_VK_SHOT_EVERY=300
RX_VK_SHOT_MENU=1
SDL_AUDIODRIVER=(value left out)
SDL_AUDIO_DRIVER=(value left out)
VK_DRIVER_FILES=(value left out)

### hookshot_log.txt

hookshot_log.txt: this run's log as it was when the report was made, 245 KB.
hookshot_log.1.txt: the run before's log (hookshot v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc), last written 2026-11-11 17:12), 117 KB.
In both, the home folder is written ~ and the user's name <user>.

### Screenshots or video

picture.png: the game's picture when the report was made, without the menu.

### What this folder holds

report.txt (this file), hookshot_log.txt, hookshot_log.1.txt, picture.png. Nothing else from this PC: no saves, no game files, not the options file itself.

The link with the answers filled in:
https://github.com/NotRexed/Hookshot/issues/new?template=1-game-problem.yml&version=v2.1.0-rc.3-177-g9c538c91%20%28source%20c4f2afbc%29&where=KokiriForest_Child%20%28child%29&gpu=NVIDIA%20GeForce%20RTX%204090%2C%20NVIDIA%20615.78.08&system=13th%20Gen%20Intel%28R%29%20Core%28TM%29%20i9-13900K%20%2832%20threads%29%2C%2094%20GB%2C%20Omarchy%20%28Linux%207.2.15-1-omarchy%29%2C%20Hyprland%20%28wayland%29&settings=Every%20Insert%20menu%20option%20is%20at%20its%20default.%0ASwitches%20set%20%28their%20values%20are%20in%20the%20attached%20report.txt%29%3A%20HOOKSHOT_NO_UPDATE_CHECK%2C%20HOS_AUDIO%2C%20HOS_DISCORD%2C%20HOS_ENV_FILE%2C%20HOS_HEADLESS%2C%20HOS_INPUT_BACKGROUND%2C%20HOS_INPUT_EXCLUSIVE%2C%20HOS_INPUT_FRAME_CLOCK%2C%20HOS_INPUT_SCRIPT%2C%20HOS_PRESENT_TRACE%2C%20RX_CPM%2C%20RX_VK_DEVICE%2C%20RX_VK_FRAME_LOG%2C%20RX_VK_MENU_SCRIPT%2C%20RX_VK_PRECOMPILE%2C%20RX_VK_SHOTS%2C%20RX_VK_SHOT_EVERY%2C%20RX_VK_SHOT_MENU%2C%20SDL_AUDIODRIVER%2C%20SDL_AUDIO_DRIVER%2C%20VK_DRIVER_FILES.%0A
```

</details>
- The link's length, signed out (`curl`, dummy links to the form,
2026-11-10; nothing of this PC in them):

  | Link length | Padding | GitHub's answer |
  |---|---|---|
| 1238 | the report's own link (then) | sign-in page, the link kept in
`return_to` |
| 1999, 2500, 2998, 3199 | `%20` | sign-in page, the link kept in
`return_to` |
  | 3500 | `%20` | 400 |
  | 4000, 4500 | `%20` | 500 after the redirect |
| 6000 | `%20` | sign-in page, but `return_to` dropped (the answers
lost) |
  | 6000, 6400 | `x` | sign-in page, the link kept |
  | 6700, 7000, 7500, 7900 | `x` | 500 |
  | 8192 and more | `x` | 414 |

## Not tested

- **Windows, at run time.** The report's GPU line there
(`VkPhysicalDeviceDriverProperties` on a Windows driver), its CPU, RAM
and system line (`__cpuid`, `GlobalMemoryStatusEx`, `RtlGetVersion`,
Wine), the path of the log stderr writes to
(`GetFinalPathNameByHandleA`), `GetLastError`'s reason, the redaction of
real `C:\Users\...` logs, File Explorer: compiled and linked only.
- **A windowed run.** Only headless runs: the picture taken with a
window, HDR (a native HDR frame is copied to 8-bit as the control
bridge's shots are) and frame generation were not seen.
- **The link in a browser.** Not opened in a browser, signed out or
signed in, and no issue was opened from it; only `curl` signed out
(table above). That GitHub fills the form's fields from it is its
documented behaviour, not seen here.
- **A real crash's last lines in the game.** The after-crash run ended
its first start with `kill -9`, which leaves no `CRASH:` line, and under
that load the first start had not reached a map yet; the test covers the
`CRASH:` line and the run before's map with made-up logs.
- AMD and Intel drivers' `driverName`/`driverInfo` text (only NVIDIA's
was read; `hardware_report.py` classifies typical RADV, AMD and Intel
strings correctly when written by hand), a Steam Deck, the AppImage, and
the Linux Builder's Play button (since #2279 it gives the game its
`hookshot_log.txt` again, so the report finds this run's log there; not
run here).

## Risks

- **Something private in a report.** The home folder (as a whole folder;
on Windows either slash and any case) and the user's name as a folder of
any path are replaced in both logs; a switch's value is written only
when it is a number, a size or on/off and its name holds none of the
words above; other environment variables are not listed; a microphone
chosen by name is not named; no saves, no options or env file, no game
files go in. Guarded by the test (made-up home and user, Windows cases,
`alice123` kept, values kept and left out) and, on this machine's real
logs, by the read-back above. Not guarded: something private a log line
carries in another form (a game folder outside the home named after a
person, a machine name in a network path), or a future switch whose
secret is a short number under a name without those words.
- **A report written while the game is busy.** The press does only cheap
work on the menu's thread (flushing the log, reading the environment,
`/etc/os-release`, the device's properties, the menu's rows); copying
the logs and writing the folder run on a thread of their own, one report
at a time (a second press while it saves does nothing). This run's log
is copied as far as it was when the copy began. The picture is waited
for at most 3 s.
- **The picture.** It shares #926's queue (16 asks at most) and its one
readback: a control-bridge shot being read back delays the report's by a
present or two. Without a report nothing changes on the present path.
The control bridge's log line for a written shot also appears for the
report's picture (`vk: present N written to ... (the control bridge)`).
- **The `map:` line in every log.** It is written only when the map or
Link's age changes, from `oot_inventory_refresh` (at most every 500 ms,
where the age is already read). A log that a player posts now says where
he was; the names are the game's own map names, which the debug tools'
teleport lines already print.
- A crash that leaves no line (a kill, a freeze, a power cut): the run
before's log is attached, but nothing in it says how it ended. A game
that stops before its menu can be used cannot make a report; the readme
says to attach `hookshot_log.1.txt` by hand then.

## Overlaps

`pr_overlap.py --branch bug-report` (run on this head, 2026-11-11): 79
open pull requests touch one of these files; 73 merge cleanly with it.
The row stores nothing (no `Options` field, no ini key, no option id),
so no option id can clash. Six conflict in this branch's files, each
hunk read:

- #2417 (the menu in 16 languages): it wraps the menu's texts in
`i18n::tr(...)`, among them the lines of `measure_help`,
`row_value_cell`'s branch for rows without a value, the help line in
`draw_options` and the compact bar's prompt (and the same line in
`tests/test_menu_tune.py`), where this branch adds the Bug report beside
the share row. Whichever lands second wraps the Bug report's texts the
same way; both stay.
- #1521, #1864, #2105, #2459, #2479 (the readme): a row each after
Microphone in the menu table (#2459 moves the Microphone row), where
this branch adds the Bug report's row; both stay.
- #2317 (saves and settings in a per-user data folder, logs in `logs/`)
now merges cleanly, but overlaps in meaning: this branch finds the game
folder through `ini_path()` and the logs in the game folder; after #2317
the settings are in the data folder and the logs in `logs/`. Whichever
lands second moves those two reads to `game_folder()` and `log_path()`.

## Questions for you

1. Is the Insert menu the place you want this, and the game folder the
place for the reports (with #2317, the data folder or `logs/` could be)?
2. The report takes the strict reading of CONTRIBUTING.md on
`hookshot_env.txt` (names, and only harmless values). Should
CONTRIBUTING.md say so, next to the line that lists the file?
3. The link carries the form's one-line answers and, when short enough,
the options and the switches' names. Do you want it at all, or only
`report.txt` to copy from?

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@Arkex-ai

Copy link
Copy Markdown
Contributor

One new integration point: #2600 appends romfs_read to MkwVkHost and advances main's API18 to19. This PR appends data_dir as19; #2594 appends take_text as20. Whichever lands second needs the complete callback layout and a version bump from the landed head, including matching host initializers. I'll adapt #2600 if either lands first and rerun its startup/font/filesystem checks. The packed backend uses a separate base-asset callback; your data-location policy remains yours.

- main's 19 is NotRexed#1930's ocarina note bindings (a changed meaning of the pad
  binding calls, no new field); MkwVkHost.data_dir, still the last field and
  last in the host's initializer, is 20.
- host/prompts.c: NotRexed#2435's keyboard face; choose_face takes the game folder
  again for prompts/keyboard (a build input), the options stay the data
  folder's. NotRexed#2435 adds no generated file of its own: all still in
  cache/prompts.
- host/nn_hid.c, docs/development.md: main's text (the ocarina's note
  controls, the RX_VK_FALLBACK_PHI_LIMIT row) with the data folder's places.
- tests/test_ocarina_note_bindings.py, tests/test_ocarina_notes.py (NotRexed#1930):
  stub hos_data_path as tests/test_bound_keys.py does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
)

**What this is for:** contributors rebuilding from source should not
spend another minute drawing the same button prompts. **What is
measured:** on an Intel Core i9-13900K running Arch Linux, the finish
stage at main `c544828f` took 67.707 and 66.898 seconds; two unchanged
runs took 0.603 and 0.651 seconds, and the final head took 0.616
seconds, clearing the run-time files as well. Other work was running
throughout. **Not measured:** Windows, a packaged Builder update, a
complete compile/lift, or gameplay performance.

Refs #2636.

## Problem

`nx2/make_game.py` removes the game folder's `prompts` directory and
runs `prompt_assets.py` on every build when fontTools is available. Most
source edits do not change anything those assets depend on.

This avoids drawing unchanged assets; it does not make the drawing
itself faster. #2636's A1 plans to measure the finish stage, but its
A1–A10 items do not already propose this prompt cache. It complements
#2674 and #2675, which address unnecessary retranslation rather than
prompt generation.

## What changed

- **`nx2/make_game.py`:** source-checkout builds request incremental
prompt generation. Packaged builders still regenerate every time, using
the finish stage's existing launcher distinction. `RX_RELEASE` cannot
distinguish them: make_game sets it for source builds too. Missing
fontTools still removes the replacement prompts, keeping the existing
fallback to the game's icons.
- **`nx2/prompt_assets.py`:** fingerprint the inputs before generating,
draw into a fresh sibling directory, and record the successful outputs
only if a second fingerprint still matches. Reuse requires both matching
inputs and every recorded output's size and SHA-256. **A failed
generation still removes the prompts, as before**, so the game shows its
own icons. An interrupted builder can leave the previous complete set
without a stamp; the next build regenerates it. The drawing, encoding
and asset formats are unchanged.
- **`tests/test_prompt_skip.py`:** game-free fixtures run the actual
finish-stage prompt call and production cache/publication functions,
with small authored files and a stand-in for expensive drawing.
- **`docs/development.md`:** explain reuse, its limits, and
`HOOKSHOT_FORCE_PROMPTS=1`. The force switch follows `env_on`: unset,
empty, `0`, `false`, `off` and `no` are off, ignoring case and
surrounding spaces.

### What prevents a wrong skip

The input record covers:

- The four prompt source modules, including controller, keyboard and
binding tables.
- The game's icon font and optional bold label font, compression
dictionary, common texture/layout archives, and matching resource-size
table names and contents.
- The repository's controller and keyboard fonts, and added, changed or
removed PNG overrides.
- Python/platform; fontTools, Pillow, ASTC encoder, texture decoder,
NumPy, zstandard and archspec availability and versions; Pillow's native
library versions and the selected ASTC backend.
- Generation parameters and relevant library switches, including
`SOURCE_DATE_EPOCH`, `FREETYPE_PROPERTIES` and the encoder's custom CPU
definitions.

Languages and saved bindings are not build inputs here: the generator
uses its source tables, and the runtime applies the selected controls.

The output record includes files **and directories**: the keyboard
directory itself is a runtime marker. Legitimately absent optional
outputs stay absent; a missing, truncated or same-size-corrupted
recorded file forces regeneration. The runtime's own compressed copies
and control-specific keys are excluded before hashing, not mistaken for
generator output. **The files the game derives at run time are still
cleared by every build**, including an unchanged hit: their own keys do
not cover changes to `host/prompts.c`. Only the known runtime filenames
are removed; if one cannot be cleared, the build regenerates instead of
keeping on doubt.

An unreadable input cannot authorize reuse. The log gives the reason,
even if a forced regeneration subsequently fails. A source edited during
generation leaves no stamp, addressing the stale-input certification
problem discussed in [#2674's
review](#2674 (comment)).

## How it was tested

2026-11-12; Arch Linux, kernel `7.2.15-1-omarchy`, x86-64, Intel Core
i9-13900K, 32 logical CPUs, Python 3.12.16. Base `c544828ff`; final head
`2ec89de8`. No game or GPU program was launched for these checks.

### Finish-stage measurements

These execute the real finish-stage statements through completion,
including binary/image copies, the unchanged shader-list check,
pipeline-list work and prompt generation, in a disposable copy of an
existing built game folder. Compilation, translation and shader
preparation are not included. They are **not whole rebuild times**.

All optional prompt packages were present: fontTools 4.68.0, Pillow
13.0.0, astc-encoder-py 0.1.12, texture2ddecoder 1.0.6, archspec 0.2.7,
NumPy 2.5.4 and zstandard 0.25.0.

| Run | Source | Finish time | 1-minute load before → after | Top other
work |
|---|---|---:|---:|---|
| Before 1 | `c544828f` | 67.707 s | 7.13 → 7.40 | Existing game 316 →
315% CPU; file indexing 66 → 79%; relay check also active |
| Before 2 | `c544828f` | 66.898 s | 7.06 → 8.24 | Existing game 315 →
318%; terminal process about 41% |
| First generation with cache | `63d5c965` | 66.400 s | 8.24 → 8.19 |
Existing game 318 → 320%; terminal process about 42% |
| Unchanged 1 | `63d5c965` | 0.603 s | 8.19 → 8.19 | Existing game 320%;
terminal process about 42% |
| Unchanged 2 | `63d5c965` | 0.651 s | 8.19 → 8.19 | Existing game 320%;
terminal process about 42% |
| Changed table source | `681208dd` | 63.147 s | 6.99 → 6.95 | Existing
game 320 → 321%; terminal process about 42% |
| Regenerate, final head | `2ec89de8` | 65.007 s | 5.77 → 5.86 |
Existing game 317 → 316%; terminal process about 43% |
| Unchanged, final head, with run-time files present | `2ec89de8` |
0.616 s | 5.79 → 5.79 | Existing game 316%; terminal process about 43% |

Other work was running; none was a quiet measurement. CPU percentages
are lifetime averages from snapshots, not interval measurements. The
first generation is included to show that this is a skip, not a faster
drawing algorithm.

The unchanged runs print:

```text
prompts: unchanged, kept
```

Adding a harmless comment to the button-table source forces a fresh
generation:

```text
prompts: regenerated (table changed)
```

The comment was removed afterwards. A timestamp-only touch does not
regenerate: the key checks content. An additional run on the final head
verifies an unchanged hit with all six runtime-derived files present:
all six are removed, while the 16 generated originals and the stamp
retain their sizes and SHA-256 values. The focused fixture also compares
the retained originals byte for byte. The final timing rows above
include that cleanup.

**Asset parity:** all 16 regenerated files, 274,704,680 bytes, compare
byte for byte with a full generation by main `c544828f`. Only the new
stamp is excluded. Both sides used the same `SOURCE_DATE_EPOCH` because
fontTools otherwise writes the current time into the OpenType header.
This comparison setting is not introduced into normal builds. A
development cache hit retains the earlier font timestamp; packaged
builds retain their old full-generation behavior.

### Game-free checks

- **23 focused tests pass** on the final change. They cover unchanged
reuse, every input kind, package versions/presence, switch/parameter
changes, missing/truncated/same-size-corrupted output, empty
directories, clearing runtime derivatives while retaining byte-identical
originals, regeneration when runtime cleanup fails, interruption,
mid-generation edits, unreadable input, force-switch parsing, incomplete
output, publication failure, and the packaged/no-fontTools paths. A
non-zero generator result removes prompts for both source and packaged
builds; interruption before that result leaves the old set unstamped.
- The same unchanged-build test against main's finish stage fails
behaviorally: `2 != 1: unchanged finish must not draw the prompts
twice`.
- Removing each of **18 guards/key components** in memory makes its
corresponding behavior test fail on the final change. Three later
corrections also fail against the earlier version of this patch:
returned failure retained old prompts in both build paths, runtime files
survived a hit, and an uncleared runtime file did not force
regeneration. The unreadable-input diagnostic has a separate
before-fails/after-passes proof.
- The whole suite one commit earlier, at `f20ea58b` (before a failed
generation was made to remove the prompts again and the run-time files
to be cleared), `python -I -B tests/run_all.py --jobs 8`:

  ```text
  472 passed, 0 failed, 20 skipped in 133 s (linux, 8 at once)
  ```

Load was 7.37 → 19.48. The first full run, at 32 checks at once, was
**471 passed, 1 failed, 20 skipped**: a clang++ child compiling
`texture_pass.cpp` for `nx2/test_depth_sample_types.py` died with
SIGKILL. That fixture and its input trees are unchanged from main. No
kernel OOM line was found, so the cause is not established. The complete
run with eight workers passes, including that check. The failed run is
retained, not counted as a pass. On the final head `2ec89de8` the
focused checks, the baseline failure and all guard mutations were rerun,
and the whole suite once more with eight workers: **471 passed, 1
failed, 20 skipped in 136 s** (load 11.14 → 22.62). The one failure is
`tests/test_release_scripts.py`,
`test_nightly_two_runs_at_once_end_at_the_newer_commit`, the
intermittent race reported in #2626; this change touches none of its
files.
- No core dumps were recorded. Nothing generated from the game is in
this change.

## Not tested

- Windows filesystem behavior or a Windows build.
- The packaged Windows Builder or Linux AppImage build/update end to
end. Their always-regenerate selection is covered by a game-free test
only.
- A full compile/lift or a new gameplay run, other CPUs, or
gameplay/frame-rate benefits.

No screenshots: the regenerated asset bytes match, and no visual change
is intended.

## Risks

The input list must grow when the generator gains new file dependencies
or outputs outside the prompt directory. A regression flags new local
module imports, but it does not replace reviewing new file reads.
Generation temporarily needs room for both old and new assets.
Publication uses completed directories and rollback on a rename error;
interruption cannot create a stamped half-set, though a process killed
between the two directory renames may require regeneration on the next
build.

## Overlaps

The open-PR scan found 47 PRs touching one of these files. The actual
conflict hunks were read:

- **#2151:** adjacent item-button generation conflicts with the
finish-stage prompt call/error branch. Preserve both steps and account
for added prompt dependencies.
- **#2261:** substantive overlap. It adds `--wheel-icons`, another input
folder and output outside `prompts`. Keep that work outside this skip or
extend the input/output proof and staging; accepting both hunks
mechanically is not enough.
- #1872, #1958, #2304, #2317, #2430, #2465, #2474, #2478, #2608, #2673,
#2675 and #2686 share code files but merged cleanly in the scan. That is
not proof that any newly added generator dependency is covered.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <noreply@openai.com>

Co-authored-by: GPT-6 Astra <noreply@openai.com>

@Joey-Magnus Joey-Magnus 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.

Most of what I raised is fixed: links are never moved (#2282's handling and test carried in), a second game folder keeps saves and settings together, the rollback clears a partial original first, the portable rule is in, and saves-in-two-places.txt (host/hos_data.c:390) covers the downgrade case. API 20 is right against main's 19.

Two things still block it:

  • CI's Windows part 1 fails on this PR's own tests/test_pipeline_share.py (:413). The test sets XDG_DATA_HOME (:409), but on Windows data_dirs() in nx2/pipeline_share.py reads %LOCALAPPDATA%\Hookshot, so the options aren't found (2048x1152). Set and restore LOCALAPPDATA there too.
  • The Windows run is still missing: game folder on a second drive, a save held open or read-only, a non-ASCII user name, a second start. This moves every Windows player's saves, so I'd want one real run, yours or a tester's.

I merged it into current main (clean) and ran the 20 checks it touches on Linux (test_data_dir, test_linux_saves_move, test_pipeline_share, test_env_file, test_utf8_paths, test_prompt_skip among them): all pass.

@Nitnek

Nitnek commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Native Windows migration evidence at 47786aaf7d4dca7e1a54434b187a38775e47e363

I exercised the unchanged host/hos_data.c from this exact head in a small guarded standalone executable using real Win32/CRT calls and the production UTF-8 manifest (GetACP() == 65001). Game-free source qualification only: no game launch, real player saves or installation changes. The PR head still matches at this pre-publication check.

The extended set has 8 migration cases: 5 conforming and 3 functional failures, plus a separate positive notification witness. All payloads are authored synthetic data, at most 88 bytes per case, under isolated fake executable/CWD/LOCALAPPDATA roots on verified distinct volumes. A native exit0 did not establish a complete active data directory.

1. Cross-volume rollback selects a partial source — two cases.

The actual enumeration was a-tree, then z-tree, each holding two files. The first entry copies completely to C:, but deleting its original removes the free file and leaves the other file: one variant uses READONLY, the other a real CreateFileW(GENERIC_READ, FILE_SHARE_READ) handle without DELETE sharing. A separate share-zero handle makes the later entry's copy fail.

Rollback cannot remove the refractory original, so the complete first-entry copy remains on C:. Nevertheless, both data= and user= select D:game/user, now 3/4 files. C: has only the first entry. Neither root alone is complete; their union retains all original sizes/hashes. Recoverability of that union is not a complete active directory or successful all-or-nothing restoration. Relevant paths: hos_data.c:218-221, 278-287, 381-405.

2. A residual source becomes active on the next startup — one case.

Without the later copy failure, the share-READ/no-DELETE case copies 4/4 files to C: and initially selects that complete directory. D: retains one held file and putting user aside fails. After the handle is closed, a fresh second process returns0 but selects D:game/user with 1/4 files, while the full C: copy remains intact. File hashes are unchanged between starts; the selected directory is not. Relevant paths: 289-301, 258-262, 381-405.

The raw harness had called this last case PASS_MIGRATION because it checked first-start completeness and unchanged snapshots. A separate adjudication records the functional FAIL; raw receipts remain unchanged.

Positive controls and verification: share-zero refusal preserves the source payload; same-volume rename can succeed despite a READONLY file; ordinary cross-volume migration and the simple READONLY-copy case remain complete across two processes. A simple same-volume rollback was causally witnessed with real ReadDirectoryChangesW: source REMOVED→ADDED and destination ADDED→REMOVED, alongside an independent add/rename/delete witness. Final hashes alone were not used to infer rollback.

An independent read-only Windows capture matched all 72 native receipts by name, size and SHA256. The parent then replayed the semantic verifier offline and directly cross-compared all72 exports against that capture: the three failures above remain confirmed. Synthetic fixtures/recovery copies are retained; no repair was applied.

Limits: SSH/non-interactive account, not an actually non-ASCII Windows account or Interactive/Limited token; no full Windows CI/test_pipeline_share.py, real save serialization, GPU/gameplay, directory ACL, disk-full/crash or hostile-reparse qualification. Linux/Windows wall clocks were not combined. No binaries or game-derived data were exported. This does not close the outstanding Windows request or the separate LOCALAPPDATA fixture issue.

Could migration keep a complete active directory when rollback or setting aside a residual source fails, rather than returning initialization success with a partial source selected?

AI-assisted evidence analysis and review: Hermes Agent (OpenAI GPT-6.1-sol).

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.

5 participants