Commit Graph
15 Commits
Author SHA1 Message Date
Vito d9f168c317 feat(nodes): per-node Save to library switch (#90) (#427)
* feat(nodes): per-node Save to library switch (#90)

Every PromptManager node (Classic, Text, Focused, Split, Pair) gets an
optional save_to_library BOOLEAN, default on and declared last. Off, the
node still returns its normal outputs but PromptManager ignores the run:

- the node saves, counts, tracks and records nothing;
- the queue hook reads the switch from the queued graph (literal, or a
  linked PrimitiveBoolean), so a cached node is not counted either; a
  switch wired from an unreadable node is left to the node itself;
- an opted-out node is no run prompt, so no tracker entry, run key,
  embedded-metadata match or history attribution links its images, and
  a run with any opted-out positive node links none of its files;
- the scan reports such files as not_saved (never stored or queued for
  Needs review), and a file whose only opted-out node is its negative is
  stored positive-only, without the pair;
- execution history records no prompt text for an opted-out role.

PromptSearchList only reads the library and gets no switch.

The switch takes the widget index older workflows used for the editor's
own DOM slot (''), so a configure step turns any non-boolean value on:
workflows saved before the switch load with it on and no earlier widget
value moves. The Focused, Split and Pair editors draw it as a "Save to
library" checkbox in their toolbar; Classic and Text show the native
toggle.

* fix(nodes): Save to library review fixes (#90)

- A switch wired from a node the graph cannot read and off at run time
  marks the node's run key, so an earlier run's tracker entry never
  links this run's images.
- The scan reports such files as not_saved_wired ("only the run itself
  can link it") instead of not_saved.
- Workflow-only metadata ignores muted and bypassed PromptManager nodes
  and reads nodes inside subgraph definitions.
- The Focused, Split and Pair footer says when the switch comes from a
  link.

* fix(usage): count cached re-runs of runtime-linked prompt text (#347)

A PromptManager-family node whose text comes from a runtime link
(PreviewAny, an LLM, a list node) counted only the job that executed it;
later jobs served from ComfyUI's output cache counted nothing.

Each execution now records a receipt (digests of its STRING outputs and
hashes of the text it stores) per run key and queue item. On
execution_cached the receipt whose digests equal the cache entry's text is
replayed once for the job, with the node's role and what the queue hook
already saw recomputed from the job's own graph. The executing node and the
replay count through one shared own_counts. Unmatched cache entries count
nothing, as before; a node that executes after its replay in the same job
skips its own count. No schema change, no backfill.

* fix(usage): never replay a node twice in one job; dedupe receipts once complete (#347)

A repeated execution_cached id or message for the same job now counts the
node once. Receipts replace equal older ones only when their job finishes
(or the next job starts), so a list still recording [A] of [A, B] no longer
drops an older [A] receipt. Node-level tests check the receipt each
execution records.

* test(coverage): baseline for the Save to library module (#90)

* test(usage): disabled node then cached jobs counts 0 (#90, #347)

A PromptManager node with Save to library off records no cached-run
receipt and is no run prompt in the replayed job's graph, so ComfyUI
serving it from its cache on later jobs counts nothing. The prompt is
saved first by an enabled run, so a replayed count would show. Covers
PromptManager, Focused, Split, Text and the Pair.

* fix(nodes): hide the editor's native widgets through one helper (#90)

The Save to library switch copied the text fallbacks' zero-height computeSize
arrow, and neither copy was ever called, so focused-node.js function coverage
fell to 87.5 (floor 90). One hideNativeWidget helper now hides both, and the
spec checks that the hidden switch and a text fallback take no node height.
2026-10-28 18:09:16 +00:00
Vito 55fa2c4870 fix(runs): count cached PromptManager nodes fed by a runtime text link on every job (#347) (#423)
* fix(usage): count cached re-runs of runtime-linked prompt text (#347)

A PromptManager-family node whose text comes from a runtime link
(PreviewAny, an LLM, a list node) counted only the job that executed it;
later jobs served from ComfyUI's output cache counted nothing.

Each execution now records a receipt (digests of its STRING outputs and
hashes of the text it stores) per run key and queue item. On
execution_cached the receipt whose digests equal the cache entry's text is
replayed once for the job, with the node's role and what the queue hook
already saw recomputed from the job's own graph. The executing node and the
replay count through one shared own_counts. Unmatched cache entries count
nothing, as before; a node that executes after its replay in the same job
skips its own count. No schema change, no backfill.

* fix(usage): never replay a node twice in one job; dedupe receipts once complete (#347)

A repeated execution_cached id or message for the same job now counts the
node once. Receipts replace equal older ones only when their job finishes
(or the next job starts), so a list still recording [A] of [A, B] no longer
drops an older [A] receipt. Node-level tests check the receipt each
execution records.

* test(comfyui): count #347's cached-run cases in the execution suite pin (#347)

The pin listed 19 execution-text-link cases per project; #347 adds five node
kinds and eight one-renderer cases, so the suite lists 32 per project and 118
in total with #332's image-attribution case from main.
2026-10-28 16:37:24 +00:00
Vito b5a56bee0b feat(images): store images' own categories and set them by route (#332, PR 1) (#401)
* feat(images): store images' own categories in the library database (#332)

One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.

* feat(images): duplicate cleanup keeps images' own categories (#332)

The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.

* feat(images): set, remove and list images' own categories (#332)

PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.

* fix(images): key symlinked output folders and keep key work off the event loop (#332)

Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.

* feat(images): set or remove a category on all N matching images (#332)

The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.

* test(vendor): count the #332 host case in the execution matrix

image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).

* test(legacy): record the image category routes in the legacy contract snapshot (#332)

#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.
2026-10-28 15:29:41 +00:00
Vito 3be0ada6a1 fix(ingestion): link an image only to a prompt that provably produced it (#344)
* fix(ingestion): link an image only to a prompt that provably produced it

An LLM fed a PromptManagerPair's prepend_text while its text editor was
empty. The node refused to save (an empty positive raised and logged a
warning), and the image monitor then linked the image to a stale tracker
entry or to the newest prompt in the database.

- Nodes: an empty text editor saves and tracks nothing (prepend and
  append text are never the stored prompt), logs at info, and marks the
  node's run key empty so no earlier entry can be used for its images.
- Tracker entries carry a run key: a digest of the prompt node and its
  whole upstream graph, the identity ComfyUI's cache uses. The monitor
  computes the same key from the image's embedded graph and uses a
  queued or snapshot entry only when the keys match (a snapshot also
  needs a known run with one entry, so a batch's last item is never
  applied to every image).
- The recency fallback is gone; anything uncertain stays unlinked and is
  logged at info.
- Recorded execution history attributes a file when exactly one run
  reported it, the file was written during that run, and the run's
  positive contexts name one stored prompt. The monitor and the output
  scan use it as their last certain source.
- The scan and pair ingestion treat a resolved pair with an empty stored
  positive as unresolved instead of raising.
- tools/review_image_links.py reviews selected links by these rules;
  dry run by default, --apply changes only named ids after a backup.

Closes #342

* test(ingestion): real-host attribution case and the #342 testing contract

The managed host disables the image monitor, so the owned fixture pack
gains a route that starts and stops the plugin's monitor singleton for
the attribution case only. The case runs in both renderers: a typed Pair
image links, a Pair image whose positive is only linked prepend text
stays unlinked with nothing stored, an unknown node feeding text links
through its run key, and its cached re-run links again.

* fix(ingestion): take a queued entry only from the run the image appeared in

Dropping a key's older queue entries whenever the node ran again let a
later queued run, executing during the monitor's processing delay, take
the earlier image's entry. The snapshot taken when the image appears now
names the key's latest run, and the queue is popped by key and run:
same-key entries of earlier runs ahead of it are stale, entries of later
runs wait for their own images.

Also keeps the attribution case out of the stock real-host suite.

* test(ingestion): host lookups degrade to no proof

* fix(ingestion): address the #344 review (history re-check, keys, repair)

- Images without an embedded API graph: the scan now asks recorded
  history before skipping a file with no prompt metadata, and the monitor
  gives a keyless unlinked image one history-only re-check when its run's
  outputs are recorded (a seal notification from the outputs write; at
  most 1000 waiting images, each for an hour).
- Run keys: an image is unkeyable when any positive node has no key, so a
  two-pass graph is never linked through the other pass; links follow
  ComfyUI's is_link exactly.
- A failed save marks the node's run key like an empty editor, so an
  older entry of that key can't stand in for this run.
- A placeholder is the latest entry: the image stays unlinked without
  falling through to history, the same rule as a popped placeholder.
- An index on execution_run_outputs(filename, subfolder, root_key,
  path_style) for the history lookup, added by migration and accepted by
  the output schema check (any other index is still refused).
- Repair tool: unlink removes only the link record and only while the
  image is still linked to the prompt it was judged on; every run prints
  what --apply does.
- Real-host case proves the re-run's PromptManager node was cached.

* test(ingestion): real-host bare-output re-check and the review-round contract

A metadata-disabled save, held open behind the fixture gate after
SaveImage, waits unlinked while its run is open and links once the run's
outputs are recorded. The fixture's monitor route lists the waiting
files. TESTING.md covers the history re-check, the read index and what
the repair tool's --apply does.

* fix(ingestion): batch history lookups instead of adding an index

An index on execution_run_outputs would make the library unopenable by
older builds, whose schema check refuses any extra index there. The
migration and the schema-check allowance are reverted, so the history
schema is exactly main's. stored_run_prompts instead looks up a batch of
files with one parameterized filename IN (...) query per chunk of at
most 500 names and matches in memory; the scan computes it once per scan
batch.

* fix(ingestion): address the #344 re-review (seal race, linked roots)

- A sealed history re-check that can't be scheduled yet (the image's own
  pass still pending, or the pending set full) is kept due and scheduled
  when a pending image finishes, instead of being dropped.
- History matches a file through ComfyUI's spelling of its output folder
  first, then with both sides resolved, so a symlinked or junction output
  folder still attributes the scan's resolved paths; waiting files are
  keyed by their resolved path.
- The scan makes its batch history lookup only when a file needs it.
- set_current_prompt pushes to the queue last, so marking a failed
  execution never adds a second queue slot.
- A keyless image outside the output folder doesn't take a waiting slot.
2026-10-25 19:03:35 -07:00
Vito 48b1292115 feat(nodes): let a link drive the prompt text of every PromptManager node (#345)
* feat(nodes): let a link drive the prompt text of every PromptManager node

The Focused, Split and Pair editors hide their widgets, and ComfyUI only
draws a widget's input socket while the widget is visible. `text` (and the
Pair's `negative_text`) therefore had no socket, and a STRING link dropped
on the editor fell through to the first free STRING input, `prepend_text`,
which is how #342's workflow was wired.

- Detach those inputs from their hidden widgets on create and after a
  workflow load, so they are ordinary sockets in Classic and Nodes 2.0. The
  schema, widget order and saved values are unchanged.
- A STRING link dropped on the editor body fills the main prompt, then the
  negative, before the frontend's own choice.
- Each editor says when its text comes from a link; the typed text is kept
  for disconnect. The Classic nodes now save the typed text under a link
  instead of null, and "use prompt" no longer overwrites it while linked.
- Real-host tests: mouse drags in both renderers, a workflow saved before
  this change, and executions proving the linked text is saved, counted,
  recorded in history and linked to the run's image.

Closes #343

* test(coverage): floor for the linked-text module

* test(nodes): PromptSearchList text takes a dragged link like a core widget

* fix(nodes): linked text review round for #343

- A legacy frontend Primitive linked to an editor's text or the Pair's
  negative now runs with its value: the graphToPrompt wrapper and the queue
  hook fill it in (through virtual reroutes), in new and pre-#343 workflows.
- Moving a linked text and releasing it on the editor keeps it on `text`:
  a link being moved counts as free, as the frontend counts it.
- A link dropped on a prompt box (or its link note) fills that box's input,
  so the Pair's Negative prompt box takes `negative_text` (Classic through
  getInputOnPos, Nodes 2.0 through the box's slot key).
- Only exact STRING links are steered; wildcards keep the frontend choice.
- A linked Classic text box gives its row back and gets it back on
  disconnect; the PromptManagerText note uses the theme colour.
- The metadata parsers map positional widget values through the widget
  table when an editor's widget-bound inputs do not line up with it.
- Real-host cases for the legacy Primitive (new and saved workflows, a real
  Run), the moved link, the Negative box, paste/clone/undo and Split.

* fix(nodes): resolve a legacy Primitive's text like the host, once per run (#343)

- Inputs still bound to their widget (Classic PromptManager/PromptManagerText)
  are left as the host resolved them; the injection no longer overwrites them.
- For the editors' detached text inputs, the Primitive's value goes through its
  "Run widget replace on values" replacement and the hidden widget's own
  serializer, so {a|b} wildcards and %date% tokens resolve as on main.
- Each queued output is filled once (graphToPrompt wrapper, or the queue hook
  for an output that did not come through it): a wildcard is drawn once.
- Editors inside subgraphs are found under their <subgraph>:<inner> id.
- Real-host runPrompt installs its request route before the run starts.
- Shared parser case pins the widget-table fallback for a core KSampler.

* fix(nodes): pin the Classic wildcard behaviour and match queued ids by type (#343)

- Classic PromptManager/PromptManagerText expose _pmTextSerializerReady, settled
  once their own text serializer is installed; the real-host specs wait on it
  and assert the Classic text is queued as written, as on main.
- The Primitive fill only touches an output entry whose class_type matches the
  graph node at that id.
- TESTING.md notes the host's harmless "Invalid widget" warning per Run.
2026-10-25 18:25:20 -07:00
Vito 8252d9d226 feat(system): Integrations and a staged LoRA import (System S10) (#336)
* fix(lora): detect LoraManager in ComfyUI's extra custom_nodes folders

Auto-detection looked only in ComfyUI's main custom_nodes folder (#144).
It now also searches every custom_nodes folder ComfyUI registers through
folder_paths, main folder first, skipping folders that cannot be read.

* fix(lora): stage imports before replacing the collection (#221)

The LoRA import deleted the whole lora-manager category first and then
saved each record in its own transaction, so a discovery, download or
database failure left the previous collection gone or partial.

The import now runs in three stages: discover and fingerprint the
metadata files, stage every record (CivitAI downloads go into the image
cache first, counted), then replace the category in one BEGIN IMMEDIATE
transaction (replace_category_prompts). Any failure leaves the previous
prompts, tags and image links exactly as they were. Prompts a saved pair
uses block the replacement; an empty replacement of a non-empty
collection needs a reviewed import.

POST /prompt_manager/lora/import/preview reviews an import without
writing or requesting anything; POST /lora/import/apply {token} runs it
as the Activity job "LoRA reference import" (records admission,
cancellable while it downloads, the safety backup policy before the
replace, refused with plan-changed when the files changed since the
review). The legacy POST /lora/scan runs the same stages with its SSE
event shape.

* feat(lora): the LoRA save takes the settings revision

POST /prompt_manager/lora/enable accepts the optional expected_revision
that GET /settings returns, so the System Save row refuses to write over
a config.json changed elsewhere (settings-changed, nothing written). The
legacy page sends none and still merges into what is on disk.

* feat(system): configure ComfyUI and LoRA integrations natively

System › Integrations replaces its hand-off with the concept's two
surfaces. ComfyUI: only the send destination and renderer the bridge
supports can be chosen; the other options and Return to canvas are
disabled with the reason, and Host compatibility shows the renderer the
open ComfyUI windows report (each bridge destination now carries
renderer: classic or nodes2) (D106). LoRA & resources: detection, the
enabled setting and the loaded trigger words read as separate facts,
the read-only LoraManager path row follows the toggle (D106), and the
CivitAI key is never filled in: typing drafts a new key, blank keeps the
saved one and Clear saved key drafts its removal (D114). They save
through the one Save row (/lora/enable with the settings revision).

LoRA reference data → Review import shows the server's read-only review
(replacement, what saved pairs block, downloads, backup box) and Start
runs the LoRA reference import Activity job (D107), with its own
Activity descriptor and Review again into Integrations when the files or
pairs overtook it.

* test(system): compare Integrations with the concept

system-integrations-concept.spec compares System › Integrations (both
themes, 1600 and 390 wide) with the concept, mapping only D106 and D114,
and the LoRA import review with the concept's record-changing review,
mapping D107; each comparison catches a removed row, a renamed label or
option, a re-enabled unsupported option and a swapped token.

On the real ComfyUI host, each bridge destination reports the renderer
its window draws nodes with, before and after switching it, and System ›
Integrations shows it.

* test(coverage): record the Integrations frontend floors

coverage:update after the full frontend run: system-integrations.mjs at
its measured floor, and the raised maintenance review, job view and
maintenance floors.

* test(system): wait for the renderer line before clicking Integrations rows

CI Firefox (390 wide) clicked the LoraManager toggle while the renderer
check's line above it changed text and moved the rows, so the click did
not reach the switch. The specs now wait for that line to settle (a
visible state) before interacting.

* fix(lora): recheck the files before the replace and stop an abandoned scan

Review fixes for the staged LoRA import (#221):

- The metadata files are listed and fingerprinted again right before the
  replace transaction, so a file changed, removed or added while the
  downloads ran aborts the import with nothing deleted (System job:
  plan-changed, Review again; legacy scan: an error frame in its own
  words). The tests now change real files behind a held download.
- A legacy scan whose page closes stops staging before its next LoRA and
  keeps its records hold until staging has stopped; CivitAI images are
  written to a temp file beside the cache entry and moved in place, so a
  half-written image is never seen as cached.
- Start refuses with plan-changed when the integration was turned off or
  moved to another install since the review.
- A failed job keeps the skipped count and the names of the LoRAs that
  could not be prepared.
- The replace binds the category instead of one parameter per prompt
  (SQLite's variable limit), /lora/status says whether a detected install
  can be used, and the legacy scan logs expected refusals as warnings.

* fix(system): partial saves, key focus and readable selects in Integrations

A save that writes the LoRA part but not the settings part now keeps the
saved part, adopts the new revision and highlights only what failed, so
the next save needs no conflict. The CivitAI key status is a persistent
live region, Clear saved key returns focus to the key field, and the key
draft is dropped when the section is left. The send-destination and
renderer selects keep their width on narrow screens. A detected
LoraManager outside custom_nodes says it cannot be enabled.

* fix(lora): keep the save outcome, honour the umask and sweep stale downloads

Leaving Integrations drops the CivitAI key draft only when a key was
typed, so the last Save's outcome survives and Sources still marks a
refused source. The key status line is rewritten only when it changes.

Cached CivitAI images are opened like a plain save, so they keep the
umask's mode, and each import removes .pm-*.part files older than an
hour from the image cache. A legacy scan stopped because its page left
ends cancelled in Activity, and staging is awaited to the end even if
the handler is cancelled again.

* fix(lora): the import review takes a lease beside a restore

The LoRA import review reads the library, so like the maintenance and
restore previews it now runs under a request lease: it is refused with
409 busy while a restore runs, and a restore waits for it before it
switches the file. Tests pin that a restore is refused while an import
runs and that the review, the reviewed import and the admin page's scan
are refused while a restore runs.
2026-10-25 10:50:48 -07:00
Vito 8bf759d6f8 feat(system): keep the library in ComfyUI's user folder (#318)
* feat(system): resolve every library path in one place

utils/library_location.py decides where the library lives: the
PROMPT_MANAGER_DATA_DIR folder (validated: absolute, never inside a folder
ComfyUI or Prompt Manager serves), an explicit database.default_path as
before, a node-folder library that was not moved, else ComfyUI's
user/__promptmanager. The database, diagnostics, config.json, logs and the
LoRA image cache all ask it, so a new install never writes into the node
folder and an existing one keeps every path until the user moves it.
"Keep it here" is persisted as library.keep_in_node_folder.

* feat(system): move the library as a verified Activity job

POST /prompt_manager/system/library/move starts "Move library", an
exclusive, non-cancellable job (database/library_move.py). It checks the
target volume's free space, copies the database with SQLite's backup API
from one read snapshot, verifies the copy (integrity check, every workflow
document, the same rows in every table), copies the safety and
pre-migration backups and config.json (owner-only, checked by size and
SHA-256), then publishes under the old file's write lock and switches the
live process (every PromptModel, the review and history observers, the
API's plans and health report). The old prompts.db and config.json are
renamed *.moved-to-<folder> only after the switch. A failure removes
everything it wrote and leaves the old library in use; a marker in the new
folder makes a restart after any crash pick the right library.

GET /prompt_manager/system/library and the Overview's "location" say where
the library lives and why; POST .../keep persists "Keep it here".

* feat(system): show where the library lives and offer the move

D129. System > Overview's Library health gains a "Library location" row
(the database path, why it is there, the "Keep it here" choice and any
PROMPT_MANAGER_DATA_DIR problem) with Move library for a node-folder
library and Details otherwise. While a node-folder library is not kept
there, the workspace shows the concept's .notice above every page:
"Keep it here" saves the choice on the server and stops the notice;
"Move library" opens the shared review (From, To, Size, what moves, what
happens to the old files) and starts the exclusive job in Activity. The
notice and the Overview read the location again when the job ends.

The Overview comparison maps the inserted row; a new comparison holds the
notice to the concept's Sources notice (markup, tokens, alignment, both
themes and widths) with a mutation check.

* feat(system): close the browser server's log files before its root goes

The logs now live in the library's data folder, which for the browser test
server is <root>/user/__promptmanager; Windows could not remove the temp
root while the server still held prompt_manager.log open. The API test's
node folder is spelled in lower case because public paths are case-folded
on Windows.

* feat(system): show the library location on the legacy admin page

A line under the admin page's statistics names the database path and why it is there (ComfyUI's user folder, PROMPT_MANAGER_DATA_DIR, config.json, or the node folder with a pointer to Move library), read once from GET /prompt_manager/system/library.

* feat(system): prove a fresh host keeps its library in user/__promptmanager

The execution suite starts one pinned CPU ComfyUI with no configuration at all on an OS-chosen port: the library is created in user/__promptmanager (database and logs), nothing but byte code is added to the node folder, the location route reports the default folder, and ComfyUI's /userdata API serves none of it (traversal spellings, a listing, a system-user header).

* feat(system): document where the library lives and how it moves

README: where the library lives, the upgrade path (Move library / Keep it here), PROMPT_MANAGER_DATA_DIR, explicit paths unchanged, and the way back to an older version. TESTING.md: D129 and the library location contract (resolution order, move steps, crash windows, coded refusals, Windows handling).

* wip(system): library move fix round in progress

* fix(system): retry a fallback image link that races the library switch

The image monitor's fallback and recent-prompt links went through
PromptDatabase.link_image_to_prompt, which logs and returns 0 on any
error, so survive_switch never saw a "database is locked" or closed
connection during Move library and the link was lost in both files.
The monitor now links through link_image_to_prompt_or_raise (one
transaction per attempt); link_image_to_prompt keeps its
log-and-return-0 contract for other callers. A link that returns 0 is
no longer logged as "Successfully linked image 0".

A switch that fails after repointing the models now closes the old
file's detached connections instead of leaving them to GC: switch()
adds them to a list the mover owns.

The mocked retry test is replaced by a real-lock race asserting the
image row lands in the new library only.

* fix(system): say which source is the default and where a symlinked library lives

Sources & scans: with no source configured, ComfyUI's output folder now
reads "ComfyUI/output · watching · default" on its own row instead of a
separate note line. GET /settings marks each effective source with
in_comfyui (its public path is relative to the ComfyUI base) so only
those get the "ComfyUI/" prefix; the scan still sends the server's
public path.

Library location: a library in the node folder reads through the node
folder's unresolved place in custom_nodes, so a node folder symlinked to
a checkout outside ComfyUI shows custom_nodes/promptmanager/prompts.db
instead of prompts.db (System library row, Move review, admin Library
line). Never absolute or "..", else the shared _public_path, which
sources and jobs keep using unchanged.
2026-10-24 12:54:09 -07:00
Vito 7c25b351f7 test(comfyui): refuse a real-host port another run holds (#310)
The real-host config waited on the /system_stats URL, which any server on
the port answers. When two runs started on 8189 within a few seconds of
each other, the second run's ComfyUI failed to bind ("Port 8189 is already
in use"), but Playwright had already seen the first run's host answer and
ran the second run's tests against it. When the first run finished and
stopped its host, the second run failed mid-test with ECONNREFUSED,
ECONNRESET or ERR_CONNECTION_REFUSED, and passed when rerun alone.

The launcher now refuses a port that is in use, and reports ready only
when /system_stats names its own base directory. The config waits for
that line instead of the URL, and every test checks again that the host
answering the port is the one its run started.
2026-10-23 09:25:32 -07:00
Vito e29ff4d4b2 feat(inspect): the Inspect page for local files (#280)
* feat(inspect): local adapter seams in the shared inspector and viewer

The inspection loader calls adapter.read when an adapter reads by itself, so a
file chosen in this browser never reaches the API. The full inspector shows the
D66 local provenance (no Path row, no Show in folder, local association line),
and the viewer uses adapter.mediaUrl exactly, so a blob: URL is never given the
_pm_viewer cache-buster and Retry media reuses it. Rooted and recorded-history
adapters keep their api.get paths and cache-busted originals.

* feat(inspect): owned local selection session

createLocalSession owns one chosen File at a time: it classifies the bytes,
creates the blob: URL only for a classified image or video, and reads it
through a real inspection controller with the local adapter. Replacement,
dispose, parent abort and pagehide each release the URL, the handle and the
record once; a released handle (and every copy) is foreign to the adapter, and
every await is fenced by a generation counter so a replaced read never
publishes. Empty, multiple and directory drops keep the previous selection.
Baseline entry for local-session.mjs; branch floors raised for the touched
inspection-state.mjs and local-inspection.mjs.

* feat(inspect): native Inspect a file page

#/inspect is now the concept's "Inspect a file" page instead of the legacy
handoff. One chosen or dropped image or video previews inline, and the one
heading action, "Open file metadata", opens the shared viewer with the full
inspector on a record read entirely in this browser. Nothing is uploaded,
imported or stored; only an explicit Send crosses the host bridge.

- inspect.mjs mounts the concept DOM (surface, file-drop, #local-preview)
  under the shell heading, owns one local session and the shared viewer and
  prompt sender, and releases media before the session revokes its URL on
  replacement, route change, page leave and dispose (D60-D65).
- A file that cannot be read after choosing opens the inspector's
  unreadable state when the browser declares a supported type, instead of
  the "unsupported" copy; the local adapter maps a browser read failure to
  an unreadable record.
- The route is native; Overview keeps its card to the legacy metadata page,
  which is unchanged.
- workspace-inspect.spec.mjs covers the chooser and viewer rail against the
  Images rail for the same bytes, drop, cancel and reselect, unsupported and
  unreadable files, video, URL ownership, mid-read replacement, exact send
  and the no-request/no-storage rule.

* test(inspect): concept parity and managed-host send for local files

Compare the native Inspect a file page with the immutable concept's
#metadata at 1600x1100 and 390x844 in both themes (initial, selected,
viewer-open), mapping D60-D66 and pinning the concept's CSS values and
the chooser's displayed file name, with a mutation check. Send a local
PNG (explicitly empty negative) and core-tags.webm to a real Prompt
Manager (+/-) node across Classic and Nodes 2.0 switches and reloads;
a connected input rejects the send unchanged. Document D60-D66, the
legacy-floor dispositions and the commands in TESTING.md.

* fix(inspect): reset a released preview video in the next task

On WebKit (GStreamer) a video replaced while its pipeline was still
starting blocked the page: pause, src removal and load() in one task
never returned, so the next chooser selection hung (CI page workflows
webkit shard 2/2, "URLs and media end with their owning session").
The src is still dropped before the URL is revoked; only the load()
reset moves to the next task. A fresh-page regression replaces the
first video as soon as it shows, fifteen times.

* fix(inspect): never reset a preview video that is still starting

Deferring load() to the next task was not enough: CI on b7db4c3 hung
WebKit one step later in the fresh-page regression. Reset with load()
only a settled video (metadata read, or failed); a starting one is
paused, loses its src before the URL is revoked and is removed, then
left to be collected. The ownership workflow waits for the video to
settle before asserting the reset.

* fix(inspect): final review round for the local Inspect page

- Each inline preview owns its listeners, so a released node is never
  pinned; a video released while still starting is reset with load()
  once it reports metadata or an error (I1).
- The viewer's video teardown on a just-started local video is guarded
  by a 15-page WebKit test; it needed no change (I2).
- The chooser keeps naming the current file after a cancelled picker
  and names the dropped file after an accepted drop (I3).
- The unrelated activity-scan.mjs floor raise is reverted (I4).
- The page owns pagehide: the viewer closes and the inline media goes
  before the session revokes the URL, and the chooser is cleared (M1,
  M2); the session gains clear() and the viewer close().
- The "metadata is available" toast shows only for readable metadata
  (M3); a rejected drop is a second, transient alert that never
  replaces the selection's status (M4).
- TESTING.md: D62/D63 updates, the WebKit loadstart follow-up and the
  pair-node follow-up (M5).
2026-10-22 16:06:07 -07:00
Vito 8bcde4fbe4 feat(library): native recipe editor UI, pair archive browsing and export (#235)
* feat(library): manage native editor drafts and recovery

* fix(library): fence pair archive retries against restored storage

* feat(library): add native recipe editor dialogs

* fix(library): read archive counts through the editor controller

* feat(library): reconcile editor saves and archived pairs

Wire the concept's heading actions (Library Export and + New prompt, Images New prompt, history ← Prompt library, Edit metadata and pair Send to ComfyUI) and the Negative variants + Add negative and Show archived controls. Saves navigate or reconcile in place through createLibraryDetail.accept, invalidate cached shelf state by write generation, browse and restore archived pairs, and offer an explicit Library reload when storage was restored.

* fix(library): refresh archived pairs and align editor outcomes

* test(library): enforce approved native editor presentation

* test(library): verify editor host and coverage compatibility

Adds the rejected connected-target host assertion, pins the stock host listing at 80 cases, documents the native editor, and keeps Save/Archive focus after a failed write.

* docs(testing): correct host case totals and cover archive focus

* fix(library): fence editor reloads after restore and settle outcome copy
2026-10-19 01:46:33 -07:00
Vito ecd21165a7 feat(history): add chronological runs and exact output inspection (#227)
* feat(history): add bounded chronological read projections (#209)

* fix(history): validate internal continuation positions (#209)

* feat(history): expose fenced history windows and run details (#209)

* fix(history): release retired projection connections (#209)

* feat(history): resolve reported outputs through current authorization (#209)

* fix(history): reject invalid inspection targets (#209)

* feat(history): add scoped history state and current-file adapter (#209)

* fix(history): preserve slideshow and clear stale reentry actions (#209)

* feat(history): render recorded executions and frozen run details (#209)

* fix(history): recover receipts and refresh capture evidence (#209)

* test(history): verify chronological history across restore and host modes (#209)

* fix(history): enforce chronological marker visual parity (#209)

* fix(history): resolve final chronological review findings

Address #209 recovery, bounded visit restoration, exact receipt availability, and portable regression coverage.

* test(history): repair Windows fixtures and review feedback

Address #209 and PR #227 native path, cleanup, media and review-comment expectations. Document Windows portability preflight without changing production behavior.

* ci: raise the viewerjs job budget for the larger browser suite

The cross-browser workflows suite grew from 396 to 496 tests on this
branch, which pushed the whole viewerjs job to about 25.5 minutes
against the 25 minute cap set in #181. Two exact-head runs on this
branch timed out without any test failing: run 39173019283 was killed
on test 474 of 496, and run 39175125474 printed "496 passed (12.8m)"
and was then cancelled during Playwright shutdown.

Raise only the job budget. Browser projects, workers and per-test
timeouts are unchanged. Splitting the browsers into parallel jobs is
tracked separately.
2026-10-18 17:17:05 -07:00
Vito 4ef236aaf7 feat(history): persist exact run output references (#214)
* feat(history): define bounded output reference contracts

* feat(database): migrate execution output evidence to schema v3

* feat(history): persist immutable run output references

* test(history): isolate mocked node gallery monitor

* feat(history): capture bounded output descriptors with shared leases

* fix(history): authenticate payload owners and release failed captures

* feat(history): attach output evidence to exact finalized attempts

* test(history): verify run output evidence on real ComfyUI

* fix(history): release idle worker payload references

* docs(history): clarify defensive lease cleanup

* test(history): align output fixtures with native path styles

* test(history): separate restore completion from writer drain

* test(history): bound batch drains by progress and total time
2026-10-17 23:26:37 -07:00
Vito bae542c9ac feat(history): persist exact execution prompt contexts (#213)
* feat(history): define exact bounded execution contexts

* fix(history): retain observable sources and validate evidence

* feat(history): migrate execution contexts to schema v2

* feat(history): persist immutable execution context snapshots

* fix(history): preserve package identity for execution contexts

* feat(history): bound context payloads beside lifecycle finalization

* fix(history): fence context commits and resolve stored run identity

* fix(history): bound persisted identifier bytes before restore reads

* feat(history): bind exact runtime context to execution attempts

* test(history): discover owner invalidation callback regression

* feat(history): capture cached prompt strings without mutating host caches

* test(history): verify durable contexts on real ComfyUI hosts

* fix(history): scope transforms to captured source paths

* test(ci): update execution context tooling fixtures (#209)

* fix(history): harden context validation and worker fixtures (#209)

* fix(history): honor close deadlines after early joins (#209)
2026-10-17 19:51:22 -07:00
Vito 763e6b37b1 feat(history): record durable ComfyUI execution attempts (#212)
* feat(history): normalize bounded execution evidence (#209)

* fix(history): bound lifecycle kind checks before hashing (#209)

* feat(history): persist execution attempts with restore fencing (#209)

* test(api): bootstrap standalone backup restore fixtures (#209)

* feat(history): isolate bounded asynchronous ledger writes (#209)

* feat(history): observe actual queue attempts without altering execution (#209)

* fix(database): honor package-scoped configuration (#211)

* test(history): exercise real cached and headless executions (#209)

* fix(history): reject unsupported uniqueness and isolate package fixtures (#209)

* test(history): support Python 3.10 package fixture cleanup (#209)
2026-10-17 16:42:07 -07:00
Vito f98f6fa08d test(ci): validate shared toolkit in real PromptManager hosts (#189)
* test(ci): prove shared toolkit with real PromptManager host coverage

* test(ci): match KikoTools Python 3.13 compatibility coverage
2026-10-16 20:56:50 -07:00