Commit Graph
45 Commits
Author SHA1 Message Date
rootandClaude Opus 5.5 13132e7ad8 feat(file-manager): v2 backend gaps — TMDB candidate entities, min size, on-existing, naming source (W9-H)
- G1 createEntityFromTmdbCandidate (sources-identity service): entity + accepted
  manual external identity + season/episode units, idempotent per TMDB id;
  POST /files/entity-from-candidate and organize items with `external`
- G2 minSizeBytes for organize (excluded below_min_size) and identify
  (skipped + excludedBySize)
- G3 onExisting passed to planImport; dryRun marks each file
  (same/skip/replace/conflict); replace = explicit grant
- G4 naming template|keep; planImport namingMode/namingSource, frozen
  plan.naming {mode, templateSource, templateVersion}; targetSegments
- G5 identify prior-record hints, GET /files/record-sources, organize
  refreshMetadata, POST /files/scrape-batch (dedupe by entity)
- G8 GET /files/tree (parent, subdirs hasChildren, video/registered counts)
- G9 POST /files/suggest-name returns 501 until the naming engine lands

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 15:08:17 +08:00
rootandClaude Opus 5.5 0105e5c6ac feat(rules): rule_group/rule_atom (0033), /rules endpoints, evaluation via referenced rule groups
- migration 0033: rule_group (layers, hard excludes, scope, source, revision,
  fingerprint, default_order) and rule_atom; acquisition_slot.rule_group_ids;
  preference tags migrated into rule groups (same id/fingerprint), suppressions
  linked; seeded default groups for acquisition / upgrade / search
- contracts: RuleGroup/RuleAtom/RuleExpr/RuleDefaults, parse/serialize/preview
  schemas; Api.rules, Api.ruleAtoms, Api.rulePolicies, Api.ruleSuppressions
- server: GET/POST/PATCH/DELETE /rules, /rules/{id}/revisions, /rules/{id}/preview,
  /rules/parse, /rules/serialize, /rules/keywords, /rules/defaults, /rules/atoms;
  disable/delete writes suppression, agent re-add -> 409
- /preference-tags now aliases rule groups; /policy-revisions and
  /preference-suppressions kept as aliases of /rules/policies, /rules/suppressions
- evaluateAcquisition resolves slot -> subscription -> default chain (+ upgrade
  defaults) and records pri / rule_group@revision / per-layer trace

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 15:08:00 +08:00
rootandClaude Opus 5.5 a0946bf480 feat(search): global search aggregation backend (W9-G)
- 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]>
2026-09-28 15:05:58 +08:00
rootandClaude Sonnet 5 623aa0d3b7 chore(release): v0.2.0 version and changelog
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-28 13:53:33 +08:00
rootandClaude Opus 5.5 d64f735810 feat(agent): agent.await suspends the session instead of holding a worker slot
When children are still running, agent.await records the wait, closes
the session (the tool call stays pending in the JSONL) and drive returns
suspended. runAgent waits with waitForFact for the children to settle,
the deadline, or cancellation, then reopens the session; agent.await is
replay-safe, so resume replays it and the model gets the real result.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 13:45:40 +08:00
rootandClaude Opus 5.5 3e37ac8ae7 refactor(agent): runAgent uses Agent upstreams only; deprecate legacy settings.pi fields
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]>
2026-09-28 13:45:40 +08:00
rootandClaude Opus 5.5 fb7727cf66 feat(identity): open identity_unresolved decisions for resources with no candidates
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]>
2026-09-28 13:44:57 +08:00
rootandClaude Opus 5.5 a0b0a64246 refactor(schedules): register pi.token_refresh and auth.cleanup as scheduled jobs
Pi token refresh (10 min) and auth session cleanup (1 h) no longer run
their own setInterval; both register in the maintain group of the
schedules registry with the same default intervals.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 13:44:48 +08:00
rootandClaude Opus 5.5 31a54186c7 copy: second-round copy review across console, settings, agent and server messages
- Trim descriptions, empty states, confirm buttons and toasts to facts
- Unify terms per glossary: 待处理/待识别/整理记录/运行记录/凭证/子会话/Agent 上游
- Subscription-login termsNote states facts only (borrowed client identity, ToS/ban risk)
- Shorter, concrete goal templates for built-in Agent checks
- Human-readable nouns instead of table names in instance CRUD errors
- Audit labels for pi_upstream / schedule / automation_grant actions
- Glossary: add 上游、订阅登录、凭证、定时任务、巡检、运行记录、子会话、停止、持续授权

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 13:29:42 +08:00
rootandClaude Opus 5.5 459991421e feat(agent): Pi subagents via agent.spawn / agent.await
- 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]>
2026-09-28 02:49:37 +08:00
rootandClaude Opus 5.5 5843527317 fix(schedules): cron sample for schema test, lint cleanups
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 02:39:56 +08:00
rootandClaude Opus 5.5 a68fcea13b feat(server): unified scheduled-job registry and scheduler; module timers moved onto it
- modules/schedules: registerScheduledJob({key, defaults, run}) registry; defaults are
  synced into scheduled_job without overriding user-changed enabled/schedule
- scheduler runs only on the io worker, one instance via pg_try_advisory_lock held on a
  dedicated connection; due = enabled and next_run_at reached, or run requested;
  jobs run in parallel, never re-entrant; interval next = finish + period, cron next match;
  run-now does not move next_run_at and merges duplicate requests; skip advances one period;
  history trimmed to the last 50 runs; interrupted runs marked failed on takeover
- agent_check jobs: trigger condition checked first, goal template rendered, runAgent
  enqueued via startRun(trigger=schedule) with allowlist and budget, one run per time slot;
  allowRecover uses the autoInvestigate grant check; results harvested back into the run
  history and optionally reported as an info decision; 3 built-in checks, off by default
- sources-identity (feeds.poll, sites.snapshot, identity.review), acquisition-organize
  (downloads.sync, cache.threshold, downloads.orchestrate, slots.reevaluate,
  subscriptions.search, subscriptions.metadata, library.reconcile) and file-manager
  (trash.purge) register jobs instead of running their own setInterval; default
  intervals unchanged; backup registered as an unconfigured placeholder
- endpoints under /schedules (admin, console session only)

TODO: pi refreshPiTokens and auth-settings session cleanup still own their timers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 02:32:33 +08:00
rootandClaude Opus 5.5 fd3ffe5a8d feat(server): pi module with upstream CRUD, server-side OAuth and token refresh
- /pi/providers, /pi/upstreams CRUD, test (auth-only by default, no cost), models
- subscription login via pi models.login with a suspended interaction: browser mode
  waits for the pasted redirect URL, device mode polls server-side; per-provider
  login lock, readable callback-port-in-use errors
- credential store locks the pi_upstream row (select for update) so api and worker
  refreshes never double-write a rotated refresh token
- refreshPiTokens task: refresh before expiry; failure marks expired and opens a
  relogin decision, closed again after a successful login
- runAgent picks upstreamId > agent_run.upstream_id > default upstream > legacy
  settings; credentials injected by the server, never logged

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 02:23:22 +08:00
rootandClaude Fable 5.1 16ab2a6c84 docs(plan): v0.2 adds Pi subagents and scheduled-jobs board
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-28 02:09:39 +08:00
rootandClaude Opus 5.5 d3de2c8971 fix(files,test): probe requires organize.execute; rename duplicate E6 to E10; trash approval replay test
- 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]>
2026-09-28 00:50:29 +08:00
rootandClaude Opus 5.5 2e3c2098fd docs: S-9 sync READMEs and porting notes with W5 code
- contracts README lists files.ts / auth.ts / browser.ts / redact.ts and auth:"session"
- adapters README fs row: FsProxy and the ./fsworker export
- server README: file-manager module, worker:agent and internal fsworker roles
- moviepilot-rust-transfer §7: compiled-binary fsworker and trash_item done (W5-E)
- 04 S9 row marked fixed (W6-B TOTP entry)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 00:50:29 +08:00
rootandClaude Opus 5.5 2ddaf7bfa6 fix(fs): S-10 hang test hook requires UMT_FS_TEST_HOOKS=1; preflight warns when set
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 00:48:45 +08:00
rootandClaude Opus 5.5 b4560be48b fix(files): S-6 identify goes through the shared TMDB limiter and caps remote queries
- invoke-limits: sharedInvokeLimiter() (one semaphore set per process); agent
  capability routes default to it
- /files/identify: catalog.lookup with includeRemote runs under the shared
  TMDB semaphore + 15s timeout; at most 20 distinct remote queries per
  request, further items fall back to local recall with a warning

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 00:48:45 +08:00
rootandClaude Opus 5.5 17cf3cbf97 fix(files,security): S-1 per-source Move authorization, S-2 session-only annotation, S-5 restore guards
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]>
2026-09-28 00:45:56 +08:00
rootandClaude Opus 5.5 9887d8b311 chore(lint): clear biome warning (unused dt2) and redundant fragment
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 00:45:56 +08:00
rootandClaude Opus 5.5 887843f777 fix(server,test): W6-A stabilize CI — sync lost update, atomic recovery summary, serial PG tests
- 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]>
2026-09-28 00:32:57 +08:00
rootandClaude Opus 5.5 36aa99b8aa feat(files): W5-A2 move/copy via planImport, organize overrides, delete records, download
- /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]>
2026-09-27 23:53:50 +08:00
rootandClaude Opus 5.5 3641ab9573 feat(server,fs): W5-E production fsworker role, settings auto-investigate mode, replace old file to trash_item
- 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]>
2026-09-27 23:50:10 +08:00
rootandClaude Opus 5.5 7aa6cac717 feat(ao,sources): W5-C subscription completion, Slot × unit reservation, deferral, pack file selection, active search
- completion by season episode_total ledger (applySlotEvent verify_passed hook overrides organize's
  complete): unknown total keeps tracking and records suspected-missing; pending never completes;
  once / continuous current-season completion writes subscription_history
- evaluation: missing = release units ∩ episodeRange − covered − in-flight; series slots may hold
  several unit-disjoint in-flight plans; overlapping/busy releases are deferred (slot_deferral) and
  re-evaluated when a plan ends (next_evaluate_at due) or on the periodic slot re-evaluation
- per-unit upgrade via slot_unit ledger; site scope (params.siteIds); site facts in evaluation context
- season packs: after submit read qB files and setFilePriority(0) for covered/in-flight episodes;
  whole pack with recorded reason when undeterminable; removed downloads release their reservation
- searchSubscription task (title/aliases × enabled sites via liveSearch, snapshot + rate limiter):
  on create, every searchIntervalHours (default 24h) and POST /subscriptions/{id}/search
- refreshMetadata writes season totals (only grows, floored by covered, lock respected) and turns
  metadata_pending subscriptions active; plan loop refreshes tracked series every 6h
- endpoints: subscription progress, episode-total, subscription-history; e9 continuous-series e2e

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 23:34:07 +08:00
rootandClaude Opus 5.5 2e2d56184c feat(fs): W4-E4 fsproxy hang isolation, partial sweep, replace-with-verify, TIMEOUT retry
- 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]>
2026-09-27 23:15:05 +08:00
rootandClaude Opus 5.5 29cd10b27e feat(server): W5-A file manager module (/files)
Roots/list/stat with inline business status, path guard (realpath within
registered roots, symlink escape, offline root 503), mkdir/rename syncing
asset_location and path_binding revisions, two-phase move/copy/organize
(planId + source facts), trash with reference checks and purgeTrash task,
identify/probe/scrape, audit + idempotency keys. Runbook docs/runbook/files.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 23:02:20 +08:00
rootandClaude Opus 5.5 ceff9cf51f feat(ao): W4-E3 manual organize guard, series completion, engine retry, v0.1.0
- 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]>
2026-09-27 22:47:11 +08:00
rootandClaude Opus 5.5 86af723b2a feat(agent): W4-E2 in-grant auto recovery for decisions with verified outcome
- settings.agent.autoInvestigateMode investigate|recover, investigateMaxAttempts, recoverVerifySec
- new capability download.recheck (execute, replay never): qB force recheck / resume, no source swap/delete
- autoInvestigate moves to agent/investigate.ts: recover mode runs execute with a recovery allowlist
  only when an active grant (published policy if bound) covers the recovery actions; otherwise degrades
  to read-only investigation and leaves suggestion + recheck option on the decision
- runAgent verifies recovery against business facts (download_task/task/source_release) before finalize;
  verified -> decision resolved by system:auto-recover, run summary '已恢复(系统核实)'; otherwise stays
  awaiting_user; per-root-cause attempt cap
- e2e E6: missingFiles -> download_failed -> recheck -> verified; no grant degrades; swap rejected;
  dedupe; attempts exhausted; budget exceeded keeps attention

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:35:03 +08:00
rootandClaude Opus 5.5 f48e1aac96 fix(security): W4-E1 approvals admin-session only, shared redact table, TMDB key redaction, prod mock gate
- POST /approvals and PATCH /approvals/{id} require a console admin session
  (EndpointDef auth:"session"); API keys incl. admin scope get 403, so an
  organize.execute key can no longer approve its own plan (S1)
- tests for single-use approval checks: expired, revision/payload hash
  mismatch, consumed, revoked, pending/rejected, plan mismatch; re-approve
  after plan change succeeds once and cannot be replayed (S4)
- one redact key table in @umt/contracts shared by server logger and agent
  redactForModel: rsskey, torrent_pass, authkey, downhash, c_secure_pass,
  passkey variants, x-api-key, cookie headers, nested objects/arrays (S2)
- TMDB network errors: message and cause redacted via redactUrl (S3)
- web Agent ?mock=1 only in DEV builds or when pi.provider is fake (S7)
- EndpointDef auth annotations for /auth/*, asserted against
  PUBLIC_AUTH_PATHS (S10); doc drift fixes (S11)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:24:44 +08:00
rootandClaude Opus 5.5 3792868816 build(docker): static ffprobe, --allow-run=ffprobe,sha256sum, three supervised processes
entrypoint runs api + worker (io) + worker:agent; compose/runbooks document
WORKER_CONCURRENCY, WORKER_AGENT_CONCURRENCY, ORGANIZE_WALK_MAX_FILES.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:15:43 +08:00
rootandClaude Opus 5.5 f6457a29db feat(settings): agent.sessionDir setting shown on settings page
null keeps the deployment default (AGENT_SESSION_DIR / DATA_DIR/agent-sessions).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:15:43 +08:00
rootandClaude Opus 5.5 f9c20c67dc perf(fs): 1MiB stream chunks, full sha256 via sha256sum subprocess, walk cap
- copy streams use 1MiB highWaterMark
- forced full sha256 uses Deno.Command(sha256sum), falls back to in-process stream;
  method recorded as hashVia and counted
- per-file organize.transfer timing log
- ORGANIZE_WALK_MAX_FILES caps manual path walks; dryRun returns truncated

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:12:16 +08:00
rootandClaude Opus 5.5 473c5e94a5 feat(server): io/agent worker groups, invoke limits for external IO capabilities, loop lag probe
- WORKER_CONCURRENCY / WORKER_AGENT_CONCURRENCY; --role worker (io), worker:agent, all (both)
- HTTP invoke of release.search/inspect, identity.resolve, catalog.lookup(remote):
  15s timeout + per-upstream semaphore (site 2, TMDB 4)
- worker event-loop lag probe: warn + count when drift > 5s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:12:16 +08:00
rootandClaude Opus 5.5 fe9693d4ca refactor(ao): background loops only orchestrate; cleanup/reconcile endpoints enqueue
- separate qB sync timer from the planning timer
- threshold cleanup, reserved-plan and pending-import sweeps, 6h reconcile enqueue
  engine tasks with time-slot idempotency keys instead of running inline
- pending-import sweep selects by completed_at (sync refreshes updated_at every round)
- POST /download-cache/cleanup and /library/reconcile return {taskId, runId} (202);
  web jumps to the engine run page
- test: a 50MB verifyAndImport in progress does not delay qB sync

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 22:12:16 +08:00
rootandClaude Opus 5.5 9e6ff33ef4 test(server): wait for engine run completion in E3 instead of asserting immediately
The business row settles before the engine marks the workflow completed; under CI load the
window is a few hundred ms and the direct assertion flaked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 21:46:37 +08:00
rootandClaude Opus 5.5 34e8751b45 feat(ops): W4-B single-container packaging, DATA_DIR, backup/restore
- DATA_DIR (/data: agent-sessions, backups, exports, tmp); entrypoint chowns it
  to PUID/PGID then drops privileges via gosu; /app binary stays root read-only
- compile with --allow-read --allow-write so mounted library roots are writable
- preflight: DATA_DIR/session dir must be writable (fatal); enabled library
  roots and downloader path mappings checked read/write (warn + audit)
- --role export-mappings (mapping manifest JSON, --compare row counts) and
  --role read-only-on (automation.readOnlyReconcile=true + audit)
- scripts/backup.sh (pg_dump custom + mappings + agent sessions, SHA256SUMS,
  BACKUP_KEEP rotation) and scripts/restore.sh (checksum, refuses with live
  connections, psql single-transaction replay compatible with PG16, read-only
  reconcile on, row-count verification)
- compose: app + postgres only, data volume, env example with key generation
- docs: runbook deploy.md, backup-restore.md with verification record

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 21:27:55 +08:00
rootandClaude Opus 5.5 d3ab54f2bd feat(server): W4-A integration endpoints and contract gaps
- POST /acquisition-plans: manual submit via download.plan -> download.submit
  (same authorization/audit/idempotency path as the agent), slotId or entityId
- read-only /media-assets, /asset-locations, /asset-unit-coverage, /path-bindings,
  /reconcile-runs, /server-mappings, /artifacts, GET /title-aliases/{id};
  unimplemented write endpoints removed from api.ts; registry test now checks both directions
- candidate evaluation upsert per (slot, source_release) + migration 0022 dedupe/unique index
- organization records expose downloadTaskId (+ list filter); inventory adds
  subscribed/candidateReasons; feed sources expose derived possibleGap
- GET /events?after=&limit= JSON replay page for API key callers (same sse_event table)
- preference tag revision history from audit_event; subscriptions/identities/aliases
  lists carry mediaEntityTitle; subscription q/mediaKind search; slot subscriptionId
  filter + currentAsset summary; policy preview samples carry title/reasons
- DecisionSummary, IdentityRevisionList, ReleaseIdentityResult, PolicyPreviewSummary exported from contracts

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 21:10:02 +08:00
rootandClaude Opus 5.5 6280d52ec3 feat(server): W2-D agent assembly, session API, runAgent/resolveDecision and backend e2e
- Capabilities: all 14 registered (AO: inventory.query, download.plan/submit, organize.plan/execute,
  policy.preview/apply; agent: task.inspect/retry/cancel); startup asserts count == 14.
  Registry injects server-side authorize (automation_grant by action/scope/policy version/budget,
  one-shot approval bound to plan id + payload hash + revision + expiry; model claims ignored;
  automation switch), audit_event, server idempotency keys and a capability_invocation op ledger
  (migration 0021) used for crash reconciliation. download.plan creates draft plans only;
  download.submit promotes draft -> reserved after authorization.
- Agent session API: POST/GET /agent/runs, GET /agent/runs/{id} (events by seq, messages, report),
  POST .../messages (wake), POST .../abort; automation-grants, approvals, capabilities invoke, tasks.
- runAgent task: Pi or fake provider from settings (apiKey via secrets), events persisted to
  agent_run_event then published, resume + ledger reconciliation when pendingOperations > 0,
  waits for goal-linked downloads/imports and reports from business tables (submitted != imported).
- Background triggers: decision.created -> bounded plan_only investigation (dedupe per decision);
  decision.updated(resolved) -> resolveDecision executes chosen option.
- W2 closure: all OpenAPI routes registered in contracts api.ts (+ subset test); TMDB key from
  settings secret_ref with env fallback; PUT /sites/{id}/credentials documented as alias of
  /site-accounts; completion events documented as existing status events.
- e2e (real PG + OpenWorkflow + testkit mocks): RSS->library, agent delegation with grant/deny,
  background continuation, worker kill -9 recovery (single qB add), auto investigation dedupe.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 20:52:12 +08:00
rootandClaude Opus 5.5 a24bda952b feat(server): W2-B sources, identity resolution, metadata and site workbench
- Sites / accounts / feeds: registry view (/site-definitions), site CRUD with
  enable/disable (history stays queryable, marked site_disabled), accounts and
  feed URLs stored as secrets (only secret_ref / redacted urlDisplay exposed),
  feed enable/disable/reset/fetch; one shared rate limiter + adapter per site.
- Tasks: ingestFeed (FeedFetchState/FeedCursor on feed_source, observation
  feed+guid upsert with digest-change updates, source_release site+id
  aggregation across feeds, auth failure -> account invalid + decision, no
  retry, gap -> feed.gap_suspected -> budgeted backfill snapshot),
  resolveObservation (parseRelease -> local recall via title_alias pg_trgm +
  accepted identities -> TMDB -> resolveIdentity; auto binds entity /
  provisional units / identities / aliases, otherwise identity_candidate pool
  with evidence version and bounded review backoff; manual lock respected),
  refreshMetadata (metadata_field + field_override, user locks win, paths
  untouched), snapshotSite (site_snapshot coverage records).
- Events: feed.due, feed.gap_suspected, release.observed (self-consumed),
  release.resolved, decision.created/updated; scheduler in worker start().
- Identity API: entity detail with redirects, manual mapping correction with
  identity_revision, merge with identity_redirect, pool list/assign/dismiss/
  retry, content unit numbering lock, aliases, external identities.
- Decisions: list/summary (by entity and kind)/detail/resolve; identity
  options executed directly.
- Workbench: /sites/{id}/releases (snapshot first, coalesced live search with
  backoff, origin + freshness) and /sites/{id}/releases/{rid} (volatile field
  recheck times, refresh keeps data on failure).
- Capabilities: catalog.lookup, release.search, release.inspect,
  identity.resolve.
- Migrations 0018-0020: site.adapter api, source_release.resolution_locked,
  site_snapshot, identity_candidate, resolution_attempt, identity_redirect,
  metadata_field, field_override.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 20:12:01 +08:00
rootandClaude Opus 5.5 829ca4f7ac feat(server): W2-C rules, downloads, import, organization records, library and dashboard
- Rules API: subscriptions, acquisition slots (revision lock), policy revisions
  (draft -> publish = automation_grant, rollback as new revision), preference
  tags with suppression records (agent cannot re-add suppressed fingerprints).
- Tasks: evaluateAcquisition (mode/evaluate/budget/cache volume -> frozen plan
  with slot reservation, candidate reasons as evidence artifacts),
  submitDownload (withAttemptToken fencing, already_exists adoption),
  verifyAndImport (frozen organization_record + file results, resumable
  copy/move/cross-volume steps, assets/locations/bindings in one tx),
  cleanupDownloadCache (verified+retention+H&R only, assertSafeToDeleteData),
  reconcileLibrary (root sentinel, suspected -> confirmed missing).
- Worker loop: qB sync with tag adoption, no-progress evidence, hard threshold
  pause / resume-watermark resume, sweepers for deferred plans and imports.
- Public organization records API incl. changes cursor (change_xid guard),
  media library list/detail, inventory overlay, Emby inventory, dashboard.
- Contracts: SlotTemplate, AcquisitionPolicyRules, CandidateEvaluation,
  library/inventory/dashboard read models.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 20:08:47 +08:00
rootandClaude Opus 5.5 1cd523d3e6 feat(server): wire PgEventBus and event-driven settings cache into process assembly
main.ts starts the PG-backed SSE bus (api/worker processes now see each other's events)
and stops it on shutdown; bootstrap passes ctx.events to the settings reader so
settings.changed invalidates every process's cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:59:54 +08:00
rootandClaude Opus 5.5 ff230e5da7 feat(server): W2-A login gate, secrets, settings, instances, audit, engine panel API and PG-backed SSE bus
- Admin login gate (docs/design/auth-reference.md): admin_user/admin_session/login_attempt
  (migration 0015), one-time setup token logged once + conditional UPDATE, argon2id via
  hash-wasm (WASM; compiled binary has no --allow-ffi), sha256-stored sessions in HttpOnly
  SameSite=Lax cookie umt_session (fixed 12h TTL, soft revoke), HMAC-derived double-submit
  CSRF, IP (5) + account (15) failure limits per 15 min, password change revokes other
  sessions, optional TOTP with replay counter, AuditAction enum.
- Unified auth middleware: cookie session or Bearer API key -> c.var.actor
  {type: admin|api_key, id, name, scopes}; requireScope works for both; requireAdminSession.
- ctx.secrets: AES-256-GCM, per-secret HKDF-SHA256 key from SECRET_MASTER_KEY, AAD bound
  to (id, purpose); ctx.settings: 30s cache invalidated by settings.changed events.
- Endpoints: /auth/*, /settings, /api-keys, /secrets, /audit-events, library-roots,
  downloader/media-server instances (+test), path-mappings (+verify),
  PUT /sites/{id}/credentials, /engine/runs|stats|cancel (read-only openworkflow schema).
- SSE: PgEventBus (sse_event table, migration 0016, LISTEN/NOTIFY umt_events, catch-up by
  seq, pruning, resync); EventBusPort is now async.
- Tests: login/rate limit/revocation/CSRF/TOTP, secrets round trip and no log leakage,
  admin APIs against mock qB/Emby, SSE across two Deno subprocesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:59:54 +08:00
rootandClaude Opus 5.5 9858b8494d feat(server): ServerModule skeleton with shared ServerContext, worker runs engine + outbox relay (B6)
modules/{auth-settings,sources-identity,acquisition-organize,agent} placeholders listed in
SERVER_MODULES; ServerModule registers routes (under /api/v1, authenticated), tasks,
outbox routes, capabilities and background loops. bootstrap() builds ServerContext (db,
config, logger, events, engine, outbox writer, secrets placeholder, settings reader,
capability registry), fills unregistered tasks with stubs and rejects duplicate task or
outbox ownership. worker starts the OpenWorkflow worker, OutboxRelay and module start().
--role migrate also migrates the engine schema; api/worker connect without migrating.
tasks: TaskRegistry.has, migrateEngineSchema.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:30:55 +08:00
rootandClaude Fable 5.1 304feea141 chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)
- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock;
  remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports)
- tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions),
  test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types
- tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl
- server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting;
  drop @hono/node-server, tsx, esbuild build.mjs
- logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests
- relative imports get explicit extensions (sites/mp-sync paths left for follow-up)
- crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas
- single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml
- docs: README, runbook, ADR-001 superseded, plan §1 validation row

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:53:06 +08:00
umtandClaude Fable 5.1 b22c440139 chore: scaffold monorepo and migrate uhd_web design system (W0-A)
- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 17:36:26 +08:00