- G6: FileRoot.pendingCount/pendingPartial for download roots (same rule as the
unidentified filter, recursive, cached 5 min, invalidated by files.changed)
- G7: FileEntry.parsedHint from parseReleasePath (file + parent dirs); the web
subline uses it and no longer parses names itself
- G8: FileIdentifyCandidate.posterUrl (TMDB poster_path via adapter and
catalog.lookup, local entities from metadata.posterPath); thumbnails in the
identify panel
- S-7 contract: FilesIdentifyResult.remoteTaskId/remotePending,
MediaServerInventory.partial, EventType identify.completed with
IdentifyCompletedPayload; identify panel (useIdentifyLive) and organize wizard
merge remote results by root + path, buffering events that beat the response
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Kysely's Migrator checks for its tables via introspection.getTables(),
which reflects every schema and calls has_schema_privilege /
pg_get_serial_sequence per schema. When another test file drops its schema
at the same moment those functions fail with "schema … does not exist" —
the intermittent server PG test failures on the shared dev database.
createMigrator now hands the Migrator a proxy whose introspection only
looks at the migration schema (or current_schema()). A regression test
churns schemas while migrating; it fails on the old migrator.
Also samples EngineRunQuery's comma-separated status/triggeredBy in the
contracts schema test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- GET /dashboard: automation.library {entities, movies, series, seasons,
episodes, assets} and automation.slotSatisfiedRate; home media overview
shows the counts instead of placeholders.
- GET /engine/runs: `status` accepts comma-separated values; new
`triggeredBy` filter (comma-separated) evaluated server-side by scanning
batches and paging with the last inspected row's cursor (scan cap 2000).
- Task stream: source filter and the "active" tab (running,pending) are now
one paginated server query.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OutboxRelay gains a `mirror` option: listed types are claimed even without a
task route and, after the claim transaction commits, published in order to
the realtime bus (publish failures are logged, delivery state unaffected).
The worker mirrors feed.*, decision.*, download.*, organization.* and
subscription.* (feed.due excluded) with their business-id payloads, so the
home page, inbox, task stream and feed tables finally receive them. Feed
tables refresh on feed.fetched / feed.health_changed; the task stream also
listens to feed and decision events.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
zod v4 records keyed by an enum require every key; the generic sample only
produced {k: "v"}, so the positive example failed after W9-E.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- slot: new manual_verify event (attention -> verifying, with reason); a
successful organize of a download whose slot is in attention goes back
through verifying and lands by verify_passed on the actually imported
assets; the download's organize_blocked decision is resolved with
resolution {key: manual_organize | organize_retry, organizationRecordId} (U1)
- acquisition: plan purpose is upgrade only when the slot already has a
current asset; a movie slot gap is a fill (U7)
- identity: entities created from TMDB details get metadata fields projected
in the same transaction; TMDB aliases deduped by normalized text, site-title
aliases skipped when the text already exists (U9)
- pi: auth-mode test of a custom Base URL no longer marks the upstream ok, and
says to use the minimal completion to probe the endpoint (U2)
- sites: PUT /sites/{id}/credentials replaces same-kind secrets in place and
names new secrets with site id + random suffix (secret.name is unique; was a
500 on update); M-Team user data uses config/definition apiBaseUrl instead
of deriving api.<domain> from the site URL (Invalid URL on IP/local bases)
- copy: setup error says 初始化口令; backup job points at umt backup
- tests: schema expected tables add site_user_data; migration round-trip
timeout 60s; fake secrets honor label as name; vertical/resolve/pi/slot/
site-center tests for the above
- docs: 09-visual-qa W10-pre status table (U3 root cause: outbox events are
never mirrored to SSE)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- site.list / site.health_check / site.refresh_login / site.user_data capabilities
over the W9-A site center services; outputs whitelisted (no secret refs,
cookies, passwords, login names); execute kinds need automation grant.
IDs use snake_case (CapabilityId regex); registry count 17 -> 21.
- settings.organize.minSizeBytes (default 50 MiB); file-manager identify /
organize and planImport take request > setting > port default.
- tests and runbook notes (sites, files).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- POST /files/suggest-name renders the effective template (entity > root >
global) with the same variables as organize: file name segment for files,
work/season dir segment for directories; identity_uncertain when no entity
- planImport plan.naming.templateSource/templateVersion now come from
effectiveNaming + the registered template version (fixes the leftover
DEFAULT_TEMPLATES reference after the naming chain landed)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- process_heartbeat / process_heartbeat_sample (0034): api / worker / worker:agent / all
upsert every 15s (event loop lag last/max, concurrency, running runs, SSE connections, memory)
- GET /system/health (admin): processes with 45s stale flag, SSE, DB latency/size/connections,
disks (DATA_DIR / library roots / download dirs, cache thresholds), mount sentinel checks
with IO timeout, engine pending/oldest/24h failure rate, schedules
- GET /system/health/history?hours=: bucketed lag / running / SSE series
- /engine/runs and /engine/runs/{id}: business {kind, refId, title, triggeredBy} joined from
run input / task / agent_run / tasks_run_meta / outbox; businessKind / businessRefId filters
- docs/runbook/ops.md: field meanings and alert thresholds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Found in v0.2 visual QA (docs/plan/09-visual-qa.md):
- actor strings (admin_session:<uuid>, user:<uuid>, system) -> 管理员/系统/API Key
in policy revisions, subscriptions, inbox, trash, business tasks, organize detail
- cache volume without key shows the downloader name instead of instance:<uuid>
- download timeline/plan: task and organize statuses, plan purpose, priority and
status reason localized; audit gets labels for capability/files/site/feed actions
- organize record reason uses conflict detail (没有可入库的主视频) instead of
blocked:no_main_video; slot/candidate reasons drop inflight:<uuid>
- identity decision summary no longer shows no_viable_candidate
- upstream test: 'Connection error.' classified as network, with a Chinese hint
- new policy draft prefills key default (placeholder looked like a value)
- dialog/sheet close sr-only text in Chinese
Co-Authored-By: Claude Opus 5.5 <[email protected]>
loginNexusPhp (captcha / two-factor / bad credentials / blocked / Cloudflare
classification, TOTP), SiteUserInfo.vip via profile.vip_css or umt.vipLevels,
maxSeedingPages option, MTorrent parser honours apiUrl; testkit mock NexusPHP
site gains takelogin with login guards and user detail pages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- GET /search?q=&scope=all|media|releases|actions&limit=: local works (title_alias
exact/prefix/trgm + name prefix, slot status summary), TMDB candidates (cached,
localEntityId), cross-site releases, page/action jump list; tookMs per group
- cross-site: enabled sites via shared per-site limiter, 15s AbortSignal timeout,
site concurrency cap 4, same-query coalescing, 30s cache; partial + failedSites;
results upserted to source_release; inventory overlay via GET /inventory
- GET /search/releases: site/resolution/source/size/free/H&R filters, sort, cursor,
facets; shares the cache
- POST /search/releases/{siteId}/{siteReleaseId}/plan: forwards in-process to
POST /acquisition-plans (same auth, validation, audit, rejections)
- contracts/search.ts, runbook, tests (partial/timeout, coalesce, cache, overlay,
filters/sort, plan)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- rules/: RuleExpr tree, '>' layers with ! & | and parens, position-aware errors
(unknown atom with suggestions, unsupported music atoms, parens, empty layer)
- builtin keyword table aligned with MP BUILTIN_RULE_SET; field-based judgement
where parsed fields exist, MP regex otherwise; custom atoms (MP 7 fields)
- chain evaluation: hard excludes, ladder pri 100-k, groups AND-ed, pri from last
laddered group, readable per-atom trace
- slot: SlotSpec.rules, rule.hard_exclude / rule.no_layer_matched violations,
pri basis switches to priority when a rule ladder is present
- differential: 66 rule chains x 32 torrents vs MP python path (parse 64/64,
pri 2090/2112, 22 registered mp_bug)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Site health/proxy, SiteAccount login state (challenge, lastLoginAt, lastError,
userData), SiteUserData history table, SiteCredentialsBody by kind,
refresh-login/health-check/user-data/delete endpoints, SitesSettings defaults.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pi.provider / baseUrl / model / thinkingLevel / apiKeySecretRef are now
DEPRECATED_SETTINGS_FIELDS: kept in the schema for stored JSON, dropped
from PATCH /settings and from the settings sections, never read by the
server. runAgent fails with a clear message when no upstream is set.
The agent demo gate checks the default upstream provider; e2e seeds a
fake upstream as default. OpenAPI summaries say Agent upstream.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
no_match resources (and lookup failures after auto-review gives up) now
open a decision in the inbox with options to set a TMDB ID, pick a local
work, or dismiss; ambiguous decisions get the same manual options.
Resolve accepts identity {tmdb | mediaEntityId} and assigns with a manual
lock. The inbox option dialog collects the TMDB type/ID or searches local
works; the identity pool links each pending item to its inbox filter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- /console/settings#<section>[/<anchor>]; legacy ?tab= redirected in clientLoader
- SETTINGS_SECTIONS in contracts with field-ownership test
- Agent section: upstream list, create wizard (provider -> credentials/model -> login/test/default),
edit drawer with OAuth browser/device login, test, set default, delete
- sites section with site registration, snapshot/rate-limit drawer and account credentials
- path mappings moved into downloader / media-server drawers; metadata and account sections
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- agent_run gains depth / spawn_reason (0029); agent_run_event types
subagent_spawned / subagent_event / subagent_completed
- agent.spawn (propose, medium, replay never): allowlist ⊆ parent, mode not
above parent, budget carved from parent's remainder, idempotent per run+input;
settings.pi.subagents {enabled, maxConcurrent, maxDepth} (subagentsEnabled kept as alias)
- child runs claim a concurrency slot under an advisory lock (waitForFact while
queued); agent.await polls business tables without calling the model
- parent abort cascades to all descendants; parent runtime budget counts
descendant usage; child events mirrored into the parent stream by parent seq
- authorize caps every child call by all ancestors' allowlists/kinds
- session page shows parent link, inherited info and the subagent tree
- e2e E11; runbook section on subagents
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The committed types.ts had the pi_upstream interface spliced into the middle of
PolicyRevisionTable (and upstream_id / DB entry in the wrong places), which broke
typecheck at HEAD. Regenerated with gen:types against a database migrated to 0028.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- PiUpstreamClient: one Models per upstream holding only its provider; credential
store adapter maps to that upstream's secret, so several accounts of one provider
coexist; host env is only read for authKind none (ambient cloud creds)
- provider catalog from builtinProviders() plus static oauth modes, env fields and
subscription terms notes; openai-compatible custom endpoints
- fake upstream provider with mock OAuth (browser paste / device approval)
- PiAgentRuntime accepts an upstream; thinking level clamped to the model
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The throttle could drop the final copy heartbeat when it landed inside the
interval (fsproxy.test saw 6291456 instead of 6291461 under ci load). Phase
completion (bytes >= total) is now sent immediately like phase changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- POST /files/probe writes media_asset.media_info, so scope organize.read -> organize.execute
- e2e e6-w4a-endpoints -> e10-w4a-endpoints (number clashed with e6-auto-recover)
- trash: reusing a consumed approval / retired planId is rejected and the file stays
Co-Authored-By: Claude Opus 5.5 <[email protected]>
S-1 (/files/move, /files/organize action=move):
- organize-guard: authorizeMoveEntities checks every source's media entities
(declared entity plus entities of registered assets under the source);
one-time approval still covers the whole request; any source that cannot
be attributed to an entity only passes with an approval; denial lists every
unauthorized item; optional non-consuming pre-check
- transfer: entities per source (directory = registered assets beneath it)
- organize: all move groups pre-checked before any group executes; any
denial -> whole request 403 approval_required
S-2:
- api.ts: GET /files/download and POST /files/trash/purge marked
auth:"session"; files routes attach requireAdminSession from the registry
annotation; handler-side purge check removed, download keeps a same-rule
defense check; permanent delete uses isAdminSession
- DEV key rule written down: equals an admin-scope key for session-only
endpoints, still admin for Move/delete approval bypass (as authorizeMove)
- api-registry.test: session list updated; new reverse assertion that no
non-session endpoint answers 403 to an admin-scope bearer
S-5 (trash restore):
- restore-to requires a writable managed library root (isWritableTarget)
- in-place restore rebuilds missing parents level by level without
following symlinks (mkdirWithin), realpath-checked per level
- cross-volume restore capped by FILES_TRASH_CROSS_MAX_BYTES; cross-volume
copies (trash in / restore out) go through fsproxy
Tests: multi-entity move 403 + listing, unattributed source, organize foreign
asset 403, download/purge 403 for DEV and admin-scope keys, restore target
not writable, symlinked parent, rebuild parents, cross-volume cap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- contracts: isSensitiveQueryParam (whole-name match on SENSITIVE_QUERY_PARAMS
or isSensitiveKey), the single source for query credential names
- rss/redact: drop local SENSITIVE_QUERY_KEYS; param checks go through
isSensitiveQueryParam (+ caller extras); REDACTED re-exported from contracts
- browser/redact: CREDENTIAL_PARAM built from SENSITIVE_QUERY_PARAMS, JSON keys
also checked with isSensitiveKey; keeps only its own rules (path passkey,
cookie names, a11y password fields); remove stray regex statement
- redact-consistency.test: 24 credential spellings (pass_key, c_secure_pass,
refresh_token, password, uid_key, auth, ...) masked by contracts, rss and
browser alike; plain params untouched; rss extract/unredact round-trip
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- downloads.ts observe: re-read download_task FOR UPDATE inside the tx and merge
from the locked row; qB sync could overwrite imported back to importing when
verifyAndImport committed between the sync read and write (E8 flake).
- agent finalize: accept summaryPrefix so the recovery verdict is written with
the terminal status in one update (E6 read a budget_exceeded run before the
"未恢复:…" annotation landed); recover-summary step kept, now idempotent no-op.
- organize.ts: drop local slotComplete and the once-subscription completion
write; completion/history are decided only by applySlotEvent(verify_passed)
(W5-C follow-up, removes double write). Manual slot organize uses the shared
subscriptions.slotComplete.
- vitest: server split into unit (parallel) and pg (file-serial, groupOrder 1)
projects; packages/tasks file-serial; explicit worker concurrency in e2e.
- docs: W6 stabilization record in 04-v0.1-acceptance.md.
Test expectations unchanged. Full `deno task ci` x3 green: 369s / 341s / 337s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- /files/move|copy go through planImport's new `transfer` variant (keep names, directory
expanded to per-file plan) → organization_record + verifyAndImport/executeRecord: per-file
ledger resumable, fsproxy, cross-volume directory move = per-file copy→verify→delete then
remove empty source dirs; shared plan store + plan_stale; asset_location follows per file,
path_binding revised when a whole directory moved (finalizeTransfer); request waits up to
FILES_TRANSFER_WAIT_SEC, else executed.pending
- /files/organize per-item overrides take effect: season/episodes (domain planOrganize
overrides), contentUnitId coverage, targetDir (no binding), operation, slotId (satisfied
after verify); warnings only for inapplicable overrides
- delete/trash/restore write organization_record (action trash|delete|restore, file-level
results; migration 0026)
- GET /files/download (admin session, nfo/subtitle/image, size cap, streamed, audited)
- GET /files/records?ids|planIds for the organize page
- fs race errors map to 503/404/403/400 instead of 500
- runbook updated
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- main.ts: `--role fsworker` dispatched before the rest of the module graph is
loaded (other imports became dynamic); runs fsWorkerMain from the new
`@umt/adapters/fsworker` export (~150 ms startup in the compiled binary)
- fsproxy: default worker command picked per runtime (deno CLI -> `deno run
fsworker.ts`, compiled binary -> `<execPath> --role fsworker`); stats expose
workerKind; UMT_FS_TEST_HANG_AFTER_BYTES acceptance switch for containers
- compile: --allow-run adds /app/umt-server (spawn self); start task allows deno
- organize: replaced old file moved into the file-manager trash layout
(<root>/.umt-trash/<day>/<uuid>/<rel>) and registered in trash_item, ledger
result follows; organize.transfer log carries fsproxy stats
- settings: autoInvestigateMode (investigate/recover) + max recover attempts
- deploy.md: container verification (subprocess self, TIMEOUT -> SIGKILL x3)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- subscription: status pending (TMDB not ready: searchable/evaluable, not completable),
last_searched_at / next_search_at
- content_unit (season): episode_total ledger with source / lock / timestamp
- acquisition_plan: unit_keys (Slot × unit reservation) and file_selection; drop the
one-in-flight-per-slot unique index (checked under slot row lock per unit instead)
- new tables slot_deferral, slot_unit (per-unit quality ledger), subscription_history
- contracts: SubscriptionParams, SubscriptionHistory, SlotDeferral, SlotUnit, SubscriptionProgress,
EpisodeTotalSet, CandidateEvaluation pri/trace/unitKeys and decision deferred;
endpoints subscriptions.search/progress/setEpisodeTotal and subscription-history
- mapping: evaluation messages via describeViolation, pri/trace passthrough
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- adapters/fs: FsProxy + fsworker run each file op in a killable subprocess;
progress-stall watchdog (FS_HANG_TIMEOUT_MS) for copy/hash, total timeout for
quick ops; SIGTERM -> SIGKILL -> FsError('TIMEOUT'); stat/checkRoot timeouts
never read as missing
- copyWithVerify: onExisting conflict/skip/replace (hardlink backup + atomic
rename, old file to .umt-trash or deleted), sweep stale partials before write,
document why permissions/owner are not copied
- filesMatch/filesMatchSha256 throw FsError on read failure instead of
match=false; crossDeviceMove skips redundant re-hash after sha256 copy
- FsErrorCode TIMEOUT + fsErrorClass; organize executeRecord routes fs ops
through fsproxy, retries TIMEOUT/IO up to FS_TRANSFER_MAX_ATTEMPTS then opens
a decision without deleting the record; reconcile errors no longer reported
as EEXIST
- domain: subtitle .default marker (ORGANIZE_DEFAULT_SUBTITLE_LANG) and
findOtherVersions (same season/episode/part, video only) as upgrade
disposal candidates
- scripts/bench/fs-bench.ts and benchmark record in porting doc
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- 0024_trash: trash_item table + unique idempotency index on artifact(files_idem)
- files.ts: TrashItemRow, delete planId/planExpiresAt, organize group warnings,
scrape returns refreshMetadata taskId
- registry tests list the new table/row schema
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- POST /organization-records: sourcePaths must resolve (realpath) under a
registered library_root or downloader path-mapping host path; `..` and
symlink escapes rejected (400), dryRun included. Move requires an admin
session, or an API key with an automation_grant covering the entity or a
matching one-time approval (consumed on use), else 403. Manual Move from a
managed download_task with unsatisfied H&R returns 409 and opens a
decision; never silently downgraded to Copy.
- Series slot completion: total episode count is unknown while units are
provisional, so the slot is not satisfied and the subscription keeps
tracking; coverage is registered per actual file (creating provisional
episode units), and already_in_library files count as verified presence.
- POST /engine/runs/{id}/retry (admin): re-enqueue by original task name and
raw input with idempotency key retry:<id>:<n>, audited; retry button on
the run detail page.
- E7 regular series e2e, E8 AT24 tag removal / task disappearance e2e.
- All workspace members and root set to version 0.1.0; CHANGELOG.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>