diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 76f66ca5..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "SeedPass", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/torch/eslint.config.mjs b/torch/eslint.config.mjs deleted file mode 100644 index e65162b3..00000000 --- a/torch/eslint.config.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import js from '@eslint/js'; -import globals from 'globals'; - -export default [ - js.configs.recommended, - { - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - ...globals.node, - ...globals.es2021, - }, - }, - rules: { - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'no-undef': 'error', - }, - ignores: ['node_modules/', 'task-logs/', 'package.json', 'package-lock.json', 'torch-config.json'], - }, -]; diff --git a/torch/package-lock.json b/torch/package-lock.json deleted file mode 100644 index 195a5ba0..00000000 --- a/torch/package-lock.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "name": "torch", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "torch-lock": "https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz" - } - }, - "node_modules/@noble/ciphers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@scure/bip32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.1.0", - "@noble/hashes": "~1.3.1", - "@scure/base": "~1.1.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.1" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nostr-tools": { - "version": "2.19.4", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.4.tgz", - "integrity": "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==", - "license": "Unlicense", - "dependencies": { - "@noble/ciphers": "^0.5.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.1", - "@scure/base": "1.1.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1", - "nostr-wasm": "0.1.0" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/nostr-wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", - "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", - "license": "MIT" - }, - "node_modules/torch-lock": { - "version": "0.1.0", - "resolved": "https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz", - "integrity": "sha512-DiVKNkw/zAdIet5W5l9URVyCZwYRGRe1ji2WmkyG5JekIwG2AsM+WpuL1KBFINnsQx5LAJvUo8gonSBVC/k7VA==", - "dependencies": { - "nostr-tools": "2.19.4", - "ws": "8.19.0" - }, - "bin": { - "torch-lock": "bin/torch-lock.mjs" - } - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/torch/package.json b/torch/package.json deleted file mode 100644 index a9f29a65..00000000 --- a/torch/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "torch-lock": "https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz" - } -} diff --git a/torch/src/cli-parser.mjs b/torch/src/cli-parser.mjs deleted file mode 100644 index 21e20b53..00000000 --- a/torch/src/cli-parser.mjs +++ /dev/null @@ -1,137 +0,0 @@ -import { DEFAULT_DASHBOARD_PORT } from './constants.mjs'; - -/** - * Parses command-line arguments into a structured object. - * - * @param {string[]} argv - Arguments from process.argv.slice(2) - * @returns {Object} - Parsed arguments (command, flags, values) - */ -export function parseArgs(argv) { - const args = { - command: null, - agent: null, - cadence: null, - json: false, - jsonFile: null, - quiet: false, - dryRun: false, - force: false, - port: DEFAULT_DASHBOARD_PORT, - host: null, - logDir: 'task-logs', - ignoreLogs: false, - id: null, - type: null, - tags: [], - pinned: null, - limit: null, - offset: null, - retentionMs: null, - windowMs: null, - timeoutMs: null, - allRelaysDownMinutes: null, - minSuccessRate: null, - windowMinutes: null, - platform: null, - model: null, - }; - let i = 0; - - if (argv.length > 0 && !argv[0].startsWith('-')) { - args.command = argv[0]; - i = 1; - } - - for (; i < argv.length; i++) { - const arg = argv[i]; - if (arg === '--agent' || arg === '-a') { - args.agent = argv[++i]; - } else if (arg === '--cadence' || arg === '-c') { - args.cadence = argv[++i]; - } else if (arg === '--json') { - args.json = true; - } else if (arg === '--json-file') { - args.jsonFile = argv[++i]; - } else if (arg.startsWith('--json-file=')) { - args.jsonFile = arg.split('=')[1]; - } else if (arg === '--quiet') { - args.quiet = true; - } else if (arg.startsWith('--agent=')) { - args.agent = arg.split('=')[1]; - } else if (arg.startsWith('--cadence=')) { - args.cadence = arg.split('=')[1]; - } else if (arg === '--dry-run') { - args.dryRun = true; - } else if (arg === '--force') { - args.force = true; - } else if (arg === '--port') { - args.port = parseInt(argv[++i], 10) || DEFAULT_DASHBOARD_PORT; - } else if (arg === '--host') { - args.host = argv[++i]; - } else if (arg === '--log-dir') { - args.logDir = argv[++i]; - } else if (arg === '--ignore-logs') { - args.ignoreLogs = true; - } else if (arg === '--id') { - args.id = argv[++i]; - } else if (arg.startsWith('--id=')) { - args.id = arg.split('=')[1]; - } else if (arg === '--type') { - args.type = argv[++i]; - } else if (arg.startsWith('--type=')) { - args.type = arg.split('=')[1]; - } else if (arg === '--tags') { - args.tags = String(argv[++i]).split(',').map((tag) => tag.trim()).filter(Boolean); - } else if (arg.startsWith('--tags=')) { - args.tags = String(arg.split('=')[1]).split(',').map((tag) => tag.trim()).filter(Boolean); - } else if (arg === '--pinned') { - const value = String(argv[++i]).toLowerCase(); - args.pinned = value === 'true' ? true : value === 'false' ? false : null; - } else if (arg.startsWith('--pinned=')) { - const value = String(arg.split('=')[1]).toLowerCase(); - args.pinned = value === 'true' ? true : value === 'false' ? false : null; - } else if (arg === '--limit') { - args.limit = parseInt(argv[++i], 10); - } else if (arg.startsWith('--limit=')) { - args.limit = parseInt(arg.split('=')[1], 10); - } else if (arg === '--offset') { - args.offset = parseInt(argv[++i], 10); - } else if (arg.startsWith('--offset=')) { - args.offset = parseInt(arg.split('=')[1], 10); - } else if (arg === '--retention-ms') { - args.retentionMs = parseInt(argv[++i], 10); - } else if (arg.startsWith('--retention-ms=')) { - args.retentionMs = parseInt(arg.split('=')[1], 10); - } else if (arg === '--window-ms') { - args.windowMs = parseInt(argv[++i], 10); - } else if (arg.startsWith('--window-ms=')) { - args.windowMs = parseInt(arg.split('=')[1], 10); - } else if (arg === '--timeout-ms') { - args.timeoutMs = parseInt(argv[++i], 10); - } else if (arg.startsWith('--timeout-ms=')) { - args.timeoutMs = parseInt(arg.split('=')[1], 10); - } else if (arg === '--all-relays-down-minutes') { - args.allRelaysDownMinutes = parseInt(argv[++i], 10); - } else if (arg.startsWith('--all-relays-down-minutes=')) { - args.allRelaysDownMinutes = parseInt(arg.split('=')[1], 10); - } else if (arg === '--min-success-rate') { - args.minSuccessRate = parseFloat(argv[++i]); - } else if (arg.startsWith('--min-success-rate=')) { - args.minSuccessRate = parseFloat(arg.split('=')[1]); - } else if (arg === '--window-minutes') { - args.windowMinutes = parseInt(argv[++i], 10); - } else if (arg.startsWith('--window-minutes=')) { - args.windowMinutes = parseInt(arg.split('=')[1], 10); - } else if (arg === '--platform') { - args.platform = argv[++i]; - } else if (arg.startsWith('--platform=')) { - args.platform = arg.split('=')[1]; - } else if (arg === '--model') { - args.model = argv[++i]; - } else if (arg.startsWith('--model=')) { - args.model = arg.split('=')[1]; - } - } - - return args; -} diff --git a/torch/src/cmd-check.mjs b/torch/src/cmd-check.mjs deleted file mode 100644 index a0d16f4f..00000000 --- a/torch/src/cmd-check.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import { - loadTorchConfig, - getRelays, - getNamespace, -} from './torch-config.mjs'; -import { getRoster } from './roster.mjs'; -import { queryLocks } from './lock-ops.mjs'; -import { todayDateStr } from './utils.mjs'; - -export async function cmdCheck(cadence, deps = {}) { - const { - loadTorchConfigFn = loadTorchConfig, - getRelaysFn = getRelays, - getNamespaceFn = getNamespace, - getRosterFn = getRoster, - queryLocksFn = queryLocks, - todayDateStrFn = todayDateStr, - } = deps; - - const relays = await getRelaysFn(); - const namespace = await getNamespaceFn(); - const dateStr = todayDateStrFn(); - const config = await loadTorchConfigFn(); - const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || []; - - console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`); - console.error(`Relays: ${relays.join(', ')}`); - if (pausedAgents.length > 0) { - console.error(`Paused agents: ${pausedAgents.join(', ')}`); - } - - const locks = await queryLocksFn(relays, cadence, dateStr, namespace); - const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))]; - const roster = await getRosterFn(cadence); - - const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])]; - const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent)); - const available = roster.filter((a) => !excludedAgents.includes(a)); - - const result = { - namespace, - cadence, - date: dateStr, - locked: lockedAgents.sort(), - paused: pausedAgents.sort(), - excluded: excludedAgents.sort(), - available: available.sort(), - lockCount: locks.length, - unknownLockedAgents: unknownLockedAgents.sort(), - locks: locks.map((l) => ({ - agent: l.agent, - eventId: l.eventId, - createdAt: l.createdAtIso, - expiresAt: l.expiresAtIso, - platform: l.platform, - })), - }; - - console.log(JSON.stringify(result, null, 2)); - return result; -} diff --git a/torch/src/cmd-list.mjs b/torch/src/cmd-list.mjs deleted file mode 100644 index 65a9166e..00000000 --- a/torch/src/cmd-list.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { - getRelays, - getNamespace, -} from './torch-config.mjs'; -import { getRoster } from './roster.mjs'; -import { queryLocks } from './lock-ops.mjs'; -import { todayDateStr, nowUnix } from './utils.mjs'; - -export async function cmdList(cadence) { - const relays = await getRelays(); - const namespace = await getNamespace(); - const dateStr = todayDateStr(); - const cadences = cadence ? [cadence] : ['daily', 'weekly']; - - console.error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`); - - const results = await Promise.all( - cadences.map(async (c) => { - const locks = await queryLocks(relays, c, dateStr, namespace); - return { c, locks }; - }), - ); - - for (const { c, locks } of results) { - console.log(`\n${'='.repeat(72)}`); - console.log(`Active ${namespace} ${c} locks (${dateStr})`); - console.log('='.repeat(72)); - - if (locks.length === 0) { - console.log(' (no active locks)'); - continue; - } - - const sorted = locks.sort((a, b) => a.createdAt - b.createdAt); - for (const lock of sorted) { - const age = nowUnix() - lock.createdAt; - const ageMin = Math.round(age / 60); - const remaining = lock.expiresAt ? lock.expiresAt - nowUnix() : null; - const remainMin = remaining ? Math.round(remaining / 60) : '?'; - - console.log( - ` ${(lock.agent ?? 'unknown').padEnd(30)} ` + - `age: ${String(ageMin).padStart(4)}m ` + - `ttl: ${String(remainMin).padStart(4)}m ` + - `platform: ${lock.platform ?? '?'} ` + - `event: ${lock.eventId?.slice(0, 12)}...`, - ); - } - - const roster = await getRoster(c); - const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean)); - const unknownLockedAgents = [...lockedAgents].filter((agent) => !roster.includes(agent)); - const available = roster.filter((a) => !lockedAgents.has(a)); - - if (unknownLockedAgents.length > 0) { - console.log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`); - } - - console.log(`\n Locked: ${lockedAgents.size}/${roster.length}`); - console.log(` Available: ${available.join(', ') || '(none)'}`); - } -} diff --git a/torch/src/cmd-lock.mjs b/torch/src/cmd-lock.mjs deleted file mode 100644 index 195baa56..00000000 --- a/torch/src/cmd-lock.mjs +++ /dev/null @@ -1,122 +0,0 @@ -import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; -import { - getRelays, - getNamespace, - getTtl, -} from './torch-config.mjs'; -import { - RACE_CHECK_DELAY_MS, - KIND_APP_DATA, -} from './constants.mjs'; -import { getRoster } from './roster.mjs'; -import { queryLocks, publishLock } from './lock-ops.mjs'; -import { todayDateStr, nowUnix } from './utils.mjs'; -import { ExitError } from './errors.mjs'; - -export async function cmdLock(agent, cadence, dryRun = false) { - const relays = await getRelays(); - const namespace = await getNamespace(); - const dateStr = todayDateStr(); - const ttl = await getTtl(); - const now = nowUnix(); - const expiresAt = now + ttl; - - console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`); - console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`); - console.error(`Relays: ${relays.join(', ')}`); - - const roster = await getRoster(cadence); - if (!roster.includes(agent)) { - console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`); - console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`); - throw new ExitError(1, 'Agent not in roster'); - } - - console.error('Step 1: Checking for existing locks...'); - const existingLocks = await queryLocks(relays, cadence, dateStr, namespace); - const conflicting = existingLocks.filter((l) => l.agent === agent); - - if (conflicting.length > 0) { - const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0]; - console.error( - `LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` + - `(created ${earliest.createdAtIso}, platform: ${earliest.platform})`, - ); - console.log('LOCK_STATUS=denied'); - console.log('LOCK_REASON=already_locked'); - console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`); - throw new ExitError(3, 'Lock denied'); - } - - console.error('Step 2: Generating ephemeral keypair...'); - const sk = generateSecretKey(); - const pk = getPublicKey(sk); - console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`); - - console.error('Step 3: Building lock event...'); - const event = finalizeEvent( - { - kind: KIND_APP_DATA, - created_at: now, - tags: [ - ['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`], - ['t', `${namespace}-agent-lock`], - ['t', `${namespace}-lock-${cadence}`], - ['t', `${namespace}-lock-${cadence}-${dateStr}`], - ['expiration', String(expiresAt)], - ], - content: JSON.stringify({ - agent, - cadence, - status: 'started', - namespace, - date: dateStr, - platform: process.env.AGENT_PLATFORM || 'unknown', - lockedAt: new Date(now * 1000).toISOString(), - expiresAt: new Date(expiresAt * 1000).toISOString(), - }), - }, - sk, - ); - - console.error(` Event ID: ${event.id}`); - - if (dryRun) { - console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent'); - console.error('RACE CHECK: won (dry run — no real contention possible)'); - } else { - console.error('Step 4: Publishing to relays...'); - await publishLock(relays, event); - - console.error('Step 5: Race check...'); - await new Promise((resolve) => setTimeout(resolve, RACE_CHECK_DELAY_MS)); - - const postLocks = await queryLocks(relays, cadence, dateStr, namespace); - const racingLocks = postLocks - .filter((l) => l.agent === agent) - .sort((a, b) => a.createdAt - b.createdAt); - - if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) { - const winner = racingLocks[0]; - console.error( - `RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`, - ); - console.log('LOCK_STATUS=race_lost'); - console.log('LOCK_REASON=earlier_claim_exists'); - console.log(`LOCK_WINNER_EVENT=${winner.eventId}`); - throw new ExitError(3, 'Race check lost'); - } - - console.error('RACE CHECK: won'); - } - - console.log('LOCK_STATUS=ok'); - console.log(`LOCK_EVENT_ID=${event.id}`); - console.log(`LOCK_PUBKEY=${pk}`); - console.log(`LOCK_AGENT=${agent}`); - console.log(`LOCK_CADENCE=${cadence}`); - console.log(`LOCK_DATE=${dateStr}`); - console.log(`LOCK_EXPIRES=${expiresAt}`); - console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`); - return { status: 'ok', eventId: event.id }; -} diff --git a/torch/src/constants.mjs b/torch/src/constants.mjs deleted file mode 100644 index 8e5a6534..00000000 --- a/torch/src/constants.mjs +++ /dev/null @@ -1,67 +0,0 @@ -export const DEFAULT_DASHBOARD_PORT = 4173; -export const RACE_CHECK_DELAY_MS = 1500; -export const DEFAULT_RELAYS = [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.primal.net', -]; -export const DEFAULT_TTL = 7200; -export const DEFAULT_NAMESPACE = 'torch'; -export const DEFAULT_QUERY_TIMEOUT_MS = 30_000; -export const DEFAULT_PUBLISH_TIMEOUT_MS = 15_000; -export const DEFAULT_MIN_SUCCESSFUL_PUBLISHES = 1; -export const DEFAULT_MIN_ACTIVE_RELAY_POOL = 1; -export const DEFAULT_RETRY_ATTEMPTS = 4; -export const DEFAULT_RETRY_BASE_DELAY_MS = 500; -export const DEFAULT_RETRY_CAP_DELAY_MS = 8000; -export const DEFAULT_ROLLING_WINDOW_SIZE = 25; -export const DEFAULT_FAILURE_THRESHOLD = 3; -export const DEFAULT_QUARANTINE_COOLDOWN_MS = 30_000; -export const DEFAULT_MAX_QUARANTINE_COOLDOWN_MS = 300_000; -export const DEFAULT_SNAPSHOT_INTERVAL_MS = 60_000; -export const VALID_CADENCES = new Set(['daily', 'weekly']); -export const KIND_APP_DATA = 30078; - -export const USAGE_TEXT = `Usage: torch-lock [options] - -Commands: - check --cadence Check locked agents (JSON) - lock --agent --cadence Claim a lock - complete --agent --cadence Mark task as completed (permanent) - list [--cadence ] Print active lock table - health --cadence Probe relay websocket + publish/read health - dashboard [--port ] [--host ] Serve the dashboard (default: ${DEFAULT_DASHBOARD_PORT}) - init [--force] Initialize torch/ directory in current project - update [--force] Update torch/ configuration (backups, merges) - - list-memories [--agent ] [--type ] [--tags ] [--pinned ] [--full] - inspect-memory --id - pin-memory --id - unpin-memory --id - trigger-prune-dry-run [--retention-ms ] - memory-stats [--window-ms ] - -Options: - --dry-run Build and sign the event but do not publish - --force Overwrite existing files (for init) or all files (for update) - --log-dir Path to task logs directory (default: task-logs) - --ignore-logs Skip checking local logs for completed tasks - --json Emit compact single-line JSON - --json-file Write JSON output to a file path - --quiet Suppress stderr progress logs (pairs well with --json) - -Environment: - NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch) - NOSTR_LOCK_RELAYS Comma-separated relay WSS URLs - NOSTR_LOCK_TTL Lock TTL in seconds (default: 7200) - NOSTR_LOCK_QUERY_TIMEOUT_MS Relay query timeout in milliseconds (default: 15000) - NOSTR_LOCK_DAILY_ROSTER Comma-separated daily roster (optional) - NOSTR_LOCK_WEEKLY_ROSTER Comma-separated weekly roster (optional) - TORCH_CONFIG_PATH Optional path to torch-config.json (default: ./torch-config.json) - AGENT_PLATFORM Platform identifier (e.g., codex) - -Exit codes: - 0 Success - 1 Usage error - 2 Relay/network error - 3 Lock denied (already locked or race lost)`; diff --git a/torch/src/dashboard.mjs b/torch/src/dashboard.mjs deleted file mode 100644 index 740357ac..00000000 --- a/torch/src/dashboard.mjs +++ /dev/null @@ -1,210 +0,0 @@ -import fs from 'node:fs'; -import fsp from 'node:fs/promises'; -import path from 'node:path'; -import http from 'node:http'; -import crypto from 'node:crypto'; -import { fileURLToPath } from 'node:url'; -import { DEFAULT_DASHBOARD_PORT } from './constants.mjs'; -import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs'; - -function timingSafeCompare(a, b) { - const bufA = Buffer.from(a); - const bufB = Buffer.from(b); - if (bufA.length !== bufB.length) { - // Still do a comparison to avoid some timing leaks, though length leak is hard to avoid entirely - crypto.timingSafeEqual(bufA, bufA); - return false; - } - return crypto.timingSafeEqual(bufA, bufB); -} - -export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.0.1') { - // Resolve package root relative to this file (src/dashboard.mjs) - // this file is in /src/dashboard.mjs, so '..' goes to - const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); - const auth = await getDashboardAuth(); - - const server = http.createServer(async (req, res) => { - try { - // Basic Auth check - if (auth) { - const authHeader = req.headers.authorization; - if (!authHeader) { - res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' }); - res.end('Authentication required'); - return; - } - const parts = authHeader.split(' '); - const type = parts[0]; - const credentials = parts[1]; - let isValid = false; - if (type === 'Basic' && credentials) { - try { - const decoded = Buffer.from(credentials, 'base64').toString(); - isValid = timingSafeCompare(decoded, auth); - } catch { - isValid = false; - } - } - - if (!isValid) { - res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' }); - res.end('Invalid credentials'); - return; - } - } - - // URL parsing - const url = new URL(req.url, `http://${req.headers.host}`); - let pathname = url.pathname; - - // Redirect / to /dashboard/ - if (pathname === '/' || pathname === '/dashboard') { - res.writeHead(302, { 'Location': '/dashboard/' }); - res.end(); - return; - } - - async function statSafe(p) { - try { - return await fsp.stat(p); - } catch { - return null; - } - } - - // Special case: /torch-config.json - // Priority: Env Var > User's CWD > Package default - if (pathname === '/torch-config.json') { - let configContent = null; - - if (process.env.TORCH_CONFIG_PATH) { - const envPath = path.resolve(process.cwd(), process.env.TORCH_CONFIG_PATH); - try { - configContent = await fsp.readFile(envPath, 'utf8'); - } catch { - // Explicitly provided config path not found, do not fall back - } - } else { - const userConfigPath = path.resolve(process.cwd(), 'torch-config.json'); - try { - configContent = await fsp.readFile(userConfigPath, 'utf8'); - } catch { - // Try package default - const packageConfigPath = path.join(packageRoot, 'torch-config.json'); - try { - configContent = await fsp.readFile(packageConfigPath, 'utf8'); - } catch { - // Not found in either location - } - } - } - - if (configContent) { - try { - const rawConfig = JSON.parse(configContent); - const safeConfig = parseTorchConfig(rawConfig); - - // Security: Sanitize sensitive fields - if (safeConfig.dashboard) { - delete safeConfig.dashboard.auth; - } - delete safeConfig.configPath; - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(safeConfig, null, 2)); - return; - } catch (err) { - console.error('Error parsing torch-config.json:', err); - res.writeHead(500); - res.end('Internal Server Error'); - return; - } - } else { - res.writeHead(404); - res.end('Not Found'); - return; - } - } - - // Security check: prevent directory traversal - const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), ''); - let filePath = path.join(packageRoot, safePath); - - // Security check: restrict access to allowed paths - const allowedPaths = [ - path.join(packageRoot, 'dashboard'), - path.join(packageRoot, 'assets'), - path.join(packageRoot, 'src', 'docs'), - path.join(packageRoot, 'src', 'constants.mjs'), - path.join(packageRoot, 'torch-config.json') - ]; - - const isAllowed = allowedPaths.some(allowedPath => { - const rel = path.relative(allowedPath, filePath); - return !rel.startsWith('..') && !path.isAbsolute(rel); - }); - - if (!isAllowed) { - res.writeHead(403); - res.end('Forbidden'); - return; - } - - // If directory, try index.html - let fileStat = await statSafe(filePath); - if (fileStat && fileStat.isDirectory()) { - filePath = path.join(filePath, 'index.html'); - fileStat = await statSafe(filePath); - } - - // Check if file exists and is a file - if (!fileStat || !fileStat.isFile()) { - res.writeHead(404); - res.end('Not Found'); - return; - } - - // MIME types - const extname = path.extname(filePath); - let contentType = 'text/plain'; - switch (extname) { - case '.html': contentType = 'text/html'; break; - case '.js': contentType = 'text/javascript'; break; - case '.mjs': contentType = 'text/javascript'; break; - case '.css': contentType = 'text/css'; break; - case '.json': contentType = 'application/json'; break; - case '.png': contentType = 'image/png'; break; - case '.jpg': contentType = 'image/jpeg'; break; - case '.svg': contentType = 'image/svg+xml'; break; - case '.ico': contentType = 'image/x-icon'; break; - case '.md': contentType = 'text/markdown'; break; - } - - res.writeHead(200, { 'Content-Type': contentType }); - fs.createReadStream(filePath).pipe(res); - } catch (err) { - console.error('Dashboard Server Error:', err); - if (!res.headersSent) { - res.writeHead(500); - res.end('Internal Server Error'); - } - } - }); - - return new Promise((resolve, reject) => { - server.listen(port, host, () => { - const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`; - console.log(`Dashboard running at ${listenUrl}`); - if (auth) { - console.log('Authentication: enabled (Basic Auth)'); - } else { - console.warn('Authentication: DISABLED (Dashboard is public)'); - } - console.log(`Serving files from ${packageRoot}`); - console.log(`Using configuration from ${process.cwd()}`); - resolve(server); - }); - server.on('error', reject); - }); -} diff --git a/torch/src/errors.mjs b/torch/src/errors.mjs deleted file mode 100644 index c997a9f8..00000000 --- a/torch/src/errors.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// Custom Error class for controlled exits -export class ExitError extends Error { - constructor(code, message) { - super(message); - this.code = code; - } -} diff --git a/torch/src/lib.mjs b/torch/src/lib.mjs deleted file mode 100644 index ec97d947..00000000 --- a/torch/src/lib.mjs +++ /dev/null @@ -1,684 +0,0 @@ -#!/usr/bin/env node - -// TORCH — Task Orchestration via Relay-Coordinated Handoff -// Generic Nostr-based task locking for multi-agent development. - -import { generateSecretKey as _generateSecretKey, getPublicKey as _getPublicKey, finalizeEvent as _finalizeEvent } from 'nostr-tools/pure'; -import { useWebSocketImplementation } from 'nostr-tools/relay'; -import WebSocket from 'ws'; -import { - loadTorchConfig as _loadTorchConfig, - getRelays as _getRelays, - getNamespace as _getNamespace, - getTtl as _getTtl, - getHashtag as _getHashtag, -} from './torch-config.mjs'; -import { - VALID_CADENCES, - KIND_APP_DATA, - RACE_CHECK_DELAY_MS, - USAGE_TEXT, -} from './constants.mjs'; -import { cmdInit, cmdUpdate } from './ops.mjs'; -import { parseArgs } from './cli-parser.mjs'; -import { getRoster as _getRoster } from './roster.mjs'; -import { queryLocks as _queryLocks, publishLock as _publishLock, parseLockEvent } from './lock-ops.mjs'; -import { cmdDashboard } from './dashboard.mjs'; -import { - inspectMemory, - listMemories, - memoryStats, - pinMemory, - triggerPruneDryRun, - unpinMemory, -} from './services/memory/index.js'; -import { ExitError } from './errors.mjs'; -import { todayDateStr, nowUnix, detectPlatform } from './utils.mjs'; -import { runRelayHealthCheck } from './relay-health.mjs'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { getCompletedAgents } from './lock-utils.mjs'; - -useWebSocketImplementation(WebSocket); - -// Re-export for backward compatibility/library usage -export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock }; - -/** - * Checks the status of the repository locks for a given cadence. - * - * It aggregates data from three sources: - * 1. Configuration (paused agents) - * 2. Local logs (completed agents) - * 3. Nostr relays (current active locks) - * - * It outputs a JSON object describing the state (locked, available, excluded agents). - * - * @param {string} cadence - 'daily' or 'weekly' - * @param {Object} [deps] - Dependency injection - * @returns {Promise} - The check result object - */ -export async function cmdCheck(cadence, deps = {}) { - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - loadTorchConfig = _loadTorchConfig, - queryLocks = _queryLocks, - getRoster = _getRoster, - getDateStr = todayDateStr, - log = console.log, - error = console.error, - logDir = 'task-logs', - ignoreLogs = false, - json = false, - jsonFile = null, - quiet = false, - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const dateStr = getDateStr(); - const config = await loadTorchConfig(); - const pausedAgents = config.scheduler.paused[cadence] || []; - - if (!quiet) { - error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`); - error(`Relays: ${relays.join(', ')}`); - if (pausedAgents.length > 0) { - error(`Paused agents: ${pausedAgents.join(', ')}`); - } - } - - let completedAgents = new Set(); - if (!ignoreLogs) { - completedAgents = await getCompletedAgents(cadence, logDir, deps); - if (!quiet && completedAgents.size > 0) { - error(`Completed agents (logs): ${[...completedAgents].join(', ')}`); - } - } - - const locks = await queryLocks(relays, cadence, dateStr, namespace); - const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))]; - const roster = await getRoster(cadence); - const rosterSet = new Set(roster); - - const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents, ...completedAgents]); - const excludedAgents = [...excludedAgentsSet]; - const unknownLockedAgents = lockedAgents.filter((agent) => !rosterSet.has(agent)); - const available = roster.filter((a) => !excludedAgentsSet.has(a)); - - const result = { - namespace, - cadence, - date: dateStr, - locked: lockedAgents.sort(), - paused: pausedAgents.sort(), - completed: [...completedAgents].sort(), - excluded: excludedAgents.sort(), - available: available.sort(), - lockCount: locks.length, - unknownLockedAgents: unknownLockedAgents.sort(), - locks: locks.map((l) => ({ - agent: l.agent, - eventId: l.eventId, - createdAt: l.createdAtIso, - expiresAt: l.expiresAtIso, - platform: l.platform, - })), - }; - - const output = json ? JSON.stringify(result) : JSON.stringify(result, null, 2); - - if (jsonFile) { - const resolvedPath = path.resolve(process.cwd(), jsonFile); - await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); - await fs.writeFile(resolvedPath, `${output}\n`, 'utf8'); - } - - if (json || !quiet) { - log(output); - } - - return result; -} - -/** - * Attempts to acquire an exclusive lock for an agent on the specified cadence. - * - * Algorithm: - * 1. Validate agent against the roster. - * 2. Query relays for existing valid locks (checking for conflicts). - * 3. Generate a new ephemeral keypair and build a lock event (kind 30078). - * 4. Publish the lock event to relays. - * 5. Wait for propagation (raceCheckDelayMs) and re-query to confirm no earlier lock won the race. - * - * @param {string} agent - Agent name - * @param {string} cadence - 'daily' or 'weekly' - * @param {boolean} [dryRun=false] - If true, skips publishing - * @param {Object} [deps] - Dependency injection - * @returns {Promise<{status: string, eventId: string}>} - * @throws {ExitError} If lock is denied (already locked, completed, or race lost) - */ -export async function cmdLock(agent, cadence, optionsOrDryRun = false, deps = {}) { - const options = typeof optionsOrDryRun === 'object' ? optionsOrDryRun : { dryRun: !!optionsOrDryRun }; - const { dryRun = false, platform = null, model = null } = options; - - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - getHashtag = _getHashtag, - getTtl = _getTtl, - queryLocks = _queryLocks, - getRoster = _getRoster, - publishLock = _publishLock, - generateSecretKey = _generateSecretKey, - getPublicKey = _getPublicKey, - finalizeEvent = _finalizeEvent, - raceCheckDelayMs = RACE_CHECK_DELAY_MS, - getDateStr = todayDateStr, - log = console.log, - error = console.error - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const hashtag = await getHashtag(); - const dateStr = getDateStr(); - const ttl = await getTtl(); - const now = nowUnix(); - const expiresAt = now + ttl; - - error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`); - error(`Hashtag: #${hashtag}`); - error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`); - error(`Relays: ${relays.join(', ')}`); - - const roster = await getRoster(cadence); - if (!roster.includes(agent)) { - error(`ERROR: agent "${agent}" is not in the ${cadence} roster`); - error(`Allowed ${cadence} agents: ${roster.join(', ')}`); - throw new ExitError(1, 'Agent not in roster'); - } - - error('Step 1: Checking for existing locks...'); - const existingLocks = await queryLocks(relays, cadence, dateStr, namespace); - const conflicting = existingLocks.filter((l) => l.agent === agent); - - if (conflicting.length > 0) { - const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0]; - - // Check if it is a completed task - if (earliest.status === 'completed') { - error(`LOCK DENIED: Task already completed by event ${earliest.eventId}`); - log('LOCK_STATUS=denied'); - log('LOCK_REASON=already_completed'); - throw new ExitError(3, 'Task already completed'); - } - - error( - `LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` + - `(created ${earliest.createdAtIso}, platform: ${earliest.platform})`, - ); - log('LOCK_STATUS=denied'); - log('LOCK_REASON=already_locked'); - log(`LOCK_EXISTING_EVENT=${earliest.eventId}`); - throw new ExitError(3, 'Lock denied'); - } - - error('Step 2: Generating ephemeral keypair...'); - const sk = generateSecretKey(); - const pk = getPublicKey(sk); - error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`); - - error('Step 3: Building lock event...'); - const event = finalizeEvent( - { - kind: KIND_APP_DATA, - created_at: now, - tags: [ - ['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`], - ['t', hashtag], - ['t', `${namespace}-lock-${cadence}`], - ['t', `${namespace}-lock-${cadence}-${dateStr}`], - ['expiration', String(expiresAt)], - ], - content: JSON.stringify({ - agent, - cadence, - status: 'started', - namespace, - date: dateStr, - platform: platform || process.env.AGENT_PLATFORM || detectPlatform() || 'unknown', - model: model || process.env.AGENT_MODEL || 'unknown', - lockedAt: new Date(now * 1000).toISOString(), - expiresAt: new Date(expiresAt * 1000).toISOString(), - }), - }, - sk, - ); - - error(` Event ID: ${event.id}`); - - if (dryRun) { - error('Step 4: [DRY RUN] Skipping publish — event built but not sent'); - error('RACE CHECK: won (dry run — no real contention possible)'); - } else { - error('Step 4: Publishing to relays...'); - await publishLock(relays, event); - - error('Step 5: Race check...'); - await new Promise((resolve) => setTimeout(resolve, raceCheckDelayMs)); - - const postLocks = await queryLocks(relays, cadence, dateStr, namespace); - const racingLocks = postLocks - .filter((l) => l.agent === agent) - .sort((a, b) => (a.createdAt - b.createdAt) || String(a.eventId).localeCompare(String(b.eventId))); - - if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) { - const winner = racingLocks[0]; - error( - `RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`, - ); - log('LOCK_STATUS=race_lost'); - log('LOCK_REASON=earlier_claim_exists'); - log(`LOCK_WINNER_EVENT=${winner.eventId}`); - throw new ExitError(3, 'Race check lost'); - } - - error('RACE CHECK: won'); - } - - log('LOCK_STATUS=ok'); - log(`LOCK_EVENT_ID=${event.id}`); - log(`LOCK_PUBKEY=${pk}`); - log(`LOCK_AGENT=${agent}`); - log(`LOCK_HASHTAG=${hashtag}`); - log(`LOCK_CADENCE=${cadence}`); - log(`LOCK_DATE=${dateStr}`); - log(`LOCK_EXPIRES=${expiresAt}`); - log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`); - return { status: 'ok', eventId: event.id }; -} - -/** - * Lists all active locks for the specified cadence (or all cadences if null). - * It prints a formatted table to stdout with lock age, TTL, and event ID. - * - * @param {string|null} cadence - Filter by cadence ('daily', 'weekly') or null for all - * @param {Object} [deps] - Dependency injection - * @returns {Promise} - */ -export async function cmdList(cadence, deps = {}) { - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - queryLocks = _queryLocks, - getRoster = _getRoster, - getDateStr = todayDateStr, - log = console.log, - error = console.error - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const dateStr = getDateStr(); - const cadences = cadence ? [cadence] : [...VALID_CADENCES]; - - error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`); - - const results = await Promise.all( - cadences.map(async (c) => { - const locks = await queryLocks(relays, c, dateStr, namespace); - return { c, locks }; - }), - ); - - for (const { c, locks } of results) { - log(`\n${'='.repeat(72)}`); - log(`Active ${namespace} ${c} locks (${dateStr})`); - log('='.repeat(72)); - - if (locks.length === 0) { - log(' (no active locks)'); - continue; - } - - const sorted = locks.sort((a, b) => a.createdAt - b.createdAt); - for (const lock of sorted) { - const age = nowUnix() - lock.createdAt; - const ageMin = Math.round(age / 60); - - let remainMin = '?'; - if (lock.status === 'completed') { - remainMin = 'done'; - } else if (lock.expiresAt) { - const remaining = lock.expiresAt - nowUnix(); - remainMin = Math.round(remaining / 60); - } - - log( - ` ${(lock.agent ?? 'unknown').padEnd(30)} ` + - `age: ${String(ageMin).padStart(4)}m ` + - `ttl: ${String(remainMin).padStart(4)} ` + - `platform: ${lock.platform ?? '?'} ` + - `event: ${lock.eventId?.slice(0, 12)}...`, - ); - } - - const roster = await getRoster(c); - const rosterSet = new Set(roster); - const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean)); - const unknownLockedAgents = [...lockedAgents].filter((agent) => !rosterSet.has(agent)); - const available = roster.filter((a) => !lockedAgents.has(a)); - - if (unknownLockedAgents.length > 0) { - log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`); - } - - log(`\n Locked: ${lockedAgents.size}/${roster.length}`); - log(` Available: ${available.join(', ') || '(none)'}`); - } -} - -/** - * Marks a task as permanently completed by publishing a new lock event with - * `status: 'completed'` and no expiration. - * - * This function: - * 1. Verifies that the agent currently holds a valid lock. - * 2. Publishes a replacement event that preserves the original `startedAt` time. - * - * @param {string} agent - Agent name - * @param {string} cadence - 'daily' or 'weekly' - * @param {boolean} [dryRun=false] - If true, skips publishing - * @param {Object} [deps] - Dependency injection - * @returns {Promise<{status: string, eventId: string}>} - * @throws {ExitError} If no active lock exists for the agent - */ -export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps = {}) { - const options = typeof optionsOrDryRun === 'object' ? optionsOrDryRun : { dryRun: !!optionsOrDryRun }; - const { dryRun = false, platform = null, model = null } = options; - - const { - getRelays = _getRelays, - getNamespace = _getNamespace, - getHashtag = _getHashtag, - queryLocks = _queryLocks, - publishLock = _publishLock, - generateSecretKey = _generateSecretKey, - getPublicKey = _getPublicKey, - finalizeEvent = _finalizeEvent, - getDateStr = todayDateStr, - log = console.log, - error = console.error - } = deps; - - const relays = await getRelays(); - const namespace = await getNamespace(); - const hashtag = await getHashtag(); - const dateStr = getDateStr(); - const now = nowUnix(); - - error(`Completing task: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`); - error(`Relays: ${relays.join(', ')}`); - - // 1. Find existing lock - const locks = await queryLocks(relays, cadence, dateStr, namespace); - const myLock = locks.find((l) => l.agent === agent); - - if (!myLock) { - error(`ERROR: No active lock found for agent "${agent}" on ${dateStr}.`); - error(`Cannot complete a task that is not locked or has already expired.`); - throw new ExitError(1, 'No active lock found'); - } - - if (myLock.status === 'completed') { - error(`Task is already marked as completed (event ${myLock.eventId}).`); - log('LOCK_STATUS=completed'); - return { status: 'completed', eventId: myLock.eventId }; - } - - // 2. Build completion event - const startedAtIso = myLock.createdAtIso; - - error('Step 1: Generating completion event...'); - const sk = generateSecretKey(); - const pk = getPublicKey(sk); - - const event = finalizeEvent( - { - kind: KIND_APP_DATA, - created_at: now, - tags: [ - ['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`], - ['t', hashtag], - ['t', `${namespace}-lock-${cadence}`], - ['t', `${namespace}-lock-${cadence}-${dateStr}`], - // No expiration tag -> permanent - ], - content: JSON.stringify({ - agent, - cadence, - status: 'completed', - namespace, - date: dateStr, - platform: platform || process.env.AGENT_PLATFORM || detectPlatform() || 'unknown', - model: model || process.env.AGENT_MODEL || 'unknown', - startedAt: startedAtIso, - completedAt: new Date(now * 1000).toISOString(), - }), - }, - sk, - ); - - error(` Event ID: ${event.id}`); - - if (dryRun) { - error('Step 2: [DRY RUN] Skipping publish — event built but not sent'); - } else { - error('Step 2: Publishing completion event...'); - await publishLock(relays, event); - error(' Published successfully.'); - } - - log('LOCK_STATUS=completed'); - log(`LOCK_EVENT_ID=${event.id}`); - log(`LOCK_PUBKEY=${pk}`); - log(`LOCK_AGENT=${agent}`); - log(`LOCK_CADENCE=${cadence}`); - log(`LOCK_DATE=${dateStr}`); - - return { status: 'completed', eventId: event.id }; -} - -function usage() { - console.error(USAGE_TEXT); -} - -/** - * Main entry point for the torch-lock CLI. - * Dispatches to specific commands (check, lock, complete, etc.) based on argv. - * - * @param {string[]} argv - Arguments from process.argv.slice(2) - */ -export async function main(argv) { - try { - const args = parseArgs(argv); - - if (!args.command) { - usage(); - throw new ExitError(1, 'No command specified'); - } - - switch (args.command) { - case 'check': { - if (!args.cadence || !VALID_CADENCES.has(args.cadence)) { - console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`); - throw new ExitError(1, 'Missing cadence'); - } - await cmdCheck(args.cadence, { - logDir: args.logDir, - ignoreLogs: args.ignoreLogs, - json: args.json, - jsonFile: args.jsonFile, - quiet: args.quiet, - }); - break; - } - - case 'lock': { - if (!args.agent) { - console.error('ERROR: --agent is required for lock'); - throw new ExitError(1, 'Missing agent'); - } - if (!args.cadence || !VALID_CADENCES.has(args.cadence)) { - console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for lock`); - throw new ExitError(1, 'Missing cadence'); - } - await cmdLock(args.agent, args.cadence, { - dryRun: args.dryRun, - platform: args.platform, - model: args.model - }); - break; - } - - case 'complete': { - if (!args.agent) { - console.error('ERROR: --agent is required for complete'); - throw new ExitError(1, 'Missing agent'); - } - if (!args.cadence || !VALID_CADENCES.has(args.cadence)) { - console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for complete`); - throw new ExitError(1, 'Missing cadence'); - } - await cmdComplete(args.agent, args.cadence, { - dryRun: args.dryRun, - platform: args.platform, - model: args.model - }); - break; - } - - case 'list': { - if (args.cadence && !VALID_CADENCES.has(args.cadence)) { - console.error(`ERROR: --cadence must be one of: ${[...VALID_CADENCES].join(', ')}`); - throw new ExitError(1, 'Invalid cadence'); - } - await cmdList(args.cadence || null); - break; - } - - case 'health': { - if (!args.cadence || !VALID_CADENCES.has(args.cadence)) { - console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for health`); - throw new ExitError(1, 'Missing cadence'); - } - const result = await runRelayHealthCheck({ - cadence: args.cadence, - ...(Number.isFinite(args.timeoutMs) ? { timeoutMs: args.timeoutMs } : {}), - ...(Number.isFinite(args.allRelaysDownMinutes) ? { allRelaysDownMinutes: args.allRelaysDownMinutes } : {}), - ...(Number.isFinite(args.minSuccessRate) ? { minSuccessRate: args.minSuccessRate } : {}), - ...(Number.isFinite(args.windowMinutes) ? { windowMinutes: args.windowMinutes } : {}), - }); - if (!result.ok) { - result.failureCategory = 'all relays unhealthy'; - } - console.log(JSON.stringify(result, null, 2)); - if (!result.ok) throw new ExitError(2, 'Relay health check failed'); - break; - } - - case 'dashboard': { - await cmdDashboard(args.port, args.host); - break; - } - - case 'init': { - await cmdInit(args.force); - break; - } - - case 'update': { - await cmdUpdate(args.force); - break; - } - - case 'list-memories': { - const result = await listMemories({ - agent_id: args.agent, - type: args.type, - tags: args.tags, - pinned: args.pinned, - limit: args.limit, - offset: args.offset, - }); - - if (!args.full && Array.isArray(result)) { - for (const memory of result) { - if (typeof memory.content === 'string' && memory.content.length > 200) { - memory.content = memory.content.slice(0, 200) + '... (truncated, use --full to see all)'; - } - } - } - - console.log(JSON.stringify(result, null, 2)); - break; - } - - case 'inspect-memory': { - if (!args.id) { - console.error('ERROR: --id is required for inspect-memory'); - throw new ExitError(1, 'Missing memory id'); - } - const result = await inspectMemory(args.id); - console.log(JSON.stringify(result, null, 2)); - break; - } - - case 'pin-memory': { - if (!args.id) { - console.error('ERROR: --id is required for pin-memory'); - throw new ExitError(1, 'Missing memory id'); - } - const result = await pinMemory(args.id); - console.log(JSON.stringify(result, null, 2)); - break; - } - - case 'unpin-memory': { - if (!args.id) { - console.error('ERROR: --id is required for unpin-memory'); - throw new ExitError(1, 'Missing memory id'); - } - const result = await unpinMemory(args.id); - console.log(JSON.stringify(result, null, 2)); - break; - } - - case 'trigger-prune-dry-run': { - const result = await triggerPruneDryRun({ retentionMs: args.retentionMs ?? undefined }); - console.log(JSON.stringify(result, null, 2)); - break; - } - - case 'memory-stats': { - const result = await memoryStats({ windowMs: args.windowMs ?? undefined }); - console.log(JSON.stringify(result, null, 2)); - break; - } - - default: - console.error(`ERROR: Unknown command: ${args.command}`); - usage(); - throw new ExitError(1, 'Unknown command'); - } - } catch (err) { - if (err instanceof ExitError) { - process.exit(err.code); - } else { - console.error(`torch-lock failed: ${err.message}`); - process.exit(2); - } - } -} diff --git a/torch/src/lock-ops.mjs b/torch/src/lock-ops.mjs deleted file mode 100644 index 5fac6227..00000000 --- a/torch/src/lock-ops.mjs +++ /dev/null @@ -1,697 +0,0 @@ -import { randomInt, randomUUID } from 'node:crypto'; -import { SimplePool } from 'nostr-tools/pool'; -import { - getQueryTimeoutMs, - getPublishTimeoutMs, - getMinSuccessfulRelayPublishes, - getRelayFallbacks, - getMinActiveRelayPool, -} from './torch-config.mjs'; -import { - KIND_APP_DATA, - DEFAULT_RETRY_ATTEMPTS, - DEFAULT_RETRY_BASE_DELAY_MS, - DEFAULT_RETRY_CAP_DELAY_MS, - DEFAULT_ROLLING_WINDOW_SIZE, - DEFAULT_FAILURE_THRESHOLD, - DEFAULT_QUARANTINE_COOLDOWN_MS, - DEFAULT_MAX_QUARANTINE_COOLDOWN_MS, - DEFAULT_SNAPSHOT_INTERVAL_MS, -} from './constants.mjs'; - -function nowUnix() { - return Math.floor(Date.now() / 1000); -} - -export function parseLockEvent(event) { - const dTag = event.tags.find((t) => t[0] === 'd')?.[1] ?? ''; - const expTag = event.tags.find((t) => t[0] === 'expiration')?.[1]; - const expiresAt = expTag ? parseInt(expTag, 10) : null; - - let content = {}; - try { - const parsed = JSON.parse(event.content); - if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { - content = parsed; - } - } catch { - // Ignore malformed JSON content - } - - return { - eventId: event.id, - pubkey: event.pubkey, - createdAt: event.created_at, - createdAtIso: new Date(event.created_at * 1000).toISOString(), - expiresAt, - expiresAtIso: expiresAt ? new Date(expiresAt * 1000).toISOString() : null, - dTag, - agent: content.agent ?? null, - cadence: content.cadence ?? null, - status: content.status ?? null, - date: content.date ?? null, - platform: content.platform ?? null, - }; -} - -function filterActiveLocks(locks) { - const now = nowUnix(); - return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now); -} - -function withTimeout(promise, timeoutMs, timeoutMessage) { - let timeoutHandle; - const timeoutPromise = new Promise((_, reject) => { - timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); - }); - return Promise.race([promise, timeoutPromise]).finally(() => { - if (timeoutHandle) clearTimeout(timeoutHandle); - }); -} - -function relayListLabel(relays) { - return relays.join(', '); -} - -function classifyPublishError(message) { - const normalized = String(message || '').toLowerCase(); - if (normalized.includes('publish timed out after') || normalized.includes('publish timeout')) { - return 'publish_timeout'; - } - if ( - normalized.includes('enotfound') - || normalized.includes('eai_again') - || normalized.includes('getaddrinfo') - || (normalized.includes('dns') && normalized.includes('websocket')) - ) { - return 'dns_resolution'; - } - if ( - normalized.includes('connect etimedout') - || normalized.includes('tcp connect timed out') - || normalized.includes('connect timeout') - ) { - return 'tcp_connect_timeout'; - } - if ( - normalized.includes('tls') - || normalized.includes('ssl') - || normalized.includes('certificate') - || normalized.includes('handshake') - ) { - return 'tls_handshake'; - } - if ( - normalized.includes('websocket') - || normalized.includes('bad response') - || normalized.includes('unexpected server response') - ) { - return 'websocket_open_failure'; - } - if (normalized.includes('timed out') || normalized.includes('timeout') || normalized.includes('etimedout')) { - return 'network_timeout'; - } - if (normalized.includes('econnreset') || normalized.includes('connection reset') || normalized.includes('socket hang up')) { - return 'connection_reset'; - } - if ( - normalized.includes('unavailable') - || normalized.includes('offline') - || normalized.includes('econnrefused') - || normalized.includes('connection refused') - || normalized.includes('enotfound') - || normalized.includes('503') - ) { - return 'relay_unavailable'; - } - return 'permanent_validation_error'; -} - -function isTransientPublishCategory(category) { - return [ - 'publish_timeout', - 'dns_resolution', - 'tcp_connect_timeout', - 'tls_handshake', - 'websocket_open_failure', - 'network_timeout', - 'connection_reset', - 'relay_unavailable', - ].includes(category); -} - -const MAX_RANDOM = 281474976710655; // 2**48 - 1 - -function secureRandom() { - return randomInt(0, MAX_RANDOM) / MAX_RANDOM; -} - -function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = secureRandom) { - const maxDelay = Math.min(capMs, baseMs * (2 ** Math.max(0, attemptNumber - 1))); - return Math.floor(randomFn() * maxDelay); -} - -function summarizeRelayMetrics(metrics, nowMs) { - const total = metrics.recentOutcomes.length; - const successCount = metrics.recentOutcomes.filter((entry) => entry.success).length; - const timeoutCount = metrics.recentOutcomes.filter((entry) => entry.timedOut).length; - const latencyEntries = metrics.recentOutcomes.map((entry) => entry.latencyMs).filter((v) => Number.isFinite(v)); - const averageLatencyMs = latencyEntries.length - ? Math.round(latencyEntries.reduce((sum, value) => sum + value, 0) / latencyEntries.length) - : null; - const successRate = total > 0 ? successCount / total : 0.5; - const timeoutRate = total > 0 ? timeoutCount / total : 0; - const quarantineRemainingMs = metrics.quarantineUntil > nowMs ? metrics.quarantineUntil - nowMs : 0; - return { - relay: metrics.relay, - sampleSize: total, - successRate, - timeoutRate, - averageLatencyMs, - failureStreak: metrics.failureStreak, - quarantined: quarantineRemainingMs > 0, - quarantineRemainingMs, - cooldownMs: metrics.cooldownMs, - lastResultAt: metrics.lastResultAt, - }; -} - -function computeRelayScore(summary) { - const latencyPenalty = summary.averageLatencyMs === null - ? 0.1 - : Math.min(0.35, summary.averageLatencyMs / 10_000); - const quarantinePenalty = summary.quarantined ? 1 : 0; - return (summary.successRate * 1.4) - (summary.timeoutRate * 0.9) - latencyPenalty - quarantinePenalty; -} - -export class RelayHealthManager { - constructor() { - this.metricsByRelay = new Map(); - this.lastSnapshotAt = 0; - } - - ensureMetrics(relay, config) { - let metrics = this.metricsByRelay.get(relay); - if (!metrics) { - metrics = { - relay, - recentOutcomes: [], - failureStreak: 0, - quarantineUntil: 0, - cooldownMs: config.quarantineCooldownMs, - lastLatencyMs: null, - lastResultAt: null, - }; - this.metricsByRelay.set(relay, metrics); - } - return metrics; - } - - rankRelays(relays, config, nowMs = Date.now()) { - const summaries = relays.map((relay) => { - const metrics = this.ensureMetrics(relay, config); - const summary = summarizeRelayMetrics(metrics, nowMs); - return { - relay, - summary, - score: computeRelayScore(summary), - }; - }); - - return summaries.sort((a, b) => { - if (b.score !== a.score) return b.score - a.score; - if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1; - if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) { - if (a.summary.averageLatencyMs === null) return 1; - if (b.summary.averageLatencyMs === null) return -1; - return a.summary.averageLatencyMs - b.summary.averageLatencyMs; - } - return a.relay.localeCompare(b.relay); - }); - } - - prioritizeRelays(relays, config, nowMs = Date.now()) { - const minActive = Math.max(1, Math.min(config.minActiveRelayPool, relays.length || 1)); - const ranked = this.rankRelays([...new Set(relays)], config, nowMs); - const active = ranked.filter((entry) => !entry.summary.quarantined); - const quarantined = ranked - .filter((entry) => entry.summary.quarantined) - .sort((a, b) => a.summary.quarantineRemainingMs - b.summary.quarantineRemainingMs); - - const selected = [...active]; - if (selected.length < minActive) { - const additionalNeeded = Math.min(minActive - selected.length, quarantined.length); - selected.push(...quarantined.slice(0, additionalNeeded)); - } - - return { - prioritized: selected.map((entry) => entry.relay), - ranked, - }; - } - - recordOutcome(relay, success, errorMessage, latencyMs, config, nowMs = Date.now()) { - const metrics = this.ensureMetrics(relay, config); - const timedOut = String(errorMessage || '').toLowerCase().includes('timeout') - || String(errorMessage || '').toLowerCase().includes('timed out'); - metrics.recentOutcomes.push({ success, timedOut, latencyMs, atMs: nowMs }); - if (metrics.recentOutcomes.length > config.rollingWindowSize) { - metrics.recentOutcomes.splice(0, metrics.recentOutcomes.length - config.rollingWindowSize); - } - metrics.lastResultAt = nowMs; - metrics.lastLatencyMs = Number.isFinite(latencyMs) ? latencyMs : null; - - if (success) { - metrics.failureStreak = 0; - if (metrics.quarantineUntil > nowMs) { - metrics.quarantineUntil = 0; - metrics.cooldownMs = config.quarantineCooldownMs; - } - return; - } - - metrics.failureStreak += 1; - if (metrics.failureStreak >= config.failureThreshold) { - metrics.quarantineUntil = nowMs + metrics.cooldownMs; - metrics.cooldownMs = Math.min(config.maxQuarantineCooldownMs, Math.floor(metrics.cooldownMs * 1.5)); - } - } - - collectSnapshot(relays, config, nowMs = Date.now()) { - const uniqueRelays = [...new Set(relays)]; - return this.rankRelays(uniqueRelays, config, nowMs).map((entry) => ({ - relay: entry.relay, - score: Number(entry.score.toFixed(4)), - ...entry.summary, - })); - } - - maybeLogSnapshot(relays, config, logger, reason, force = false, nowMs = Date.now()) { - const intervalReached = nowMs - this.lastSnapshotAt >= config.snapshotIntervalMs; - if (!force && !intervalReached) return; - this.lastSnapshotAt = nowMs; - logger(JSON.stringify({ - event: 'relay_health_snapshot', - reason, - relays: this.collectSnapshot(relays, config, nowMs), - })); - } - - reset() { - this.metricsByRelay.clear(); - this.lastSnapshotAt = 0; - } -} - -export const defaultHealthManager = new RelayHealthManager(); - -function buildRelayHealthConfig(deps) { - return { - rollingWindowSize: deps.rollingWindowSize ?? DEFAULT_ROLLING_WINDOW_SIZE, - failureThreshold: deps.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD, - quarantineCooldownMs: deps.quarantineCooldownMs ?? DEFAULT_QUARANTINE_COOLDOWN_MS, - maxQuarantineCooldownMs: deps.maxQuarantineCooldownMs ?? DEFAULT_MAX_QUARANTINE_COOLDOWN_MS, - snapshotIntervalMs: deps.snapshotIntervalMs ?? DEFAULT_SNAPSHOT_INTERVAL_MS, - minActiveRelayPool: Math.max(1, deps.minActiveRelayPool), - }; -} - -function mergeRelayList(primaryRelays, fallbackRelays) { - return [...new Set([...primaryRelays, ...fallbackRelays])]; -} - -export async function queryLocks(relays, cadence, dateStr, namespace, deps = {}) { - const { - poolFactory = () => new SimplePool(), - getQueryTimeoutMsFn = getQueryTimeoutMs, - getRelayFallbacksFn = getRelayFallbacks, - getMinActiveRelayPoolFn = getMinActiveRelayPool, - errorLogger = console.error, - healthLogger = console.error, - healthManager = defaultHealthManager, - } = deps; - - const pool = poolFactory(); - const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`; - const queryTimeoutMs = await getQueryTimeoutMsFn(); - const fallbackRelays = (await getRelayFallbacksFn()).filter((relay) => !relays.includes(relay)); - const healthConfig = buildRelayHealthConfig({ - ...deps, - minActiveRelayPool: await getMinActiveRelayPoolFn(), - }); - - const runQuery = async (relaySet, phase) => { - const { prioritized } = healthManager.prioritizeRelays(relaySet, healthConfig); - if (prioritized.length > 0) { - errorLogger(`[${phase}] Querying ${prioritized.length} relays (${relayListLabel(prioritized)})...`); - } - - const startedAtMs = Date.now(); - try { - const events = await withTimeout( - pool.querySync(prioritized, { - kinds: [KIND_APP_DATA], - '#t': [tagFilter], - }), - queryTimeoutMs, - `[${phase}] Relay query timed out after ${queryTimeoutMs}ms (relays: ${relayListLabel(prioritized)})`, - ); - const elapsedMs = Date.now() - startedAtMs; - for (const relay of prioritized) { - healthManager.recordOutcome(relay, true, null, elapsedMs, healthConfig); - } - return filterActiveLocks(events.map(parseLockEvent)); - } catch (err) { - const elapsedMs = Date.now() - startedAtMs; - const message = err instanceof Error ? err.message : String(err); - for (const relay of prioritized) { - healthManager.recordOutcome(relay, false, message, elapsedMs, healthConfig); - } - throw new Error( - `[${phase}] Relay query failed (timeout=${queryTimeoutMs}ms, relays=${relayListLabel(prioritized)}): ${message}`, - { cause: err }, - ); - } - }; - - const allRelays = mergeRelayList(relays, fallbackRelays); - - try { - healthManager.maybeLogSnapshot(allRelays, healthConfig, healthLogger, 'query:periodic'); - try { - return await runQuery(relays, 'query:primary'); - } catch (primaryErr) { - if (!fallbackRelays.length) { - healthManager.maybeLogSnapshot(allRelays, healthConfig, healthLogger, 'query:failure', true); - throw primaryErr; - } - errorLogger(`WARN: ${primaryErr.message}`); - errorLogger(`WARN: retrying query with fallback relays (${relayListLabel(fallbackRelays)})`); - return await runQuery(fallbackRelays, 'query:fallback'); - } - } finally { - pool.close(allRelays); - } -} - -async function publishToRelays(pool, relays, event, publishTimeoutMs, phase) { - const publishPromises = pool.publish(relays, event); - const settled = await Promise.allSettled( - publishPromises.map((publishPromise, index) => withTimeout( - publishPromise, - publishTimeoutMs, - `[${phase}] Publish timed out after ${publishTimeoutMs}ms (relay=${relays[index]})`, - )), - ); - - return settled.map((result, index) => { - if (result.status === 'fulfilled') { - return { - relay: relays[index], - success: true, - phase, - latencyMs: null, - }; - } - const reason = result.reason; - const message = reason instanceof Error ? reason.message : String(reason ?? 'unknown'); - return { - relay: relays[index], - success: false, - phase, - message, - latencyMs: null, - }; - }); -} - -export class LockPublisher { - constructor(relays, event, deps = {}) { - this.relays = relays; - this.event = event; - this.deps = deps; - this.pool = null; - this.healthConfig = null; - this.allRelays = []; - this.fallbackRelays = []; - this.publishTimeoutMs = 0; - this.minSuccesses = 0; - this.minActiveRelayPool = 0; - this.maxAttempts = 0; - this.retryBaseDelayMs = 0; - this.retryCapDelayMs = 0; - this.sleepFn = null; - this.randomFn = null; - this.telemetryLogger = null; - this.healthLogger = null; - this.healthManager = null; - this.correlationId = null; - this.attemptId = null; - } - - async publish() { - const { - poolFactory = () => new SimplePool(), - retryAttempts = DEFAULT_RETRY_ATTEMPTS, - retryBaseDelayMs = DEFAULT_RETRY_BASE_DELAY_MS, - retryCapDelayMs = DEFAULT_RETRY_CAP_DELAY_MS, - sleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - randomFn = Math.random, - telemetryLogger = console.error, - healthLogger = console.error, - diagnostics = {}, - healthManager = defaultHealthManager, - } = this.deps; - - this.healthManager = healthManager; - this.pool = poolFactory(); - this.publishTimeoutMs = this.deps.resolvedConfig?.publishTimeoutMs; - this.minSuccesses = this.deps.resolvedConfig?.minSuccesses; - this.fallbackRelays = (this.deps.resolvedConfig?.fallbackRelays || []).filter((relay) => !this.relays.includes(relay)); - this.maxAttempts = Math.max(1, Math.floor(retryAttempts)); - this.healthConfig = buildRelayHealthConfig({ - ...this.deps, - minActiveRelayPool: this.deps.resolvedConfig?.minActiveRelayPool, - }); - - this.retryBaseDelayMs = retryBaseDelayMs; - this.retryCapDelayMs = retryCapDelayMs; - this.sleepFn = sleepFn; - this.randomFn = randomFn; - this.telemetryLogger = telemetryLogger; - this.healthLogger = healthLogger; - this.healthManager = healthManager; - this.correlationId = diagnostics.correlationId || randomUUID(); - this.attemptId = diagnostics.attemptId || randomUUID(); - - this.allRelays = mergeRelayList(this.relays, this.fallbackRelays); - - try { - this.healthManager.maybeLogSnapshot(this.allRelays, this.healthConfig, this.healthLogger, 'publish:periodic'); - let lastAttemptState = null; - const retryTimeline = []; - const overallStartedAt = Date.now(); - let terminalFailureCategory = 'relay_publish_non_retryable'; - - for (let attemptNumber = 1; attemptNumber <= this.maxAttempts; attemptNumber += 1) { - const startedAtMs = Date.now(); - lastAttemptState = await this.executePublishCycle(); - const elapsedMs = Date.now() - startedAtMs; - retryTimeline.push({ - publishAttempt: attemptNumber, - successCount: lastAttemptState.successCount, - relayAttemptedCount: lastAttemptState.attempted.size, - elapsedMs, - }); - - if (lastAttemptState.successCount >= this.minSuccesses) { - this.logSuccess(attemptNumber, lastAttemptState, retryTimeline, overallStartedAt); - return this.event; - } - - const { hasTransientFailure, hasPermanentFailure } = this.analyzeFailures(lastAttemptState.failures); - const canRetry = attemptNumber < this.maxAttempts && hasTransientFailure && !hasPermanentFailure; - - terminalFailureCategory = this.determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber); - - if (!canRetry) { - break; - } - - const nextDelayMs = calculateBackoffDelayMs(attemptNumber, this.retryBaseDelayMs, this.retryCapDelayMs, this.randomFn); - this.logRetry(attemptNumber, lastAttemptState.failures, elapsedMs, nextDelayMs); - await this.sleepFn(nextDelayMs); - } - - this.handleFinalFailure(lastAttemptState, terminalFailureCategory, retryTimeline, overallStartedAt); - } finally { - this.pool.close(this.allRelays); - } - } - - async executePublishCycle() { - const attempted = new Set(); - const publishResults = []; - - await this.attemptPhase(this.relays, 'publish:primary', attempted, publishResults); - - let successCount = publishResults.filter((result) => result.success).length; - if (successCount < this.minSuccesses && this.fallbackRelays.length > 0) { - await this.attemptPhase(this.fallbackRelays, 'publish:fallback', attempted, publishResults); - successCount = publishResults.filter((result) => result.success).length; - } - - const failures = publishResults - .filter((result) => !result.success) - .map((result) => ({ - ...result, - category: classifyPublishError(result.message), - })); - - return { attempted, publishResults, successCount, failures }; - } - - async attemptPhase(phaseRelays, phaseName, attempted, publishResults) { - if (!phaseRelays.length) return; - const { prioritized } = this.healthManager.prioritizeRelays(phaseRelays, this.healthConfig); - if (!prioritized.length) return; - - console.error(`[${phaseName}] Publishing to ${prioritized.length} relays (${prioritized.join(', ')})...`); - - const startedAtMs = Date.now(); - for (const relay of prioritized) { - attempted.add(relay); - } - const phaseResults = await publishToRelays(this.pool, prioritized, this.event, this.publishTimeoutMs, phaseName); - const elapsedMs = Date.now() - startedAtMs; - for (const result of phaseResults) { - result.latencyMs = elapsedMs; - this.healthManager.recordOutcome(result.relay, result.success, result.message, elapsedMs, this.healthConfig); - } - publishResults.push(...phaseResults); - } - - analyzeFailures(failures) { - const hasTransientFailure = failures.some((failure) => isTransientPublishCategory(failure.category)); - const hasPermanentFailure = failures.some((failure) => !isTransientPublishCategory(failure.category)); - return { hasTransientFailure, hasPermanentFailure }; - } - - determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber) { - if (hasTransientFailure && !hasPermanentFailure && attemptNumber >= this.maxAttempts) { - return 'relay_publish_quorum_failure'; - } else if (hasPermanentFailure) { - return 'relay_publish_non_retryable'; - } else { - return 'relay_publish_quorum_failure'; - } - } - - logSuccess(attemptNumber, lastAttemptState, retryTimeline, overallStartedAt) { - this.telemetryLogger(JSON.stringify({ - event: 'lock_publish_quorum_met', - correlationId: this.correlationId, - attemptId: this.attemptId, - publishAttempt: attemptNumber, - successCount: lastAttemptState.successCount, - relayAttemptedCount: lastAttemptState.attempted.size, - requiredSuccesses: this.minSuccesses, - timeoutMs: this.publishTimeoutMs, - retryTimeline, - totalElapsedMs: Date.now() - overallStartedAt, - })); - console.error( - ` Published to ${lastAttemptState.successCount}/${lastAttemptState.attempted.size} relays ` - + `(required=${this.minSuccesses}, timeout=${this.publishTimeoutMs}ms)`, - ); - } - - logRetry(attemptNumber, failures, elapsedMs, nextDelayMs) { - for (const failure of failures) { - if (!isTransientPublishCategory(failure.category)) continue; - this.telemetryLogger(JSON.stringify({ - event: 'lock_publish_retry', - correlationId: this.correlationId, - attemptId: this.attemptId, - publishAttempt: attemptNumber, - relayUrl: failure.relay, - errorCategory: failure.category, - elapsedMs, - nextDelayMs, - })); - } - } - - handleFinalFailure(lastAttemptState, terminalFailureCategory, retryTimeline, overallStartedAt) { - const reasonDistribution = {}; - const failureLines = lastAttemptState.failures - .map((result) => { - reasonDistribution[result.category] = (reasonDistribution[result.category] || 0) + 1; - this.telemetryLogger(JSON.stringify({ - event: 'lock_publish_failure', - correlationId: this.correlationId, - attemptId: this.attemptId, - relayUrl: result.relay, - phase: result.phase, - reason: result.category, - message: result.message, - })); - return `${result.relay} (${result.phase}, reason=${result.category}): ${result.message}`; - }); - - this.telemetryLogger(JSON.stringify({ - event: 'lock_publish_quorum_failed', - correlationId: this.correlationId, - attemptId: this.attemptId, - errorCategory: terminalFailureCategory, - successCount: lastAttemptState.successCount, - relayAttemptedCount: lastAttemptState.attempted.size, - requiredSuccesses: this.minSuccesses, - timeoutMs: this.publishTimeoutMs, - attempts: this.maxAttempts, - reasonDistribution, - retryTimeline, - totalElapsedMs: Date.now() - overallStartedAt, - })); - - this.healthManager.maybeLogSnapshot(this.allRelays, this.healthConfig, this.healthLogger, 'publish:failure', true); - throw new Error( - `Failed relay publish quorum in publish phase: ${lastAttemptState.successCount}/${lastAttemptState.attempted.size} successful ` - + `(required=${this.minSuccesses}, timeout=${this.publishTimeoutMs}ms, attempts=${this.maxAttempts}, attempt_id=${this.attemptId}, correlation_id=${this.correlationId}, error_category=${terminalFailureCategory}, total_retry_timeline_ms=${Date.now() - overallStartedAt})\n` - + ` retry timeline: ${retryTimeline.map((item) => `#${item.publishAttempt}:${item.elapsedMs}ms`).join(', ')}\n` - + ` ${failureLines.join('\n ')}`, - ); - } -} - -export async function publishLock(relays, event, deps = {}) { - const { - getPublishTimeoutMsFn = getPublishTimeoutMs, - getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes, - getRelayFallbacksFn = getRelayFallbacks, - getMinActiveRelayPoolFn = getMinActiveRelayPool, - } = deps; - - const [publishTimeoutMs, minSuccesses, fallbackRelays, minActiveRelayPool] = await Promise.all([ - getPublishTimeoutMsFn(), - getMinSuccessfulRelayPublishesFn(), - getRelayFallbacksFn(), - getMinActiveRelayPoolFn(), - ]); - - return new LockPublisher(relays, event, { - ...deps, - resolvedConfig: { - publishTimeoutMs, - minSuccesses, - fallbackRelays, - minActiveRelayPool, - }, - }).publish(); -} - -export function _resetRelayHealthState() { - defaultHealthManager.reset(); -} - -export const _secureRandom = secureRandom; diff --git a/torch/src/lock-utils.mjs b/torch/src/lock-utils.mjs deleted file mode 100644 index 29863e48..00000000 --- a/torch/src/lock-utils.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { todayDateStr, getIsoWeekStr } from './utils.mjs'; - -/** - * Scans the local log directory to identify agents that have already completed their task - * for the current period (daily or weekly). - * - * @param {string} cadence - 'daily' or 'weekly' - * @param {string} logDir - Path to the log directory (e.g., 'task-logs') - * @param {Object} deps - Dependency injection for testing - * @returns {Promise>} - Set of agent names that have completed their task - */ -export async function getCompletedAgents(cadence, logDir, deps) { - const { readdir = fs.readdir, getDateStr = todayDateStr, getIsoWeek = getIsoWeekStr } = deps; - const completed = new Set(); - const targetDir = path.join(logDir, cadence); - - try { - const files = await readdir(targetDir); - const today = getDateStr(); - const currentWeek = getIsoWeek(); - - for (const file of files) { - // Format: YYYY-MM-DDTHH-mm-ssZ____.md - const match = file.match(/^(\d{4}-\d{2}-\d{2})T.*__([a-zA-Z0-9-_]+)__(.*)\.md$/); - if (!match) continue; - - const [, datePart, agent, status] = match; - - if (status === 'completed') { - if (cadence === 'daily' && datePart === today) { - completed.add(agent); - } else if (cadence === 'weekly') { - const fileWeek = getIsoWeek(datePart); // datePart is YYYY-MM-DD - if (fileWeek === currentWeek) { - completed.add(agent); - } - } - } - } - } catch (err) { - if (err.code !== 'ENOENT') { - console.error(`Warning: Failed to read log dir ${targetDir}: ${err.message}`); - } - } - - return completed; -} diff --git a/torch/src/ops.mjs b/torch/src/ops.mjs deleted file mode 100644 index 34063417..00000000 --- a/torch/src/ops.mjs +++ /dev/null @@ -1,449 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import crypto from 'node:crypto'; -import readline from 'node:readline/promises'; -import { stdin as input, stdout as output } from 'node:process'; -import { DEFAULT_RELAYS } from './constants.mjs'; - -const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); - -// Source paths (in the package) -const SRC_PROMPTS_DIR = path.join(PKG_ROOT, 'src', 'prompts'); - -// Files to treat as "Static" (always overwrite on update, with transformations) -const STATIC_FILES = [ - 'META_PROMPTS.md', - 'scheduler-flow.md', - 'daily-scheduler.md', - 'weekly-scheduler.md', -]; - -// Directories containing "Evolving" files (copy if missing, preserve if present) -const EVOLVING_DIRS = ['daily', 'weekly']; - -// New constants for full application install -const APP_DIRS = ['src', 'bin', 'dashboard', 'landing', 'assets', 'scripts']; -const APP_FILES = ['package.json', 'build.mjs', 'README.md', 'torch-config.example.json', 'TORCH.md']; - -function getPaths(root, installDirName) { - const torchDir = path.resolve(root, installDirName); - return { - root, - torchDir, - promptsDir: path.join(torchDir, 'prompts'), - roster: path.join(torchDir, 'roster.json'), - }; -} - -function ensureDir(dir) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } -} - -function copyDir(src, dest) { - if (fs.existsSync(src)) { - fs.cpSync(src, dest, { recursive: true }); - } -} - -function transformContent(content, installDirName) { - // Replace source paths with user paths - // We assume the user is running from root, so 'src/prompts/daily/' becomes 'torch/prompts/daily/' - // If installDirName is different, we should use that. - // Note: These replacements are somewhat fragile regexes. - // We'll replace 'src/' with 'installDirName/' essentially? - // The original code replaced 'src/prompts/' with 'torch/prompts/'. - - // If installDirName is '.', we want 'prompts/daily/'. - // If installDirName is 'torch', we want 'torch/prompts/daily/'. - - const prefix = installDirName === '.' ? '' : `${installDirName}/`; - - return content - .replace(/src\/prompts\/daily\//g, `${prefix}prompts/daily/`) - .replace(/src\/prompts\/weekly\//g, `${prefix}prompts/weekly/`) - .replace(/src\/prompts\/roster\.json/g, `${prefix}roster.json`) - .replace(/src\/prompts\/scheduler-flow\.md/g, `${prefix}scheduler-flow.md`) - .replace(/TORCH\.md/g, `${prefix}TORCH.md`); -} - -function copyFile(src, dest, transform = false, overwrite = true, installDirName = 'torch') { - if (fs.existsSync(dest) && !overwrite) { - return false; // Skipped - } - if (!fs.existsSync(src)) return false; - - const content = fs.readFileSync(src, 'utf8'); - const finalContent = transform ? transformContent(content, installDirName) : content; - fs.writeFileSync(dest, finalContent, 'utf8'); - return true; // Copied/Overwritten -} - -function syncAppDirectories(torchDir, verb = 'Copied') { - console.log(`${verb === 'Copied' ? 'Copying' : 'Updating'} application directories...`); - for (const dir of APP_DIRS) { - const src = path.join(PKG_ROOT, dir); - const dest = path.join(torchDir, dir); - if (fs.existsSync(src)) { - copyDir(src, dest); - console.log(` ${verb} ${dir}/`); - } - } -} - -function syncAppFiles(torchDir, installDir, verb = 'Copied') { - console.log(`${verb === 'Copied' ? 'Copying' : 'Updating'} application files...`); - for (const file of APP_FILES) { - const src = path.join(PKG_ROOT, file); - const dest = path.join(torchDir, file); - if (fs.existsSync(src)) { - if (installDir === '.' && file === 'package.json') { - if (verb === 'Copied' && fs.existsSync(dest)) { - console.warn(' Skipping package.json to avoid overwriting host package.json (installing to root).'); - continue; - } - if (verb === 'Updated') { - console.log(' Skipping package.json update (installed in root).'); - continue; - } - } - - fs.copyFileSync(src, dest); - console.log(` ${verb} ${file}`); - } - } -} - -async function interactiveInit(cwd) { - const rl = readline.createInterface({ input, output }); - const currentDirName = path.basename(cwd); - - console.log('\n🔥 TORCH Initialization 🔥\n'); - - try { - // 1. Install Directory - let defaultDir = 'torch'; - if (currentDirName === 'torch') { - defaultDir = '.'; - } - - const dirAnswer = await rl.question(`Install directory (default: ${defaultDir}): `); - const installDir = dirAnswer.trim() || defaultDir; - - // 2. Namespace - const randomSuffix = crypto.randomBytes(4).toString('hex'); - const defaultNamespace = `torch-${randomSuffix}`; - const namespaceAnswer = await rl.question(`Nostr Namespace (default: ${defaultNamespace}): `); - const namespace = namespaceAnswer.trim() || defaultNamespace; - - // 3. Relays - console.log(`\nDefault Relays:\n ${DEFAULT_RELAYS.join('\n ')}`); - const relaysAnswer = await rl.question('Enter relays (comma-separated) or press Enter to use defaults: '); - let relays = DEFAULT_RELAYS; - if (relaysAnswer.trim()) { - relays = relaysAnswer.split(',').map(r => r.trim()).filter(Boolean); - } - - return { installDir, namespace, relays }; - } finally { - rl.close(); - } -} - -async function resolveConfiguration(cwd, mockAnswers) { - if (mockAnswers) { - return mockAnswers; - } - return await interactiveInit(cwd); -} - -function ensureInstallDirectory(paths, force, installDir) { - if (fs.existsSync(paths.torchDir) && !force) { - const entries = fs.readdirSync(paths.torchDir); - if (entries.length > 0 && installDir !== '.') { - throw new Error(`Directory ${paths.torchDir} already exists and is not empty. Use --force to overwrite.`); - } - } - ensureDir(paths.torchDir); - ensureDir(paths.promptsDir); -} - -function installAppAssets(torchDir, installDir) { - // 1. Copy App Directories - syncAppDirectories(torchDir, 'Copied'); - - // 2. Copy App Files - syncAppFiles(torchDir, installDir, 'Copied'); -} - -function installTorchAssets(paths, installDir) { - // 3. Copy Roster - const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json'); - if (fs.existsSync(srcRoster)) { - copyFile(srcRoster, paths.roster, false, true, installDir); - console.log(`Created ${path.relative(paths.root, paths.roster)}`); - } - - // 4. Copy Static Files - for (const file of STATIC_FILES) { - const src = path.join(SRC_PROMPTS_DIR, file); - const dest = path.join(paths.torchDir, file); - if (fs.existsSync(src)) { - copyFile(src, dest, true, true, installDir); - console.log(`Created ${path.relative(paths.root, dest)}`); - } - } - - // 5. Copy Prompts - for (const dir of EVOLVING_DIRS) { - const srcDir = path.join(SRC_PROMPTS_DIR, dir); - const destDir = path.join(paths.promptsDir, dir); - ensureDir(destDir); - - if (fs.existsSync(srcDir)) { - const files = fs.readdirSync(srcDir); - for (const file of files) { - const srcFile = path.join(srcDir, file); - const destFile = path.join(destDir, file); - copyFile(srcFile, destFile, false, true, installDir); - } - console.log(`Created ${files.length} files in ${path.relative(paths.root, destDir)}/`); - } - } -} - -function configureTorch(cwd, paths, installDir, namespace, relays) { - // 6. Create/Update torch-config.json - const configPath = path.join(paths.root, 'torch-config.json'); - - let configData = {}; - - // Try to load existing or example - if (fs.existsSync(configPath)) { - try { - configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); - console.log(`Updating existing ${path.relative(cwd, configPath)}...`); - } catch (e) { - console.warn(`Could not parse existing config: ${e.message}`); - } - } else { - const exampleConfigPath = path.join(PKG_ROOT, 'torch-config.example.json'); - if (fs.existsSync(exampleConfigPath)) { - configData = JSON.parse(fs.readFileSync(exampleConfigPath, 'utf8')); - } - } - - // Apply user choices - if (!configData.nostrLock) configData.nostrLock = {}; - configData.nostrLock.namespace = namespace; - configData.nostrLock.relays = relays; - - // Configure memory policy with correct paths - if (!configData.scheduler) configData.scheduler = {}; - // Always ensure memory policy exists and points to the correct scripts - // We use the installDir to construct the path - const scriptPrefix = installDir === '.' ? '' : `${installDir}/`; - - if (!configData.scheduler.memoryPolicyByCadence) { - configData.scheduler.memoryPolicyByCadence = {}; - } - - // Populate daily/weekly if missing - ['daily', 'weekly'].forEach(cadence => { - if (!configData.scheduler.memoryPolicyByCadence[cadence]) { - configData.scheduler.memoryPolicyByCadence[cadence] = { - mode: "required", - retrieveCommand: `node ${scriptPrefix}scripts/memory/retrieve.mjs`, - storeCommand: `node ${scriptPrefix}scripts/memory/store.mjs`, - retrieveSuccessMarkers: ["MEMORY_RETRIEVED"], - storeSuccessMarkers: ["MEMORY_STORED"], - retrieveArtifacts: [`.scheduler-memory/latest/${cadence}/retrieve.ok`], - storeArtifacts: [`.scheduler-memory/latest/${cadence}/store.ok`] - }; - } - }); - - fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf8'); - console.log(`Saved configuration to ${path.relative(cwd, configPath)}`); -} - -function injectHostScriptsIfNeeded(paths, installDir) { - // 7. Inject Scripts into Host Package.json - // If we are NOT installing to '.', the host package.json is in paths.root - if (installDir !== '.') { - injectScriptsIntoHost(paths.root, installDir); - } -} - -export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = null) { - const config = await resolveConfiguration(cwd, mockAnswers); - const { installDir, namespace, relays } = config; - - const paths = getPaths(cwd, installDir); - console.log(`\nInitializing torch in ${paths.torchDir}...`); - - ensureInstallDirectory(paths, force, installDir); - - installAppAssets(paths.torchDir, installDir); - installTorchAssets(paths, installDir); - configureTorch(cwd, paths, installDir, namespace, relays); - injectHostScriptsIfNeeded(paths, installDir); - - console.log('\nInitialization complete.'); - console.log('You can now customize the files in ' + path.relative(cwd, paths.torchDir) + '/'); -} - -function injectScriptsIntoHost(hostRoot, installDirName) { - const hostPkgPath = path.join(hostRoot, 'package.json'); - if (!fs.existsSync(hostPkgPath)) { - console.warn('No package.json found in host root. Skipping script injection.'); - return; - } - - try { - const pkg = JSON.parse(fs.readFileSync(hostPkgPath, 'utf8')); - if (!pkg.scripts) pkg.scripts = {}; - - const scriptsToAdd = { - 'torch:dashboard': `npm run --prefix ${installDirName} dashboard:serve`, - 'torch:check': `npm run --prefix ${installDirName} lock:check:daily`, // Default to daily check - 'torch:lock': `npm run --prefix ${installDirName} lock:lock`, - 'torch:health': `npm run --prefix ${installDirName} lock:health`, - 'torch:memory:list': `node ${installDirName === '.' ? '' : installDirName + '/'}bin/torch-lock.mjs list-memories`, - 'torch:memory:inspect': `node ${installDirName === '.' ? '' : installDirName + '/'}bin/torch-lock.mjs inspect-memory`, - }; - - let modified = false; - for (const [key, cmd] of Object.entries(scriptsToAdd)) { - if (!pkg.scripts[key]) { - pkg.scripts[key] = cmd; - console.log(` Added script: "${key}"`); - modified = true; - } else { - console.log(` Script "${key}" already exists, skipping.`); - } - } - - if (modified) { - fs.writeFileSync(hostPkgPath, JSON.stringify(pkg, null, 2), 'utf8'); - console.log('Updated package.json with convenience scripts.'); - } - - } catch (e) { - console.error(`Failed to inject scripts: ${e.message}`); - } -} - -export function cmdUpdate(force = false, cwd = process.cwd()) { - // Update logic needs to know WHERE torch is installed. - // We can look for torch directory? Or assume 'torch'? - // For now, let's look for 'torch' directory first, then fallback to '.' if we detect torch files? - // Or just default to 'torch' and let user move files if they changed it? - // Realistically, 'update' should probably take an argument for the dir, or we just default to 'torch'. - - // If the user installed to 'custom-dir', cmdUpdate will fail unless we auto-detect. - // Auto-detection strategy: check if 'torch' exists. If not, check if 'package.json' has 'torch-lock' name? - - let installDirName = 'torch'; - if (!fs.existsSync(path.join(cwd, 'torch')) && fs.existsSync(path.join(cwd, 'package.json'))) { - // Check if current dir is the torch dir - try { - const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')); - if (pkg.name === 'torch-lock') { - installDirName = '.'; - } - } catch { - // Ignore error if package.json is missing or invalid - } - } - - const paths = getPaths(cwd, installDirName); - console.log(`Updating torch configuration in ${paths.torchDir}...`); - - if (!fs.existsSync(paths.torchDir)) { - throw new Error(`${paths.torchDir} not found. Run 'torch-lock init' first.`); - } - - // 1. Backup - const backupName = `backup_${new Date().toISOString().replace(/[:.]/g, '-')}`; - const backupRoot = path.join(paths.torchDir, '_backups'); - const thisBackupDir = path.join(backupRoot, backupName); - - ensureDir(thisBackupDir); - console.log(`Creating backup at ${path.relative(paths.root, thisBackupDir)}...`); - - // We backup EVERYTHING in torchDir except _backups and node_modules - const entries = fs.readdirSync(paths.torchDir); - for (const entry of entries) { - if (entry === '_backups' || entry === 'node_modules' || entry === '.git') continue; - - const srcPath = path.join(paths.torchDir, entry); - const destPath = path.join(thisBackupDir, entry); - fs.cpSync(srcPath, destPath, { recursive: true }); - } - - // 2. Update App Directories (Overwrite) - syncAppDirectories(paths.torchDir, 'Updated'); - - // 3. Update App Files (Overwrite) - syncAppFiles(paths.torchDir, installDirName, 'Updated'); - - // 4. Update Static Files (Always Overwrite) - console.log('Updating static files...'); - for (const file of STATIC_FILES) { - const src = path.join(SRC_PROMPTS_DIR, file); - const dest = path.join(paths.torchDir, file); - if (fs.existsSync(src)) { - copyFile(src, dest, true, true, installDirName); - console.log(` Updated ${file}`); - } - } - - // 5. Update Roster (Preserve unless force) - const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json'); - if (fs.existsSync(srcRoster)) { - if (force) { - copyFile(srcRoster, paths.roster, false, true, installDirName); - console.log(' Overwrote roster.json (forced)'); - } else { - console.log(' Skipped roster.json (preserved)'); - } - } - - // 6. Update Prompts (Copy missing, preserve existing unless force) - console.log('Updating prompts...'); - for (const dir of EVOLVING_DIRS) { - const srcDir = path.join(SRC_PROMPTS_DIR, dir); - const destDir = path.join(paths.promptsDir, dir); - ensureDir(destDir); - - if (fs.existsSync(srcDir)) { - const files = fs.readdirSync(srcDir); - let added = 0; - let updated = 0; - let skipped = 0; - - for (const file of files) { - const srcFile = path.join(srcDir, file); - const destFile = path.join(destDir, file); - - if (force) { - copyFile(srcFile, destFile, false, true, installDirName); - updated++; - } else { - if (!fs.existsSync(destFile)) { - copyFile(srcFile, destFile, false, true, installDirName); - added++; - } else { - skipped++; - } - } - } - console.log(` ${dir}/: ${added} added, ${updated} updated, ${skipped} preserved`); - } - } - - console.log('\nUpdate complete.'); -} diff --git a/torch/src/prompts/daily/ci-health-agent.md b/torch/src/prompts/daily/ci-health-agent.md deleted file mode 100644 index 1ce0b343..00000000 --- a/torch/src/prompts/daily/ci-health-agent.md +++ /dev/null @@ -1,9 +0,0 @@ -# CI Health Agent - -This agent is responsible for ensuring the repository is healthy by running tests and linters. - -1. *Run repository checks.* - - Execute `flake8 .` from the root directory to check code style. - - Execute `pytest` from the root directory to run tests. -2. *Report status.* - - Log the output of the checks. diff --git a/torch/src/prompts/roster.json b/torch/src/prompts/roster.json deleted file mode 100644 index ee25987e..00000000 --- a/torch/src/prompts/roster.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "daily": [ - "ci-health-agent" - ] -} diff --git a/torch/src/relay-health.mjs b/torch/src/relay-health.mjs deleted file mode 100644 index 0c0a824d..00000000 --- a/torch/src/relay-health.mjs +++ /dev/null @@ -1,290 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; -import WebSocket from 'ws'; -import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; -import { getNamespace, getRelays } from './torch-config.mjs'; - -const PROBE_KIND = 27235; - -function withTimeout(promise, timeoutMs, message) { - let timeoutId; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); - }); - return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId)); -} - -function nowIso() { - return new Date().toISOString(); -} - -export function summarizeHistory(historyEntries, { nowMs = Date.now(), windowMinutes = 60 } = {}) { - const windowStartMs = nowMs - (windowMinutes * 60_000); - const recent = historyEntries.filter((entry) => Date.parse(entry.timestamp || '') >= windowStartMs); - const relayProbeCount = recent.reduce((sum, entry) => sum + (entry.summary?.totalRelays || 0), 0); - const successCount = recent.reduce((sum, entry) => sum + (entry.summary?.healthyRelays || 0), 0); - const successRate = relayProbeCount > 0 ? successCount / relayProbeCount : 1; - - let lastHealthyAtMs = null; - for (let i = historyEntries.length - 1; i >= 0; i -= 1) { - if ((historyEntries[i].summary?.healthyRelays || 0) > 0) { - lastHealthyAtMs = Date.parse(historyEntries[i].timestamp || ''); - break; - } - } - - const allDownDurationMinutes = lastHealthyAtMs === null - ? null - : Math.max(0, Math.floor((nowMs - lastHealthyAtMs) / 60_000)); - - return { - windowMinutes, - sampleCount: recent.length, - successRate, - allDownDurationMinutes, - }; -} - -export function evaluateAlertThresholds(currentResult, historyEntries, thresholds, { nowMs = Date.now() } = {}) { - const stats = summarizeHistory(historyEntries, { nowMs, windowMinutes: thresholds.windowMinutes }); - const alerts = []; - - if (currentResult.summary.allRelaysUnhealthy && stats.allDownDurationMinutes !== null && stats.allDownDurationMinutes >= thresholds.allRelaysDownMinutes) { - alerts.push({ - type: 'all_relays_down_duration', - thresholdMinutes: thresholds.allRelaysDownMinutes, - actualMinutes: stats.allDownDurationMinutes, - severity: 'critical', - }); - } - - if (stats.sampleCount > 0 && stats.successRate < thresholds.minSuccessRate) { - alerts.push({ - type: 'relay_success_rate_below_threshold', - threshold: thresholds.minSuccessRate, - actual: Number(stats.successRate.toFixed(4)), - windowMinutes: thresholds.windowMinutes, - severity: 'warning', - }); - } - - return { alerts, stats }; -} - -async function probeWebSocketReachability(relayUrl, timeoutMs) { - const startedAt = Date.now(); - return withTimeout(new Promise((resolve, reject) => { - const ws = new WebSocket(relayUrl); - let settled = false; - - const finish = (fn) => (value) => { - if (settled) return; - settled = true; - try { - ws.close(); - } catch { - // ignore - } - fn(value); - }; - - ws.once('open', finish(() => resolve({ ok: true, latencyMs: Date.now() - startedAt }))); - ws.once('error', finish((err) => reject(err))); - }), timeoutMs, `WebSocket reachability timeout after ${timeoutMs}ms`); -} - -async function probePublishRead(relayUrl, namespace, timeoutMs) { - const ws = new WebSocket(relayUrl); - const sk = generateSecretKey(); - const pk = getPublicKey(sk); - const createdAt = Math.floor(Date.now() / 1000); - const nonce = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`; - const probeTag = `${namespace}-health-probe-${nonce}`; - const event = finalizeEvent({ - kind: PROBE_KIND, - created_at: createdAt, - tags: [ - ['t', `${namespace}-relay-health`], - ['d', probeTag], - ], - content: JSON.stringify({ type: 'relay_health_probe', nonce, namespace, pubkey: pk }), - }, sk); - - const subId = `probe-${nonce}`; - - return withTimeout(new Promise((resolve, reject) => { - let okAck = false; - let readSeen = false; - let settled = false; - - const settle = (fn) => (value) => { - if (settled) return; - settled = true; - try { - ws.close(); - } catch { - // ignore - } - fn(value); - }; - - ws.once('open', () => { - ws.send(JSON.stringify(['EVENT', event])); - ws.send(JSON.stringify(['REQ', subId, { ids: [event.id], limit: 1 }])); - }); - - ws.on('message', (raw) => { - let message; - try { - message = JSON.parse(raw.toString()); - } catch { - // Ignore malformed messages. The relay might be sending non-JSON data - // or we might have a protocol mismatch. We only care about valid JSON. - return; - } - if (!Array.isArray(message) || message.length < 2) return; - const type = message[0]; - - if (type === 'OK' && message[1] === event.id) { - okAck = Boolean(message[2]); - if (!okAck) { - settle(reject)(new Error(`Relay rejected health probe event: ${String(message[3] || 'unknown reason')}`)); - return; - } - } - - if (type === 'EVENT' && message[1] === subId && message[2]?.id === event.id) { - readSeen = true; - } - - if (okAck && readSeen) { - ws.send(JSON.stringify(['CLOSE', subId])); - settle(resolve)({ ok: true, eventId: event.id }); - } - }); - - ws.once('error', settle(reject)); - }), timeoutMs, `Publish/read probe timeout after ${timeoutMs}ms`); -} - -async function readHistory(historyPath) { - try { - const raw = await fs.readFile(historyPath, 'utf8'); - return raw - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - try { - return JSON.parse(line); - } catch { - return null; - } - }) - .filter((entry) => entry && typeof entry === 'object'); - } catch (error) { - if (error && error.code === 'ENOENT') return []; - throw error; - } -} - -async function appendHistory(historyPath, entry) { - await fs.mkdir(path.dirname(historyPath), { recursive: true }); - await fs.appendFile(historyPath, `${JSON.stringify(entry)}\n`, 'utf8'); -} - -export async function runRelayHealthCheck(options = {}) { - const { - cadence = 'daily', - timeoutMs = 6000, - allRelaysDownMinutes = 10, - minSuccessRate = 0.7, - windowMinutes = 60, - } = options; - - let { - relays, - namespace, - historyPath, - } = options; - - if (!relays) relays = await getRelays(); - if (!namespace) namespace = await getNamespace(); - if (!historyPath) historyPath = path.resolve(process.cwd(), 'task-logs', 'relay-health', `${cadence}.jsonl`); - - const relayResults = []; - for (const relay of relays) { - const relayResult = { - relay, - websocketReachable: false, - publishReadOk: false, - healthy: false, - checks: {}, - }; - - try { - const wsProbe = await probeWebSocketReachability(relay, timeoutMs); - relayResult.websocketReachable = true; - relayResult.checks.websocket = wsProbe; - } catch (error) { - relayResult.checks.websocket = { ok: false, error: error instanceof Error ? error.message : String(error) }; - } - - if (relayResult.websocketReachable) { - try { - const prProbe = await probePublishRead(relay, namespace, timeoutMs); - relayResult.publishReadOk = true; - relayResult.checks.publishRead = prProbe; - } catch (error) { - relayResult.checks.publishRead = { ok: false, error: error instanceof Error ? error.message : String(error) }; - } - } - - relayResult.healthy = relayResult.websocketReachable && relayResult.publishReadOk; - relayResults.push(relayResult); - } - - const summary = { - totalRelays: relayResults.length, - healthyRelays: relayResults.filter((relay) => relay.healthy).length, - }; - summary.unhealthyRelays = summary.totalRelays - summary.healthyRelays; - summary.allRelaysUnhealthy = summary.healthyRelays === 0; - - const result = { - ok: !summary.allRelaysUnhealthy, - cadence, - namespace, - timestamp: nowIso(), - relays, - relayResults, - summary, - incidentSignal: null, - alerts: [], - alertStats: null, - historyPath, - }; - - const historyEntries = await readHistory(historyPath); - const prospectiveHistory = [...historyEntries, result]; - const thresholdEval = evaluateAlertThresholds(result, prospectiveHistory, { - allRelaysDownMinutes, - minSuccessRate, - windowMinutes, - }); - result.alerts = thresholdEval.alerts; - result.alertStats = thresholdEval.stats; - - if (summary.allRelaysUnhealthy) { - const incidentId = `relay-health-${cadence}-${Date.now()}`; - result.incidentSignal = { - id: incidentId, - reason: 'All relays unhealthy before lock acquisition', - severity: 'critical', - suggestedAction: 'Defer scheduler run and investigate relay availability before retrying lock acquisition.', - }; - } - - await appendHistory(historyPath, result); - return result; -} diff --git a/torch/src/roster.mjs b/torch/src/roster.mjs deleted file mode 100644 index 99ab4149..00000000 --- a/torch/src/roster.mjs +++ /dev/null @@ -1,114 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { loadTorchConfig } from './torch-config.mjs'; - -const ROSTER_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'prompts/roster.json'); -const USER_ROSTER_FILE = path.resolve(process.cwd(), 'torch/roster.json'); -const CWD_ROSTER_FILE = path.resolve(process.cwd(), 'roster.json'); - -const FALLBACK_ROSTER = { - daily: [ - 'audit-agent', - 'ci-health-agent', - 'const-refactor-agent', - 'content-audit-agent', - 'decompose-agent', - 'deps-security-agent', - 'design-system-audit-agent', - 'docs-agent', - 'docs-alignment-agent', - 'docs-code-investigator', - 'innerhtml-migration-agent', - 'known-issues-agent', - 'load-test-agent', - 'onboarding-audit-agent', - 'perf-agent', - 'prompt-curator-agent', - 'protocol-research-agent', - 'scheduler-update-agent', - 'style-agent', - 'test-audit-agent', - 'todo-triage-agent', - ], - weekly: [ - 'bug-reproducer-agent', - 'changelog-agent', - 'dead-code-agent', - 'event-schema-agent', - 'frontend-console-debug-agent', - 'fuzz-agent', - 'interop-agent', - 'perf-deepdive-agent', - 'perf-optimization-agent', - 'pr-review-agent', - 'race-condition-agent', - 'refactor-agent', - 'smoke-agent', - 'telemetry-agent', - 'test-coverage-agent', - 'weekly-synthesis-agent', - ], -}; - -let cachedCanonicalRoster = null; - -function loadCanonicalRoster() { - if (cachedCanonicalRoster) return cachedCanonicalRoster; - - let rosterPath = ROSTER_FILE; - - // Prefer user-managed roster if present - if (fs.existsSync(USER_ROSTER_FILE)) { - rosterPath = USER_ROSTER_FILE; - } else if (fs.existsSync(CWD_ROSTER_FILE)) { - rosterPath = CWD_ROSTER_FILE; - } - - try { - const parsed = JSON.parse(fs.readFileSync(rosterPath, 'utf8')); - const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : []; - const weekly = Array.isArray(parsed.weekly) - ? parsed.weekly.map((item) => String(item).trim()).filter(Boolean) - : []; - - if (daily.length > 0 && weekly.length > 0) { - cachedCanonicalRoster = { daily, weekly }; - return cachedCanonicalRoster; - } - - console.error(`WARNING: Roster file is missing daily/weekly entries, falling back: ${rosterPath}`); - } catch { - // It's okay if roster file is missing when used as a library/CLI without the file present - } - - cachedCanonicalRoster = FALLBACK_ROSTER; - return cachedCanonicalRoster; -} - -function parseEnvRoster(value) { - if (!value) return null; - return value - .split(',') - .map((item) => item.trim()) - .filter(Boolean); -} - -export async function getRoster(cadence) { - const config = await loadTorchConfig(); - const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER); - const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER); - const canonical = loadCanonicalRoster(); - const dailyFromConfig = config.nostrLock.dailyRoster; - const weeklyFromConfig = config.nostrLock.weeklyRoster; - - if (cadence === 'daily') { - if (dailyFromEnv && dailyFromEnv.length) return dailyFromEnv; - if (dailyFromConfig && dailyFromConfig.length) return dailyFromConfig; - return canonical.daily; - } - - if (weeklyFromEnv && weeklyFromEnv.length) return weeklyFromEnv; - if (weeklyFromConfig && weeklyFromConfig.length) return weeklyFromConfig; - return canonical.weekly; -} diff --git a/torch/src/services/memory/cache.js b/torch/src/services/memory/cache.js deleted file mode 100644 index 63170421..00000000 --- a/torch/src/services/memory/cache.js +++ /dev/null @@ -1,218 +0,0 @@ -const DEFAULT_MAX_NAMESPACES = 128; -const DEFAULT_MAX_EVENTS_PER_NAMESPACE = 200; -const DEFAULT_MAX_TOTAL_EVENTS = 5000; - -function normalizeScope(scope = {}) { - const signerId = typeof scope.signer_id === 'string' && scope.signer_id.trim() - ? scope.signer_id.trim() - : 'anonymous-signer'; - const sessionId = typeof scope.session_id === 'string' && scope.session_id.trim() - ? scope.session_id.trim() - : 'anonymous-session'; - - return { signer_id: signerId, session_id: sessionId }; -} - -function getNamespaceKey(scope) { - const normalized = normalizeScope(scope); - return `${normalized.signer_id}::${normalized.session_id}`; -} - -function isEventSensitive(event) { - if (!event || typeof event !== 'object') return false; - if (event.is_decrypted === true || event.session_sensitive === true) return true; - - const classification = typeof event.classification === 'string' ? event.classification.toLowerCase() : ''; - return classification === 'decrypted' || classification === 'session-sensitive'; -} - -function canPromoteToDurableStorage(event) { - if (!isEventSensitive(event)) return true; - return event.sanitized_for_durable === true; -} - -/** - * @param {{ maxNamespaces?: number, maxEventsPerNamespace?: number, maxTotalEvents?: number }} [options] - * @returns {{ - * get: (key: string) => unknown, - * set: (key: string, value: unknown) => void, - * delete: (key: string) => boolean, - * clear: () => void, - * setRuntimeEvent: (scope: { signer_id?: string, session_id?: string }, event: Record, ttlMs?: number) => boolean, - * getRecentRuntimeEvents: (scope: { signer_id?: string, session_id?: string }, params?: { limit?: number, since?: number }) => Record[], - * clearScope: (scope: { signer_id?: string, session_id?: string }) => void, - * getMetrics: () => Record - * }} - */ -export function createMemoryCache(options = {}) { - const store = new Map(); - const runtimeNamespaces = new Map(); - const maxNamespaces = Number.isFinite(options.maxNamespaces) && options.maxNamespaces > 0 - ? Math.floor(options.maxNamespaces) - : DEFAULT_MAX_NAMESPACES; - const maxEventsPerNamespace = Number.isFinite(options.maxEventsPerNamespace) && options.maxEventsPerNamespace > 0 - ? Math.floor(options.maxEventsPerNamespace) - : DEFAULT_MAX_EVENTS_PER_NAMESPACE; - const maxTotalEvents = Number.isFinite(options.maxTotalEvents) && options.maxTotalEvents > 0 - ? Math.floor(options.maxTotalEvents) - : DEFAULT_MAX_TOTAL_EVENTS; - - let totalRuntimeEvents = 0; - const metrics = { - runtime_events_added: 0, - runtime_events_expired: 0, - runtime_events_lru_evicted: 0, - runtime_scopes_cleared: 0, - runtime_sensitive_events_blocked: 0, - }; - - function touchNamespace(key) { - const bucket = runtimeNamespaces.get(key); - if (!bucket) return; - runtimeNamespaces.delete(key); - runtimeNamespaces.set(key, bucket); - } - - function evictNamespace(key) { - const bucket = runtimeNamespaces.get(key); - if (!bucket) return; - totalRuntimeEvents -= bucket.events.length; - metrics.runtime_events_lru_evicted += bucket.events.length; - runtimeNamespaces.delete(key); - } - - function evictOldestNamespaceIfNeeded() { - while (runtimeNamespaces.size > maxNamespaces) { - const oldestKey = runtimeNamespaces.keys().next().value; - if (!oldestKey) return; - evictNamespace(oldestKey); - } - } - - function removeExpired(now = Date.now()) { - for (const [namespaceKey, bucket] of runtimeNamespaces.entries()) { - if (!bucket.events.length) continue; - - const originalLength = bucket.events.length; - bucket.events = bucket.events.filter((entry) => entry.expiresAt > now); - const removed = originalLength - bucket.events.length; - if (removed > 0) { - totalRuntimeEvents -= removed; - metrics.runtime_events_expired += removed; - } - - if (bucket.events.length === 0) { - runtimeNamespaces.delete(namespaceKey); - } - } - } - - function enforceEventBounds(namespaceKey) { - const bucket = runtimeNamespaces.get(namespaceKey); - if (!bucket) return; - - while (bucket.events.length > maxEventsPerNamespace) { - bucket.events.shift(); - totalRuntimeEvents -= 1; - metrics.runtime_events_lru_evicted += 1; - } - - while (totalRuntimeEvents > maxTotalEvents) { - const oldestNamespaceKey = runtimeNamespaces.keys().next().value; - if (!oldestNamespaceKey) return; - const oldestBucket = runtimeNamespaces.get(oldestNamespaceKey); - if (!oldestBucket || oldestBucket.events.length === 0) { - runtimeNamespaces.delete(oldestNamespaceKey); - continue; - } - - oldestBucket.events.shift(); - totalRuntimeEvents -= 1; - metrics.runtime_events_lru_evicted += 1; - if (oldestBucket.events.length === 0) { - runtimeNamespaces.delete(oldestNamespaceKey); - } - } - } - - return { - get(key) { - return store.get(key); - }, - set(key, value) { - store.set(key, value); - }, - delete(key) { - return store.delete(key); - }, - clear() { - store.clear(); - runtimeNamespaces.clear(); - totalRuntimeEvents = 0; - }, - setRuntimeEvent(scope, event, ttlMs = 60_000) { - const now = Date.now(); - removeExpired(now); - - const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : 60_000; - const expiresAt = now + safeTtlMs; - const namespaceKey = getNamespaceKey(scope); - const bucket = runtimeNamespaces.get(namespaceKey) ?? { events: [] }; - - bucket.events.push({ - event, - createdAt: now, - expiresAt, - durableEligible: canPromoteToDurableStorage(event), - }); - - totalRuntimeEvents += 1; - metrics.runtime_events_added += 1; - if (!canPromoteToDurableStorage(event)) { - metrics.runtime_sensitive_events_blocked += 1; - } - - runtimeNamespaces.set(namespaceKey, bucket); - touchNamespace(namespaceKey); - evictOldestNamespaceIfNeeded(); - enforceEventBounds(namespaceKey); - return true; - }, - getRecentRuntimeEvents(scope, params = {}) { - const now = Date.now(); - removeExpired(now); - - const namespaceKey = getNamespaceKey(scope); - const bucket = runtimeNamespaces.get(namespaceKey); - if (!bucket) return []; - - touchNamespace(namespaceKey); - - const limit = Number.isFinite(params.limit) && params.limit > 0 ? Math.floor(params.limit) : 50; - const since = Number.isFinite(params.since) ? params.since : 0; - - return bucket.events - .filter((entry) => entry.createdAt >= since) - .slice(-limit) - .map((entry) => ({ - ...entry.event, - durable_eligible: entry.durableEligible, - })); - }, - clearScope(scope) { - const namespaceKey = getNamespaceKey(scope); - const bucket = runtimeNamespaces.get(namespaceKey); - if (!bucket) return; - totalRuntimeEvents -= bucket.events.length; - metrics.runtime_scopes_cleared += 1; - runtimeNamespaces.delete(namespaceKey); - }, - getMetrics() { - return { - ...metrics, - runtime_active_namespaces: runtimeNamespaces.size, - runtime_active_events: totalRuntimeEvents, - }; - }, - }; -} diff --git a/torch/src/services/memory/embedder.js b/torch/src/services/memory/embedder.js deleted file mode 100644 index ba26c63d..00000000 --- a/torch/src/services/memory/embedder.js +++ /dev/null @@ -1,134 +0,0 @@ -const DEFAULT_VECTOR_BACKEND = 'inmemory'; -const VECTOR_BACKEND_ENV = 'MEMORY_VECTOR_BACKEND'; - -/** - * @typedef {{ - * embedText: (text: string) => Promise | number[], - * upsertVector: (input: { id: string, vector: number[], metadata?: Record }) => Promise | void, - * queryVector: (input: { vector: number[], k?: number, filter?: Record }) => Promise }>> | Array<{ id: string, score: number, vector: number[], metadata: Record }>, - * deleteVector: (id: string) => Promise | boolean, - * }} EmbedderAdapter - */ - -/** - * @param {string} text - * @returns {number[]} - */ -function fallbackEmbedding(_text) { - // Disabled as per configuration: Jules uses text-based memory, not vector embeddings. - return []; -} - -function cosineSimilarity(left, right) { - const size = Math.max(left.length, right.length); - let dot = 0; - let leftNorm = 0; - let rightNorm = 0; - - for (let index = 0; index < size; index += 1) { - const leftValue = Number(left[index] ?? 0); - const rightValue = Number(right[index] ?? 0); - dot += leftValue * rightValue; - leftNorm += leftValue ** 2; - rightNorm += rightValue ** 2; - } - - if (leftNorm === 0 || rightNorm === 0) return 0; - return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm)); -} - -/** @implements {EmbedderAdapter} */ -class InMemoryEmbedderAdapter { - constructor() { - this.vectors = new Map(); - } - - /** @param {string} text */ - async embedText(text) { - return fallbackEmbedding(text); - } - - /** @param {{ id: string, vector: number[], metadata?: Record }} input */ - async upsertVector({ id, vector, metadata = {} }) { - if (typeof id !== 'string' || id.length === 0) { - throw new TypeError('upsertVector requires a non-empty id'); - } - if (!Array.isArray(vector) || vector.length === 0) { - // In strict mode this might throw, but since we return [] from embedText, - // ingestor won't call this. If someone calls it manually with [], we can just ignore or warn. - // But adhering to the interface, we should probably throw or just return. - // Given we want to support "no embedding", simply returning is safer than throwing. - return; - } - this.vectors.set(id, { vector: [...vector], metadata: { ...metadata } }); - } - - /** @param {{ vector: number[], k?: number, filter?: Record }} input */ - async queryVector({ vector, k = 5, filter = {} }) { - if (!Array.isArray(vector) || vector.length === 0) return []; - - const matchesFilter = (metadata) => Object.entries(filter).every(([key, value]) => metadata?.[key] === value); - - return [...this.vectors.entries()] - .filter(([, value]) => matchesFilter(value.metadata)) - .map(([id, value]) => ({ - id, - score: cosineSimilarity(vector, value.vector), - vector: [...value.vector], - metadata: { ...value.metadata }, - })) - .sort((left, right) => right.score - left.score) - .slice(0, Number.isFinite(k) && k > 0 ? Math.floor(k) : 5); - } - - /** @param {string} id */ - async deleteVector(id) { - if (typeof id !== 'string' || id.length === 0) return false; - return this.vectors.delete(id); - } -} - -const ADAPTER_FACTORIES = { - inmemory: () => new InMemoryEmbedderAdapter(), - local: () => new InMemoryEmbedderAdapter(), - mock: () => new InMemoryEmbedderAdapter(), -}; - -let defaultAdapter; - -/** - * @param {{ backend?: string }} [options] - * @returns {EmbedderAdapter} - */ -export function createEmbedderAdapter(options = {}) { - const configuredBackend = String(options.backend ?? process.env[VECTOR_BACKEND_ENV] ?? DEFAULT_VECTOR_BACKEND).trim().toLowerCase(); - const factory = ADAPTER_FACTORIES[configuredBackend]; - if (!factory) { - throw new Error(`Unsupported vector backend "${configuredBackend}". Configure ${VECTOR_BACKEND_ENV}=inmemory (default) until provider adapters are added.`); - } - return factory(); -} - -/** - * @returns {EmbedderAdapter} - */ -export function getDefaultEmbedderAdapter() { - if (!defaultAdapter) { - defaultAdapter = createEmbedderAdapter(); - } - return defaultAdapter; -} - -/** - * @param {string} text - * @param {{ embedText?: (value: string) => Promise | number[], adapter?: EmbedderAdapter }} [options] - * @returns {Promise} - */ -export async function embedText(text, options = {}) { - if (typeof options.embedText === 'function') { - return options.embedText(text); - } - - const adapter = options.adapter ?? getDefaultEmbedderAdapter(); - return adapter.embedText(text); -} diff --git a/torch/src/services/memory/feature-flags.js b/torch/src/services/memory/feature-flags.js deleted file mode 100644 index 784f3760..00000000 --- a/torch/src/services/memory/feature-flags.js +++ /dev/null @@ -1,61 +0,0 @@ -const FALSEY = new Set(['0', 'false', 'off', 'no', 'disabled']); -const TRUTHY = new Set(['1', 'true', 'on', 'yes', 'enabled', 'all']); - -function normalize(value) { - return String(value ?? '').trim(); -} - -function parseAgentToggle(value) { - const raw = normalize(value); - if (!raw) return { kind: 'unset' }; - - const lower = raw.toLowerCase(); - if (FALSEY.has(lower)) return { kind: 'off' }; - if (TRUTHY.has(lower)) return { kind: 'all' }; - - const allowList = raw - .split(',') - .map((item) => item.trim()) - .filter(Boolean); - - return allowList.length > 0 ? { kind: 'allow-list', allowList } : { kind: 'off' }; -} - -function isToggleEnabledForAgent(value, agentId, fallbackWhenUnset) { - const parsed = parseAgentToggle(value); - if (parsed.kind === 'unset') return fallbackWhenUnset; - if (parsed.kind === 'off') return false; - if (parsed.kind === 'all') return true; - if (!agentId) return false; - return parsed.allowList.includes(String(agentId)); -} - -export function isMemoryEnabled(env = process.env) { - return isToggleEnabledForAgent(env.TORCH_MEMORY_ENABLED, undefined, true); -} - -export function isMemoryIngestEnabled(agentId, env = process.env) { - if (!isMemoryEnabled(env)) return false; - return isToggleEnabledForAgent(env.TORCH_MEMORY_INGEST_ENABLED, agentId, true); -} - -export function isMemoryRetrievalEnabled(agentId, env = process.env) { - if (!isMemoryEnabled(env)) return false; - return isToggleEnabledForAgent(env.TORCH_MEMORY_RETRIEVAL_ENABLED, agentId, false); -} - -export function getMemoryPruneMode(env = process.env) { - if (!isMemoryEnabled(env)) return 'off'; - - const raw = normalize(env.TORCH_MEMORY_PRUNE_ENABLED); - if (!raw) return 'active'; - - const lower = raw.toLowerCase(); - if (FALSEY.has(lower)) return 'off'; - if (['dry-run', 'dryrun', 'report', 'audit'].includes(lower)) return 'dry-run'; - return 'active'; -} - -export function isMemoryPruneEnabled(env = process.env) { - return getMemoryPruneMode(env) === 'active'; -} diff --git a/torch/src/services/memory/formatter.js b/torch/src/services/memory/formatter.js deleted file mode 100644 index 3f3509b8..00000000 --- a/torch/src/services/memory/formatter.js +++ /dev/null @@ -1,194 +0,0 @@ -import { isMemoryRetrievalEnabled } from './feature-flags.js'; -export { isMemoryRetrievalEnabled } from './feature-flags.js'; -const DEFAULT_TOKEN_BUDGET = 1000; -const DEFAULT_MAX_SUMMARY_CHARS = 240; -const DEFAULT_MAX_CONTEXT_CHARS = 320; -const MEMORY_CONTEXT_HEADER = '[CONTEXT — Relevant memories]'; - -function clampString(value, maxChars) { - if (typeof value !== 'string') return ''; - if (!Number.isFinite(maxChars) || maxChars <= 0) return ''; - if (value.length <= maxChars) return value; - return `${value.slice(0, Math.max(0, maxChars - 1))}…`; -} - -/** - * Fast heuristic (approx 4 chars/token) suitable for guardrail budgeting. - * @param {string} text - */ -export function estimateTokenCount(text) { - const normalized = String(text ?? ''); - if (normalized.length === 0) return 0; - return Math.ceil(normalized.length / 4); -} - -/** - * @param {import('./schema.js').MemoryRecord} memory - * @param {{ maxSummaryChars?: number, maxContextChars?: number }} [options] - */ -export function formatMemoryBlock(memory, options = {}) { - const summary = clampString(memory.summary ?? '', options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS); - const shortContext = clampString(memory.content ?? '', options.maxContextChars ?? DEFAULT_MAX_CONTEXT_CHARS); - const timestamp = new Date(memory.created_at).toISOString(); - const tags = Array.isArray(memory.tags) ? memory.tags.join(',') : ''; - - return { - id: memory.id, - timestamp, - importance: Number(memory.importance ?? 0).toFixed(2), - summary, - short_context: shortContext, - tags, - }; -} - - -function fitBlockToBudget(memory, options, maxTokens) { - let maxSummaryChars = options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS; - let maxContextChars = options.maxContextChars ?? DEFAULT_MAX_CONTEXT_CHARS; - - while (maxSummaryChars >= 8 && maxContextChars >= 8) { - const block = formatMemoryBlock(memory, { maxSummaryChars, maxContextChars }); - const line = blockToLine(block); - if (estimateTokenCount(line) <= maxTokens) { - return { block, line }; - } - - if (maxContextChars >= maxSummaryChars) { - maxContextChars = Math.max(8, Math.floor(maxContextChars * 0.7)); - } else { - maxSummaryChars = Math.max(8, Math.floor(maxSummaryChars * 0.7)); - } - - if (maxSummaryChars === 8 && maxContextChars === 8) break; - } - - const block = formatMemoryBlock(memory, { maxSummaryChars: 8, maxContextChars: 8 }); - return { block, line: blockToLine(block) }; -} -function blockToLine(block) { - return `[${block.id}, ${block.timestamp}, ${block.importance}, ${block.summary}, ${block.short_context}, ${block.tags}]`; -} - -/** - * @param {import('./schema.js').MemoryRecord[]} memories - * @param {{ k?: number, tokenBudget?: number, maxSummaryChars?: number, maxContextChars?: number }} [options] - */ -export function formatMemoriesForPrompt(memories, options = {}) { - const k = Number.isFinite(options.k) && options.k > 0 ? Math.floor(options.k) : memories.length; - const tokenBudget = Number.isFinite(options.tokenBudget) && options.tokenBudget > 0 - ? Math.floor(options.tokenBudget) - : DEFAULT_TOKEN_BUDGET; - - const blocks = []; - let usedTokens = 0; - - for (const memory of memories.slice(0, k)) { - const remainingTokens = tokenBudget - usedTokens; - if (remainingTokens <= 0) break; - - const fitted = fitBlockToBudget(memory, options, remainingTokens); - const block = fitted.block; - const line = fitted.line; - const lineTokens = estimateTokenCount(line); - - if (usedTokens + lineTokens > tokenBudget) { - break; - } - - blocks.push(block); - usedTokens += lineTokens; - } - - return { - blocks, - text: blocks.map(blockToLine).join('\n'), - usedTokens, - truncated: blocks.length < Math.min(k, memories.length), - }; -} - -function toContextString(agentContext) { - if (typeof agentContext === 'string') { - return agentContext.trim(); - } - - if (agentContext == null) { - return ''; - } - - if (Array.isArray(agentContext)) { - return agentContext.map((value) => String(value)).join('\n').trim(); - } - - if (typeof agentContext === 'object') { - try { - return JSON.stringify(agentContext); - } catch { - return String(agentContext).trim(); - } - } - - return String(agentContext).trim(); -} - -/** - * Builds a final prompt with retrieved memory context inserted ahead of the base prompt. - * - * @param {{ - * basePrompt: string, - * userRequest: string, - * agentContext?: unknown, - * agent_id: string, - * memoryService: { getRelevantMemories: (params: { agent_id: string, query: string, k?: number }) => Promise | import('./schema.js').MemoryRecord[] }, - * retrievalK?: number, - * tokenBudget?: number, - * env?: Record - * }} params - */ -export async function renderPromptWithMemoryContext(params) { - const { - basePrompt, - userRequest, - agentContext, - agent_id, - memoryService, - retrievalK = 5, - tokenBudget = 320, - env = process.env, - } = params; - - if (!isMemoryRetrievalEnabled(agent_id, env)) { - return String(basePrompt ?? ''); - } - - const requestText = String(userRequest ?? '').trim(); - const contextText = toContextString(agentContext); - const query = [requestText, contextText].filter(Boolean).join('\n\n'); - - if (!query || !memoryService?.getRelevantMemories) { - return String(basePrompt ?? ''); - } - - const memories = await memoryService.getRelevantMemories({ - agent_id, - query, - k: retrievalK, - }); - - if (memoryService?.updateMemoryUsage) { - const now = Date.now(); - await Promise.all((memories ?? []).map((memory) => memoryService.updateMemoryUsage(memory.id, now))); - } - - const rendered = formatMemoriesForPrompt(memories ?? [], { - k: retrievalK, - tokenBudget, - }); - - if (!rendered.text) { - return String(basePrompt ?? ''); - } - - return `${MEMORY_CONTEXT_HEADER}\n${rendered.text}\n\n${String(basePrompt ?? '')}`; -} diff --git a/torch/src/services/memory/index.js b/torch/src/services/memory/index.js deleted file mode 100644 index cf7375fb..00000000 --- a/torch/src/services/memory/index.js +++ /dev/null @@ -1,439 +0,0 @@ -import { createMemoryCache } from './cache.js'; -import { ingestMemoryWindow } from './ingestor.js'; -import { listPruneCandidates, selectPrunableMemories } from './pruner.js'; -import { filterAndRankMemories, updateMemoryUsage } from './retriever.js'; -import { startScheduler } from './scheduler.js'; -import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js'; -import fs from 'node:fs'; -import path from 'node:path'; -import util from 'node:util'; - -const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json'); -const debug = util.debuglog('torch-memory'); - -function loadMemoryStore() { - try { - if (fs.existsSync(MEMORY_FILE_PATH)) { - const data = fs.readFileSync(MEMORY_FILE_PATH, 'utf8'); - const entries = JSON.parse(data); - if (Array.isArray(entries)) { - return new Map(entries); - } - } - } catch (err) { - console.error('Failed to load memory store:', err); - } - return new Map(); -} - -function saveMemoryStore(store) { - try { - const dir = path.dirname(MEMORY_FILE_PATH); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - const entries = [...store.entries()]; - fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify(entries, null, 2), 'utf8'); - } catch (err) { - console.error('Failed to save memory store:', err); - } -} - -const memoryStore = loadMemoryStore(); -const cache = createMemoryCache(); - -const memoryRepository = { - async insertMemory(memory) { - memoryStore.set(memory.id, memory); - saveMemoryStore(memoryStore); - return memory; - }, - async updateMemoryUsage(id, lastSeen = Date.now()) { - const existing = memoryStore.get(id); - if (!existing) return null; - const updated = { ...existing, last_seen: lastSeen }; - memoryStore.set(id, updated); - saveMemoryStore(memoryStore); - return updated; - }, - async listPruneCandidates({ cutoff }) { - return [...memoryStore.values()].filter((memory) => !memory.pinned && memory.last_seen < cutoff); - }, - async markMerged(id, mergedInto) { - const existing = memoryStore.get(id); - if (!existing) return false; - memoryStore.set(id, { ...existing, merged_into: mergedInto, last_seen: Date.now() }); - saveMemoryStore(memoryStore); - return true; - }, - async setPinned(id, pinned) { - const existing = memoryStore.get(id); - if (!existing) return null; - const updated = { ...existing, pinned, last_seen: Date.now() }; - memoryStore.set(id, updated); - saveMemoryStore(memoryStore); - return updated; - }, - async getMemoryById(id) { - return memoryStore.get(id) ?? null; - }, - async listMemories() { - return [...memoryStore.values()]; - }, -}; - -const memoryStatsState = { - ingested: [], - retrieved: [], - pruned: [], - archived: 0, - deleted: 0, -}; - -const ESTIMATED_INDEX_BYTES_PER_RECORD = 512; - -function toSafeTelemetryPayload(payload = {}) { - const blockedFields = new Set(['content', 'summary', 'query', 'raw']); - return Object.entries(payload).reduce((safe, [key, value]) => { - if (blockedFields.has(key)) return safe; - if (typeof value === 'string' && value.length > 300) { - safe[key] = `${value.slice(0, 300)}…`; - return safe; - } - safe[key] = value; - return safe; - }, {}); -} - -function buildTelemetryEmitter(options = {}) { - if (typeof options.emitTelemetry === 'function') { - return (event, payload) => options.emitTelemetry(event, toSafeTelemetryPayload(payload)); - } - - if (typeof options.telemetry?.emit === 'function') { - return (event, payload) => options.telemetry.emit(event, toSafeTelemetryPayload(payload)); - } - - return (event, payload) => { - debug('memory_telemetry', { - event, - payload: toSafeTelemetryPayload(payload), - ts: Date.now(), - }); - }; -} - -function emitMetric(options = {}, metric, payload) { - if (typeof options.emitMetric === 'function') { - options.emitMetric(metric, payload); - return; - } - - if (typeof options.metrics?.emit === 'function') { - options.metrics.emit(metric, payload); - return; - } - - debug('memory_metric', { metric, payload, ts: Date.now() }); -} - -function applyMemoryFilters(memories, filters = {}) { - const tags = Array.isArray(filters.tags) - ? filters.tags.map((tag) => String(tag).trim()).filter(Boolean) - : []; - - return memories.filter((memory) => { - if (filters.agent_id && memory.agent_id !== filters.agent_id) return false; - if (filters.type && memory.type !== filters.type) return false; - if (typeof filters.pinned === 'boolean' && memory.pinned !== filters.pinned) return false; - if (typeof filters.includeMerged === 'boolean' && !filters.includeMerged && memory.merged_into) return false; - if (tags.length > 0) { - const memoryTags = new Set(memory.tags); - if (!tags.every((tag) => memoryTags.has(tag))) return false; - } - return true; - }); -} - -function recordThroughput(samples, now) { - samples.push(now); - if (samples.length > 10_000) { - samples.splice(0, samples.length - 10_000); - } -} - -/** - * Ingests agent events and stores them as memory records. - * - * @param {import('./schema.js').MemoryEvent[]} events - Ordered list of raw events to persist. - * @param {{ maxSummaryLength?: number, embedText?: (value: string) => Promise | number[], repository?: typeof memoryRepository }} [options] - Optional ingest tuning. - * @returns {Promise} Stored memory records. - */ -export async function ingestEvents(events, options = {}) { - const repository = options.repository ?? memoryRepository; - const telemetry = buildTelemetryEmitter(options); - if (!isMemoryIngestEnabled(options.agent_id, options.env)) { - telemetry('memory:ingest_skipped', { reason: 'flag_disabled', agent_id: options.agent_id ?? null }); - return []; - } - const records = await ingestMemoryWindow({ events }, { - ...options, - repository, - emitTelemetry: (event, payload) => { - telemetry(event, payload); - if (event === 'memory:ingested') { - recordThroughput(memoryStatsState.ingested, Date.now()); - emitMetric(options, 'memory_ingested_total', { count: 1 }); - } - }, - }); - cache.clear(); - return records; -} - -/** - * Retrieves top-k memories for an agent query. - * - * @param {{ agent_id: string, query?: string, tags?: string[], timeframe?: { from?: number, to?: number }, k?: number, repository?: typeof memoryRepository }} params - Retrieval filter and ranking inputs. - * @returns {Promise} Sorted list of relevant memories. - */ -export async function getRelevantMemories(params) { - const { - repository = memoryRepository, - ranker = filterAndRankMemories, - cache: cacheProvider = cache, - ...queryParams - } = params; - const telemetry = buildTelemetryEmitter(params); - const cacheKey = JSON.stringify(queryParams); - const cached = cacheProvider.get(cacheKey); - if (cached) { - telemetry('memory:retrieved', { - agent_id: queryParams.agent_id, - result_count: cached.length, - cache_hit: true, - query_present: Boolean(queryParams.query), - query_length: typeof queryParams.query === 'string' ? queryParams.query.length : 0, - tags_count: Array.isArray(queryParams.tags) ? queryParams.tags.length : 0, - }); - emitMetric(params, 'memory_retrieved_total', { count: cached.length, cache_hit: true }); - recordThroughput(memoryStatsState.retrieved, Date.now()); - return cached; - } - - const source = typeof repository.listMemories === 'function' - ? await repository.listMemories(queryParams) - : [...memoryStore.values()]; - - const ranked = await ranker(source, queryParams); - await updateMemoryUsage(repository, ranked.map((memory) => memory.id)); - cacheProvider.set(cacheKey, ranked); - - telemetry('memory:retrieved', { - agent_id: queryParams.agent_id, - result_count: ranked.length, - cache_hit: false, - query_present: Boolean(queryParams.query), - query_length: typeof queryParams.query === 'string' ? queryParams.query.length : 0, - tags_count: Array.isArray(queryParams.tags) ? queryParams.tags.length : 0, - }); - emitMetric(params, 'memory_retrieved_total', { count: ranked.length, cache_hit: false }); - recordThroughput(memoryStatsState.retrieved, Date.now()); - - return ranked; -} - -/** - * Runs a memory pruning pass and removes unpinned stale records. - * - * @param {{ retentionMs?: number, now?: number, scheduleEveryMs?: number, repository?: typeof memoryRepository }} [options] - Prune configuration; if scheduleEveryMs is set, returns a scheduler handle. - * @returns {Promise<{ pruned: import('./schema.js').MemoryRecord[], scheduler?: { stop: () => void } }>} Pruned records and optional scheduler handle. - */ -export async function runPruneCycle(options = {}) { - const repository = options.repository ?? memoryRepository; - const telemetry = buildTelemetryEmitter(options); - const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30); - const pruneMode = options.pruneMode ?? getMemoryPruneMode(options.env); - const dbCandidates = await listPruneCandidates(repository, { - retentionMs, - now: options.now, - }); - - const prunable = dbCandidates.length > 0 - ? dbCandidates - : selectPrunableMemories([...memoryStore.values()], { - retentionMs, - now: options.now, - }); - - if (pruneMode === 'off') { - telemetry('memory:prune_skipped', { reason: 'flag_disabled', retention_ms: retentionMs }); - return { pruned: [], mode: 'off' }; - } - - if (pruneMode === 'dry-run') { - telemetry('memory:pruned', { - pruned_count: prunable.length, - retention_ms: retentionMs, - dry_run: true, - }); - emitMetric(options, 'memory_pruned_total', { count: 0, retention_ms: retentionMs, dry_run: true }); - return { pruned: [], candidates: prunable, mode: 'dry-run' }; - } - - for (const memory of prunable) { - memoryStore.delete(memory.id); - } - - if (prunable.length > 0) { - saveMemoryStore(memoryStore); - } - - memoryStatsState.deleted += prunable.length; - recordThroughput(memoryStatsState.pruned, Date.now()); - telemetry('memory:pruned', { - pruned_count: prunable.length, - retention_ms: retentionMs, - }); - emitMetric(options, 'memory_pruned_total', { count: prunable.length, retention_ms: retentionMs }); - - cache.clear(); - - const result = { pruned: prunable }; - if (Number.isFinite(options.scheduleEveryMs) && options.scheduleEveryMs > 0) { - result.scheduler = startScheduler(() => runPruneCycle({ retentionMs, repository, env: options.env, pruneMode: options.pruneMode }), { - intervalMs: options.scheduleEveryMs, - }); - } - - return result; -} - -/** - * Pins a memory to prevent pruning and boost retrieval ranking. - * - * @param {string} id - Memory record identifier. - * @returns {Promise} Updated memory record or null when not found. - */ -export async function pinMemory(id, options = {}) { - const repository = options.repository ?? memoryRepository; - const updated = await repository.setPinned(id, true); - cache.clear(); - return updated; -} - -/** - * Removes a pin from a memory so it can be pruned normally. - * - * @param {string} id - Memory record identifier. - * @returns {Promise} Updated memory record or null when not found. - */ -export async function unpinMemory(id, options = {}) { - const repository = options.repository ?? memoryRepository; - const updated = await repository.setPinned(id, false); - cache.clear(); - return updated; -} - -/** - * Marks one memory as merged into another. - * - * @param {string} id - * @param {string} mergedInto - */ -export async function markMemoryMerged(id, mergedInto, options = {}) { - const repository = options.repository ?? memoryRepository; - const merged = await repository.markMerged(id, mergedInto); - cache.clear(); - return merged; -} - -export async function listMemories(filters = {}, options = {}) { - const repository = options.repository ?? memoryRepository; - const source = typeof repository.listMemories === 'function' - ? await repository.listMemories(filters) - : [...memoryStore.values()]; - - const filtered = applyMemoryFilters(source, filters) - .sort((left, right) => right.last_seen - left.last_seen); - - const offset = Number.isFinite(filters.offset) ? Math.max(0, Number(filters.offset)) : 0; - const limit = Number.isFinite(filters.limit) - ? Math.max(1, Math.floor(Number(filters.limit))) - : filtered.length; - - return filtered.slice(offset, offset + limit); -} - -export async function inspectMemory(id, options = {}) { - const repository = options.repository ?? memoryRepository; - if (typeof repository.getMemoryById === 'function') { - return repository.getMemoryById(id); - } - return memoryStore.get(id) ?? null; -} - -export async function triggerPruneDryRun(options = {}) { - const repository = options.repository ?? memoryRepository; - const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30); - const dbCandidates = await listPruneCandidates(repository, { - retentionMs, - now: options.now, - }); - - const candidates = dbCandidates.length > 0 - ? dbCandidates - : selectPrunableMemories(await listMemories({}, { repository }), { - retentionMs, - now: options.now, - }); - - return { - dryRun: true, - retentionMs, - candidateCount: candidates.length, - candidates: candidates.map((memory) => ({ - id: memory.id, - agent_id: memory.agent_id, - type: memory.type, - pinned: memory.pinned, - last_seen: memory.last_seen, - merged_into: memory.merged_into, - })), - }; -} - -export async function memoryStats(options = {}) { - const repository = options.repository ?? memoryRepository; - const now = options.now ?? Date.now(); - const windowMs = Number.isFinite(options.windowMs) ? Math.max(1, Number(options.windowMs)) : (60 * 60 * 1000); - const memories = await listMemories({}, { repository }); - - const countsByType = memories.reduce((acc, memory) => { - acc[memory.type] = (acc[memory.type] ?? 0) + 1; - return acc; - }, {}); - - const archivedCount = memories.filter((memory) => Boolean(memory.merged_into)).length; - const deletedRate = memoryStatsState.deleted / Math.max(1, memoryStatsState.ingested.length); - const archivedRate = archivedCount / Math.max(1, memories.length); - - const ingestedInWindow = memoryStatsState.ingested.filter((timestamp) => timestamp >= (now - windowMs)).length; - const ingestThroughputPerMinute = ingestedInWindow / Math.max(1, (windowMs / 60_000)); - - return { - countsByType, - totals: { - total: memories.length, - pinned: memories.filter((memory) => memory.pinned).length, - archived: archivedCount, - deletedObserved: memoryStatsState.deleted, - }, - rates: { - archivedRate, - deletedRate, - }, - indexSizeEstimateBytes: memories.length * ESTIMATED_INDEX_BYTES_PER_RECORD, - ingestThroughputPerMinute, - }; -} diff --git a/torch/src/services/memory/ingestor.js b/torch/src/services/memory/ingestor.js deleted file mode 100644 index 5f9ac422..00000000 --- a/torch/src/services/memory/ingestor.js +++ /dev/null @@ -1,291 +0,0 @@ -import { createHash, randomUUID } from 'node:crypto'; -import { createMemoryRecord, normalizeEvent, validateMemoryItem } from './schema.js'; -import { embedText, getDefaultEmbedderAdapter } from './embedder.js'; -import { summarizeEvents } from './summarizer.js'; - -const WINDOW_BUCKET_MS = 60_000; -const MAX_DEDUPE_KEYS = 2000; -const dedupeWindow = new Map(); - -function pruneDedupeWindow() { - while (dedupeWindow.size > MAX_DEDUPE_KEYS) { - const oldestKey = dedupeWindow.keys().next().value; - if (!oldestKey) break; - dedupeWindow.delete(oldestKey); - } -} - -function redactObviousPii(text) { - if (typeof text !== 'string' || text.length === 0) return ''; - return text - .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted:email]') - .replace(/\b(?:\+?\d{1,2}[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})\b/g, '[redacted:phone]') - .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[redacted:ssn]'); -} - -function estimateTokens(text) { - if (!text) return 0; - return Math.ceil(text.length / 4); -} - -function chunkEvents(events, options = {}) { - const maxChunkChars = Number.isFinite(options.maxChunkChars) && options.maxChunkChars > 0 - ? Math.floor(options.maxChunkChars) - : 1_200; - const maxChunkTokens = Number.isFinite(options.maxChunkTokens) && options.maxChunkTokens > 0 - ? Math.floor(options.maxChunkTokens) - : 300; - - const chunks = []; - let currentChunk = []; - let currentChars = 0; - let currentTokens = 0; - - const flush = () => { - if (currentChunk.length > 0) { - chunks.push(currentChunk); - currentChunk = []; - currentChars = 0; - currentTokens = 0; - } - }; - - for (const event of events) { - const eventChars = event.content.length; - const eventTokens = estimateTokens(event.content); - const overBudget = (currentChars + eventChars > maxChunkChars) || (currentTokens + eventTokens > maxChunkTokens); - if (overBudget) { - flush(); - } - - currentChunk.push(event); - currentChars += eventChars; - currentTokens += eventTokens; - } - - flush(); - return chunks; -} - -function hashIngestionWindow({ agentId, bucket, content }) { - return createHash('sha256').update(`${agentId}|${bucket}|${content}`).digest('hex'); -} - -async function isDuplicateWindow(repository, dedupeKey) { - if (dedupeWindow.has(dedupeKey)) return true; - if (typeof repository?.hasIngestionFingerprint === 'function') { - const exists = await repository.hasIngestionFingerprint(dedupeKey); - if (exists) return true; - } - return false; -} - -async function rememberWindowFingerprint(repository, dedupeKey, payload) { - dedupeWindow.set(dedupeKey, payload); - pruneDedupeWindow(); - if (typeof repository?.storeIngestionFingerprint === 'function') { - await repository.storeIngestionFingerprint(dedupeKey, payload); - } -} - - -function compactContextExcerpt(text, maxChars = 280) { - if (typeof text !== 'string' || text.length === 0) return ''; - const compact = text.replace(/\s+/g, ' ').trim(); - if (compact.length <= maxChars) return compact; - return `${compact.slice(0, maxChars - 1)}…`; -} - -function buildTelemetryEmitter(options = {}) { - if (typeof options.emitTelemetry === 'function') return options.emitTelemetry; - if (typeof options.telemetry?.emit === 'function') { - return (event, payload) => options.telemetry.emit(event, payload); - } - return () => {}; -} - -/** - * @param {import('./schema.js').MemoryEvent[]} events - * @returns {import('./schema.js').MemoryEvent[]} - */ -export function normalizeEvents(events) { - return events.map(normalizeEvent); -} - -/** - * @param {{ - * events?: import('./schema.js').MemoryEvent[], - * runtimeCache?: { getRecentRuntimeEvents?: (scope: Record, params?: { since?: number, limit?: number }) => Record[] }, - * runtimeScope?: Record, - * logSource?: { getEvents?: (params: { from: number, to: number, agent_id?: string }) => Promise[]> | Record[], fetchEvents?: (params: { from: number, to: number, agent_id?: string }) => Promise[]> | Record[] }, - * agent_id?: string, - * windowStart?: number, - * windowEnd?: number, - * }} input - * @param {{ - * repository?: { insertMemory: (memory: import('./schema.js').MemoryRecord) => Promise, hasIngestionFingerprint?: (dedupeKey: string) => Promise, storeIngestionFingerprint?: (dedupeKey: string, payload: Record) => Promise, linkEmbedding?: (memoryId: string, embedding: { id: string, vector: number[] }) => Promise }, - * maxSummaryLength?: number, - * maxChunkChars?: number, - * maxChunkTokens?: number, - * embedText?: (value: string) => Promise | number[], - * adapter?: import('./embedder.js').EmbedderAdapter, - * embedderAdapter?: import('./embedder.js').EmbedderAdapter, - * contextExcerptChars?: number, - * emitTelemetry?: (event: string, payload: Record) => void, - * telemetry?: { emit: (event: string, payload: Record) => void }, - * now?: number, - * windowBucketMs?: number, - * }} options - * @returns {Promise} - */ -export async function ingestMemoryWindow(input = {}, options = {}) { - const repository = options.repository; - if (!repository || typeof repository.insertMemory !== 'function') { - throw new TypeError('Memory ingest requires a repository with insertMemory'); - } - - const emitTelemetry = buildTelemetryEmitter(options); - const windowStart = Number.isFinite(input.windowStart) ? Number(input.windowStart) : 0; - const windowEnd = Number.isFinite(input.windowEnd) ? Number(input.windowEnd) : (Number.isFinite(options.now) ? Number(options.now) : Date.now()); - - const runtimeEvents = typeof input.runtimeCache?.getRecentRuntimeEvents === 'function' - ? input.runtimeCache.getRecentRuntimeEvents(input.runtimeScope ?? {}, { since: windowStart, limit: 500 }) - : []; - - const logReader = input.logSource?.fetchEvents ?? input.logSource?.getEvents; - const logEvents = typeof logReader === 'function' - ? await logReader({ from: windowStart, to: windowEnd, agent_id: input.agent_id }) - : []; - - const normalizedEvents = normalizeEvents([ - ...(Array.isArray(input.events) ? input.events : []), - ...(Array.isArray(runtimeEvents) ? runtimeEvents : []), - ...(Array.isArray(logEvents) ? logEvents : []), - ]); - - const redactedEvents = normalizedEvents.map((event) => ({ - ...event, - content: redactObviousPii(event.content), - })); - - const eventsByAgent = new Map(); - for (const event of redactedEvents) { - const events = eventsByAgent.get(event.agent_id) ?? []; - events.push(event); - eventsByAgent.set(event.agent_id, events); - } - - const insertedRecords = []; - const bucketMs = Number.isFinite(options.windowBucketMs) && options.windowBucketMs > 0 - ? Math.floor(options.windowBucketMs) - : WINDOW_BUCKET_MS; - const bucket = Math.floor(windowEnd / bucketMs); - - for (const [agentId, agentEvents] of eventsByAgent.entries()) { - if (!agentEvents.length) continue; - const sorted = [...agentEvents].sort((a, b) => a.timestamp - b.timestamp); - const dedupeContent = sorted.map((event) => `${event.timestamp}:${event.content}`).join('\n'); - const dedupeKey = hashIngestionWindow({ agentId, bucket, content: dedupeContent }); - - if (await isDuplicateWindow(repository, dedupeKey)) { - continue; - } - - const chunks = chunkEvents(sorted, options); - for (const chunk of chunks) { - const summarization = await summarizeEvents(chunk, options); - const contextText = chunk.map((event) => event.content).join('\n'); - const summaryEmbedding = await embedText(summarization.summary, options); - const contextExcerpt = compactContextExcerpt(contextText, options.contextExcerptChars); - const chunkStart = Math.min(...chunk.map((event) => event.timestamp)); - const chunkEnd = Math.max(...chunk.map((event) => event.timestamp)); - - const record = createMemoryRecord({ - agent_id: agentId, - session_id: typeof chunk[0]?.metadata?.session_id === 'string' ? chunk[0].metadata.session_id : 'unknown', - type: 'batch_window', - content: contextText, - summary: summarization.summary, - tags: [...new Set(chunk.flatMap((event) => event.tags))], - importance: Number.isFinite(chunk[0]?.metadata?.importance) - ? Number(chunk[0].metadata.importance) - : summarization.importance, - embedding_id: Array.isArray(summaryEmbedding) && summaryEmbedding.length > 0 ? randomUUID() : null, - created_at: chunkStart, - last_seen: chunkEnd, - source: typeof chunk[0]?.metadata?.source === 'string' ? chunk[0].metadata.source : 'ingest', - ttl_days: Number.isFinite(chunk[0]?.metadata?.ttl_days) ? Number(chunk[0].metadata.ttl_days) : null, - merged_into: null, - pinned: false, - }); - - const validation = validateMemoryItem(record); - if (!validation.valid) { - console.error('memory_validation_error', { - stage: 'ingest', - reason: 'record_failed_schema_validation', - fields: validation.errors, - item: record, - }); - throw new TypeError('Memory ingest rejected: invalid durable record format'); - } - - await insertMemory(repository, record); - insertedRecords.push(record); - - if (record.embedding_id) { - const vectorMetadata = { - memory_id: record.id, - agent_id: record.agent_id, - summary: record.summary, - context_excerpt: contextExcerpt, - source: record.source, - created_at: record.created_at, - last_seen: record.last_seen, - }; - - if (typeof repository.linkEmbedding === 'function') { - await repository.linkEmbedding(record.id, { - id: record.embedding_id, - vector: summaryEmbedding, - metadata: vectorMetadata, - }); - } - - const adapter = options.embedderAdapter ?? options.adapter ?? getDefaultEmbedderAdapter(); - if (typeof adapter?.upsertVector === 'function') { - await adapter.upsertVector({ - id: record.embedding_id, - vector: summaryEmbedding, - metadata: vectorMetadata, - }); - } - } - - emitTelemetry('memory:ingested', { - memory_id: record.id, - agent_id: record.agent_id, - dedupe_key: dedupeKey, - chunk_events: chunk.length, - window_start: windowStart, - window_end: windowEnd, - }); - } - - await rememberWindowFingerprint(repository, dedupeKey, { - agent_id: agentId, - window_start: windowStart, - window_end: windowEnd, - }); - } - - return insertedRecords; -} - -/** - * @param {{ insertMemory: (memory: import('./schema.js').MemoryRecord) => Promise }} repository - * @param {import('./schema.js').MemoryRecord} memory - */ -export async function insertMemory(repository, memory) { - return repository.insertMemory(memory); -} diff --git a/torch/src/services/memory/pruner.js b/torch/src/services/memory/pruner.js deleted file mode 100644 index bf996d6a..00000000 --- a/torch/src/services/memory/pruner.js +++ /dev/null @@ -1,313 +0,0 @@ -import { loadMemoryPromptTemplates } from './summarizer.js'; - -const DAY_MS = 24 * 60 * 60 * 1000; - -/** - * @typedef {'keep' | 'archive' | 'delete'} LifecycleAction - */ - -/** - * @param {number[] | null | undefined} a - * @param {number[] | null | undefined} b - */ -function cosineSimilarity(a, b) { - if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) { - return 0; - } - - let dot = 0; - let normA = 0; - let normB = 0; - - for (let i = 0; i < a.length; i += 1) { - const av = Number(a[i]) || 0; - const bv = Number(b[i]) || 0; - dot += av * bv; - normA += av * av; - normB += bv * bv; - } - - const denom = Math.sqrt(normA) * Math.sqrt(normB); - return denom > 0 ? dot / denom : 0; -} - -/** - * @param {string[]} left - * @param {string[]} right - */ -function hasTagOverlap(left = [], right = []) { - if (!left.length || !right.length) return false; - const rightSet = new Set(right); - return left.some((tag) => rightSet.has(tag)); -} - -/** - * @param {import('./schema.js').MemoryRecord} memory - * @param {{ now: number, retentionMs: number, lowImportanceThreshold: number, recentUsageMs: number }} policy - */ -function evaluateCandidate(memory, policy) { - const ttlExpired = Number.isInteger(memory.ttl_days) - ? memory.created_at + (memory.ttl_days * DAY_MS) <= policy.now - : false; - const staleByRetention = memory.last_seen < (policy.now - policy.retentionMs); - const lowImportance = memory.importance <= policy.lowImportanceThreshold; - const lowRecentUsage = memory.last_seen <= (policy.now - policy.recentUsageMs); - - return { - ttlExpired, - staleByRetention, - lowImportance, - lowRecentUsage, - candidate: !memory.pinned && (ttlExpired || staleByRetention) && lowImportance && lowRecentUsage, - }; -} - -/** - * @param {import('./schema.js').MemoryRecord[]} memories - * @param {{ getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, similarityThreshold: number, duplicateWindowMs: number }} options - */ -function groupNearDuplicates(memories, options) { - const groups = []; - const consumed = new Set(); - - for (let i = 0; i < memories.length; i += 1) { - const base = memories[i]; - if (consumed.has(base.id) || base.merged_into) continue; - - const baseEmbedding = options.getEmbedding?.(base); - const group = [base]; - - for (let j = i + 1; j < memories.length; j += 1) { - const probe = memories[j]; - if (consumed.has(probe.id) || probe.merged_into || probe.id === base.id) continue; - - const withinWindow = Math.abs(base.created_at - probe.created_at) <= options.duplicateWindowMs; - if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue; - - const similarity = cosineSimilarity(baseEmbedding, options.getEmbedding?.(probe)); - if (similarity >= options.similarityThreshold) { - group.push(probe); - } - } - - if (group.length > 1) { - group.forEach((item) => consumed.add(item.id)); - groups.push(group.sort((a, b) => b.last_seen - a.last_seen)); - } - } - - return groups; -} - -/** - * @param {import('./schema.js').MemoryRecord[]} group - * @param {(prompt: string) => Promise | string} [generateSummary] - * @param {string} condenseTemplate - */ -async function condenseGroup(group, generateSummary, condenseTemplate) { - const [primary, ...rest] = group; - const fallbackSummary = group.map((item) => item.summary || item.content).filter(Boolean).join(' ').slice(0, 280); - const fallback = { - summary: fallbackSummary || primary.summary, - importance: Math.max(...group.map((item) => item.importance)), - }; - - if (typeof generateSummary !== 'function') { - return { merged: { ...primary, summary: fallback.summary, importance: fallback.importance }, mergedIds: rest.map((item) => item.id), usedFallback: true }; - } - - const sourceBlock = group - .map((item) => `- id=${item.id}; tags=${item.tags.join(',')}; ts=${item.created_at}; summary=${item.summary || item.content}`) - .join('\n'); - const prompt = [ - 'Condense the following near-duplicate memories into one factual summary.', - 'Return STRICT JSON: {"summary":"...","importance":0.0}', - 'Do not invent facts.', - '', - `Memories:\n${sourceBlock}`, - '', - 'If your first response is malformed, repair it using this policy:', - condenseTemplate, - ].join('\n'); - - try { - const raw = await generateSummary(prompt); - const parsed = JSON.parse(raw); - const summary = typeof parsed?.summary === 'string' && parsed.summary.trim() ? parsed.summary.trim() : fallback.summary; - const importance = Number.isFinite(Number(parsed?.importance)) - ? Math.max(0, Math.min(1, Number(parsed.importance))) - : fallback.importance; - - return { - merged: { - ...primary, - summary, - importance, - tags: [...new Set(group.flatMap((item) => item.tags))], - last_seen: Math.max(...group.map((item) => item.last_seen)), - }, - mergedIds: rest.map((item) => item.id), - usedFallback: false, - }; - } catch { - return { merged: { ...primary, summary: fallback.summary, importance: fallback.importance }, mergedIds: rest.map((item) => item.id), usedFallback: true }; - } -} - -/** - * @param {import('./schema.js').MemoryRecord[]} memories - * @param {{ - * retentionMs: number, - * now?: number, - * lowImportanceThreshold?: number, - * deleteImportanceThreshold?: number, - * recentUsageMs?: number, - * duplicateWindowMs?: number, - * similarityThreshold?: number, - * getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, - * generateSummary?: (prompt: string) => Promise | string, - * templateDir?: string, - * }} options - */ -export async function createLifecyclePlan(memories, options) { - const now = options.now ?? Date.now(); - const policy = { - now, - retentionMs: options.retentionMs, - lowImportanceThreshold: options.lowImportanceThreshold ?? 0.35, - deleteImportanceThreshold: options.deleteImportanceThreshold ?? 0.12, - recentUsageMs: options.recentUsageMs ?? Math.max(Math.floor(options.retentionMs / 2), DAY_MS), - duplicateWindowMs: options.duplicateWindowMs ?? (7 * DAY_MS), - similarityThreshold: options.similarityThreshold ?? 0.93, - }; - - const candidateById = new Map(memories.map((memory) => [memory.id, evaluateCandidate(memory, policy)])); - const groups = groupNearDuplicates(memories.filter((memory) => !memory.pinned), { - getEmbedding: options.getEmbedding, - similarityThreshold: policy.similarityThreshold, - duplicateWindowMs: policy.duplicateWindowMs, - }); - - const templates = loadMemoryPromptTemplates({ templateDir: options.templateDir }); - const condensedGroups = []; - const mergedIntoMap = new Map(); - - for (const group of groups) { - const condensed = await condenseGroup(group, options.generateSummary, templates.condense); - condensedGroups.push(condensed); - for (const mergedId of condensed.mergedIds) { - mergedIntoMap.set(mergedId, condensed.merged.id); - } - } - - const protectedFromDelete = new Set(mergedIntoMap.keys()); - const actions = []; - const policyLogs = []; - - for (const memory of memories) { - const candidate = candidateById.get(memory.id) ?? evaluateCandidate(memory, policy); - - /** @type {LifecycleAction} */ - let action = 'keep'; - let reason = 'retained: does not satisfy low-value prune gates'; - - if (memory.pinned) { - action = 'keep'; - reason = 'retained: pinned memories are never deleted'; - } else if (mergedIntoMap.has(memory.id)) { - action = 'archive'; - reason = `archived: merged into ${mergedIntoMap.get(memory.id)} and retained as source provenance`; - } else if (candidate.candidate) { - const veryLowImportance = memory.importance <= policy.deleteImportanceThreshold; - const veryOld = memory.last_seen < (now - (policy.retentionMs * 2)); - const decisiveDelete = candidate.ttlExpired && candidate.staleByRetention && veryLowImportance && veryOld; - - if (protectedFromDelete.has(memory.id)) { - action = 'archive'; - reason = 'archived: referenced by retained merged summary'; - } else if (decisiveDelete) { - action = 'delete'; - reason = 'deleted: expired, very low importance, and long-unseen beyond retention grace'; - } else { - action = 'archive'; - reason = 'archived: borderline low-value candidate; archive preferred over delete'; - } - } - - actions.push({ id: memory.id, action, merged_into: mergedIntoMap.get(memory.id) ?? null, reason }); - policyLogs.push({ - id: memory.id, - action, - reason, - signals: { - ttlExpired: candidate.ttlExpired, - staleByRetention: candidate.staleByRetention, - lowImportance: candidate.lowImportance, - lowRecentUsage: candidate.lowRecentUsage, - }, - }); - } - - return { - actions, - policyLogs, - condensedGroups, - }; -} - -/** - * @param {{ - * archiveMemory?: (id: string, reason: string) => Promise | unknown, - * deleteMemory?: (id: string, reason: string) => Promise | unknown, - * keepMemory?: (id: string, reason: string) => Promise | unknown, - * markMerged?: (id: string, mergedInto: string) => Promise | unknown, - * }} repository - * @param {Awaited>} plan - */ -export async function applyLifecycleActions(repository, plan) { - const results = []; - - for (const decision of plan.actions) { - if (decision.merged_into && typeof repository.markMerged === 'function') { - await repository.markMerged(decision.id, decision.merged_into); - } - - if (decision.action === 'archive') { - await repository.archiveMemory?.(decision.id, decision.reason); - } else if (decision.action === 'delete') { - await repository.deleteMemory?.(decision.id, decision.reason); - } else { - await repository.keepMemory?.(decision.id, decision.reason); - } - - results.push(decision); - } - - return { - applied: results, - policyLogs: plan.policyLogs, - }; -} - -/** - * @param {import('./schema.js').MemoryRecord[]} memories - * @param {{ retentionMs: number, now?: number }} options - * @returns {import('./schema.js').MemoryRecord[]} - */ -export function selectPrunableMemories(memories, options) { - const now = options.now ?? Date.now(); - const cutoff = now - options.retentionMs; - - return memories.filter((memory) => !memory.pinned && memory.last_seen < cutoff); -} - - -/** - * @param {{ listPruneCandidates: (options: { cutoff: number, limit?: number }) => Promise }} repository - * @param {{ retentionMs: number, now?: number, limit?: number }} options - */ -export async function listPruneCandidates(repository, options) { - const now = options.now ?? Date.now(); - const cutoff = now - options.retentionMs; - return repository.listPruneCandidates({ cutoff, limit: options.limit }); -} diff --git a/torch/src/services/memory/retriever.js b/torch/src/services/memory/retriever.js deleted file mode 100644 index 8b3d76f4..00000000 --- a/torch/src/services/memory/retriever.js +++ /dev/null @@ -1,181 +0,0 @@ -const DEFAULT_WEIGHTS = { - w_sim: 0.6, - w_imp: 0.25, - w_rec: 0.15, - pinBoost: 0.15, -}; - -const DEFAULT_RECENCY_HALF_LIFE_MS = 1000 * 60 * 60 * 24 * 7; - -function cosineSimilarity(left = [], right = []) { - const size = Math.max(left.length, right.length); - let dot = 0; - let leftNorm = 0; - let rightNorm = 0; - - for (let index = 0; index < size; index += 1) { - const leftValue = Number(left[index] ?? 0); - const rightValue = Number(right[index] ?? 0); - dot += leftValue * rightValue; - leftNorm += leftValue ** 2; - rightNorm += rightValue ** 2; - } - - if (leftNorm === 0 || rightNorm === 0) return 0; - return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm)); -} - -function computeRecencyDecay(timestamp, now, halfLifeMs) { - if (!Number.isFinite(timestamp) || !Number.isFinite(now) || halfLifeMs <= 0) return 0; - const age = Math.max(0, now - timestamp); - return Math.exp(-(age / halfLifeMs)); -} - -function textMatchSimilarity(query, memory) { - const normalizedQuery = String(query ?? '').trim().toLowerCase(); - if (!normalizedQuery) return 0; - - const haystack = `${memory.summary} ${memory.content}`.toLowerCase(); - if (haystack.includes(normalizedQuery)) return 1; - - const queryTerms = normalizedQuery.split(/\s+/).filter(Boolean); - if (queryTerms.length === 0) return 0; - - const matchedTerms = queryTerms.filter((term) => haystack.includes(term)).length; - return matchedTerms / queryTerms.length; -} - -/** - * @param {import('./schema.js').MemoryRecord} memory - * @param {{ tags: string[], timeframe?: { from?: number, to?: number }, pinnedPreference?: 'prefer' | 'only' | 'exclude' }} params - */ -function matchesMetadataFilters(memory, params) { - const inTimeframe = !params.timeframe - || ((params.timeframe.from == null || memory.created_at >= params.timeframe.from) - && (params.timeframe.to == null || memory.created_at <= params.timeframe.to)); - - if (!inTimeframe) return false; - - if (params.tags.length > 0) { - const memoryTags = new Set(memory.tags); - const matchesAllTags = params.tags.every((tag) => memoryTags.has(tag)); - if (!matchesAllTags) return false; - } - - if (params.pinnedPreference === 'only' && !memory.pinned) return false; - if (params.pinnedPreference === 'exclude' && memory.pinned) return false; - - return true; -} - -/** - * @param {import('./schema.js').MemoryRecord[]} memories - * @param {{ query: string, agent_id: string, k: number, vectorAdapter?: { embedText: (text: string) => Promise | number[], queryVector: (input: { vector: number[], k?: number, filter?: Record }) => Promise> | Array<{ id: string, score: number }> }, embedText?: (value: string) => Promise | number[] }} params - */ -async function buildSimilarityIndex(memories, params) { - const similarityById = new Map(); - const normalizedQuery = params.query.trim(); - - if (!normalizedQuery) { - return similarityById; - } - - if (params.vectorAdapter && typeof params.vectorAdapter.queryVector === 'function') { - const queryVector = typeof params.embedText === 'function' - ? await params.embedText(normalizedQuery) - : await params.vectorAdapter.embedText(normalizedQuery); - - const vectorHits = await params.vectorAdapter.queryVector({ - vector: queryVector, - k: Math.max(params.k * 4, memories.length), - filter: { agent_id: params.agent_id }, - }); - - for (const hit of vectorHits) { - if (typeof hit.id === 'string' && Number.isFinite(hit.score)) { - similarityById.set(hit.id, Math.max(0, Number(hit.score))); - } - } - - if (similarityById.size > 0) { - return similarityById; - } - } - - for (const memory of memories) { - const lexicalScore = textMatchSimilarity(normalizedQuery, memory); - if (lexicalScore > 0) { - similarityById.set(memory.id, lexicalScore); - } - } - - return similarityById; -} - -/** - * @param {import('./schema.js').MemoryRecord[]} memories - * @param {{ agent_id: string, query?: string, tags?: string[], timeframe?: { from?: number, to?: number }, k?: number, pinnedPreference?: 'prefer' | 'only' | 'exclude', weights?: { w_sim?: number, w_imp?: number, w_rec?: number, pinBoost?: number }, now?: number, recencyHalfLifeMs?: number, vectorAdapter?: { embedText: (text: string) => Promise | number[], queryVector: (input: { vector: number[], k?: number, filter?: Record }) => Promise> | Array<{ id: string, score: number }> }, embedText?: (value: string) => Promise | number[] }} params - * @returns {Promise} - */ -export async function filterAndRankMemories(memories, params) { - const { - agent_id, - query = '', - tags = [], - timeframe, - k = 10, - pinnedPreference = 'prefer', - weights = {}, - now = Date.now(), - recencyHalfLifeMs = DEFAULT_RECENCY_HALF_LIFE_MS, - vectorAdapter, - embedText, - } = params; - - const mergedWeights = { - ...DEFAULT_WEIGHTS, - ...weights, - }; - - const filtered = memories.filter((memory) => { - if (memory.agent_id !== agent_id) return false; - return matchesMetadataFilters(memory, { tags, timeframe, pinnedPreference }); - }); - - const similarityById = await buildSimilarityIndex(filtered, { - query, - agent_id, - k, - vectorAdapter, - embedText, - }); - - return filtered - .map((memory) => { - const memoryTimestamp = Number.isFinite(memory.last_seen) ? memory.last_seen : memory.created_at; - const recencyDecay = computeRecencyDecay(memoryTimestamp, now, recencyHalfLifeMs); - const similarity = similarityById.get(memory.id) ?? 0; - const pinBoost = memory.pinned && pinnedPreference !== 'exclude' ? mergedWeights.pinBoost : 0; - - const score = (similarity * mergedWeights.w_sim) - + (memory.importance * mergedWeights.w_imp) - + (recencyDecay * mergedWeights.w_rec) - + pinBoost; - - return { memory, score }; - }) - .sort((left, right) => right.score - left.score) - .slice(0, k) - .map((entry) => entry.memory); -} - -/** - * @param {{ updateMemoryUsage: (id: string, lastSeen?: number) => Promise }} repository - * @param {string[]} ids - * @param {number} [lastSeen] - */ -export async function updateMemoryUsage(repository, ids, lastSeen = Date.now()) { - await Promise.all(ids.map((id) => repository.updateMemoryUsage(id, lastSeen))); -} - -export { cosineSimilarity }; diff --git a/torch/src/services/memory/scheduler.js b/torch/src/services/memory/scheduler.js deleted file mode 100644 index 533734d9..00000000 --- a/torch/src/services/memory/scheduler.js +++ /dev/null @@ -1,317 +0,0 @@ -import { getMemoryPruneMode, isMemoryEnabled, isMemoryIngestEnabled } from './feature-flags.js'; -const MINUTE_MS = 60_000; -const HOUR_MS = 60 * MINUTE_MS; -const DAY_MS = 24 * HOUR_MS; -const WEEK_MS = 7 * DAY_MS; - -const JOBS = { - ingestRecentRuntimeEvents: { - lockKey: 'memory:job:ingest-recent-runtime-events', - minIntervalMs: MINUTE_MS, - maxIntervalMs: 5 * MINUTE_MS, - }, - consolidateObservations: { - lockKey: 'memory:job:consolidate-observations', - intervalMs: HOUR_MS, - }, - pruningCycle: { - lockKey: 'memory:job:pruning-cycle', - intervalMs: DAY_MS, - }, - deepMergeArchivalMaintenance: { - lockKey: 'memory:job:deep-merge-archival-maintenance', - intervalMs: WEEK_MS, - }, -}; - -/** - * @param {() => Promise | unknown} task - * @param {{ intervalMs: number }} options - * @returns {{ stop: () => void }} - */ -export function startScheduler(task, options) { - const handle = setInterval(() => { - void task(); - }, options.intervalMs); - - return { - stop() { - clearInterval(handle); - }, - }; -} - -function normalizeItemCount(result) { - if (typeof result === 'number' && Number.isFinite(result)) { - return Math.max(0, Math.floor(result)); - } - - if (result && typeof result === 'object') { - const count = result.itemCount ?? result.count ?? result.processed ?? result.rows; - if (typeof count === 'number' && Number.isFinite(count)) { - return Math.max(0, Math.floor(count)); - } - } - - return 0; -} - -/** - * @param {string} key - */ -function hashLockKey(key) { - let hash = 0; - for (let i = 0; i < key.length; i += 1) { - hash = ((hash * 31) + key.charCodeAt(i)) | 0; - } - return hash; -} - -/** - * @param {{ query: (sql: string, params?: unknown[]) => Promise<{ rows?: any[] }> }} db - * @param {{ namespace?: string }} [options] - */ -export function createDbAdvisoryLockProvider(db, options = {}) { - const namespace = options.namespace ?? 'torch-memory-jobs'; - - return { - /** - * @param {string} key - * @param {() => Promise} task - */ - async withLock(key, task) { - const lockId = `${namespace}:${key}`; - const keyA = hashLockKey(namespace); - const keyB = hashLockKey(lockId); - const acquiredResult = await db.query('SELECT pg_try_advisory_lock($1, $2) AS locked', [keyA, keyB]); - const acquired = Boolean(acquiredResult?.rows?.[0]?.locked); - if (!acquired) { - return { acquired: false, result: null }; - } - - try { - const result = await task(); - return { acquired: true, result }; - } finally { - await db.query('SELECT pg_advisory_unlock($1, $2)', [keyA, keyB]); - } - }, - }; -} - -function createInMemoryLockProvider() { - const locks = new Set(); - return { - async withLock(key, task) { - if (locks.has(key)) { - return { acquired: false, result: null }; - } - - locks.add(key); - try { - const result = await task(); - return { acquired: true, result }; - } finally { - locks.delete(key); - } - }, - }; -} - -/** - * @param {{ - * jobName: string, - * lockKey: string, - * handler: () => Promise | unknown, - * lockProvider: { withLock: (key: string, task: () => Promise) => Promise<{ acquired: boolean, result: unknown }> }, - * maxRetries: number, - * retryDelayMs: number, - * emitMetric?: (metricName: string, payload: Record) => void, - * }} options - */ -async function runScheduledJob(options) { - const startedAt = Date.now(); - let failures = 0; - let retries = 0; - - for (let attempt = 0; attempt <= options.maxRetries; attempt += 1) { - try { - const lockRun = await options.lockProvider.withLock(options.lockKey, async () => options.handler()); - - if (!lockRun.acquired) { - options.emitMetric?.('memory_scheduler_job', { - job: options.jobName, - status: 'skipped_lock_unavailable', - durationMs: Date.now() - startedAt, - itemCount: 0, - failures, - retries, - }); - return; - } - - options.emitMetric?.('memory_scheduler_job', { - job: options.jobName, - status: 'success', - durationMs: Date.now() - startedAt, - itemCount: normalizeItemCount(lockRun.result), - failures, - retries, - }); - return; - } catch (error) { - failures += 1; - if (attempt < options.maxRetries) { - retries += 1; - await new Promise((resolve) => { - setTimeout(resolve, options.retryDelayMs); - }); - continue; - } - - options.emitMetric?.('memory_scheduler_job', { - job: options.jobName, - status: 'failed', - durationMs: Date.now() - startedAt, - itemCount: 0, - failures, - retries, - error: error instanceof Error ? error.message : String(error), - }); - return; - } - } -} - -function shouldRunJob(jobName, options) { - const env = options.env ?? process.env; - if (!isMemoryEnabled(env)) return false; - - if (jobName === 'ingestRecentRuntimeEvents') { - return isMemoryIngestEnabled(options.ingestAgentId, env); - } - - if (jobName === 'pruningCycle') { - return getMemoryPruneMode(env) !== 'off'; - } - - return true; -} - -/** - * @param {{ - * handlers: { - * ingestRecentRuntimeEvents: () => Promise | unknown, - * consolidateObservations: () => Promise | unknown, - * pruningCycle: () => Promise | unknown, - * deepMergeArchivalMaintenance: () => Promise | unknown, - * }, - * lockProvider?: { withLock: (key: string, task: () => Promise) => Promise<{ acquired: boolean, result: unknown }> }, - * emitMetric?: (metricName: string, payload: Record) => void, - * maxRetries?: number, - * retryDelayMs?: number, - * runImmediately?: boolean, - * random?: () => number, - * }} options - */ -export function startMemoryMaintenanceScheduler(options) { - const lockProvider = options.lockProvider ?? createInMemoryLockProvider(); - const maxRetries = Number.isFinite(options.maxRetries) ? Math.max(0, Math.floor(options.maxRetries)) : 2; - const retryDelayMs = Number.isFinite(options.retryDelayMs) ? Math.max(0, Math.floor(options.retryDelayMs)) : 1_000; - const random = typeof options.random === 'function' ? options.random : Math.random; - const handles = []; - const timeouts = new Set(); - - const runJob = async (jobName, definition, handler) => runScheduledJob({ - jobName, - lockKey: definition.lockKey, - handler, - lockProvider, - maxRetries, - retryDelayMs, - emitMetric: options.emitMetric, - }); - - const startFixedIntervalJob = (jobName, definition, handler) => { - const run = () => { - if (!shouldRunJob(jobName, options)) { - options.emitMetric?.('memory_scheduler_job', { - job: jobName, - status: 'skipped_flag_disabled', - durationMs: 0, - itemCount: 0, - failures: 0, - retries: 0, - }); - return; - } - void runJob(jobName, definition, handler); - }; - - if (options.runImmediately !== false) run(); - const intervalHandle = setInterval(run, definition.intervalMs); - handles.push(() => clearInterval(intervalHandle)); - }; - - const startRandomIntervalJob = (jobName, definition, handler) => { - const scheduleNext = () => { - const jitterRatio = Math.min(1, Math.max(0, random())); - const delayMs = definition.minIntervalMs + Math.floor((definition.maxIntervalMs - definition.minIntervalMs) * jitterRatio); - const timeout = setTimeout(async () => { - timeouts.delete(timeout); - if (!shouldRunJob(jobName, options)) { - options.emitMetric?.('memory_scheduler_job', { - job: jobName, - status: 'skipped_flag_disabled', - durationMs: 0, - itemCount: 0, - failures: 0, - retries: 0, - }); - scheduleNext(); - return; - } - await runJob(jobName, definition, handler); - scheduleNext(); - }, delayMs); - timeouts.add(timeout); - }; - - if (options.runImmediately !== false) { - if (!shouldRunJob(jobName, options)) { - options.emitMetric?.('memory_scheduler_job', { - job: jobName, - status: 'skipped_flag_disabled', - durationMs: 0, - itemCount: 0, - failures: 0, - retries: 0, - }); - scheduleNext(); - return; - } - void runJob(jobName, definition, handler).then(() => { - scheduleNext(); - }); - } else { - scheduleNext(); - } - }; - - startRandomIntervalJob('ingestRecentRuntimeEvents', JOBS.ingestRecentRuntimeEvents, options.handlers.ingestRecentRuntimeEvents); - startFixedIntervalJob('consolidateObservations', JOBS.consolidateObservations, options.handlers.consolidateObservations); - startFixedIntervalJob('pruningCycle', JOBS.pruningCycle, options.handlers.pruningCycle); - startFixedIntervalJob('deepMergeArchivalMaintenance', JOBS.deepMergeArchivalMaintenance, options.handlers.deepMergeArchivalMaintenance); - - return { - stop() { - for (const stopHandle of handles) { - stopHandle(); - } - for (const timeout of timeouts) { - clearTimeout(timeout); - } - timeouts.clear(); - }, - }; -} diff --git a/torch/src/services/memory/schema.js b/torch/src/services/memory/schema.js deleted file mode 100644 index ef374aaf..00000000 --- a/torch/src/services/memory/schema.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @typedef {Object} MemoryEvent - * @property {string} agent_id - * @property {string} content - * @property {number} [timestamp] - * @property {string[]} [tags] - * @property {Record} [metadata] - */ - -export const SCHEMA_VERSION = 1; - -/** - * @typedef {Object} MemoryRecord - * @property {number} schema_version - * @property {string} id - * @property {string} agent_id - * @property {string} session_id - * @property {string} type - * @property {string} content - * @property {string} summary - * @property {string[]} tags - * @property {number} importance - * @property {string | null} embedding_id - * @property {number} created_at - * @property {number} last_seen - * @property {string} source - * @property {number | null} ttl_days - * @property {string | null} merged_into - * @property {boolean} pinned - */ - -/** - * @param {MemoryEvent} event - * @returns {MemoryEvent} - */ -export function normalizeEvent(event) { - return { - ...event, - timestamp: Number.isFinite(event?.timestamp) ? Number(event.timestamp) : Date.now(), - tags: Array.isArray(event?.tags) - ? event.tags.map((tag) => String(tag)).filter((tag) => tag.length > 0) - : [], - metadata: event?.metadata && typeof event.metadata === 'object' ? event.metadata : {}, - }; -} - -/** - * @param {Partial} input - * @returns {MemoryRecord} - */ -import crypto from 'node:crypto'; - -export function normalizeMemoryItem(input = {}) { - const now = Date.now(); - - return { - schema_version: SCHEMA_VERSION, - id: typeof input.id === 'string' && input.id.trim() ? input.id.trim() : crypto.randomUUID(), - agent_id: typeof input.agent_id === 'string' ? input.agent_id.trim() : '', - session_id: typeof input.session_id === 'string' ? input.session_id.trim() : 'unknown', - type: typeof input.type === 'string' && input.type.trim() ? input.type.trim() : 'event', - content: typeof input.content === 'string' ? input.content : '', - summary: typeof input.summary === 'string' ? input.summary : '', - tags: Array.isArray(input.tags) - ? input.tags.map((tag) => String(tag).trim()).filter((tag) => tag.length > 0) - : [], - importance: Number.isFinite(input.importance) - ? Math.max(0, Math.min(1, Number(input.importance))) - : 0.5, - embedding_id: typeof input.embedding_id === 'string' && input.embedding_id.trim() - ? input.embedding_id.trim() - : null, - created_at: Number.isFinite(input.created_at) ? Number(input.created_at) : now, - last_seen: Number.isFinite(input.last_seen) ? Number(input.last_seen) : now, - source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'ingest', - ttl_days: Number.isFinite(input.ttl_days) ? Math.max(0, Math.floor(Number(input.ttl_days))) : null, - merged_into: typeof input.merged_into === 'string' && input.merged_into.trim() - ? input.merged_into.trim() - : null, - pinned: Boolean(input.pinned), - }; -} - -/** - * @param {unknown} item - * @returns {{ valid: boolean, errors: Array<{ field: string, expected: string, received: string, value: unknown }> }} - */ -export function validateMemoryItem(item) { - const errors = []; - - const check = (condition, field, expected, value) => { - if (!condition) { - errors.push({ - field, - expected, - received: Array.isArray(value) ? 'array' : typeof value, - value, - }); - } - }; - - check(item && typeof item === 'object', 'item', 'object', item); - if (!item || typeof item !== 'object') { - return { valid: false, errors }; - } - - check(item.schema_version === SCHEMA_VERSION, 'schema_version', `number (${SCHEMA_VERSION})`, item.schema_version); - check(typeof item.id === 'string' && item.id.length > 0, 'id', 'non-empty string', item.id); - check(typeof item.agent_id === 'string' && item.agent_id.length > 0, 'agent_id', 'non-empty string', item.agent_id); - check(typeof item.session_id === 'string' && item.session_id.length > 0, 'session_id', 'non-empty string', item.session_id); - check(typeof item.type === 'string' && item.type.length > 0, 'type', 'non-empty string', item.type); - check(typeof item.content === 'string', 'content', 'string', item.content); - check(typeof item.summary === 'string', 'summary', 'string', item.summary); - check(Array.isArray(item.tags) && item.tags.every((tag) => typeof tag === 'string'), 'tags', 'string[]', item.tags); - check(Number.isFinite(item.importance) && item.importance >= 0 && item.importance <= 1, 'importance', 'number between 0 and 1', item.importance); - check(item.embedding_id === null || (typeof item.embedding_id === 'string' && item.embedding_id.length > 0), 'embedding_id', 'null or non-empty string', item.embedding_id); - check(Number.isFinite(item.created_at), 'created_at', 'finite number', item.created_at); - check(Number.isFinite(item.last_seen), 'last_seen', 'finite number', item.last_seen); - check(typeof item.source === 'string' && item.source.length > 0, 'source', 'non-empty string', item.source); - check(item.ttl_days === null || (Number.isInteger(item.ttl_days) && item.ttl_days >= 0), 'ttl_days', 'null or non-negative integer', item.ttl_days); - check(item.merged_into === null || (typeof item.merged_into === 'string' && item.merged_into.length > 0), 'merged_into', 'null or non-empty string', item.merged_into); - check(typeof item.pinned === 'boolean', 'pinned', 'boolean', item.pinned); - - return { - valid: errors.length === 0, - errors, - }; -} - -/** - * @param {Partial} record - * @returns {MemoryRecord} - */ -export function createMemoryRecord(record) { - return normalizeMemoryItem(record); -} diff --git a/torch/src/services/memory/summarizer.js b/torch/src/services/memory/summarizer.js deleted file mode 100644 index 0f128509..00000000 --- a/torch/src/services/memory/summarizer.js +++ /dev/null @@ -1,151 +0,0 @@ -import { readFileSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const DEFAULT_TEMPLATE_DIR = path.resolve(__dirname, '../../prompts/memory'); -const TEMPLATE_FILES = { - summarize: 'summarize_prompt.txt', - condense: 'condense_prompt.txt', - score: 'score_prompt.txt', - prune: 'prune_prompt.txt', -}; - -const templateCache = new Map(); - -function redactObviousPii(text) { - if (typeof text !== 'string' || text.length === 0) return ''; - return text - .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted:email]') - .replace(/\b(?:\+?\d{1,2}[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})\b/g, '[redacted:phone]') - .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[redacted:ssn]'); -} - -function renderTemplate(template, replacements) { - return Object.entries(replacements).reduce( - (output, [key, value]) => output.replaceAll(`{{${key}}}`, String(value ?? '')), - template - ); -} - -export function loadMemoryPromptTemplates(options = {}) { - const templateDir = options.templateDir ?? DEFAULT_TEMPLATE_DIR; - if (templateCache.has(templateDir)) { - return templateCache.get(templateDir); - } - - const templates = Object.fromEntries( - Object.entries(TEMPLATE_FILES).map(([key, fileName]) => { - const filePath = path.join(templateDir, fileName); - const content = readFileSync(filePath, 'utf8').trim(); - return [key, content]; - }) - ); - - templateCache.set(templateDir, templates); - return templates; -} - -function parseSummaryPayload(rawResponse) { - const parsed = JSON.parse(rawResponse); - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - throw new TypeError('Response must be a JSON object'); - } - - const summary = typeof parsed.summary === 'string' ? parsed.summary.trim() : ''; - if (!summary) { - throw new TypeError('Response.summary must be a non-empty string'); - } - - const rawImportance = Number(parsed.importance); - const importance = Number.isFinite(rawImportance) - ? Math.min(1, Math.max(0, rawImportance)) - : 0.25; - - return { summary, importance }; -} - -function deterministicFallback(events, maxSummaryLength) { - const joined = events.map((event) => redactObviousPii(event.content)).join(' ').trim(); - const bounded = joined.length > maxSummaryLength ? `${joined.slice(0, maxSummaryLength - 1)}…` : joined; - return { summary: bounded, importance: 0.25 }; -} - -function isLikelyFactual(summary, sourceText) { - const sourceTokens = new Set( - sourceText - .toLowerCase() - .split(/[^a-z0-9]+/) - .filter((token) => token.length > 3) - ); - const summaryTokens = summary - .toLowerCase() - .split(/[^a-z0-9]+/) - .filter((token) => token.length > 3); - - if (!summaryTokens.length) return true; - const overlap = summaryTokens.filter((token) => sourceTokens.has(token)).length; - return overlap / summaryTokens.length >= 0.35; -} - -function sanitizeResult(parsed, sourceText, maxSummaryLength, fallback) { - const sanitizedSummary = redactObviousPii(parsed.summary); - if (!isLikelyFactual(sanitizedSummary, sourceText)) { - return fallback; - } - - return { - summary: sanitizedSummary.length > maxSummaryLength - ? `${sanitizedSummary.slice(0, maxSummaryLength - 1)}…` - : sanitizedSummary, - importance: parsed.importance, - }; -} - -/** - * @param {import('./schema.js').MemoryEvent[]} events - * @param {{ - * maxSummaryLength?: number, - * generateSummary?: (prompt: string) => Promise | string, - * templateDir?: string, - * }} [options] - * @returns {Promise<{ summary: string, importance: number }>} - */ -export async function summarizeEvents(events, options = {}) { - const maxSummaryLength = options.maxSummaryLength ?? 280; - const fallback = deterministicFallback(events, maxSummaryLength); - if (!fallback.summary) return fallback; - - const templates = loadMemoryPromptTemplates(options); - const sourceText = events.map((event) => redactObviousPii(event.content)).join('\n').trim(); - const generateSummary = options.generateSummary; - - if (typeof generateSummary !== 'function') { - return fallback; - } - - const summaryPrompt = renderTemplate(templates.summarize, { - EVENTS: sourceText, - MAX_SUMMARY_LENGTH: maxSummaryLength, - }); - - let firstResponse = ''; - try { - firstResponse = await generateSummary(summaryPrompt); - const parsed = parseSummaryPayload(firstResponse); - return sanitizeResult(parsed, sourceText, maxSummaryLength, fallback); - } catch (firstError) { - try { - const repairPrompt = renderTemplate(templates.condense, { - RAW_RESPONSE: firstResponse, - PARSE_ERROR: firstError instanceof Error ? firstError.message : 'unknown_parse_error', - }); - const repairedResponse = await generateSummary(repairPrompt); - const parsed = parseSummaryPayload(repairedResponse); - return sanitizeResult(parsed, sourceText, maxSummaryLength, fallback); - } catch { - return fallback; - } - } -} diff --git a/torch/src/todo/2026-02-13-dom-migration-todo.md b/torch/src/todo/2026-02-13-dom-migration-todo.md deleted file mode 100644 index 8d38192e..00000000 --- a/torch/src/todo/2026-02-13-dom-migration-todo.md +++ /dev/null @@ -1,12 +0,0 @@ -# TODO — Rendering Migration (Example) - -## Pending -- [ ] Replace remaining string-based rendering paths. -- [ ] Add safe helper utilities where needed. -- [ ] Update related tests. - -## Completed -- [x] Audited current rendering entry points. - -## Blocked -- [ ] Confirm final acceptance criteria with maintainers. diff --git a/torch/src/todo/TODO_2026-02-14.md b/torch/src/todo/TODO_2026-02-14.md deleted file mode 100644 index 5be3b018..00000000 --- a/torch/src/todo/TODO_2026-02-14.md +++ /dev/null @@ -1,11 +0,0 @@ -# TODO Checklist - -- [ ] Update `src/constants.mjs` with defaults. -- [ ] Update `src/torch-config.mjs` with config getters. -- [ ] Create `src/roster.mjs` with roster logic. -- [ ] Create `src/lock-ops.mjs` with lock operations. -- [ ] Create `src/dashboard.mjs` with dashboard logic. -- [ ] Update `src/lib.mjs` to import new modules. -- [ ] Verify `npm run lint` passes. -- [ ] Verify `npm test` passes. -- [ ] Update baseline using `scripts/check-file-size.mjs --update`. diff --git a/torch/src/todo/TODO_2026-02-14T04-53-53Z.md b/torch/src/todo/TODO_2026-02-14T04-53-53Z.md deleted file mode 100644 index 1f600ada..00000000 --- a/torch/src/todo/TODO_2026-02-14T04-53-53Z.md +++ /dev/null @@ -1 +0,0 @@ -# Todo diff --git a/torch/src/todo/TODO_2026-02-14T06-04-19Z.md b/torch/src/todo/TODO_2026-02-14T06-04-19Z.md deleted file mode 100644 index b882ef9c..00000000 --- a/torch/src/todo/TODO_2026-02-14T06-04-19Z.md +++ /dev/null @@ -1,6 +0,0 @@ -# Audit Run TODO - -- [ ] Run check-file-size.mjs -- [ ] Run check-innerhtml.mjs -- [ ] Run npm run lint -- [ ] Create summary diff --git a/torch/src/todo/TODO_2026-02-14T13-01-37Z.md b/torch/src/todo/TODO_2026-02-14T13-01-37Z.md deleted file mode 100644 index 8bb31aac..00000000 --- a/torch/src/todo/TODO_2026-02-14T13-01-37Z.md +++ /dev/null @@ -1,5 +0,0 @@ -# Todo: Content Audit 2026-02-14T13-01-37Z - -- [ ] Check if /content directory exists -- [ ] Inventory claims if content exists -- [ ] Verify claims if content exists diff --git a/torch/src/todo/TODO_2026-02-14T13-30-00Z.md b/torch/src/todo/TODO_2026-02-14T13-30-00Z.md deleted file mode 100644 index 01527ef8..00000000 --- a/torch/src/todo/TODO_2026-02-14T13-30-00Z.md +++ /dev/null @@ -1,11 +0,0 @@ -# Decomposition TODO - -- [x] Create `src/errors.mjs` with `ExitError`. -- [x] Create `src/utils.mjs` with `todayDateStr`, `nowUnix`. -- [x] Create `src/cmd-check.mjs`. -- [x] Create `src/cmd-lock.mjs`. -- [x] Create `src/cmd-list.mjs`. -- [x] Update `src/lib.mjs` to import new modules. -- [x] Verify with lint. -- [ ] Verify with tests. -- [ ] Update file size baseline. diff --git a/torch/src/todo/TODO_2026-02-14T23-00-00Z.md b/torch/src/todo/TODO_2026-02-14T23-00-00Z.md deleted file mode 100644 index 542d8427..00000000 --- a/torch/src/todo/TODO_2026-02-14T23-00-00Z.md +++ /dev/null @@ -1,10 +0,0 @@ -# TODO: dashboard/index.html innerHTML Migration - -- [ ] `summaryBar.innerHTML = cards.map(...).join('')` (Line ~436) - - Strategy: Replace with `document.createElement` loop and `replaceChildren`. -- [ ] `lockGrid.innerHTML = ''` (Line ~448) - - Strategy: `lockGrid.replaceChildren()` + `appendChild(emptyState)`. -- [ ] `lockGrid.innerHTML = locks.map(...).join('')` (Line ~455) - - Strategy: Create `createLockCard` function using `document.createElement`, then `lockGrid.replaceChildren(...cards)`. -- [ ] `rawLog.innerHTML = ''` (Line ~534) - - Strategy: `rawLog.replaceChildren()`. diff --git a/torch/src/todo/TODO_2026-02-15.md b/torch/src/todo/TODO_2026-02-15.md deleted file mode 100644 index bc05a2e4..00000000 --- a/torch/src/todo/TODO_2026-02-15.md +++ /dev/null @@ -1,19 +0,0 @@ -# Todo List (2026-02-15) - Protocol Research Agent - -- **agent**: protocol-research-agent -- **cadence**: daily -- **run-start**: 2026-02-15T00:00:00Z -- **prompt**: src/prompts/daily/protocol-research-agent.md - -## Pending Tasks -- [x] Scan for protocol references. -- [x] Create `PROTOCOL_INVENTORY.md`. -- [x] Create `protocol-report-2026-02-15.md`. - -## Completed Tasks -- Scanned repository for `nostr-tools` and `ws` (executed by `protocol-research-agent`). -- Created inventory of protocols. -- Generated protocol report. - -## Blocked Tasks -- None. diff --git a/torch/src/todo/TODO_2026-02-15T01-03-31Z.md b/torch/src/todo/TODO_2026-02-15T01-03-31Z.md deleted file mode 100644 index 999c23ca..00000000 --- a/torch/src/todo/TODO_2026-02-15T01-03-31Z.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -agent: onboarding-audit-agent -cadence: daily -run-start: 2026-02-15T01:03:31Z -prompt: src/prompts/daily/onboarding-audit-agent.md ---- - -# TODO Update - 2026-02-15T01:03:31Z - -Agent: onboarding-audit-agent -Prompt: src/prompts/daily/onboarding-audit-agent.md - -## Pending Tasks -- [ ] Fix prompt contract violations in `src/prompts/` files. diff --git a/torch/src/todo/TODO_2026-02-15T01-22-33Z.md b/torch/src/todo/TODO_2026-02-15T01-22-33Z.md deleted file mode 100644 index cb0d27cb..00000000 --- a/torch/src/todo/TODO_2026-02-15T01-22-33Z.md +++ /dev/null @@ -1,5 +0,0 @@ -# TODO for Perf Agent Run 2026-02-15T01-22-33Z - -- [x] Scan codebase -- [x] Generate report -- [ ] Fix critical issues (none identified yet) diff --git a/torch/src/todo/TODO_2026-02-15T05-06-49Z.md b/torch/src/todo/TODO_2026-02-15T05-06-49Z.md deleted file mode 100644 index 59828dbd..00000000 --- a/torch/src/todo/TODO_2026-02-15T05-06-49Z.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -agent: prompt-fixer-agent -cadence: weekly -run-start: 2026-02-15T05:03:00Z -prompt: src/prompts/weekly/prompt-fixer-agent.md ---- -# Todo - Prompt Safety Fixes - -## Completed Tasks -- [x] Read `artifacts/prompt-safety-audit.md` -- [x] Identify agents needing fixes (24 total) -- [x] Create `fix_prompts.py` script to automate fixes -- [x] Apply fixes to 23 agents via script -- [x] Manually fix `protocol-research-agent.md` (no-op logic insertion) -- [x] Verify changes (checked `const-refactor-agent.md` and script output) -- [x] Generate run artifacts -- [ ] Run lint checks -- [ ] Complete lock diff --git a/torch/src/todo/TODO_2026-02-15T08-00-00Z.md b/torch/src/todo/TODO_2026-02-15T08-00-00Z.md deleted file mode 100644 index 1f600ada..00000000 --- a/torch/src/todo/TODO_2026-02-15T08-00-00Z.md +++ /dev/null @@ -1 +0,0 @@ -# Todo diff --git a/torch/src/todo/TODO_2026-02-15T15-22-42Z.md b/torch/src/todo/TODO_2026-02-15T15-22-42Z.md deleted file mode 100644 index bc7f30c4..00000000 --- a/torch/src/todo/TODO_2026-02-15T15-22-42Z.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -agent: race-condition-agent -cadence: weekly -run-start: 2026-02-15T15:22:42Z -prompt: src/prompts/weekly/race-condition-agent.md ---- -## Completed Tasks -- [x] Audit lock logic. -- [x] Implement fix in `src/lib.mjs`. -- [x] Add regression test in `test/lib.test.mjs`. -- [x] Verify tests pass. -- [x] Create weekly report. -- [x] Complete memory storage. - -## Pending Tasks -- [ ] Verify artifacts. -- [ ] Complete lock. diff --git a/torch/src/todo/TODO_2026-02-15T16-15-05Z.md b/torch/src/todo/TODO_2026-02-15T16-15-05Z.md deleted file mode 100644 index f8957791..00000000 --- a/torch/src/todo/TODO_2026-02-15T16-15-05Z.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -agent: test-audit-agent -cadence: daily -run-start: 2026-02-15T16-15-05Z -prompt: src/prompts/daily/test-audit-agent.md ---- - -# Pending tasks -- [ ] Run tests with coverage -- [ ] Detect flakiness -- [ ] Run static analysis -- [ ] Generate report - -# Completed tasks -- [x] Initialize artifacts diff --git a/torch/src/todo/TODO_2026-02-15T18-00-00Z.md b/torch/src/todo/TODO_2026-02-15T18-00-00Z.md deleted file mode 100644 index ade295c5..00000000 --- a/torch/src/todo/TODO_2026-02-15T18-00-00Z.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -agent: todo-triage-agent -cadence: daily -run-start: 2026-02-15T18:00:00Z -prompt: src/prompts/daily/todo-triage-agent.md ---- - -# Todo - -## Pending tasks -- None (Scan complete, no actionable items found). - -## Completed tasks -- Scan repository for TODOs. -- Generate inventory `artifacts/todos.txt`. -- Analyze findings (0 actionable items). diff --git a/torch/src/todo/TODO_2026-02-15T18-51-10Z.md b/torch/src/todo/TODO_2026-02-15T18-51-10Z.md deleted file mode 100644 index c14e1d68..00000000 --- a/torch/src/todo/TODO_2026-02-15T18-51-10Z.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -agent: audit-agent -cadence: daily -run-start: 2026-02-15T18-51-10Z -prompt: src/prompts/daily/audit-agent.md ---- -# Pending Tasks - -- [ ] Run file size check -- [ ] Run innerHTML check -- [ ] Run lint check -- [ ] Synthesize summary diff --git a/torch/src/todo/TODO_2026-02-15T19-12-02Z.md b/torch/src/todo/TODO_2026-02-15T19-12-02Z.md deleted file mode 100644 index 9817903a..00000000 --- a/torch/src/todo/TODO_2026-02-15T19-12-02Z.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -agent: ci-health-agent -cadence: daily -run-start: 2026-02-15T19-12-02Z -prompt: src/prompts/daily/ci-health-agent.md ---- - -# Todo for ci-health-agent -## Pending -- None -## Completed -- Run tests (5 loops) -- Run lint -Prompt: src/prompts/daily/ci-health-agent.md diff --git a/torch/src/todo/TODO_2026-02-15T20-00-00Z.md b/torch/src/todo/TODO_2026-02-15T20-00-00Z.md deleted file mode 100644 index b1182bc1..00000000 --- a/torch/src/todo/TODO_2026-02-15T20-00-00Z.md +++ /dev/null @@ -1,2 +0,0 @@ -# Todo -- [x] Replace 30078 with KIND_APP_DATA in src/cmd-lock.mjs diff --git a/torch/src/todo/TODO_2026-02-15T23-20-19Z.md b/torch/src/todo/TODO_2026-02-15T23-20-19Z.md deleted file mode 100644 index 401c0e3a..00000000 --- a/torch/src/todo/TODO_2026-02-15T23-20-19Z.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -agent: decompose-agent -cadence: daily -run-start: 2026-02-15T23:20:19Z -prompt: src/prompts/daily/decompose-agent.md ---- - -# Pending tasks -- [ ] Run `scripts/check-file-size.mjs --report` -- [ ] Select largest file -- [ ] Verify recent changes via git log -- [ ] Plan decomposition blocks -- [ ] Create extraction plan - -# Completed tasks -- [x] Initialized artifacts diff --git a/torch/src/todo/TODO_2026-02-16.md b/torch/src/todo/TODO_2026-02-16.md deleted file mode 100644 index f23f62f6..00000000 --- a/torch/src/todo/TODO_2026-02-16.md +++ /dev/null @@ -1,10 +0,0 @@ -# TODO: perf-agent (2026-02-16) - -- [ ] Create context file. -- [ ] Run grep searches. -- [ ] Create hits file. -- [ ] Implement visibility gating in `dashboard/index.html`. -- [ ] Document decisions. -- [ ] Create test logs. -- [ ] Create daily report. -- [ ] Commit and push. diff --git a/torch/src/todo/TODO_2026-02-16T01-18-43Z.md b/torch/src/todo/TODO_2026-02-16T01-18-43Z.md deleted file mode 100644 index 06999e7a..00000000 --- a/torch/src/todo/TODO_2026-02-16T01-18-43Z.md +++ /dev/null @@ -1,13 +0,0 @@ -# Todo - -**Agent**: docs-alignment-agent -**Cadence**: daily -**Session**: session-1771204722 -**Run-Start**: 2026-02-16T01-18-43Z -**Prompt**: src/prompts/daily/docs-alignment-agent.md - -## Pending Tasks -- [ ] Update other docs - -## Completed Tasks -- [x] Check README.md vs package.json diff --git a/torch/src/todo/TODO_2026-02-16T02-11-24Z.md b/torch/src/todo/TODO_2026-02-16T02-11-24Z.md deleted file mode 100644 index 55b2a199..00000000 --- a/torch/src/todo/TODO_2026-02-16T02-11-24Z.md +++ /dev/null @@ -1,12 +0,0 @@ -# Todo -- Agent: docs-code-investigator -- Cadence: daily -- Run-Start: 2026-02-16T02:11:24Z -- Prompt Path: src/prompts/daily/docs-code-investigator.md - -## Pending Tasks -- [ ] [pending] Review `src/lock-ops.mjs` - -## Completed Tasks -- [x] [completed] Select candidate file (`src/lock-ops.mjs`) -- [x] [completed] Create overview document diff --git a/torch/src/todo/TODO_2026-02-16T11-47-38Z.md b/torch/src/todo/TODO_2026-02-16T11-47-38Z.md deleted file mode 100644 index 79240ef3..00000000 --- a/torch/src/todo/TODO_2026-02-16T11-47-38Z.md +++ /dev/null @@ -1,5 +0,0 @@ -# TODO: bug-reproducer-agent run 2026-02-16T11:47:38Z - -- Agent: bug-reproducer-agent -- Status: Completed -- Resolution: Fixed XSS vulnerability in docs/assets/theme.js diff --git a/torch/src/todo/TODO_2026-02-17T00-00-00Z.md b/torch/src/todo/TODO_2026-02-17T00-00-00Z.md deleted file mode 100644 index d349d292..00000000 --- a/torch/src/todo/TODO_2026-02-17T00-00-00Z.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -agent: perf-agent -cadence: daily -run-start: 2026-02-17T00-00-00Z -prompt: src/prompts/daily/perf-agent.md ---- -# Tasks - -## Pending Tasks -- [x] Analyze search results - -## Completed Tasks -- [x] Run search patterns - -References: src/prompts/daily/perf-agent.md diff --git a/torch/src/todo/TODO_2026-02-17T02-04-05Z.md b/torch/src/todo/TODO_2026-02-17T02-04-05Z.md deleted file mode 100644 index 8764d20d..00000000 --- a/torch/src/todo/TODO_2026-02-17T02-04-05Z.md +++ /dev/null @@ -1,5 +0,0 @@ -# TODO for Scheduler Update Agent - -- [x] Scan prompt directories -- [x] Update roster.json -- [x] Update scheduler markdown tables diff --git a/torch/src/todo/TODO_2026-02-17T02-04-31Z.md b/torch/src/todo/TODO_2026-02-17T02-04-31Z.md deleted file mode 100644 index 491cee4f..00000000 --- a/torch/src/todo/TODO_2026-02-17T02-04-31Z.md +++ /dev/null @@ -1,10 +0,0 @@ -# TODO for Scheduler Update Agent - -**Agent**: scheduler-update-agent -**Cadence**: daily -**Session**: manual-jules-1771293773 -Prompt: src/prompts/daily/scheduler-update-agent.md - -- [x] Scan prompt directories -- [x] Update roster.json -- [x] Update scheduler markdown tables diff --git a/torch/src/todo/TODO_2026-02-17T05-04-24Z.md b/torch/src/todo/TODO_2026-02-17T05-04-24Z.md deleted file mode 100644 index a7cb26f3..00000000 --- a/torch/src/todo/TODO_2026-02-17T05-04-24Z.md +++ /dev/null @@ -1,15 +0,0 @@ -# Todo -- Agent: changelog-agent -- Cadence: weekly -- Session: 2026-02-17T05-04-24Z -- Prompt Path: src/prompts/weekly/changelog-agent.md - -## Pending Tasks -- [x] Read git log -- [x] Classify changes -- [x] Create release draft -- [x] Create memory update - -## Completed Tasks -- Generated changelog draft -- Updated artifacts diff --git a/torch/src/todo/TODO_2026-02-17T15-00-00Z.md b/torch/src/todo/TODO_2026-02-17T15-00-00Z.md deleted file mode 100644 index a26e55a8..00000000 --- a/torch/src/todo/TODO_2026-02-17T15-00-00Z.md +++ /dev/null @@ -1,17 +0,0 @@ -# Todo - Feature Proposer Agent Run - -**Agent**: feature-proposer-agent -**Cadence**: weekly -**Run-Start**: 2026-02-17T15:00:00Z -**Prompt-Path**: src/prompts/weekly/feature-proposer-agent.md - -## Completed -- [x] Analyze repository for potential features. -- [x] Select `log-summary.mjs` as the target feature. -- [x] Create `features/` directory. -- [x] Implement `features/log-summary.mjs`. -- [x] Verify the script functionality. -- [x] Follow instructions in `src/prompts/weekly/feature-proposer-agent.md`. - -## Pending -- [ ] Incorporate into main CLI (future work). diff --git a/torch/src/todo/TODO_2026-02-17T16-37-11Z.md b/torch/src/todo/TODO_2026-02-17T16-37-11Z.md deleted file mode 100644 index d0e1bbfb..00000000 --- a/torch/src/todo/TODO_2026-02-17T16-37-11Z.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -agent: torch-garbage-collection-agent -cadence: daily -session: 2026-02-17T16-37-11Z -run-start: 2026-02-17T16-37-11Z ---- -# TODO for torch-garbage-collection-agent - -Active prompt: src/prompts/daily/torch-garbage-collection-agent.md - -## Completed -- [x] Scan for stale files -- [x] Verify no stale files exist diff --git a/torch/src/todo/TODO_2026-02-17T18-04-00Z.md b/torch/src/todo/TODO_2026-02-17T18-04-00Z.md deleted file mode 100644 index dfa09b8e..00000000 --- a/torch/src/todo/TODO_2026-02-17T18-04-00Z.md +++ /dev/null @@ -1,18 +0,0 @@ -# Todo - - - -## Pending tasks -- [ ] Run memory retrieval -- [ ] Run audit scripts -- [ ] Parse results -- [ ] Generate summary -- [ ] Run memory storage - -## Completed tasks -- [x] Initialize artifacts -- [x] Run `src/prompts/daily/audit-agent.md` workflow diff --git a/torch/src/todo/TODO_2026-02-17T19-36-32Z.md b/torch/src/todo/TODO_2026-02-17T19-36-32Z.md deleted file mode 100644 index c24aa23d..00000000 --- a/torch/src/todo/TODO_2026-02-17T19-36-32Z.md +++ /dev/null @@ -1,10 +0,0 @@ -# Pending Tasks -- [ ] Run `npm test` to identify flakes. -- [ ] Fix identified flakes. -- [ ] Document findings. - -- agent: ci-health-agent -- cadence: daily -- session: 2026-02-17T19-36-32Z -- run-start: 2026-02-17T19-36-32Z -- prompt-path: src/prompts/daily/ci-health-agent.md diff --git a/torch/src/todo/TODO_2026-02-17T19-43-02Z.md b/torch/src/todo/TODO_2026-02-17T19-43-02Z.md deleted file mode 100644 index 96de603a..00000000 --- a/torch/src/todo/TODO_2026-02-17T19-43-02Z.md +++ /dev/null @@ -1 +0,0 @@ -- [x] Extract lock-ops constants to src/constants.mjs diff --git a/torch/src/todo/TODO_2026-02-17T21-29-25Z.md b/torch/src/todo/TODO_2026-02-17T21-29-25Z.md deleted file mode 100644 index 00b76424..00000000 --- a/torch/src/todo/TODO_2026-02-17T21-29-25Z.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -agent: content-audit-agent -cadence: daily -run-start: 2026-02-17T21:29:25Z -prompt-path: src/prompts/daily/content-audit-agent.md ---- - -# Audit Tasks - -## Completed Tasks -- [x] Check for existence of `/content` directory -- [x] If exists, inventory claims (Skipped - missing content) -- [x] If missing, report finding and exit (Done) diff --git a/torch/src/todo/TODO_2026-02-17T22-29-00Z.md b/torch/src/todo/TODO_2026-02-17T22-29-00Z.md deleted file mode 100644 index 4b931687..00000000 --- a/torch/src/todo/TODO_2026-02-17T22-29-00Z.md +++ /dev/null @@ -1,20 +0,0 @@ -# Todo: Decompose scripts/agent/run-scheduler-cycle.mjs - -- [ ] Extract `scripts/agent/scheduler-utils.mjs` - - [ ] `runCommand` - - [ ] `getSafeEnv` - - [ ] `sleep` - - [ ] `parseJsonFromOutput` - - [ ] `parseJsonEventsFromOutput` - - [ ] `redactSensitive` - - [ ] `excerptText` - - [ ] `parseNonNegativeInt` - - [ ] `parseBooleanFlag` - - [ ] `readJson` - - [ ] `normalizeStringList` -- [ ] Extract `scripts/agent/scheduler-lock.mjs` - - [ ] `acquireLockWithRetry` - - [ ] `summarizeLockFailureReasons` - - [ ] `classifyLockBackendError` - - [ ] `buildLockBackendRemediation` - - [ ] `runLockHealthPreflight` diff --git a/torch/src/todo/TODO_2026-02-17T23-13-04Z.md b/torch/src/todo/TODO_2026-02-17T23-13-04Z.md deleted file mode 100644 index 862a9b1b..00000000 --- a/torch/src/todo/TODO_2026-02-17T23-13-04Z.md +++ /dev/null @@ -1,11 +0,0 @@ -# Tasks - -- Agent: deps-security-agent -- Cadence: daily -- Run Start: 2026-02-17T23-13-04Z -- Prompt: src/prompts/daily/deps-security-agent.md - -## Completed Tasks -- [x] Document `nostr-tools` update availability in report. -- [x] Document `eslint` vulnerabilities in report. -- [x] Create `artifacts/deps-report.md`. diff --git a/torch/src/todo/TODO_2026-02-18.md b/torch/src/todo/TODO_2026-02-18.md deleted file mode 100644 index 58b7ef1d..00000000 --- a/torch/src/todo/TODO_2026-02-18.md +++ /dev/null @@ -1,7 +0,0 @@ -# Todo: Docs Agent Run 2026-02-18 - -- [ ] Create `AGENTS.md` defining repo-wide policies. -- [ ] Create `docs/event-schemas.md`. -- [ ] Create `docs/runtime-fallback.md`. -- [ ] Add `npm run format` script (e.g., using Prettier) to `package.json`. -- [ ] Investigate why `test:unit` is not standardized in `package.json`. diff --git a/torch/src/todo/TODO_daily_style-agent.md b/torch/src/todo/TODO_daily_style-agent.md deleted file mode 100644 index 3cd61042..00000000 --- a/torch/src/todo/TODO_daily_style-agent.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -agent: style-agent -cadence: daily -run-start: 2026-02-15T08:00:00Z -prompt: src/prompts/daily/style-agent.md ---- - -# Todo: Style Agent Daily Run - -## Pending tasks - -## Completed tasks -- [x] Run `npm run lint` -- [x] Run `npm run lint -- --fix` (not needed) -- [x] Run `node scripts/check-innerhtml.mjs` -- [x] Document results in test log diff --git a/torch/src/todo/TODO_manual-jules-1771293773.md b/torch/src/todo/TODO_manual-jules-1771293773.md deleted file mode 100644 index 8e60593c..00000000 --- a/torch/src/todo/TODO_manual-jules-1771293773.md +++ /dev/null @@ -1,14 +0,0 @@ -# TODO for Scheduler Update Agent - -**Agent**: scheduler-update-agent -**Cadence**: daily -**Session**: manual-jules-1771293773 -Prompt: src/prompts/daily/scheduler-update-agent.md - -## Pending Tasks -- None - -## Completed Tasks -- [x] Scan prompt directories -- [x] Update roster.json -- [x] Update scheduler markdown tables diff --git a/torch/src/todo/TODO_weekly-synthesis-agent.md b/torch/src/todo/TODO_weekly-synthesis-agent.md deleted file mode 100644 index d17ad5ca..00000000 --- a/torch/src/todo/TODO_weekly-synthesis-agent.md +++ /dev/null @@ -1,10 +0,0 @@ -# Todo: Weekly Synthesis Agent - -- [x] Acquire lock. -- [x] Retrieve memory. -- [x] Gather git logs. -- [x] Gather task logs. -- [x] Analyze data. -- [x] Generate weekly report. -- [ ] Store memory. -- [ ] Publish completion. diff --git a/torch/src/torch-config.mjs b/torch/src/torch-config.mjs deleted file mode 100644 index a759051e..00000000 --- a/torch/src/torch-config.mjs +++ /dev/null @@ -1,311 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { - DEFAULT_RELAYS, - DEFAULT_TTL, - DEFAULT_NAMESPACE, - DEFAULT_QUERY_TIMEOUT_MS, - DEFAULT_PUBLISH_TIMEOUT_MS, - DEFAULT_MIN_SUCCESSFUL_PUBLISHES, - DEFAULT_MIN_ACTIVE_RELAY_POOL, -} from './constants.mjs'; - -const DEFAULT_CONFIG_PATH = 'torch-config.json'; - -let cachedConfig = null; -const MIN_TIMEOUT_MS = 100; -const MAX_TIMEOUT_MS = 120_000; - -function parsePositiveInteger(value) { - if (!Number.isFinite(value) || value <= 0) return null; - return Math.floor(value); -} - -function parseStringList(value) { - if (!Array.isArray(value)) return null; - const parsed = value.map((item) => String(item).trim()).filter(Boolean); - return parsed.length > 0 ? parsed : null; -} - -function assertValidRelayUrl(relay, sourceLabel) { - let parsed; - try { - parsed = new URL(relay); - } catch { - throw new Error(`Invalid relay URL in ${sourceLabel}: "${relay}" (must be an absolute ws:// or wss:// URL)`); - } - if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') { - throw new Error(`Invalid relay URL in ${sourceLabel}: "${relay}" (protocol must be ws:// or wss://)`); - } -} - -function assertTimeoutInRange(value, sourceLabel) { - if (!Number.isInteger(value) || value < MIN_TIMEOUT_MS || value > MAX_TIMEOUT_MS) { - throw new Error( - `Invalid ${sourceLabel}: ${value} (must be an integer between ${MIN_TIMEOUT_MS} and ${MAX_TIMEOUT_MS} ms)`, - ); - } -} - -function assertPositiveCount(value, sourceLabel) { - if (!Number.isInteger(value) || value <= 0) { - throw new Error(`Invalid ${sourceLabel}: ${value} (must be a positive integer)`); - } -} - -function normalizeCadence(value, fallback) { - const normalized = String(value || '').trim().toLowerCase(); - return normalized === 'daily' || normalized === 'weekly' || normalized === 'all' ? normalized : fallback; -} - -function normalizeStatus(value, fallback) { - const normalized = String(value || '').trim().toLowerCase(); - return normalized === 'active' || normalized === 'all' ? normalized : fallback; -} - - -export function getTorchConfigPath() { - const explicitPath = (process.env.TORCH_CONFIG_PATH || '').trim(); - if (explicitPath) return path.resolve(process.cwd(), explicitPath); - - const localPath = path.resolve(process.cwd(), DEFAULT_CONFIG_PATH); - if (fs.existsSync(localPath)) return localPath; - - const parentPath = path.resolve(process.cwd(), '..', DEFAULT_CONFIG_PATH); - if (fs.existsSync(parentPath)) return parentPath; - - return localPath; -} - -export function parseTorchConfig(raw, configPath = null) { - const nostrLock = raw.nostrLock || {}; - const dashboard = raw.dashboard || {}; - const scheduler = raw.scheduler || {}; - const firstPromptByCadence = scheduler.firstPromptByCadence || {}; - const paused = scheduler.paused || {}; - - return { - configPath, - raw, - nostrLock: { - namespace: typeof nostrLock.namespace === 'string' ? nostrLock.namespace.trim() : null, - relays: parseStringList(nostrLock.relays), - relayFallbacks: parseStringList(nostrLock.relayFallbacks), - ttlSeconds: Number.isFinite(nostrLock.ttlSeconds) && nostrLock.ttlSeconds > 0 - ? Math.floor(nostrLock.ttlSeconds) - : null, - queryTimeoutMs: parsePositiveInteger(nostrLock.queryTimeoutMs), - publishTimeoutMs: parsePositiveInteger(nostrLock.publishTimeoutMs), - minSuccessfulRelayPublishes: parsePositiveInteger(nostrLock.minSuccessfulRelayPublishes), - minActiveRelayPool: parsePositiveInteger(nostrLock.minActiveRelayPool), - dailyRoster: parseStringList(nostrLock.dailyRoster), - weeklyRoster: parseStringList(nostrLock.weeklyRoster), - }, - dashboard: { - defaultCadenceView: normalizeCadence(dashboard.defaultCadenceView, 'daily'), - defaultStatusView: normalizeStatus(dashboard.defaultStatusView, 'active'), - relays: parseStringList(dashboard.relays), - namespace: typeof dashboard.namespace === 'string' ? dashboard.namespace.trim() : null, - hashtag: typeof dashboard.hashtag === 'string' ? dashboard.hashtag.trim() : null, - auth: typeof dashboard.auth === 'string' ? dashboard.auth.trim() : null, - }, - scheduler: { - firstPromptByCadence: { - daily: typeof firstPromptByCadence.daily === 'string' ? firstPromptByCadence.daily.trim() : null, - weekly: typeof firstPromptByCadence.weekly === 'string' ? firstPromptByCadence.weekly.trim() : null, - }, - paused: { - daily: parseStringList(paused.daily) || [], - weekly: parseStringList(paused.weekly) || [], - }, - }, - }; -} - -/** @internal */ -export function _resetTorchConfigCache() { - cachedConfig = null; -} - -export async function loadTorchConfig(fileSystem = fs) { - if (cachedConfig) return cachedConfig; - - const configPath = getTorchConfigPath(); - let raw = {}; - - try { - const content = await fileSystem.promises.readFile(configPath, 'utf8'); - raw = JSON.parse(content); - } catch (err) { - if (err.code !== 'ENOENT') { - throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err }); - } - // If file missing (ENOENT), we use empty object defaults - } - - cachedConfig = parseTorchConfig(raw, configPath); - validateLockBackendConfig(cachedConfig); - return cachedConfig; -} - -function validateLockBackendConfig(config) { - const relays = config.nostrLock.relays || []; - const relayFallbacks = config.nostrLock.relayFallbacks || []; - - for (const relay of relays) { - assertValidRelayUrl(relay, 'nostrLock.relays'); - } - for (const relay of relayFallbacks) { - assertValidRelayUrl(relay, 'nostrLock.relayFallbacks'); - } - - if (config.nostrLock.queryTimeoutMs !== null) { - assertTimeoutInRange(config.nostrLock.queryTimeoutMs, 'nostrLock.queryTimeoutMs'); - } - if (config.nostrLock.publishTimeoutMs !== null) { - assertTimeoutInRange(config.nostrLock.publishTimeoutMs, 'nostrLock.publishTimeoutMs'); - } - if (config.nostrLock.minSuccessfulRelayPublishes !== null) { - assertPositiveCount(config.nostrLock.minSuccessfulRelayPublishes, 'nostrLock.minSuccessfulRelayPublishes'); - } - if (config.nostrLock.minActiveRelayPool !== null) { - assertPositiveCount(config.nostrLock.minActiveRelayPool, 'nostrLock.minActiveRelayPool'); - } -} - -function parseEnvRelayList(envValue, envName) { - const relays = envValue.split(',').map((r) => r.trim()).filter(Boolean); - for (const relay of relays) { - assertValidRelayUrl(relay, envName); - } - return relays; -} - -function parseEnvInteger(envValue, envName) { - const parsed = parseInt(envValue, 10); - if (Number.isNaN(parsed)) { - throw new Error(`Invalid ${envName}: "${envValue}" (must be an integer)`); - } - return parsed; -} - -function resolveIntegerConfig(envKey, configValue, defaultValue, validator, configLabel) { - const envValue = process.env[envKey]; - if (envValue) { - const parsed = parseEnvInteger(envValue, envKey); - validator(parsed, envKey); - return parsed; - } - const value = configValue || defaultValue; - validator(value, `effective ${configLabel}`); - return value; -} - -export async function getRelays() { - const config = await loadTorchConfig(); - const envRelays = process.env.NOSTR_LOCK_RELAYS; - if (envRelays) { - return parseEnvRelayList(envRelays, 'NOSTR_LOCK_RELAYS'); - } - if (config.nostrLock.relays?.length) { - return config.nostrLock.relays; - } - for (const relay of DEFAULT_RELAYS) { - assertValidRelayUrl(relay, 'DEFAULT_RELAYS'); - } - return DEFAULT_RELAYS; -} - -export async function getRelayFallbacks() { - const config = await loadTorchConfig(); - const envRelays = process.env.NOSTR_LOCK_RELAY_FALLBACKS; - if (envRelays) { - return parseEnvRelayList(envRelays, 'NOSTR_LOCK_RELAY_FALLBACKS'); - } - return config.nostrLock.relayFallbacks || []; -} - -export async function getNamespace() { - const config = await loadTorchConfig(); - const namespace = (process.env.NOSTR_LOCK_NAMESPACE || config.nostrLock.namespace || DEFAULT_NAMESPACE).trim(); - return namespace || DEFAULT_NAMESPACE; -} - -export async function getTtl() { - const config = await loadTorchConfig(); - const envTtl = process.env.NOSTR_LOCK_TTL; - if (envTtl) { - const parsed = parseInt(envTtl, 10); - if (!Number.isNaN(parsed) && parsed > 0) return parsed; - } - if (config.nostrLock.ttlSeconds) { - return config.nostrLock.ttlSeconds; - } - return DEFAULT_TTL; -} - -export async function getQueryTimeoutMs() { - const config = await loadTorchConfig(); - return resolveIntegerConfig( - 'NOSTR_LOCK_QUERY_TIMEOUT_MS', - config.nostrLock.queryTimeoutMs, - DEFAULT_QUERY_TIMEOUT_MS, - assertTimeoutInRange, - 'query timeout', - ); -} - -export async function getPublishTimeoutMs() { - const config = await loadTorchConfig(); - return resolveIntegerConfig( - 'NOSTR_LOCK_PUBLISH_TIMEOUT_MS', - config.nostrLock.publishTimeoutMs, - DEFAULT_PUBLISH_TIMEOUT_MS, - assertTimeoutInRange, - 'publish timeout', - ); -} - -export async function getMinSuccessfulRelayPublishes() { - const config = await loadTorchConfig(); - return resolveIntegerConfig( - 'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES', - config.nostrLock.minSuccessfulRelayPublishes, - DEFAULT_MIN_SUCCESSFUL_PUBLISHES, - assertPositiveCount, - 'min successful relay publishes', - ); -} - -export async function getMinActiveRelayPool() { - const config = await loadTorchConfig(); - return resolveIntegerConfig( - 'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL', - config.nostrLock.minActiveRelayPool, - DEFAULT_MIN_ACTIVE_RELAY_POOL, - assertPositiveCount, - 'min active relay pool', - ); -} - -export async function getHashtag() { - const config = await loadTorchConfig(); - const envValue = process.env.NOSTR_LOCK_HASHTAG; - if (envValue) { - return envValue.trim(); - } - if (config.dashboard.hashtag) { - return config.dashboard.hashtag; - } - const namespace = await getNamespace(); - return `${namespace}-agent-lock`; -} - -export async function getDashboardAuth() { - const config = await loadTorchConfig(); - const envValue = process.env.TORCH_DASHBOARD_AUTH; - if (envValue) { - return envValue.trim(); - } - return config.dashboard.auth; -} diff --git a/torch/src/utils.mjs b/torch/src/utils.mjs deleted file mode 100644 index 4b7c76c7..00000000 --- a/torch/src/utils.mjs +++ /dev/null @@ -1,36 +0,0 @@ -export function todayDateStr() { - return new Date().toISOString().slice(0, 10); -} - -export function nowUnix() { - return Math.floor(Date.now() / 1000); -} - -export function getIsoWeekStr(dateInput) { - // Parse input to ensure we work with a Date object - // If dateInput is YYYY-MM-DD string, new Date() treats it as UTC. - const d = dateInput ? new Date(dateInput) : new Date(); - - // If valid date, use UTC components to avoid local timezone issues - if (isNaN(d.getTime())) return ''; - - // Copy date so we don't mutate the original if passed by reference (though new Date() handles that) - const date = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); - - // ISO week date system: Thursday determines the year - // day: 0 (Sun) -> 6 (Sat). We want 1 (Mon) -> 7 (Sun) - const day = date.getUTCDay() || 7; - date.setUTCDate(date.getUTCDate() + 4 - day); - - const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); - const weekNo = Math.ceil((((date - yearStart) / 86400000) + 1) / 7); - - return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; -} - -export function detectPlatform() { - if (process.env.JULES_API_KEY || process.env.JULES_SESSION_ID) return 'jules'; - if (process.env.CODEX_API_KEY || process.env.CODEX_SESSION_ID) return 'codex'; - if (process.env.GOOSE_API_KEY || process.env.GOOSE_SESSION_ID) return 'goose'; - return null; -} diff --git a/torch/task-logs/daily/2026-02-17T16-56-00Z__ci-health-agent__failed.md b/torch/task-logs/daily/2026-02-17T16-56-00Z__ci-health-agent__failed.md deleted file mode 100644 index 1c024b3a..00000000 --- a/torch/task-logs/daily/2026-02-17T16-56-00Z__ci-health-agent__failed.md +++ /dev/null @@ -1,10 +0,0 @@ -# CI Health Agent Check Failed - -The repository checks failed during the CI run. - -## Check Failures - -1. **flake8**: Found numerous style violations (E501 line too long, unused imports, etc.). -2. **pytest**: Failed to start due to configuration error (`unrecognized arguments: -n`). - -This indicates the codebase is currently unhealthy or the environment is misconfigured. diff --git a/torch/task-logs/daily/2026-02-17T17-37-50Z__ci-health-agent__failed.md b/torch/task-logs/daily/2026-02-17T17-37-50Z__ci-health-agent__failed.md deleted file mode 100644 index 1cde8404..00000000 --- a/torch/task-logs/daily/2026-02-17T17-37-50Z__ci-health-agent__failed.md +++ /dev/null @@ -1,3262 +0,0 @@ -# CI Health Agent Check Failed - -The repository checks failed during the CI run. - -## Check Failures - -1. **flake8**: Failed (exit code 1). - - Found style violations (E402, E501, etc.). -2. **pytest**: Passed (exit code 0). - - Tests passed successfully. - -## Details - -### flake8 Output -``` -./scripts/generate_test_profile.py:32:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:34:80: E501 line too long (80 > 79 characters) -./scripts/generate_test_profile.py:39:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:40:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:41:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:42:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:43:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:44:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:45:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:46:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:47:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:48:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:49:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:50:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:51:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:52:1: E402 module level import not at top of file -./scripts/generate_test_profile.py:60:80: E501 line too long (98 > 79 characters) -./scripts/generate_test_profile.py:107:80: E501 line too long (80 > 79 characters) -./scripts/generate_test_profile.py:139:80: E501 line too long (84 > 79 characters) -./scripts/generate_test_profile.py:141:80: E501 line too long (88 > 79 characters) -./scripts/generate_test_profile.py:172:80: E501 line too long (82 > 79 characters) -./scripts/generate_test_profile.py:189:80: E501 line too long (86 > 79 characters) -./scripts/generate_test_profile.py:218:80: E501 line too long (87 > 79 characters) -./scripts/generate_test_profile.py:221:80: E501 line too long (86 > 79 characters) -./scripts/update_checksum.py:10:1: E402 module level import not at top of file -./scripts/update_checksum.py:11:1: E402 module level import not at top of file -./scripts/update_checksum.py:15:80: E501 line too long (86 > 79 characters) -./src/constants.py:39:80: E501 line too long (86 > 79 characters) -./src/local_bip85/__init__.py:12:80: E501 line too long (94 > 79 characters) -./src/local_bip85/bip85.py:6:80: E501 line too long (97 > 79 characters) -./src/local_bip85/bip85.py:7:80: E501 line too long (101 > 79 characters) -./src/local_bip85/bip85.py:8:80: E501 line too long (100 > 79 characters) -./src/local_bip85/bip85.py:10:80: E501 line too long (160 > 79 characters) -./src/local_bip85/bip85.py:11:80: E501 line too long (167 > 79 characters) -./src/local_bip85/bip85.py:13:80: E501 line too long (87 > 79 characters) -./src/local_bip85/bip85.py:16:1: F401 'sys' imported but unused -./src/local_bip85/bip85.py:20:1: F401 'os' imported but unused -./src/local_bip85/bip85.py:25:80: E501 line too long (82 > 79 characters) -./src/local_bip85/bip85.py:27:1: F401 'cryptography.hazmat.primitives.kdf.hkdf.HKDF' imported but unused -./src/local_bip85/bip85.py:28:1: F401 'cryptography.hazmat.primitives.hashes' imported but unused -./src/local_bip85/bip85.py:29:1: F401 'cryptography.hazmat.backends.default_backend' imported but unused -./src/local_bip85/bip85.py:54:80: E501 line too long (83 > 79 characters) -./src/local_bip85/bip85.py:57:80: E501 line too long (82 > 79 characters) -./src/local_bip85/bip85.py:74:80: E501 line too long (80 > 79 characters) -./src/local_bip85/bip85.py:77:80: E501 line too long (82 > 79 characters) -./src/local_bip85/bip85.py:109:80: E501 line too long (102 > 79 characters) -./src/local_bip85/bip85.py:112:80: E501 line too long (119 > 79 characters) -./src/local_bip85/bip85.py:115:80: E501 line too long (102 > 79 characters) -./src/local_bip85/bip85.py:139:80: E501 line too long (82 > 79 characters) -./src/local_bip85/bip85.py:152:80: E501 line too long (83 > 79 characters) -./src/main.py:5:80: E501 line too long (82 > 79 characters) -./src/main.py:10:1: E402 module level import not at top of file -./src/main.py:11:1: E402 module level import not at top of file -./src/main.py:12:1: E402 module level import not at top of file -./src/main.py:13:1: E402 module level import not at top of file -./src/main.py:14:1: E402 module level import not at top of file -./src/main.py:15:1: E402 module level import not at top of file -./src/main.py:16:1: E402 module level import not at top of file -./src/main.py:17:1: E402 module level import not at top of file -./src/main.py:18:1: E402 module level import not at top of file -./src/main.py:19:1: E402 module level import not at top of file -./src/main.py:20:1: E402 module level import not at top of file -./src/main.py:21:1: E402 module level import not at top of file -./src/main.py:23:1: E402 module level import not at top of file -./src/main.py:24:1: F401 'nostr.client.NostrClient' imported but unused -./src/main.py:24:1: E402 module level import not at top of file -./src/main.py:25:1: E402 module level import not at top of file -./src/main.py:26:1: E402 module level import not at top of file -./src/main.py:27:1: E402 module level import not at top of file -./src/main.py:28:1: E402 module level import not at top of file -./src/main.py:33:1: E402 module level import not at top of file -./src/main.py:40:1: E402 module level import not at top of file -./src/main.py:41:1: E402 module level import not at top of file -./src/main.py:42:1: E402 module level import not at top of file -./src/main.py:47:1: E402 module level import not at top of file -./src/main.py:65:80: E501 line too long (85 > 79 characters) -./src/main.py:177:80: E501 line too long (97 > 79 characters) -./src/main.py:197:80: E501 line too long (87 > 79 characters) -./src/main.py:202:80: E501 line too long (88 > 79 characters) -./src/main.py:243:80: E501 line too long (128 > 79 characters) -./src/main.py:263:80: E501 line too long (85 > 79 characters) -./src/main.py:305:80: E501 line too long (80 > 79 characters) -./src/main.py:335:80: E501 line too long (81 > 79 characters) -./src/main.py:442:80: E501 line too long (87 > 79 characters) -./src/main.py:455:80: E501 line too long (80 > 79 characters) -./src/main.py:481:80: E501 line too long (83 > 79 characters) -./src/main.py:489:80: E501 line too long (107 > 79 characters) -./src/main.py:491:80: E501 line too long (85 > 79 characters) -./src/main.py:503:80: E501 line too long (88 > 79 characters) -./src/main.py:604:80: E501 line too long (99 > 79 characters) -./src/main.py:813:80: E501 line too long (84 > 79 characters) -./src/main.py:863:80: E501 line too long (85 > 79 characters) -./src/main.py:864:80: E501 line too long (81 > 79 characters) -./src/main.py:900:80: E501 line too long (85 > 79 characters) -./src/main.py:901:80: E501 line too long (81 > 79 characters) -./src/main.py:922:80: E501 line too long (81 > 79 characters) -./src/main.py:971:80: E501 line too long (81 > 79 characters) -./src/main.py:1015:80: E501 line too long (81 > 79 characters) -./src/main.py:1048:80: E501 line too long (82 > 79 characters) -./src/main.py:1089:80: E501 line too long (84 > 79 characters) -./src/main.py:1092:80: E501 line too long (85 > 79 characters) -./src/main.py:1117:80: E501 line too long (84 > 79 characters) -./src/main.py:1138:80: E501 line too long (81 > 79 characters) -./src/main.py:1152:80: E501 line too long (85 > 79 characters) -./src/main.py:1180:80: E501 line too long (85 > 79 characters) -./src/main.py:1256:80: E501 line too long (85 > 79 characters) -./src/main.py:1283:80: E501 line too long (82 > 79 characters) -./src/main.py:1286:80: E501 line too long (82 > 79 characters) -./src/main.py:1354:80: E501 line too long (84 > 79 characters) -./src/main.py:1385:80: E501 line too long (82 > 79 characters) -./src/main.py:1388:80: E501 line too long (87 > 79 characters) -./src/main.py:1391:80: E501 line too long (81 > 79 characters) -./src/main.py:1392:80: E501 line too long (82 > 79 characters) -./src/main.py:1395:80: E501 line too long (81 > 79 characters) -./src/main.py:1396:80: E501 line too long (82 > 79 characters) -./src/main.py:1429:80: E501 line too long (83 > 79 characters) -./src/main.py:1468:80: E501 line too long (85 > 79 characters) -./src/main.py:1469:80: E501 line too long (87 > 79 characters) -./src/main.py:1484:80: E501 line too long (84 > 79 characters) -./src/nostr/client.py:1:1: F401 'asyncio' imported but unused -./src/nostr/client.py:2:1: F401 'base64' imported but unused -./src/nostr/client.py:3:1: F401 'json' imported but unused -./src/nostr/client.py:5:1: F401 'time' imported but unused -./src/nostr/client.py:7:1: F401 'datetime.timedelta' imported but unused -./src/nostr/client.py:10:1: F401 'websockets' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.EventBuilder' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.Filter' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.Kind' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.KindStandard' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.Tag' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.RelayUrl' imported but unused -./src/nostr/client.py:11:1: F401 'nostr_sdk.PublicKey' imported but unused -./src/nostr/client.py:22:1: F401 'nostr_sdk.EventId' imported but unused -./src/nostr/client.py:22:1: F401 'nostr_sdk.Timestamp' imported but unused -./src/nostr/client.py:24:1: F401 'constants.MAX_RETRIES' imported but unused -./src/nostr/client.py:24:1: F401 'constants.RETRY_DELAY' imported but unused -./src/nostr/client.py:27:1: F401 '.backup_models.ChunkMeta' imported but unused -./src/nostr/client.py:27:1: F401 '.backup_models.KIND_DELTA' imported but unused -./src/nostr/client.py:27:1: F401 '.backup_models.KIND_MANIFEST' imported but unused -./src/nostr/client.py:27:1: F401 '.backup_models.KIND_SNAPSHOT_CHUNK' imported but unused -./src/nostr/coincurve_keys.py:4:1: F401 'coincurve.PublicKey' imported but unused -./src/nostr/connection.py:93:80: E501 line too long (80 > 79 characters) -./src/nostr/connection.py:99:80: E501 line too long (84 > 79 characters) -./src/nostr/connection.py:103:80: E501 line too long (83 > 79 characters) -./src/nostr/connection.py:132:80: E501 line too long (87 > 79 characters) -./src/nostr/connection.py:140:80: E501 line too long (81 > 79 characters) -./src/nostr/connection.py:167:80: E501 line too long (85 > 79 characters) -./src/nostr/connection.py:209:80: E501 line too long (82 > 79 characters) -./src/nostr/key_manager.py:22:80: E501 line too long (82 > 79 characters) -./src/nostr/key_manager.py:33:80: E501 line too long (82 > 79 characters) -./src/nostr/key_manager.py:50:80: E501 line too long (89 > 79 characters) -./src/nostr/key_manager.py:100:80: E501 line too long (86 > 79 characters) -./src/nostr/key_manager.py:108:80: E501 line too long (81 > 79 characters) -./src/nostr/key_manager.py:119:80: E501 line too long (85 > 79 characters) -./src/nostr/snapshot.py:34:27: E203 whitespace before ':' -./src/nostr/snapshot.py:34:80: E501 line too long (82 > 79 characters) -./src/nostr/snapshot.py:58:80: E501 line too long (82 > 79 characters) -./src/nostr/snapshot.py:90:80: E501 line too long (83 > 79 characters) -./src/nostr/snapshot.py:94:80: E501 line too long (80 > 79 characters) -./src/nostr/snapshot.py:121:80: E501 line too long (86 > 79 characters) -./src/nostr/snapshot.py:188:80: E501 line too long (82 > 79 characters) -./src/nostr/snapshot.py:230:80: E501 line too long (87 > 79 characters) -./src/nostr/snapshot.py:279:80: E501 line too long (81 > 79 characters) -./src/nostr/snapshot.py:296:80: E501 line too long (89 > 79 characters) -./src/nostr/snapshot.py:300:80: E501 line too long (84 > 79 characters) -./src/nostr/snapshot.py:343:80: E501 line too long (88 > 79 characters) -./src/nostr/snapshot.py:375:80: E501 line too long (82 > 79 characters) -./src/nostr/snapshot.py:385:80: E501 line too long (82 > 79 characters) -./src/nostr/snapshot.py:408:80: E501 line too long (81 > 79 characters) -./src/seedpass/api.py:10:1: F401 'typing.Optional' imported but unused -./src/seedpass/api.py:35:80: E501 line too long (80 > 79 characters) -./src/seedpass/api.py:67:80: E501 line too long (88 > 79 characters) -./src/seedpass/api.py:87:80: E501 line too long (82 > 79 characters) -./src/seedpass/api.py:167:80: E501 line too long (80 > 79 characters) -./src/seedpass/api.py:187:80: E501 line too long (83 > 79 characters) -./src/seedpass/api.py:288:80: E501 line too long (81 > 79 characters) -./src/seedpass/api.py:363:80: E501 line too long (81 > 79 characters) -./src/seedpass/api.py:422:80: E501 line too long (80 > 79 characters) -./src/seedpass/api.py:475:80: E501 line too long (83 > 79 characters) -./src/seedpass/api.py:514:80: E501 line too long (88 > 79 characters) -./src/seedpass/api.py:521:80: E501 line too long (84 > 79 characters) -./src/seedpass/api.py:564:80: E501 line too long (82 > 79 characters) -./src/seedpass/api.py:690:80: E501 line too long (85 > 79 characters) -./src/seedpass/cli/__init__.py:42:1: E402 module level import not at top of file -./src/seedpass/cli/__init__.py:117:80: E501 line too long (87 > 79 characters) -./src/seedpass/cli/__init__.py:121:80: E501 line too long (87 > 79 characters) -./src/seedpass/cli/__init__.py:125:80: E501 line too long (87 > 79 characters) -./src/seedpass/cli/__init__.py:135:80: E501 line too long (89 > 79 characters) -./src/seedpass/cli/__init__.py:148:80: E501 line too long (88 > 79 characters) -./src/seedpass/cli/__init__.py:167:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/__init__.py:175:80: E501 line too long (84 > 79 characters) -./src/seedpass/cli/api.py:12:80: E501 line too long (85 > 79 characters) -./src/seedpass/cli/api.py:16:80: E501 line too long (90 > 79 characters) -./src/seedpass/cli/common.py:6:1: F401 'seedpass.core.entry_types.EntryType' imported but unused -./src/seedpass/cli/common.py:7:1: F401 'seedpass.core.api.ChangePasswordRequest' imported but unused -./src/seedpass/cli/common.py:7:1: F401 'seedpass.core.api.UnlockRequest' imported but unused -./src/seedpass/cli/common.py:7:1: F401 'seedpass.core.api.BackupParentSeedRequest' imported but unused -./src/seedpass/cli/common.py:7:1: F401 'seedpass.core.api.ProfileSwitchRequest' imported but unused -./src/seedpass/cli/common.py:7:1: F401 'seedpass.core.api.ProfileRemoveRequest' imported but unused -./src/seedpass/cli/config.py:61:80: E501 line too long (86 > 79 characters) -./src/seedpass/cli/config.py:72:80: E501 line too long (85 > 79 characters) -./src/seedpass/cli/config.py:107:80: E501 line too long (87 > 79 characters) -./src/seedpass/cli/entry.py:30:80: E501 line too long (80 > 79 characters) -./src/seedpass/cli/entry.py:90:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:129:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:175:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:176:80: E501 line too long (81 > 79 characters) -./src/seedpass/cli/entry.py:200:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:217:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:238:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:255:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/entry.py:288:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/fingerprint.py:29:80: E501 line too long (81 > 79 characters) -./src/seedpass/cli/nostr.py:8:80: E501 line too long (101 > 79 characters) -./src/seedpass/cli/util.py:20:80: E501 line too long (82 > 79 characters) -./src/seedpass/cli/vault.py:46:80: E501 line too long (84 > 79 characters) -./src/seedpass/cli/vault.py:93:80: E501 line too long (80 > 79 characters) -./src/seedpass/cli/vault.py:97:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/api.py:5:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/api.py:142:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/api.py:206:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/api.py:218:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/api.py:224:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/api.py:251:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/api.py:292:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/api.py:301:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/api.py:303:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/api.py:541:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/api.py:570:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/api.py:668:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/backup.py:6:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/backup.py:7:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/backup.py:11:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/backup.py:24:1: F401 'constants.APP_DIR' imported but unused -./src/seedpass/core/backup.py:34:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/backup.py:58:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/backup.py:68:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/backup.py:81:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/backup.py:87:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/backup.py:96:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/backup.py:104:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/backup.py:124:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/backup.py:131:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/backup.py:134:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/backup.py:139:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/backup.py:143:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/backup.py:154:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/backup.py:159:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/backup.py:162:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/backup.py:166:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/backup.py:188:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/backup.py:194:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/backup.py:199:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/backup.py:209:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/backup.py:212:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/backup.py:217:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/backup.py:221:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/config_manager.py:88:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/config_manager.py:101:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/config_manager.py:265:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:11:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:38:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:40:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:50:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:69:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/encryption.py:78:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:85:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/encryption.py:88:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:101:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/encryption.py:127:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/encryption.py:142:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:146:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:159:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:164:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:169:80: E501 line too long (98 > 79 characters) -./src/seedpass/core/encryption.py:174:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:177:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:181:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/encryption.py:186:80: E501 line too long (96 > 79 characters) -./src/seedpass/core/encryption.py:201:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:207:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/encryption.py:209:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/encryption.py:218:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:220:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/encryption.py:240:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/encryption.py:245:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/encryption.py:248:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:277:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/encryption.py:287:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:292:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:293:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/encryption.py:298:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/encryption.py:326:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:331:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:375:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/encryption.py:393:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/encryption.py:403:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:418:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/encryption.py:430:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/encryption.py:467:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/encryption.py:474:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/encryption.py:494:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/encryption.py:500:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/encryption.py:504:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/encryption.py:519:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:523:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/encryption.py:537:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/encryption.py:552:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/encryption.py:576:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/encryption.py:579:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/encryption.py:585:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/encryption.py:606:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/encryption.py:619:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/encryption.py:627:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/encryption.py:634:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/encryption.py:635:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/entry_management.py:7:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:8:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/entry_management.py:11:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:13:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/entry_management.py:14:80: E501 line too long (92 > 79 characters) -./src/seedpass/core/entry_management.py:15:80: E501 line too long (101 > 79 characters) -./src/seedpass/core/entry_management.py:23:5: F401 'json as json_lib' imported but unused -./src/seedpass/core/entry_management.py:70:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:74:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:92:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_management.py:117:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/entry_management.py:179:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/entry_management.py:225:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:233:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:369:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_management.py:375:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:435:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:529:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:747:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_management.py:773:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/entry_management.py:775:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:784:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_management.py:799:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/entry_management.py:805:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:810:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_management.py:813:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/entry_management.py:862:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/entry_management.py:866:80: E501 line too long (103 > 79 characters) -./src/seedpass/core/entry_management.py:872:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:976:80: E501 line too long (102 > 79 characters) -./src/seedpass/core/entry_management.py:982:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:985:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:988:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:992:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:997:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:1001:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:1006:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_management.py:1021:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/entry_management.py:1062:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/entry_management.py:1071:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:1075:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/entry_management.py:1077:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:1131:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:1141:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:1151:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_management.py:1154:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:1162:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_management.py:1172:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_management.py:1185:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:1187:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/entry_management.py:1191:80: E501 line too long (106 > 79 characters) -./src/seedpass/core/entry_management.py:1198:80: E501 line too long (91 > 79 characters) -./src/seedpass/core/entry_management.py:1204:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:1207:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/entry_management.py:1210:80: E501 line too long (123 > 79 characters) -./src/seedpass/core/entry_management.py:1215:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:1218:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/entry_management.py:1235:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_management.py:1237:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:1252:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_management.py:1262:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_management.py:1304:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:1309:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/entry_management.py:1313:80: E501 line too long (103 > 79 characters) -./src/seedpass/core/entry_management.py:1319:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/entry_management.py:1321:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_management.py:1326:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/entry_management.py:1339:27: F541 f-string is missing placeholders -./src/seedpass/core/entry_management.py:1357:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:1370:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:1379:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_management.py:1383:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:1412:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_management.py:1436:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_service.py:41:80: E501 line too long (90 > 79 characters) -./src/seedpass/core/entry_service.py:47:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/entry_service.py:51:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_service.py:54:80: E501 line too long (123 > 79 characters) -./src/seedpass/core/entry_service.py:66:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_service.py:70:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_service.py:80:80: E501 line too long (119 > 79 characters) -./src/seedpass/core/entry_service.py:85:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_service.py:92:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_service.py:94:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_service.py:99:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_service.py:114:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_service.py:132:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/entry_service.py:146:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_service.py:168:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/entry_service.py:183:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_service.py:185:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/entry_service.py:191:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/entry_service.py:193:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/entry_service.py:195:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/entry_service.py:197:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/entry_service.py:205:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/entry_service.py:231:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:6:80: E501 line too long (92 > 79 characters) -./src/seedpass/core/manager.py:7:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:8:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:23:1: F401 'builtins' imported but unused -./src/seedpass/core/manager.py:44:1: F401 'utils.key_derivation.derive_key_from_parent_seed' imported but unused -./src/seedpass/core/manager.py:53:1: F401 'utils.checksum.json_checksum' imported but unused -./src/seedpass/core/manager.py:69:1: F401 'utils.terminal_utils.clear_screen' imported but unused -./src/seedpass/core/manager.py:69:1: F401 'utils.terminal_utils.clear_and_print_profile_chain' imported but unused -./src/seedpass/core/manager.py:81:1: F401 'constants.PARENT_SEED_FILE' imported but unused -./src/seedpass/core/manager.py:81:1: F401 'constants.MIN_PASSWORD_LENGTH' imported but unused -./src/seedpass/core/manager.py:81:1: F401 'constants.MAX_PASSWORD_LENGTH' imported but unused -./src/seedpass/core/manager.py:100:1: F401 'bip_utils.Bip39MnemonicGenerator' imported but unused -./src/seedpass/core/manager.py:100:1: F401 'bip_utils.Bip39Languages' imported but unused -./src/seedpass/core/manager.py:100:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:102:1: F811 redefinition of unused 'datetime' from line 22 -./src/seedpass/core/manager.py:137:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:231:80: E501 line too long (101 > 79 characters) -./src/seedpass/core/manager.py:232:80: E501 line too long (103 > 79 characters) -./src/seedpass/core/manager.py:233:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/manager.py:236:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:245:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:320:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:381:80: E501 line too long (92 > 79 characters) -./src/seedpass/core/manager.py:423:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:530:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:548:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:559:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:561:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:563:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:567:80: E501 line too long (120 > 79 characters) -./src/seedpass/core/manager.py:600:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:603:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:617:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:651:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:664:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:678:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:692:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:697:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:714:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:737:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:738:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:739:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:767:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:777:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:785:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:786:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:788:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:841:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:842:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:850:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:908:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:919:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:924:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:937:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:943:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:947:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:966:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:971:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/manager.py:991:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:993:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1012:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:1019:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:1028:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1058:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1064:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1075:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:1083:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1091:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1098:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1106:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1111:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1113:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1137:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1154:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:1172:80: E501 line too long (105 > 79 characters) -./src/seedpass/core/manager.py:1175:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1180:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1187:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1193:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1201:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1208:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1212:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1214:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1219:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1221:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1225:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1232:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1284:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1285:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1288:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1289:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1294:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1301:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1313:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1314:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1335:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1355:80: E501 line too long (90 > 79 characters) -./src/seedpass/core/manager.py:1361:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1362:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1363:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1396:80: E501 line too long (114 > 79 characters) -./src/seedpass/core/manager.py:1407:80: E501 line too long (106 > 79 characters) -./src/seedpass/core/manager.py:1423:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1468:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:1469:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:1504:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1510:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:1524:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:1537:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1548:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1564:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1575:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1591:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1598:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1605:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:1611:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1624:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:1648:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1670:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:1674:13: F841 local variable 'loop' is assigned to but never used -./src/seedpass/core/manager.py:1706:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1709:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/manager.py:1721:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:1729:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1732:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1737:13: F841 local variable 'loop' is assigned to but never used -./src/seedpass/core/manager.py:1785:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1789:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1808:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1816:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:1828:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1835:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1842:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1866:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1878:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1882:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1909:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:1919:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:1932:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1949:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:1958:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1961:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:1969:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1978:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1984:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:1985:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:1991:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:2010:80: E501 line too long (117 > 79 characters) -./src/seedpass/core/manager.py:2019:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:2052:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2068:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/manager.py:2073:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:2111:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2122:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2124:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2129:80: E501 line too long (102 > 79 characters) -./src/seedpass/core/manager.py:2159:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2180:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:2185:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2206:80: E501 line too long (98 > 79 characters) -./src/seedpass/core/manager.py:2211:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:2248:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2257:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:2260:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:2266:80: E501 line too long (113 > 79 characters) -./src/seedpass/core/manager.py:2275:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/manager.py:2313:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2327:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2347:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:2354:80: E501 line too long (114 > 79 characters) -./src/seedpass/core/manager.py:2390:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2399:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:2404:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2413:80: E501 line too long (117 > 79 characters) -./src/seedpass/core/manager.py:2432:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:2459:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2460:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2464:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2482:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:2541:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:2572:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:2579:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/manager.py:2584:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:2628:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:2664:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2672:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2687:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:2689:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:2739:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2767:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2773:80: E501 line too long (139 > 79 characters) -./src/seedpass/core/manager.py:2788:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2815:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2816:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2823:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/manager.py:2845:80: E501 line too long (128 > 79 characters) -./src/seedpass/core/manager.py:2853:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2861:80: E501 line too long (102 > 79 characters) -./src/seedpass/core/manager.py:2866:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:2880:80: E501 line too long (124 > 79 characters) -./src/seedpass/core/manager.py:2890:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2892:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2900:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:2902:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:2903:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:2910:80: E501 line too long (98 > 79 characters) -./src/seedpass/core/manager.py:2931:80: E501 line too long (120 > 79 characters) -./src/seedpass/core/manager.py:2956:80: E501 line too long (117 > 79 characters) -./src/seedpass/core/manager.py:2969:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:2985:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:2992:80: E501 line too long (114 > 79 characters) -./src/seedpass/core/manager.py:3012:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3021:80: E501 line too long (134 > 79 characters) -./src/seedpass/core/manager.py:3026:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3044:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:3049:80: E501 line too long (96 > 79 characters) -./src/seedpass/core/manager.py:3062:80: E501 line too long (124 > 79 characters) -./src/seedpass/core/manager.py:3085:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:3106:80: E501 line too long (138 > 79 characters) -./src/seedpass/core/manager.py:3118:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:3122:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:3144:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3153:80: E501 line too long (136 > 79 characters) -./src/seedpass/core/manager.py:3158:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3194:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3201:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/manager.py:3232:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:3245:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3250:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3279:80: E501 line too long (97 > 79 characters) -./src/seedpass/core/manager.py:3292:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/manager.py:3299:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3304:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:3309:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:3313:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:3315:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3319:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3350:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3355:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3360:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3373:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/manager.py:3380:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3386:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3424:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3429:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3434:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/manager.py:3439:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:3444:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3457:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/manager.py:3464:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3469:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:3480:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:3482:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3486:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3518:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:3523:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:3527:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3534:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:3541:80: E501 line too long (91 > 79 characters) -./src/seedpass/core/manager.py:3547:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3553:80: E501 line too long (97 > 79 characters) -./src/seedpass/core/manager.py:3566:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/manager.py:3573:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3578:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:3583:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:3587:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:3589:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3593:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3616:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3678:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:3708:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3712:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:3743:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3761:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:3767:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3770:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3773:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:3775:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3787:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3806:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:3807:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3814:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3825:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:3826:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:3831:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:3865:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:3879:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:3947:80: E501 line too long (100 > 79 characters) -./src/seedpass/core/manager.py:3974:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:3975:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:3984:80: E501 line too long (96 > 79 characters) -./src/seedpass/core/manager.py:3997:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:4000:80: E501 line too long (126 > 79 characters) -./src/seedpass/core/manager.py:4012:80: E501 line too long (91 > 79 characters) -./src/seedpass/core/manager.py:4018:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:4037:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4063:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:4066:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4067:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4072:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4077:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:4081:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4085:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:4088:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/manager.py:4111:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4160:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4217:80: E501 line too long (100 > 79 characters) -./src/seedpass/core/manager.py:4228:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:4254:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:4267:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/manager.py:4285:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4287:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4319:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4321:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4388:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:4392:80: E501 line too long (101 > 79 characters) -./src/seedpass/core/manager.py:4407:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/manager.py:4417:80: E501 line too long (99 > 79 characters) -./src/seedpass/core/manager.py:4428:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4432:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/manager.py:4434:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4439:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4447:80: E501 line too long (112 > 79 characters) -./src/seedpass/core/manager.py:4454:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4458:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:4459:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/manager.py:4496:80: E501 line too long (122 > 79 characters) -./src/seedpass/core/manager.py:4508:80: E501 line too long (94 > 79 characters) -./src/seedpass/core/manager.py:4516:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4529:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/manager.py:4534:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4549:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4557:80: E501 line too long (90 > 79 characters) -./src/seedpass/core/manager.py:4560:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4561:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4633:80: E501 line too long (97 > 79 characters) -./src/seedpass/core/manager.py:4641:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4659:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4722:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:4725:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/manager.py:4732:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/manager.py:4735:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:4738:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/manager.py:4744:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/manager.py:4749:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/manager.py:4752:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/menu_handler.py:46:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/menu_handler.py:75:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/menu_handler.py:79:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/menu_handler.py:108:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/menu_handler.py:141:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/menu_handler.py:149:80: E501 line too long (108 > 79 characters) -./src/seedpass/core/menu_handler.py:153:80: E501 line too long (109 > 79 characters) -./src/seedpass/core/menu_handler.py:162:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/menu_handler.py:170:80: E501 line too long (108 > 79 characters) -./src/seedpass/core/menu_handler.py:174:80: E501 line too long (104 > 79 characters) -./src/seedpass/core/menu_handler.py:179:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/password_generation.py:6:80: E501 line too long (98 > 79 characters) -./src/seedpass/core/password_generation.py:7:80: E501 line too long (90 > 79 characters) -./src/seedpass/core/password_generation.py:10:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/password_generation.py:12:80: E501 line too long (142 > 79 characters) -./src/seedpass/core/password_generation.py:13:80: E501 line too long (167 > 79 characters) -./src/seedpass/core/password_generation.py:21:1: F401 'os' imported but unused -./src/seedpass/core/password_generation.py:26:1: F401 'base64' imported but unused -./src/seedpass/core/password_generation.py:27:1: F401 'typing.Optional' imported but unused -./src/seedpass/core/password_generation.py:30:1: F401 'pathlib.Path' imported but unused -./src/seedpass/core/password_generation.py:31:1: F401 'shutil' imported but unused -./src/seedpass/core/password_generation.py:73:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/password_generation.py:90:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/password_generation.py:91:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/password_generation.py:103:80: E501 line too long (103 > 79 characters) -./src/seedpass/core/password_generation.py:106:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/password_generation.py:108:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/password_generation.py:119:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/password_generation.py:125:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/password_generation.py:126:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/password_generation.py:131:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/password_generation.py:141:9: F841 local variable 'hkdf_derived' is assigned to but never used -./src/seedpass/core/password_generation.py:155:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/password_generation.py:178:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/password_generation.py:185:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/password_generation.py:206:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/password_generation.py:209:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/password_generation.py:213:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/password_generation.py:216:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/password_generation.py:252:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/password_generation.py:265:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/password_generation.py:279:80: E501 line too long (97 > 79 characters) -./src/seedpass/core/password_generation.py:280:80: E501 line too long (92 > 79 characters) -./src/seedpass/core/password_generation.py:312:80: E501 line too long (146 > 79 characters) -./src/seedpass/core/password_generation.py:337:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/password_generation.py:344:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/password_generation.py:358:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/password_generation.py:363:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/password_generation.py:364:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/password_generation.py:372:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/password_generation.py:374:80: E501 line too long (102 > 79 characters) -./src/seedpass/core/password_generation.py:390:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/password_generation.py:394:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/password_generation.py:406:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/password_generation.py:408:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/password_generation.py:409:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/password_generation.py:414:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/password_generation.py:416:80: E501 line too long (97 > 79 characters) -./src/seedpass/core/password_generation.py:425:80: E501 line too long (136 > 79 characters) -./src/seedpass/core/password_generation.py:431:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/password_generation.py:432:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/password_generation.py:472:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/password_generation.py:516:80: E501 line too long (84 > 79 characters) -./src/seedpass/core/portable_backup.py:57:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/portable_backup.py:110:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/profile_service.py:14:5: F401 'nostr.client.NostrClient' imported but unused -./src/seedpass/core/profile_service.py:23:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/profile_service.py:37:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/profile_service.py:38:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/profile_service.py:39:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/profile_service.py:48:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/profile_service.py:52:80: E501 line too long (95 > 79 characters) -./src/seedpass/core/profile_service.py:71:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/profile_service.py:96:80: E501 line too long (93 > 79 characters) -./src/seedpass/core/profile_service.py:101:80: E501 line too long (86 > 79 characters) -./src/seedpass/core/profile_service.py:107:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/profile_service.py:108:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/pubsub.py:9:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/pubsub.py:21:80: E501 line too long (85 > 79 characters) -./src/seedpass/core/totp.py:45:80: E501 line too long (83 > 79 characters) -./src/seedpass/core/totp.py:71:80: E501 line too long (92 > 79 characters) -./src/seedpass/core/vault.py:70:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/vault.py:84:80: E501 line too long (80 > 79 characters) -./src/seedpass/core/vault.py:92:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/vault.py:93:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/vault.py:98:80: E501 line too long (88 > 79 characters) -./src/seedpass/core/vault.py:105:80: E501 line too long (87 > 79 characters) -./src/seedpass/core/vault.py:132:80: E501 line too long (92 > 79 characters) -./src/seedpass/core/vault.py:150:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/vault.py:172:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/vault.py:184:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/vault.py:199:80: E501 line too long (89 > 79 characters) -./src/seedpass/core/vault.py:210:80: E501 line too long (82 > 79 characters) -./src/seedpass/core/vault.py:222:80: E501 line too long (81 > 79 characters) -./src/seedpass/core/vault.py:260:80: E501 line too long (80 > 79 characters) -./src/seedpass_gui/app.py:34:80: E501 line too long (80 > 79 characters) -./src/seedpass_gui/app.py:93:80: E501 line too long (81 > 79 characters) -./src/seedpass_gui/app.py:252:80: E501 line too long (85 > 79 characters) -./src/seedpass_gui/app.py:451:80: E501 line too long (85 > 79 characters) -./src/tests/conftest.py:10:1: E402 module level import not at top of file -./src/tests/conftest.py:11:1: E402 module level import not at top of file -./src/tests/conftest.py:12:1: E402 module level import not at top of file -./src/tests/conftest.py:13:1: E402 module level import not at top of file -./src/tests/conftest.py:14:1: E402 module level import not at top of file -./src/tests/helpers.py:2:1: F401 'time' imported but unused -./src/tests/helpers.py:8:1: E402 module level import not at top of file -./src/tests/helpers.py:9:1: E402 module level import not at top of file -./src/tests/helpers.py:10:1: E402 module level import not at top of file -./src/tests/helpers.py:14:1: E402 module level import not at top of file -./src/tests/helpers.py:16:80: E501 line too long (107 > 79 characters) -./src/tests/helpers.py:37:1: E402 module level import not at top of file -./src/tests/helpers.py:38:1: F401 'asyncio' imported but unused -./src/tests/helpers.py:38:1: E402 module level import not at top of file -./src/tests/helpers.py:39:1: E402 module level import not at top of file -./src/tests/helpers.py:41:1: E402 module level import not at top of file -./src/tests/helpers.py:49:80: E501 line too long (88 > 79 characters) -./src/tests/helpers.py:228:80: E501 line too long (83 > 79 characters) -./src/tests/helpers.py:258:80: E501 line too long (81 > 79 characters) -./src/tests/test_add_new_fingerprint_words.py:23:80: E501 line too long (87 > 79 characters) -./src/tests/test_add_tags_from_retrieve.py:10:1: E402 module level import not at top of file -./src/tests/test_add_tags_from_retrieve.py:11:1: E402 module level import not at top of file -./src/tests/test_add_tags_from_retrieve.py:12:1: E402 module level import not at top of file -./src/tests/test_add_tags_from_retrieve.py:13:1: E402 module level import not at top of file -./src/tests/test_additional_backup.py:25:80: E501 line too long (83 > 79 characters) -./src/tests/test_additional_backup.py:36:80: E501 line too long (84 > 79 characters) -./src/tests/test_api.py:11:1: E402 module level import not at top of file -./src/tests/test_api.py:12:1: E402 module level import not at top of file -./src/tests/test_api.py:63:80: E501 line too long (82 > 79 characters) -./src/tests/test_api.py:66:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:91:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:104:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:117:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:130:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:136:80: E501 line too long (82 > 79 characters) -./src/tests/test_api.py:172:80: E501 line too long (82 > 79 characters) -./src/tests/test_api.py:181:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:188:80: E501 line too long (83 > 79 characters) -./src/tests/test_api.py:191:80: E501 line too long (82 > 79 characters) -./src/tests/test_api.py:206:80: E501 line too long (87 > 79 characters) -./src/tests/test_api.py:207:80: E501 line too long (82 > 79 characters) -./src/tests/test_api.py:216:80: E501 line too long (81 > 79 characters) -./src/tests/test_api.py:222:80: E501 line too long (82 > 79 characters) -./src/tests/test_api.py:253:80: E501 line too long (81 > 79 characters) -./src/tests/test_api_new_endpoints.py:8:1: F401 'test_api.client' imported but unused -./src/tests/test_api_new_endpoints.py:9:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_api_new_endpoints.py:11:1: F401 'seedpass.core.password_generation.PasswordPolicy' imported but unused -./src/tests/test_api_new_endpoints.py:17:45: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:71:44: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:107:35: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:115:80: E501 line too long (82 > 79 characters) -./src/tests/test_api_new_endpoints.py:121:42: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:141:37: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:143:80: E501 line too long (88 > 79 characters) -./src/tests/test_api_new_endpoints.py:152:36: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:165:80: E501 line too long (82 > 79 characters) -./src/tests/test_api_new_endpoints.py:171:45: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:180:38: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:184:80: E501 line too long (80 > 79 characters) -./src/tests/test_api_new_endpoints.py:188:80: E501 line too long (83 > 79 characters) -./src/tests/test_api_new_endpoints.py:213:35: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:217:80: E501 line too long (86 > 79 characters) -./src/tests/test_api_new_endpoints.py:236:38: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:264:40: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:290:47: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:308:52: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:325:36: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:350:37: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:376:38: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:398:44: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:403:80: E501 line too long (81 > 79 characters) -./src/tests/test_api_new_endpoints.py:429:58: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:447:43: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:447:51: F811 redefinition of unused 'dummy_nostr_client' from line 9 -./src/tests/test_api_new_endpoints.py:447:80: E501 line too long (83 > 79 characters) -./src/tests/test_api_new_endpoints.py:468:80: E501 line too long (83 > 79 characters) -./src/tests/test_api_new_endpoints.py:479:80: E501 line too long (83 > 79 characters) -./src/tests/test_api_new_endpoints.py:497:51: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_new_endpoints.py:527:48: F811 redefinition of unused 'client' from line 8 -./src/tests/test_api_notifications.py:1:1: F401 'test_api.client' imported but unused -./src/tests/test_api_notifications.py:9:39: F811 redefinition of unused 'client' from line 1 -./src/tests/test_api_notifications.py:12:80: E501 line too long (83 > 79 characters) -./src/tests/test_api_notifications.py:13:80: E501 line too long (86 > 79 characters) -./src/tests/test_api_notifications.py:26:52: F811 redefinition of unused 'client' from line 1 -./src/tests/test_api_notifications.py:29:80: E501 line too long (83 > 79 characters) -./src/tests/test_api_notifications.py:43:62: F811 redefinition of unused 'client' from line 1 -./src/tests/test_api_profile_stats.py:1:1: F401 'test_api.client' imported but unused -./src/tests/test_api_profile_stats.py:6:39: F811 redefinition of unused 'client' from line 1 -./src/tests/test_api_profile_stats.py:12:80: E501 line too long (85 > 79 characters) -./src/tests/test_api_rate_limit.py:4:1: F811 redefinition of unused 'importlib' from line 1 -./src/tests/test_api_rate_limit.py:24:80: E501 line too long (84 > 79 characters) -./src/tests/test_api_rate_limit.py:27:80: E501 line too long (81 > 79 characters) -./src/tests/test_api_rate_limit.py:37:80: E501 line too long (82 > 79 characters) -./src/tests/test_api_rate_limit.py:46:80: E501 line too long (87 > 79 characters) -./src/tests/test_api_reload_relays.py:21:80: E501 line too long (80 > 79 characters) -./src/tests/test_archive_from_retrieve.py:11:1: E402 module level import not at top of file -./src/tests/test_archive_from_retrieve.py:12:1: E402 module level import not at top of file -./src/tests/test_archive_from_retrieve.py:13:1: E402 module level import not at top of file -./src/tests/test_archive_from_retrieve.py:14:1: E402 module level import not at top of file -./src/tests/test_archive_nonpassword.py:9:1: E402 module level import not at top of file -./src/tests/test_archive_nonpassword.py:10:1: E402 module level import not at top of file -./src/tests/test_archive_nonpassword.py:11:1: E402 module level import not at top of file -./src/tests/test_archive_nonpassword.py:12:1: E402 module level import not at top of file -./src/tests/test_archive_restore.py:7:1: F401 'pytest' imported but unused -./src/tests/test_archive_restore.py:13:1: E402 module level import not at top of file -./src/tests/test_archive_restore.py:14:1: E402 module level import not at top of file -./src/tests/test_archive_restore.py:15:1: E402 module level import not at top of file -./src/tests/test_archive_restore.py:16:1: E402 module level import not at top of file -./src/tests/test_archive_restore.py:17:1: E402 module level import not at top of file -./src/tests/test_atomic_write.py:20:80: E501 line too long (86 > 79 characters) -./src/tests/test_audit_logger.py:8:1: F401 'importlib' imported but unused -./src/tests/test_audit_logger.py:9:1: F401 'pytest' imported but unused -./src/tests/test_audit_logger.py:39:80: E501 line too long (87 > 79 characters) -./src/tests/test_audit_logger.py:44:80: E501 line too long (80 > 79 characters) -./src/tests/test_audit_logger.py:46:80: E501 line too long (87 > 79 characters) -./src/tests/test_audit_logger.py:50:32: E741 ambiguous variable name 'l' -./src/tests/test_auto_sync.py:10:1: E402 module level import not at top of file -./src/tests/test_background_relay_check.py:9:1: E402 module level import not at top of file -./src/tests/test_background_relay_check.py:10:1: E402 module level import not at top of file -./src/tests/test_background_relay_check.py:19:80: E501 line too long (84 > 79 characters) -./src/tests/test_background_sync_always.py:7:1: E402 module level import not at top of file -./src/tests/test_background_sync_always.py:8:1: E402 module level import not at top of file -./src/tests/test_background_sync_always.py:29:80: E501 line too long (82 > 79 characters) -./src/tests/test_background_sync_always.py:30:80: E501 line too long (85 > 79 characters) -./src/tests/test_background_sync_always.py:31:80: E501 line too long (87 > 79 characters) -./src/tests/test_background_sync_always.py:53:80: E501 line too long (84 > 79 characters) -./src/tests/test_background_sync_always.py:54:80: E501 line too long (83 > 79 characters) -./src/tests/test_background_sync_always.py:55:80: E501 line too long (86 > 79 characters) -./src/tests/test_background_sync_always.py:56:80: E501 line too long (82 > 79 characters) -./src/tests/test_backup_restore.py:11:1: E402 module level import not at top of file -./src/tests/test_backup_restore.py:12:1: E402 module level import not at top of file -./src/tests/test_backup_restore.py:104:80: E501 line too long (83 > 79 characters) -./src/tests/test_bip85_init.py:6:1: E402 module level import not at top of file -./src/tests/test_bip85_init.py:7:1: E402 module level import not at top of file -./src/tests/test_bip85_init.py:8:1: E402 module level import not at top of file -./src/tests/test_bip85_init.py:10:80: E501 line too long (127 > 79 characters) -./src/tests/test_bip85_vectors.py:7:1: E402 module level import not at top of file -./src/tests/test_bip85_vectors.py:8:1: E402 module level import not at top of file -./src/tests/test_bip85_vectors.py:12:1: E402 module level import not at top of file -./src/tests/test_bip85_vectors.py:14:80: E501 line too long (127 > 79 characters) -./src/tests/test_bip85_vectors.py:16:80: E501 line too long (87 > 79 characters) -./src/tests/test_bip85_vectors.py:18:80: E501 line too long (175 > 79 characters) -./src/tests/test_bip85_vectors.py:20:80: E501 line too long (86 > 79 characters) -./src/tests/test_bip85_vectors.py:22:80: E501 line too long (85 > 79 characters) -./src/tests/test_checksum_utils.py:3:1: F401 'pathlib.Path' imported but unused -./src/tests/test_cli_clipboard_flag.py:30:80: E501 line too long (82 > 79 characters) -./src/tests/test_cli_clipboard_flag.py:40:80: E501 line too long (80 > 79 characters) -./src/tests/test_cli_core_services.py:3:1: F401 'typer' imported but unused -./src/tests/test_cli_core_services.py:21:80: E501 line too long (87 > 79 characters) -./src/tests/test_cli_core_services.py:50:80: E501 line too long (82 > 79 characters) -./src/tests/test_cli_doc_examples.py:9:1: E402 module level import not at top of file -./src/tests/test_cli_doc_examples.py:10:1: E402 module level import not at top of file -./src/tests/test_cli_doc_examples.py:11:1: E402 module level import not at top of file -./src/tests/test_cli_doc_examples.py:12:1: E402 module level import not at top of file -./src/tests/test_cli_doc_examples.py:13:1: E402 module level import not at top of file -./src/tests/test_cli_doc_examples.py:19:80: E501 line too long (93 > 79 characters) -./src/tests/test_cli_doc_examples.py:25:80: E501 line too long (87 > 79 characters) -./src/tests/test_cli_doc_examples.py:28:80: E501 line too long (118 > 79 characters) -./src/tests/test_cli_doc_examples.py:30:80: E501 line too long (100 > 79 characters) -./src/tests/test_cli_doc_examples.py:55:80: E501 line too long (80 > 79 characters) -./src/tests/test_cli_doc_examples.py:88:80: E501 line too long (86 > 79 characters) -./src/tests/test_cli_doc_examples.py:111:1: E402 module level import not at top of file -./src/tests/test_cli_entry_add_commands.py:7:1: F401 'helpers.TEST_SEED' imported but unused -./src/tests/test_cli_entry_add_commands.py:139:80: E501 line too long (82 > 79 characters) -./src/tests/test_cli_export_import.py:8:1: E402 module level import not at top of file -./src/tests/test_cli_export_import.py:9:1: E402 module level import not at top of file -./src/tests/test_cli_export_import.py:10:1: E402 module level import not at top of file -./src/tests/test_cli_export_import.py:11:1: E402 module level import not at top of file -./src/tests/test_cli_export_import.py:12:1: E402 module level import not at top of file -./src/tests/test_cli_integration.py:26:80: E501 line too long (87 > 79 characters) -./src/tests/test_cli_integration.py:27:80: E501 line too long (85 > 79 characters) -./src/tests/test_cli_integration.py:28:80: E501 line too long (85 > 79 characters) -./src/tests/test_cli_integration.py:30:80: E501 line too long (81 > 79 characters) -./src/tests/test_cli_integration.py:35:80: E501 line too long (85 > 79 characters) -./src/tests/test_cli_integration.py:56:80: E501 line too long (88 > 79 characters) -./src/tests/test_cli_integration.py:61:80: E501 line too long (87 > 79 characters) -./src/tests/test_cli_invalid_input.py:10:1: E402 module level import not at top of file -./src/tests/test_cli_relays.py:41:80: E501 line too long (84 > 79 characters) -./src/tests/test_cli_subcommands.py:7:1: E402 module level import not at top of file -./src/tests/test_cli_subcommands.py:8:1: E402 module level import not at top of file -./src/tests/test_cli_subcommands.py:17:51: E741 ambiguous variable name 'l' -./src/tests/test_cli_subcommands.py:43:80: E501 line too long (86 > 79 characters) -./src/tests/test_cli_subcommands.py:56:80: E501 line too long (82 > 79 characters) -./src/tests/test_cli_subcommands.py:63:80: E501 line too long (86 > 79 characters) -./src/tests/test_cli_subcommands.py:103:80: E501 line too long (82 > 79 characters) -./src/tests/test_cli_subcommands.py:132:80: E501 line too long (86 > 79 characters) -./src/tests/test_cli_toggle_secret_mode.py:1:1: F401 'types' imported but unused -./src/tests/test_cli_toggle_secret_mode.py:31:80: E501 line too long (82 > 79 characters) -./src/tests/test_clipboard_utils.py:10:1: E402 module level import not at top of file -./src/tests/test_clipboard_utils.py:12:1: E402 module level import not at top of file -./src/tests/test_concurrency_stress.py:10:1: E402 module level import not at top of file -./src/tests/test_concurrency_stress.py:11:1: E402 module level import not at top of file -./src/tests/test_concurrency_stress.py:12:1: E402 module level import not at top of file -./src/tests/test_concurrency_stress.py:13:1: E402 module level import not at top of file -./src/tests/test_concurrency_stress.py:14:1: E402 module level import not at top of file -./src/tests/test_concurrency_stress.py:51:80: E501 line too long (82 > 79 characters) -./src/tests/test_config_manager.py:10:1: E402 module level import not at top of file -./src/tests/test_config_manager.py:11:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_config_manager.py:11:1: E402 module level import not at top of file -./src/tests/test_config_manager.py:12:1: E402 module level import not at top of file -./src/tests/test_config_manager.py:13:1: E402 module level import not at top of file -./src/tests/test_config_manager.py:49:1: E402 module level import not at top of file -./src/tests/test_config_manager.py:125:80: E501 line too long (85 > 79 characters) -./src/tests/test_core_api_services.py:4:1: F401 'threading.Lock' imported but unused -./src/tests/test_core_api_services.py:8:1: F401 'seedpass.core.api.VaultExportResponse' imported but unused -./src/tests/test_core_api_services.py:8:1: F401 'seedpass.core.api.UnlockResponse' imported but unused -./src/tests/test_core_api_services.py:8:1: F401 'seedpass.core.api.PasswordPolicyOptions' imported but unused -./src/tests/test_core_api_services.py:8:1: F401 'seedpass.core.api.GeneratePasswordRequest' imported but unused -./src/tests/test_core_api_services.py:8:1: F401 'seedpass.core.api.GeneratePasswordResponse' imported but unused -./src/tests/test_core_api_services.py:8:1: F401 'seedpass.core.api.AddPasswordEntryRequest' imported but unused -./src/tests/test_core_api_services.py:31:1: F401 'seedpass.core.entry_types.EntryType' imported but unused -./src/tests/test_core_api_services.py:92:80: E501 line too long (86 > 79 characters) -./src/tests/test_core_api_services.py:100:80: E501 line too long (83 > 79 characters) -./src/tests/test_core_api_services.py:114:80: E501 line too long (83 > 79 characters) -./src/tests/test_core_api_services.py:141:80: E501 line too long (80 > 79 characters) -./src/tests/test_core_api_services.py:164:80: E501 line too long (88 > 79 characters) -./src/tests/test_core_api_services.py:181:80: E501 line too long (84 > 79 characters) -./src/tests/test_core_api_services.py:189:80: E501 line too long (85 > 79 characters) -./src/tests/test_core_api_services.py:225:80: E501 line too long (83 > 79 characters) -./src/tests/test_core_api_services.py:262:80: E501 line too long (82 > 79 characters) -./src/tests/test_core_api_services.py:267:80: E501 line too long (88 > 79 characters) -./src/tests/test_core_api_services.py:272:80: E501 line too long (87 > 79 characters) -./src/tests/test_core_api_services.py:286:80: E501 line too long (83 > 79 characters) -./src/tests/test_core_api_services.py:418:80: E501 line too long (88 > 79 characters) -./src/tests/test_core_api_services.py:422:80: E501 line too long (84 > 79 characters) -./src/tests/test_core_api_services.py:427:80: E501 line too long (85 > 79 characters) -./src/tests/test_core_api_services.py:453:80: E501 line too long (84 > 79 characters) -./src/tests/test_core_api_services.py:456:80: E501 line too long (86 > 79 characters) -./src/tests/test_core_api_services.py:469:80: E501 line too long (82 > 79 characters) -./src/tests/test_core_api_services.py:489:80: E501 line too long (85 > 79 characters) -./src/tests/test_core_api_services.py:510:80: E501 line too long (80 > 79 characters) -./src/tests/test_core_services.py:1:1: F401 'types' imported but unused -./src/tests/test_core_services.py:4:80: E501 line too long (84 > 79 characters) -./src/tests/test_core_services.py:51:80: E501 line too long (80 > 79 characters) -./src/tests/test_custom_fields_display.py:8:80: E501 line too long (83 > 79 characters) -./src/tests/test_custom_fields_display.py:10:70: E741 ambiguous variable name 'l' -./src/tests/test_custom_fields_display.py:10:80: E501 line too long (80 > 79 characters) -./src/tests/test_decrypt_messages.py:25:80: E501 line too long (81 > 79 characters) -./src/tests/test_decrypt_messages.py:32:80: E501 line too long (84 > 79 characters) -./src/tests/test_decrypt_messages.py:37:80: E501 line too long (85 > 79 characters) -./src/tests/test_decrypt_messages.py:45:80: E501 line too long (81 > 79 characters) -./src/tests/test_decrypt_messages.py:56:80: E501 line too long (81 > 79 characters) -./src/tests/test_default_encryption_mode.py:6:1: E402 module level import not at top of file -./src/tests/test_default_encryption_mode.py:7:1: E402 module level import not at top of file -./src/tests/test_default_encryption_mode.py:9:1: E402 module level import not at top of file -./src/tests/test_default_encryption_mode.py:10:1: E402 module level import not at top of file -./src/tests/test_default_encryption_mode.py:26:80: E501 line too long (80 > 79 characters) -./src/tests/test_delta_merge.py:4:1: F401 'pytest' imported but unused -./src/tests/test_duplicate_seed_profile_creation.py:1:1: F401 'pytest' imported but unused -./src/tests/test_duplicate_seed_profile_creation.py:9:80: E501 line too long (108 > 79 characters) -./src/tests/test_duplicate_seed_profile_creation.py:15:80: E501 line too long (81 > 79 characters) -./src/tests/test_duplicate_seed_profile_creation.py:16:80: E501 line too long (82 > 79 characters) -./src/tests/test_duplicate_seed_profile_creation.py:17:80: E501 line too long (86 > 79 characters) -./src/tests/test_duplicate_seed_profile_creation.py:30:80: E501 line too long (80 > 79 characters) -./src/tests/test_duplicate_seed_profile_creation.py:38:80: E501 line too long (81 > 79 characters) -./src/tests/test_encryption_checksum.py:12:1: E402 module level import not at top of file -./src/tests/test_encryption_checksum.py:13:1: E402 module level import not at top of file -./src/tests/test_encryption_files.py:12:1: E402 module level import not at top of file -./src/tests/test_encryption_validate_seed.py:4:1: F401 'pytest' imported but unused -./src/tests/test_encryption_validate_seed.py:9:1: E402 module level import not at top of file -./src/tests/test_encryption_validate_seed.py:19:80: E501 line too long (107 > 79 characters) -./src/tests/test_encryption_validate_seed.py:27:80: E501 line too long (111 > 79 characters) -./src/tests/test_entries_empty.py:8:1: E402 module level import not at top of file -./src/tests/test_entries_empty.py:9:1: E402 module level import not at top of file -./src/tests/test_entries_empty.py:10:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_entries_empty.py:10:1: E402 module level import not at top of file -./src/tests/test_entries_empty.py:11:1: E402 module level import not at top of file -./src/tests/test_entry_add.py:4:1: F401 'unittest.mock.patch' imported but unused -./src/tests/test_entry_add.py:11:1: E402 module level import not at top of file -./src/tests/test_entry_add.py:12:1: E402 module level import not at top of file -./src/tests/test_entry_add.py:13:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_entry_add.py:13:1: E402 module level import not at top of file -./src/tests/test_entry_add.py:14:1: E402 module level import not at top of file -./src/tests/test_entry_add.py:29:80: E501 line too long (84 > 79 characters) -./src/tests/test_entry_management_checksum_path.py:8:1: E402 module level import not at top of file -./src/tests/test_entry_management_checksum_path.py:9:1: E402 module level import not at top of file -./src/tests/test_entry_management_checksum_path.py:10:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_entry_management_checksum_path.py:10:1: E402 module level import not at top of file -./src/tests/test_entry_management_checksum_path.py:11:1: E402 module level import not at top of file -./src/tests/test_entry_policy_override.py:11:1: E402 module level import not at top of file -./src/tests/test_entry_policy_override.py:12:1: E402 module level import not at top of file -./src/tests/test_entry_policy_override.py:13:1: E402 module level import not at top of file -./src/tests/test_entry_policy_override.py:14:1: E402 module level import not at top of file -./src/tests/test_entry_policy_override.py:15:1: E402 module level import not at top of file -./src/tests/test_entry_policy_override.py:24:80: E501 line too long (88 > 79 characters) -./src/tests/test_export_totp_codes.py:3:1: F401 'tempfile.TemporaryDirectory' imported but unused -./src/tests/test_export_totp_codes.py:11:1: E402 module level import not at top of file -./src/tests/test_export_totp_codes.py:12:1: E402 module level import not at top of file -./src/tests/test_export_totp_codes.py:13:1: E402 module level import not at top of file -./src/tests/test_export_totp_codes.py:14:1: E402 module level import not at top of file -./src/tests/test_export_totp_codes.py:15:1: F401 'seedpass.core.totp.TotpManager' imported but unused -./src/tests/test_export_totp_codes.py:15:1: E402 module level import not at top of file -./src/tests/test_export_totp_codes.py:45:80: E501 line too long (88 > 79 characters) -./src/tests/test_file_lock.py:52:80: E501 line too long (82 > 79 characters) -./src/tests/test_file_locking.py:24:1: E402 module level import not at top of file -./src/tests/test_file_locking.py:27:80: E501 line too long (82 > 79 characters) -./src/tests/test_file_locking.py:38:80: E501 line too long (86 > 79 characters) -./src/tests/test_fingerprint_encryption.py:11:1: E402 module level import not at top of file -./src/tests/test_fingerprint_encryption.py:12:1: E402 module level import not at top of file -./src/tests/test_fingerprint_encryption.py:16:80: E501 line too long (106 > 79 characters) -./src/tests/test_fingerprint_encryption.py:18:80: E501 line too long (85 > 79 characters) -./src/tests/test_fingerprint_manager_utils.py:6:80: E501 line too long (108 > 79 characters) -./src/tests/test_full_sync_roundtrip.py:5:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_full_sync_roundtrip.py:32:30: F811 redefinition of unused 'dummy_nostr_client' from line 5 -./src/tests/test_full_sync_roundtrip_new.py:5:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_full_sync_roundtrip_new.py:32:30: F811 redefinition of unused 'dummy_nostr_client' from line 5 -./src/tests/test_fuzz_key_derivation.py:8:1: F811 redefinition of unused 'os' from line 1 -./src/tests/test_fuzz_key_derivation.py:29:80: E501 line too long (85 > 79 characters) -./src/tests/test_fuzz_key_derivation.py:37:80: E501 line too long (81 > 79 characters) -./src/tests/test_generate_test_profile.py:18:80: E501 line too long (88 > 79 characters) -./src/tests/test_generate_test_profile.py:27:80: E501 line too long (82 > 79 characters) -./src/tests/test_generate_test_profile_sync.py:8:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_generate_test_profile_sync.py:13:80: E501 line too long (84 > 79 characters) -./src/tests/test_generate_test_profile_sync.py:15:80: E501 line too long (87 > 79 characters) -./src/tests/test_generate_test_profile_sync.py:22:50: F811 redefinition of unused 'dummy_nostr_client' from line 8 -./src/tests/test_generate_test_profile_sync.py:34:80: E501 line too long (88 > 79 characters) -./src/tests/test_get_entry_summaries_archived_view.py:14:80: E501 line too long (85 > 79 characters) -./src/tests/test_gui_features.py:9:1: E402 module level import not at top of file -./src/tests/test_gui_features.py:10:1: E402 module level import not at top of file -./src/tests/test_gui_features.py:11:1: E402 module level import not at top of file -./src/tests/test_gui_features.py:33:80: E501 line too long (87 > 79 characters) -./src/tests/test_gui_features.py:35:80: E501 line too long (84 > 79 characters) -./src/tests/test_gui_features.py:82:80: E501 line too long (88 > 79 characters) -./src/tests/test_gui_headless.py:37:80: E501 line too long (86 > 79 characters) -./src/tests/test_gui_headless.py:67:80: E501 line too long (81 > 79 characters) -./src/tests/test_gui_headless.py:91:5: F841 local variable 'app' is assigned to but never used -./src/tests/test_gui_headless.py:158:80: E501 line too long (84 > 79 characters) -./src/tests/test_gui_sync.py:12:80: E501 line too long (87 > 79 characters) -./src/tests/test_import.py:4:5: F401 'bip_utils.Bip39SeedGenerator' imported but unused -./src/tests/test_inactivity_lock.py:10:1: E402 module level import not at top of file -./src/tests/test_index_cache.py:22:80: E501 line too long (81 > 79 characters) -./src/tests/test_index_import_export.py:4:1: F401 'pytest' imported but unused -./src/tests/test_index_import_export.py:11:1: E402 module level import not at top of file -./src/tests/test_index_import_export.py:12:1: E402 module level import not at top of file -./src/tests/test_index_import_export.py:13:1: E402 module level import not at top of file -./src/tests/test_index_import_export.py:15:80: E501 line too long (102 > 79 characters) -./src/tests/test_kdf_iteration_fallback.py:21:80: E501 line too long (87 > 79 characters) -./src/tests/test_kdf_iteration_fallback.py:46:80: E501 line too long (86 > 79 characters) -./src/tests/test_kdf_modes.py:6:1: F401 'types.SimpleNamespace' imported but unused -./src/tests/test_kdf_modes.py:19:80: E501 line too long (107 > 79 characters) -./src/tests/test_kdf_modes.py:34:80: E501 line too long (80 > 79 characters) -./src/tests/test_kdf_modes.py:44:80: E501 line too long (80 > 79 characters) -./src/tests/test_kdf_modes.py:90:80: E501 line too long (87 > 79 characters) -./src/tests/test_key_derivation.py:3:1: F811 redefinition of unused 'logging' from line 1 -./src/tests/test_key_derivation.py:38:80: E501 line too long (106 > 79 characters) -./src/tests/test_key_derivation.py:46:80: E501 line too long (106 > 79 characters) -./src/tests/test_key_derivation.py:57:80: E501 line too long (87 > 79 characters) -./src/tests/test_key_derivation.py:61:80: E501 line too long (87 > 79 characters) -./src/tests/test_key_hierarchy.py:15:80: E501 line too long (106 > 79 characters) -./src/tests/test_key_manager_helpers.py:1:1: F401 'pytest' imported but unused -./src/tests/test_key_manager_helpers.py:19:80: E501 line too long (84 > 79 characters) -./src/tests/test_key_validation_failures.py:27:80: E501 line too long (82 > 79 characters) -./src/tests/test_key_validation_failures.py:36:80: E501 line too long (85 > 79 characters) -./src/tests/test_key_validation_failures.py:45:80: E501 line too long (80 > 79 characters) -./src/tests/test_key_value_entry.py:9:1: E402 module level import not at top of file -./src/tests/test_key_value_entry.py:10:1: E402 module level import not at top of file -./src/tests/test_key_value_entry.py:11:1: E402 module level import not at top of file -./src/tests/test_last_used_fingerprint.py:8:1: F401 'seedpass.core.manager.EncryptionMode' imported but unused -./src/tests/test_last_used_fingerprint.py:33:80: E501 line too long (85 > 79 characters) -./src/tests/test_last_used_fingerprint.py:41:80: E501 line too long (81 > 79 characters) -./src/tests/test_last_used_fingerprint.py:44:80: E501 line too long (84 > 79 characters) -./src/tests/test_last_used_fingerprint.py:52:80: E501 line too long (83 > 79 characters) -./src/tests/test_legacy_format_exception.py:27:80: E501 line too long (86 > 79 characters) -./src/tests/test_legacy_format_exception.py:38:80: E501 line too long (88 > 79 characters) -./src/tests/test_legacy_migration.py:83:80: E501 line too long (84 > 79 characters) -./src/tests/test_legacy_migration.py:170:80: E501 line too long (85 > 79 characters) -./src/tests/test_legacy_migration.py:179:80: E501 line too long (87 > 79 characters) -./src/tests/test_legacy_migration.py:207:80: E501 line too long (85 > 79 characters) -./src/tests/test_legacy_migration.py:216:80: E501 line too long (88 > 79 characters) -./src/tests/test_legacy_migration.py:223:80: E501 line too long (80 > 79 characters) -./src/tests/test_legacy_migration.py:264:80: E501 line too long (87 > 79 characters) -./src/tests/test_legacy_migration.py:272:80: E501 line too long (84 > 79 characters) -./src/tests/test_legacy_migration.py:298:80: E501 line too long (85 > 79 characters) -./src/tests/test_legacy_migration.py:304:80: E501 line too long (87 > 79 characters) -./src/tests/test_legacy_migration.py:340:80: E501 line too long (85 > 79 characters) -./src/tests/test_legacy_migration_iterations.py:8:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:10:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:11:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:12:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:13:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:17:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:18:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:19:1: E402 module level import not at top of file -./src/tests/test_legacy_migration_iterations.py:23:80: E501 line too long (87 > 79 characters) -./src/tests/test_legacy_migration_iterations.py:25:80: E501 line too long (81 > 79 characters) -./src/tests/test_legacy_migration_iterations.py:56:80: E501 line too long (88 > 79 characters) -./src/tests/test_legacy_migration_iterations.py:69:80: E501 line too long (81 > 79 characters) -./src/tests/test_legacy_migration_prompt.py:17:80: E501 line too long (81 > 79 characters) -./src/tests/test_legacy_migration_prompt.py:34:80: E501 line too long (85 > 79 characters) -./src/tests/test_legacy_migration_prompt.py:50:80: E501 line too long (85 > 79 characters) -./src/tests/test_legacy_migration_prompt.py:53:80: E501 line too long (81 > 79 characters) -./src/tests/test_legacy_migration_second_session.py:33:80: E501 line too long (85 > 79 characters) -./src/tests/test_list_entries_all_types.py:56:80: E501 line too long (107 > 79 characters) -./src/tests/test_list_entries_all_types.py:63:80: E501 line too long (84 > 79 characters) -./src/tests/test_list_entries_all_types.py:69:80: E501 line too long (81 > 79 characters) -./src/tests/test_list_entries_all_types.py:84:80: E501 line too long (81 > 79 characters) -./src/tests/test_list_entries_sort_filter.py:9:1: E402 module level import not at top of file -./src/tests/test_list_entries_sort_filter.py:10:1: E402 module level import not at top of file -./src/tests/test_list_entries_sort_filter.py:11:1: E402 module level import not at top of file -./src/tests/test_list_entries_sort_filter.py:12:1: E402 module level import not at top of file -./src/tests/test_load_global_config.py:6:1: F401 'pytest' imported but unused -./src/tests/test_managed_account.py:12:1: E402 module level import not at top of file -./src/tests/test_managed_account.py:13:1: E402 module level import not at top of file -./src/tests/test_managed_account.py:14:1: E402 module level import not at top of file -./src/tests/test_managed_account.py:59:80: E501 line too long (83 > 79 characters) -./src/tests/test_managed_account.py:72:80: E501 line too long (81 > 79 characters) -./src/tests/test_managed_account_entry.py:12:1: E402 module level import not at top of file -./src/tests/test_managed_account_entry.py:13:1: E402 module level import not at top of file -./src/tests/test_managed_account_entry.py:14:1: E402 module level import not at top of file -./src/tests/test_managed_account_entry.py:15:1: E402 module level import not at top of file -./src/tests/test_managed_account_entry.py:16:1: E402 module level import not at top of file -./src/tests/test_managed_account_entry.py:17:1: E402 module level import not at top of file -./src/tests/test_managed_account_entry.py:88:80: E501 line too long (83 > 79 characters) -./src/tests/test_managed_account_entry.py:101:80: E501 line too long (81 > 79 characters) -./src/tests/test_manager_add_password.py:4:1: F401 'types.SimpleNamespace' imported but unused -./src/tests/test_manager_add_password.py:6:1: F401 'pytest' imported but unused -./src/tests/test_manager_add_password.py:8:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_manager_add_password.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_add_password.py:13:1: E402 module level import not at top of file -./src/tests/test_manager_add_password.py:14:1: E402 module level import not at top of file -./src/tests/test_manager_add_password.py:15:1: E402 module level import not at top of file -./src/tests/test_manager_add_password.py:16:1: E402 module level import not at top of file -./src/tests/test_manager_add_password.py:24:43: F811 redefinition of unused 'dummy_nostr_client' from line 8 -./src/tests/test_manager_add_password.py:67:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_add_password.py:68:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_add_password.py:93:55: F811 redefinition of unused 'dummy_nostr_client' from line 8 -./src/tests/test_manager_add_password.py:93:80: E501 line too long (82 > 79 characters) -./src/tests/test_manager_add_password.py:137:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_add_password.py:138:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_add_password.py:154:54: F811 redefinition of unused 'dummy_nostr_client' from line 8 -./src/tests/test_manager_add_password.py:154:80: E501 line too long (81 > 79 characters) -./src/tests/test_manager_add_password.py:187:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_add_password.py:188:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_add_totp.py:4:1: F401 'types.SimpleNamespace' imported but unused -./src/tests/test_manager_add_totp.py:10:1: E402 module level import not at top of file -./src/tests/test_manager_add_totp.py:11:1: E402 module level import not at top of file -./src/tests/test_manager_add_totp.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_add_totp.py:13:1: E402 module level import not at top of file -./src/tests/test_manager_add_totp.py:54:80: E501 line too long (83 > 79 characters) -./src/tests/test_manager_add_totp.py:56:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_checksum_backup.py:6:1: E402 module level import not at top of file -./src/tests/test_manager_checksum_backup.py:7:1: E402 module level import not at top of file -./src/tests/test_manager_checksum_backup.py:33:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_checksum_backup.py:44:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_checksum_backup.py:54:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_current_notification.py:8:1: F401 'seedpass.core.manager.Notification' imported but unused -./src/tests/test_manager_current_notification.py:8:1: E402 module level import not at top of file -./src/tests/test_manager_current_notification.py:9:1: E402 module level import not at top of file -./src/tests/test_manager_current_notification.py:23:80: E501 line too long (82 > 79 characters) -./src/tests/test_manager_display_totp_codes.py:24:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_display_totp_codes.py:25:80: E501 line too long (88 > 79 characters) -./src/tests/test_manager_display_totp_codes.py:40:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_display_totp_codes.py:48:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_display_totp_codes.py:49:80: E501 line too long (88 > 79 characters) -./src/tests/test_manager_edit_totp.py:9:1: E402 module level import not at top of file -./src/tests/test_manager_edit_totp.py:10:1: E402 module level import not at top of file -./src/tests/test_manager_edit_totp.py:11:1: E402 module level import not at top of file -./src/tests/test_manager_edit_totp.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_edit_totp.py:48:80: E501 line too long (88 > 79 characters) -./src/tests/test_manager_edit_totp.py:52:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_edit_totp.py:53:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_import_database.py:8:1: E402 module level import not at top of file -./src/tests/test_manager_import_database.py:33:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_import_database.py:50:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_list_entries.py:13:1: E402 module level import not at top of file -./src/tests/test_manager_list_entries.py:14:1: E402 module level import not at top of file -./src/tests/test_manager_list_entries.py:15:1: E402 module level import not at top of file -./src/tests/test_manager_list_entries.py:16:1: E402 module level import not at top of file -./src/tests/test_manager_list_entries.py:17:1: E402 module level import not at top of file -./src/tests/test_manager_list_entries.py:78:80: E501 line too long (88 > 79 characters) -./src/tests/test_manager_list_entries.py:82:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_list_entries.py:137:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_list_entries.py:141:74: E741 ambiguous variable name 'l' -./src/tests/test_manager_list_entries.py:141:80: E501 line too long (87 > 79 characters) -./src/tests/test_manager_list_entries.py:176:80: E501 line too long (86 > 79 characters) -./src/tests/test_manager_list_entries.py:177:80: E501 line too long (81 > 79 characters) -./src/tests/test_manager_list_entries.py:178:80: E501 line too long (81 > 79 characters) -./src/tests/test_manager_list_entries.py:180:70: E741 ambiguous variable name 'l' -./src/tests/test_manager_list_entries.py:180:80: E501 line too long (83 > 79 characters) -./src/tests/test_manager_list_entries.py:182:80: E501 line too long (87 > 79 characters) -./src/tests/test_manager_list_entries.py:298:74: E741 ambiguous variable name 'l' -./src/tests/test_manager_list_entries.py:298:80: E501 line too long (87 > 79 characters) -./src/tests/test_manager_list_entries.py:306:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_list_entries.py:307:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_list_entries.py:309:80: E501 line too long (83 > 79 characters) -./src/tests/test_manager_list_entries.py:315:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_retrieve_totp.py:9:1: E402 module level import not at top of file -./src/tests/test_manager_retrieve_totp.py:10:1: E402 module level import not at top of file -./src/tests/test_manager_retrieve_totp.py:11:1: F401 'seedpass.core.manager.TotpManager' imported but unused -./src/tests/test_manager_retrieve_totp.py:11:1: E402 module level import not at top of file -./src/tests/test_manager_retrieve_totp.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_retrieve_totp.py:48:80: E501 line too long (88 > 79 characters) -./src/tests/test_manager_retrieve_totp.py:52:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_search_display.py:10:1: E402 module level import not at top of file -./src/tests/test_manager_search_display.py:11:1: E402 module level import not at top of file -./src/tests/test_manager_search_display.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_search_display.py:13:1: E402 module level import not at top of file -./src/tests/test_manager_search_display.py:37:80: E501 line too long (88 > 79 characters) -./src/tests/test_manager_search_display.py:41:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_search_display.py:42:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_seed_setup.py:17:80: E501 line too long (85 > 79 characters) -./src/tests/test_manager_seed_setup.py:35:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_seed_setup.py:55:80: E501 line too long (80 > 79 characters) -./src/tests/test_manager_warning_notifications.py:8:1: E402 module level import not at top of file -./src/tests/test_manager_warning_notifications.py:9:1: E402 module level import not at top of file -./src/tests/test_manager_warning_notifications.py:10:1: E402 module level import not at top of file -./src/tests/test_manager_warning_notifications.py:11:1: E402 module level import not at top of file -./src/tests/test_manager_warning_notifications.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_warning_notifications.py:37:80: E501 line too long (84 > 79 characters) -./src/tests/test_manager_workflow.py:8:1: E402 module level import not at top of file -./src/tests/test_manager_workflow.py:9:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_manager_workflow.py:9:1: E402 module level import not at top of file -./src/tests/test_manager_workflow.py:10:1: E402 module level import not at top of file -./src/tests/test_manager_workflow.py:11:1: E402 module level import not at top of file -./src/tests/test_manager_workflow.py:12:1: E402 module level import not at top of file -./src/tests/test_manager_workflow.py:37:80: E501 line too long (81 > 79 characters) -./src/tests/test_manager_workflow.py:81:80: E501 line too long (83 > 79 characters) -./src/tests/test_manager_workflow.py:91:80: E501 line too long (83 > 79 characters) -./src/tests/test_manifest_id_privacy.py:3:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_manifest_id_privacy.py:6:42: F811 redefinition of unused 'dummy_nostr_client' from line 3 -./src/tests/test_manifest_state_restore.py:1:1: F401 'asyncio' imported but unused -./src/tests/test_manifest_state_restore.py:38:80: E501 line too long (80 > 79 characters) -./src/tests/test_manifest_state_restore.py:51:80: E501 line too long (81 > 79 characters) -./src/tests/test_memory_protection.py:6:1: E402 module level import not at top of file -./src/tests/test_memory_protection.py:8:1: E402 module level import not at top of file -./src/tests/test_menu_navigation.py:9:1: E402 module level import not at top of file -./src/tests/test_menu_navigation.py:46:80: E501 line too long (85 > 79 characters) -./src/tests/test_menu_notifications.py:10:1: E402 module level import not at top of file -./src/tests/test_menu_notifications.py:19:80: E501 line too long (80 > 79 characters) -./src/tests/test_menu_options.py:9:1: E402 module level import not at top of file -./src/tests/test_menu_options.py:48:80: E501 line too long (85 > 79 characters) -./src/tests/test_menu_search.py:9:1: E402 module level import not at top of file -./src/tests/test_migrations.py:8:1: E402 module level import not at top of file -./src/tests/test_migrations.py:36:80: E501 line too long (86 > 79 characters) -./src/tests/test_migrations.py:104:80: E501 line too long (81 > 79 characters) -./src/tests/test_modify_ssh_managed_entries.py:9:1: E402 module level import not at top of file -./src/tests/test_modify_ssh_managed_entries.py:10:1: E402 module level import not at top of file -./src/tests/test_modify_ssh_managed_entries.py:11:1: E402 module level import not at top of file -./src/tests/test_modify_ssh_managed_entries.py:27:80: E501 line too long (82 > 79 characters) -./src/tests/test_multiple_deltas_sync.py:5:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_multiple_deltas_sync.py:32:39: F811 redefinition of unused 'dummy_nostr_client' from line 5 -./src/tests/test_multiple_deltas_sync.py:68:47: F811 redefinition of unused 'dummy_nostr_client' from line 5 -./src/tests/test_multiple_fingerprint_prompt.py:12:80: E501 line too long (81 > 79 characters) -./src/tests/test_multiple_fingerprint_prompt.py:32:80: E501 line too long (85 > 79 characters) -./src/tests/test_multiple_fingerprint_prompt.py:40:80: E501 line too long (81 > 79 characters) -./src/tests/test_multiple_fingerprint_prompt.py:43:80: E501 line too long (84 > 79 characters) -./src/tests/test_multiple_fingerprint_prompt.py:51:80: E501 line too long (83 > 79 characters) -./src/tests/test_new_seed_profile_creation.py:1:1: F401 'pytest' imported but unused -./src/tests/test_new_seed_profile_creation.py:10:80: E501 line too long (108 > 79 characters) -./src/tests/test_new_seed_profile_creation.py:16:80: E501 line too long (81 > 79 characters) -./src/tests/test_new_seed_profile_creation.py:18:80: E501 line too long (82 > 79 characters) -./src/tests/test_new_seed_profile_creation.py:19:80: E501 line too long (86 > 79 characters) -./src/tests/test_new_seed_profile_creation.py:32:80: E501 line too long (80 > 79 characters) -./src/tests/test_new_seed_profile_creation.py:48:80: E501 line too long (84 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:37:80: E501 line too long (81 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:40:80: E501 line too long (84 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:43:80: E501 line too long (82 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:46:9: F841 local variable 'pm' is assigned to but never used -./src/tests/test_noninteractive_init_unlock.py:46:80: E501 line too long (83 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:66:80: E501 line too long (83 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:76:80: E501 line too long (81 > 79 characters) -./src/tests/test_noninteractive_init_unlock.py:79:80: E501 line too long (84 > 79 characters) -./src/tests/test_nostr_backup.py:10:1: E402 module level import not at top of file -./src/tests/test_nostr_backup.py:11:1: E402 module level import not at top of file -./src/tests/test_nostr_backup.py:12:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_nostr_backup.py:12:1: E402 module level import not at top of file -./src/tests/test_nostr_backup.py:13:1: E402 module level import not at top of file -./src/tests/test_nostr_backup.py:14:1: E402 module level import not at top of file -./src/tests/test_nostr_backup.py:42:80: E501 line too long (80 > 79 characters) -./src/tests/test_nostr_client.py:12:1: E402 module level import not at top of file -./src/tests/test_nostr_client.py:13:1: E402 module level import not at top of file -./src/tests/test_nostr_client.py:14:1: E402 module level import not at top of file -./src/tests/test_nostr_client.py:15:1: F401 'constants' imported but unused -./src/tests/test_nostr_client.py:15:1: E402 module level import not at top of file -./src/tests/test_nostr_client.py:27:13: F841 local variable 'mock_builder' is assigned to but never used -./src/tests/test_nostr_client.py:28:80: E501 line too long (83 > 79 characters) -./src/tests/test_nostr_client.py:83:80: E501 line too long (83 > 79 characters) -./src/tests/test_nostr_client.py:198:80: E501 line too long (84 > 79 characters) -./src/tests/test_nostr_client.py:199:80: E501 line too long (80 > 79 characters) -./src/tests/test_nostr_contract.py:11:1: E402 module level import not at top of file -./src/tests/test_nostr_contract.py:12:1: F401 'nostr.client.Manifest' imported but unused -./src/tests/test_nostr_contract.py:12:1: E402 module level import not at top of file -./src/tests/test_nostr_contract.py:68:80: E501 line too long (80 > 79 characters) -./src/tests/test_nostr_contract.py:70:80: E501 line too long (83 > 79 characters) -./src/tests/test_nostr_dummy_client.py:6:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_nostr_dummy_client.py:12:1: F401 'constants' imported but unused -./src/tests/test_nostr_dummy_client.py:34:40: F811 redefinition of unused 'dummy_nostr_client' from line 6 -./src/tests/test_nostr_dummy_client.py:55:35: F811 redefinition of unused 'dummy_nostr_client' from line 6 -./src/tests/test_nostr_dummy_client.py:72:51: F811 redefinition of unused 'dummy_nostr_client' from line 6 -./src/tests/test_nostr_dummy_client.py:72:80: E501 line too long (83 > 79 characters) -./src/tests/test_nostr_dummy_client.py:74:5: F401 'gzip' imported but unused -./src/tests/test_nostr_dummy_client.py:120:40: F811 redefinition of unused 'dummy_nostr_client' from line 6 -./src/tests/test_nostr_dummy_client.py:147:66: F811 redefinition of unused 'dummy_nostr_client' from line 6 -./src/tests/test_nostr_dummy_client.py:147:80: E501 line too long (85 > 79 characters) -./src/tests/test_nostr_entry.py:11:1: E402 module level import not at top of file -./src/tests/test_nostr_entry.py:12:1: E402 module level import not at top of file -./src/tests/test_nostr_entry.py:13:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_nostr_entry.py:13:1: E402 module level import not at top of file -./src/tests/test_nostr_entry.py:14:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:14:1: F811 redefinition of unused 'os' from line 1 -./src/tests/test_nostr_index_size.py:18:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:19:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:20:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:21:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:22:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:23:1: E402 module level import not at top of file -./src/tests/test_nostr_index_size.py:59:80: E501 line too long (82 > 79 characters) -./src/tests/test_nostr_index_size.py:74:80: E501 line too long (88 > 79 characters) -./src/tests/test_nostr_index_size.py:80:80: E501 line too long (84 > 79 characters) -./src/tests/test_nostr_index_size.py:88:80: E501 line too long (84 > 79 characters) -./src/tests/test_nostr_index_size.py:91:80: E501 line too long (83 > 79 characters) -./src/tests/test_nostr_legacy_decrypt_fallback.py:17:80: E501 line too long (84 > 79 characters) -./src/tests/test_nostr_legacy_key_fallback.py:6:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_nostr_legacy_key_fallback.py:11:45: F811 redefinition of unused 'dummy_nostr_client' from line 6 -./src/tests/test_nostr_legacy_key_fallback.py:29:80: E501 line too long (86 > 79 characters) -./src/tests/test_nostr_qr.py:9:1: E402 module level import not at top of file -./src/tests/test_nostr_qr.py:10:1: E402 module level import not at top of file -./src/tests/test_nostr_qr.py:11:1: F401 'seedpass.core.manager.TotpManager' imported but unused -./src/tests/test_nostr_qr.py:11:1: E402 module level import not at top of file -./src/tests/test_nostr_qr.py:12:1: E402 module level import not at top of file -./src/tests/test_nostr_qr.py:13:1: E402 module level import not at top of file -./src/tests/test_nostr_real.py:16:1: E402 module level import not at top of file -./src/tests/test_nostr_real.py:17:1: E402 module level import not at top of file -./src/tests/test_nostr_real.py:41:80: E501 line too long (80 > 79 characters) -./src/tests/test_nostr_restore_flow.py:32:80: E501 line too long (81 > 79 characters) -./src/tests/test_nostr_sdk_workflow.py:45:80: E501 line too long (106 > 79 characters) -./src/tests/test_nostr_snapshot.py:92:80: E501 line too long (87 > 79 characters) -./src/tests/test_nostr_snapshot.py:99:80: E501 line too long (84 > 79 characters) -./src/tests/test_offline_mode_profile_creation.py:4:1: F401 'pytest' imported but unused -./src/tests/test_parent_seed_backup.py:9:1: E402 module level import not at top of file -./src/tests/test_parent_seed_backup.py:10:1: F401 'constants.DEFAULT_SEED_BACKUP_FILENAME' imported but unused -./src/tests/test_parent_seed_backup.py:10:1: E402 module level import not at top of file -./src/tests/test_parent_seed_backup.py:18:80: E501 line too long (87 > 79 characters) -./src/tests/test_parent_seed_backup.py:24:80: E501 line too long (81 > 79 characters) -./src/tests/test_parent_seed_backup.py:29:80: E501 line too long (80 > 79 characters) -./src/tests/test_parent_seed_backup.py:47:80: E501 line too long (80 > 79 characters) -./src/tests/test_parent_seed_backup.py:50:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_change.py:11:1: E402 module level import not at top of file -./src/tests/test_password_change.py:12:1: E402 module level import not at top of file -./src/tests/test_password_change.py:13:1: E402 module level import not at top of file -./src/tests/test_password_change.py:14:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_password_change.py:14:1: E402 module level import not at top of file -./src/tests/test_password_change.py:15:1: E402 module level import not at top of file -./src/tests/test_password_change.py:42:80: E501 line too long (83 > 79 characters) -./src/tests/test_password_generation_policy.py:7:1: E402 module level import not at top of file -./src/tests/test_password_generation_policy.py:16:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_helpers.py:11:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_length_constraints.py:7:1: E402 module level import not at top of file -./src/tests/test_password_length_constraints.py:8:1: E402 module level import not at top of file -./src/tests/test_password_length_constraints.py:17:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_notes_display.py:10:1: E402 module level import not at top of file -./src/tests/test_password_notes_display.py:11:1: E402 module level import not at top of file -./src/tests/test_password_notes_display.py:12:1: E402 module level import not at top of file -./src/tests/test_password_notes_display.py:13:1: E402 module level import not at top of file -./src/tests/test_password_notes_display.py:30:74: E741 ambiguous variable name 'l' -./src/tests/test_password_notes_display.py:30:80: E501 line too long (84 > 79 characters) -./src/tests/test_password_notes_display.py:39:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_notes_display.py:41:80: E501 line too long (80 > 79 characters) -./src/tests/test_password_prompt.py:4:1: F401 'pytest' imported but unused -./src/tests/test_password_prompt.py:12:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_prompt.py:19:80: E501 line too long (82 > 79 characters) -./src/tests/test_password_prompt.py:27:80: E501 line too long (85 > 79 characters) -./src/tests/test_password_properties.py:8:1: E402 module level import not at top of file -./src/tests/test_password_properties.py:9:1: E402 module level import not at top of file -./src/tests/test_password_properties.py:18:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_shuffle_consistency.py:6:1: E402 module level import not at top of file -./src/tests/test_password_shuffle_consistency.py:15:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_special_chars.py:7:1: E402 module level import not at top of file -./src/tests/test_password_special_chars.py:9:1: E402 module level import not at top of file -./src/tests/test_password_special_chars.py:18:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_special_modes.py:7:1: E402 module level import not at top of file -./src/tests/test_password_special_modes.py:8:1: E402 module level import not at top of file -./src/tests/test_password_special_modes.py:17:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_unlock_after_change.py:10:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:11:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:12:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:13:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:14:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:15:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:16:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:17:1: E402 module level import not at top of file -./src/tests/test_password_unlock_after_change.py:19:80: E501 line too long (102 > 79 characters) -./src/tests/test_password_unlock_after_change.py:70:80: E501 line too long (88 > 79 characters) -./src/tests/test_password_unlock_after_change.py:84:80: E501 line too long (83 > 79 characters) -./src/tests/test_password_unlock_after_change.py:85:80: E501 line too long (86 > 79 characters) -./src/tests/test_password_unlock_after_change.py:86:80: E501 line too long (88 > 79 characters) -./src/tests/test_pgp_entry.py:9:1: E402 module level import not at top of file -./src/tests/test_pgp_entry.py:10:1: E402 module level import not at top of file -./src/tests/test_pgp_entry.py:11:1: E402 module level import not at top of file -./src/tests/test_pgp_entry.py:54:80: E501 line too long (85 > 79 characters) -./src/tests/test_portable_backup.py:12:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:13:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:14:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:15:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:16:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:17:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:18:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:19:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:20:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:22:80: E501 line too long (102 > 79 characters) -./src/tests/test_portable_backup.py:73:1: E402 module level import not at top of file -./src/tests/test_portable_backup.py:91:80: E501 line too long (80 > 79 characters) -./src/tests/test_portable_backup.py:143:80: E501 line too long (81 > 79 characters) -./src/tests/test_post_sync_messages.py:7:1: E402 module level import not at top of file -./src/tests/test_profile_cleanup.py:21:80: E501 line too long (81 > 79 characters) -./src/tests/test_profile_cleanup.py:43:80: E501 line too long (83 > 79 characters) -./src/tests/test_profile_deletion_sync.py:9:1: E402 module level import not at top of file -./src/tests/test_profile_deletion_sync.py:10:1: E402 module level import not at top of file -./src/tests/test_profile_deletion_sync.py:11:1: E402 module level import not at top of file -./src/tests/test_profile_export_import.py:1:1: F401 'pathlib.Path' imported but unused -./src/tests/test_profile_init_integration.py:11:80: E501 line too long (84 > 79 characters) -./src/tests/test_profile_init_integration.py:13:80: E501 line too long (87 > 79 characters) -./src/tests/test_profile_init_integration.py:27:80: E501 line too long (83 > 79 characters) -./src/tests/test_profile_init_integration.py:39:80: E501 line too long (83 > 79 characters) -./src/tests/test_profile_init_integration.py:40:80: E501 line too long (86 > 79 characters) -./src/tests/test_profile_management.py:12:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:13:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:14:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:15:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_profile_management.py:15:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:16:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:17:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:18:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:19:1: E402 module level import not at top of file -./src/tests/test_profile_management.py:30:80: E501 line too long (83 > 79 characters) -./src/tests/test_profile_management.py:41:80: E501 line too long (84 > 79 characters) -./src/tests/test_profile_management.py:43:80: E501 line too long (85 > 79 characters) -./src/tests/test_profile_management.py:55:80: E501 line too long (80 > 79 characters) -./src/tests/test_profiles.py:7:1: E402 module level import not at top of file -./src/tests/test_profiles.py:8:1: E402 module level import not at top of file -./src/tests/test_profiles.py:9:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_profiles.py:9:1: E402 module level import not at top of file -./src/tests/test_profiles.py:10:1: E402 module level import not at top of file -./src/tests/test_profiles.py:11:1: E402 module level import not at top of file -./src/tests/test_profiles.py:13:80: E501 line too long (108 > 79 characters) -./src/tests/test_profiles.py:39:80: E501 line too long (86 > 79 characters) -./src/tests/test_profiles.py:40:80: E501 line too long (83 > 79 characters) -./src/tests/test_profiles.py:41:80: E501 line too long (86 > 79 characters) -./src/tests/test_profiles.py:43:80: E501 line too long (82 > 79 characters) -./src/tests/test_profiles.py:55:51: F811 redefinition of unused 'dummy_nostr_client' from line 9 -./src/tests/test_profiles.py:86:60: F811 redefinition of unused 'dummy_nostr_client' from line 9 -./src/tests/test_publish_json_result.py:12:1: E402 module level import not at top of file -./src/tests/test_publish_json_result.py:13:1: E402 module level import not at top of file -./src/tests/test_publish_json_result.py:22:80: E501 line too long (83 > 79 characters) -./src/tests/test_publish_json_result.py:85:80: E501 line too long (88 > 79 characters) -./src/tests/test_publish_json_result.py:86:80: E501 line too long (82 > 79 characters) -./src/tests/test_quick_unlock_default.py:6:1: F401 'pytest' imported but unused -./src/tests/test_quick_unlock_default.py:10:1: E402 module level import not at top of file -./src/tests/test_quick_unlock_default.py:11:1: E402 module level import not at top of file -./src/tests/test_quick_unlock_default.py:12:1: E402 module level import not at top of file -./src/tests/test_quick_unlock_default.py:34:80: E501 line too long (87 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:31:80: E501 line too long (81 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:32:80: E501 line too long (82 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:65:80: E501 line too long (83 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:77:80: E501 line too long (88 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:119:80: E501 line too long (83 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:137:80: E501 line too long (82 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:144:80: E501 line too long (80 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:154:80: E501 line too long (82 > 79 characters) -./src/tests/test_restore_from_nostr_setup.py:167:80: E501 line too long (81 > 79 characters) -./src/tests/test_retrieve_pause_sensitive_entries.py:8:1: E402 module level import not at top of file -./src/tests/test_search_entries.py:9:1: E402 module level import not at top of file -./src/tests/test_search_entries.py:10:1: E402 module level import not at top of file -./src/tests/test_search_entries.py:11:1: E402 module level import not at top of file -./src/tests/test_search_entries.py:12:1: E402 module level import not at top of file -./src/tests/test_search_entries.py:31:80: E501 line too long (88 > 79 characters) -./src/tests/test_search_entries.py:43:80: E501 line too long (83 > 79 characters) -./src/tests/test_search_entries.py:56:80: E501 line too long (83 > 79 characters) -./src/tests/test_search_entries.py:65:9: F841 local variable 'idx_pw' is assigned to but never used -./src/tests/test_search_entries.py:87:9: F841 local variable 'idx' is assigned to but never used -./src/tests/test_search_entries.py:98:9: F841 local variable 'idx' is assigned to but never used -./src/tests/test_search_entries.py:122:80: E501 line too long (81 > 79 characters) -./src/tests/test_search_entries.py:134:80: E501 line too long (81 > 79 characters) -./src/tests/test_search_entries.py:151:80: E501 line too long (80 > 79 characters) -./src/tests/test_secret_mode.py:11:1: E402 module level import not at top of file -./src/tests/test_secret_mode.py:12:1: E402 module level import not at top of file -./src/tests/test_secret_mode.py:13:1: E402 module level import not at top of file -./src/tests/test_secret_mode.py:14:1: E402 module level import not at top of file -./src/tests/test_secret_mode.py:28:70: E741 ambiguous variable name 'l' -./src/tests/test_secret_mode.py:28:80: E501 line too long (80 > 79 characters) -./src/tests/test_secret_mode.py:65:80: E501 line too long (88 > 79 characters) -./src/tests/test_secret_mode.py:115:80: E501 line too long (88 > 79 characters) -./src/tests/test_secret_mode_profile_creation.py:3:1: F401 'types.SimpleNamespace' imported but unused -./src/tests/test_secret_mode_profile_creation.py:5:1: F401 'pytest' imported but unused -./src/tests/test_seed_entry.py:10:1: E402 module level import not at top of file -./src/tests/test_seed_entry.py:11:1: E402 module level import not at top of file -./src/tests/test_seed_entry.py:12:1: E402 module level import not at top of file -./src/tests/test_seed_entry.py:13:1: E402 module level import not at top of file -./src/tests/test_seed_entry.py:14:1: E402 module level import not at top of file -./src/tests/test_seed_entry.py:15:1: E402 module level import not at top of file -./src/tests/test_seed_generation.py:11:80: E501 line too long (89 > 79 characters) -./src/tests/test_seed_generation.py:21:80: E501 line too long (81 > 79 characters) -./src/tests/test_seed_import.py:11:1: E402 module level import not at top of file -./src/tests/test_seed_import.py:12:1: E402 module level import not at top of file -./src/tests/test_seed_migration.py:13:1: E402 module level import not at top of file -./src/tests/test_seed_prompt.py:20:80: E501 line too long (82 > 79 characters) -./src/tests/test_seed_prompt.py:61:80: E501 line too long (84 > 79 characters) -./src/tests/test_seed_prompt.py:89:80: E501 line too long (80 > 79 characters) -./src/tests/test_seed_word_by_word_flow.py:52:80: E501 line too long (80 > 79 characters) -./src/tests/test_seed_word_by_word_flow.py:85:80: E501 line too long (80 > 79 characters) -./src/tests/test_seedqr_encoding.py:6:1: E402 module level import not at top of file -./src/tests/test_service_classes.py:5:1: F401 'pytest' imported but unused -./src/tests/test_service_classes.py:7:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_service_classes.py:43:50: F811 redefinition of unused 'dummy_nostr_client' from line 7 -./src/tests/test_service_classes.py:69:80: E501 line too long (86 > 79 characters) -./src/tests/test_service_classes.py:70:80: E501 line too long (84 > 79 characters) -./src/tests/test_service_classes.py:128:80: E501 line too long (81 > 79 characters) -./src/tests/test_settings_menu.py:16:1: E402 module level import not at top of file -./src/tests/test_settings_menu.py:17:1: E402 module level import not at top of file -./src/tests/test_settings_menu.py:18:1: E402 module level import not at top of file -./src/tests/test_settings_menu.py:19:1: E402 module level import not at top of file -./src/tests/test_settings_menu.py:20:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_settings_menu.py:20:1: E402 module level import not at top of file -./src/tests/test_settings_menu.py:21:1: E402 module level import not at top of file -./src/tests/test_settings_menu.py:60:80: E501 line too long (107 > 79 characters) -./src/tests/test_settings_menu.py:63:80: E501 line too long (89 > 79 characters) -./src/tests/test_settings_menu.py:116:80: E501 line too long (81 > 79 characters) -./src/tests/test_settings_menu.py:136:80: E501 line too long (81 > 79 characters) -./src/tests/test_settings_menu.py:153:80: E501 line too long (88 > 79 characters) -./src/tests/test_settings_menu.py:161:80: E501 line too long (82 > 79 characters) -./src/tests/test_ssh_entry.py:9:1: E402 module level import not at top of file -./src/tests/test_ssh_entry.py:10:1: E402 module level import not at top of file -./src/tests/test_ssh_entry.py:11:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_ssh_entry.py:11:1: E402 module level import not at top of file -./src/tests/test_ssh_entry.py:12:1: E402 module level import not at top of file -./src/tests/test_ssh_entry_valid.py:9:1: E402 module level import not at top of file -./src/tests/test_ssh_entry_valid.py:10:1: E402 module level import not at top of file -./src/tests/test_ssh_entry_valid.py:11:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_ssh_entry_valid.py:11:1: E402 module level import not at top of file -./src/tests/test_ssh_entry_valid.py:12:1: E402 module level import not at top of file -./src/tests/test_ssh_entry_valid.py:13:1: E402 module level import not at top of file -./src/tests/test_stats_screen.py:4:1: F401 'pytest' imported but unused -./src/tests/test_stats_screen.py:9:1: E402 module level import not at top of file -./src/tests/test_stats_screen.py:50:80: E501 line too long (85 > 79 characters) -./src/tests/test_sync_race_conditions.py:35:80: E501 line too long (82 > 79 characters) -./src/tests/test_tag_persistence.py:9:1: E402 module level import not at top of file -./src/tests/test_tag_persistence.py:10:1: E402 module level import not at top of file -./src/tests/test_tag_persistence.py:11:1: E402 module level import not at top of file -./src/tests/test_tag_persistence.py:12:1: E402 module level import not at top of file -./src/tests/test_totp.py:9:1: E402 module level import not at top of file -./src/tests/test_totp.py:10:1: E402 module level import not at top of file -./src/tests/test_totp.py:28:80: E501 line too long (83 > 79 characters) -./src/tests/test_totp_entry.py:4:1: F401 'unittest.mock.patch' imported but unused -./src/tests/test_totp_entry.py:6:1: F401 'pytest' imported but unused -./src/tests/test_totp_entry.py:12:1: E402 module level import not at top of file -./src/tests/test_totp_entry.py:13:1: E402 module level import not at top of file -./src/tests/test_totp_entry.py:14:1: F401 'seedpass.core.vault.Vault' imported but unused -./src/tests/test_totp_entry.py:14:1: E402 module level import not at top of file -./src/tests/test_totp_entry.py:15:1: E402 module level import not at top of file -./src/tests/test_totp_entry.py:16:1: E402 module level import not at top of file -./src/tests/test_totp_entry.py:17:1: E402 module level import not at top of file -./src/tests/test_totp_entry.py:101:80: E501 line too long (86 > 79 characters) -./src/tests/test_totp_uri.py:8:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:7:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:9:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:10:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:11:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:12:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:13:1: E402 module level import not at top of file -./src/tests/test_typer_cli.py:21:80: E501 line too long (81 > 79 characters) -./src/tests/test_typer_cli.py:62:69: E741 ambiguous variable name 'l' -./src/tests/test_typer_cli.py:62:80: E501 line too long (80 > 79 characters) -./src/tests/test_typer_cli.py:79:80: E501 line too long (82 > 79 characters) -./src/tests/test_typer_cli.py:80:80: E501 line too long (81 > 79 characters) -./src/tests/test_typer_cli.py:94:80: E501 line too long (82 > 79 characters) -./src/tests/test_typer_cli.py:95:80: E501 line too long (81 > 79 characters) -./src/tests/test_typer_cli.py:113:80: E501 line too long (82 > 79 characters) -./src/tests/test_typer_cli.py:115:80: E501 line too long (85 > 79 characters) -./src/tests/test_typer_cli.py:131:80: E501 line too long (87 > 79 characters) -./src/tests/test_typer_cli.py:133:80: E501 line too long (86 > 79 characters) -./src/tests/test_typer_cli.py:187:80: E501 line too long (83 > 79 characters) -./src/tests/test_typer_cli.py:215:80: E501 line too long (82 > 79 characters) -./src/tests/test_typer_cli.py:323:80: E501 line too long (83 > 79 characters) -./src/tests/test_typer_cli.py:398:80: E501 line too long (86 > 79 characters) -./src/tests/test_typer_cli.py:412:80: E501 line too long (81 > 79 characters) -./src/tests/test_typer_cli.py:481:80: E501 line too long (82 > 79 characters) -./src/tests/test_typer_cli.py:491:80: E501 line too long (80 > 79 characters) -./src/tests/test_typer_cli.py:506:80: E501 line too long (80 > 79 characters) -./src/tests/test_typer_cli.py:599:80: E501 line too long (80 > 79 characters) -./src/tests/test_unlock_sync.py:10:1: E402 module level import not at top of file -./src/tests/test_unlock_sync.py:11:1: E402 module level import not at top of file -./src/tests/test_unlock_sync.py:26:80: E501 line too long (82 > 79 characters) -./src/tests/test_unlock_sync.py:45:80: E501 line too long (83 > 79 characters) -./src/tests/test_unlock_sync.py:75:80: E501 line too long (84 > 79 characters) -./src/tests/test_unlock_sync.py:76:80: E501 line too long (82 > 79 characters) -./src/tests/test_vault_initialization.py:4:1: F401 'unittest.mock.patch' imported but unused -./src/tests/test_vault_initialization.py:8:1: E402 module level import not at top of file -./src/tests/test_vault_initialization.py:9:1: E402 module level import not at top of file -./src/tests/test_vault_initialization.py:11:80: E501 line too long (108 > 79 characters) -./src/tests/test_vault_initialization.py:23:80: E501 line too long (86 > 79 characters) -./src/tests/test_verbose_timing.py:5:1: F401 'helpers.dummy_nostr_client' imported but unused -./src/tests/test_verbose_timing.py:18:80: E501 line too long (87 > 79 characters) -./src/tests/test_verbose_timing.py:23:37: F811 redefinition of unused 'dummy_nostr_client' from line 5 -./src/utils/__init__.py:9:1: E402 module level import not at top of file -./src/utils/__init__.py:10:1: E402 module level import not at top of file -./src/utils/__init__.py:19:1: E402 module level import not at top of file -./src/utils/__init__.py:27:1: E402 module level import not at top of file -./src/utils/__init__.py:28:1: E402 module level import not at top of file -./src/utils/__init__.py:29:1: E402 module level import not at top of file -./src/utils/__init__.py:30:1: E402 module level import not at top of file -./src/utils/__init__.py:31:1: E402 module level import not at top of file -./src/utils/__init__.py:37:1: E402 module level import not at top of file -./src/utils/__init__.py:42:80: E501 line too long (81 > 79 characters) -./src/utils/checksum.py:6:80: E501 line too long (89 > 79 characters) -./src/utils/checksum.py:7:80: E501 line too long (85 > 79 characters) -./src/utils/checksum.py:10:80: E501 line too long (87 > 79 characters) -./src/utils/checksum.py:15:1: F401 'sys' imported but unused -./src/utils/checksum.py:16:1: F401 'os' imported but unused -./src/utils/checksum.py:22:1: F401 'constants.APP_DIR' imported but unused -./src/utils/checksum.py:22:1: F401 'constants.SCRIPT_CHECKSUM_FILE' imported but unused -./src/utils/checksum.py:48:80: E501 line too long (82 > 79 characters) -./src/utils/checksum.py:59:80: E501 line too long (80 > 79 characters) -./src/utils/checksum.py:62:80: E501 line too long (87 > 79 characters) -./src/utils/checksum.py:72:80: E501 line too long (84 > 79 characters) -./src/utils/checksum.py:97:80: E501 line too long (85 > 79 characters) -./src/utils/checksum.py:102:80: E501 line too long (82 > 79 characters) -./src/utils/checksum.py:125:80: E501 line too long (88 > 79 characters) -./src/utils/checksum.py:129:80: E501 line too long (87 > 79 characters) -./src/utils/checksum.py:133:80: E501 line too long (84 > 79 characters) -./src/utils/checksum.py:140:80: E501 line too long (80 > 79 characters) -./src/utils/checksum.py:142:80: E501 line too long (92 > 79 characters) -./src/utils/checksum.py:156:80: E501 line too long (83 > 79 characters) -./src/utils/checksum.py:160:80: E501 line too long (81 > 79 characters) -./src/utils/checksum.py:167:80: E501 line too long (80 > 79 characters) -./src/utils/checksum.py:183:80: E501 line too long (89 > 79 characters) -./src/utils/checksum.py:194:80: E501 line too long (89 > 79 characters) -./src/utils/checksum.py:202:80: E501 line too long (83 > 79 characters) -./src/utils/checksum.py:209:80: E501 line too long (92 > 79 characters) -./src/utils/checksum.py:219:80: E501 line too long (87 > 79 characters) -./src/utils/clipboard.py:19:80: E501 line too long (82 > 79 characters) -./src/utils/clipboard.py:27:80: E501 line too long (85 > 79 characters) -./src/utils/clipboard.py:30:80: E501 line too long (86 > 79 characters) -./src/utils/clipboard.py:36:80: E501 line too long (86 > 79 characters) -./src/utils/file_lock.py:1:80: E501 line too long (80 > 79 characters) -./src/utils/fingerprint.py:6:80: E501 line too long (83 > 79 characters) -./src/utils/fingerprint.py:7:80: E501 line too long (80 > 79 characters) -./src/utils/fingerprint.py:36:80: E501 line too long (81 > 79 characters) -./src/utils/fingerprint_manager.py:3:1: F401 'os' imported but unused -./src/utils/fingerprint_manager.py:23:80: E501 line too long (82 > 79 characters) -./src/utils/fingerprint_manager.py:48:80: E501 line too long (89 > 79 characters) -./src/utils/fingerprint_manager.py:65:80: E501 line too long (80 > 79 characters) -./src/utils/fingerprint_manager.py:69:80: E501 line too long (84 > 79 characters) -./src/utils/fingerprint_manager.py:70:80: E501 line too long (86 > 79 characters) -./src/utils/fingerprint_manager.py:79:80: E501 line too long (81 > 79 characters) -./src/utils/fingerprint_manager.py:83:80: E501 line too long (83 > 79 characters) -./src/utils/fingerprint_manager.py:105:80: E501 line too long (98 > 79 characters) -./src/utils/fingerprint_manager.py:142:80: E501 line too long (84 > 79 characters) -./src/utils/fingerprint_manager.py:178:80: E501 line too long (85 > 79 characters) -./src/utils/fingerprint_manager.py:242:80: E501 line too long (81 > 79 characters) -./src/utils/fingerprint_manager.py:248:80: E501 line too long (84 > 79 characters) -./src/utils/imghdr_stub.py:103:80: E501 line too long (82 > 79 characters) -./src/utils/imghdr_stub.py:112:80: E501 line too long (82 > 79 characters) -./src/utils/imghdr_stub.py:121:80: E501 line too long (82 > 79 characters) -./src/utils/imghdr_stub.py:195:80: E501 line too long (81 > 79 characters) -./src/utils/key_derivation.py:24:1: F401 'typing.Optional' imported but unused -./src/utils/key_derivation.py:78:80: E501 line too long (99 > 79 characters) -./src/utils/key_derivation.py:80:80: E501 line too long (97 > 79 characters) -./src/utils/key_derivation.py:81:80: E501 line too long (99 > 79 characters) -./src/utils/key_derivation.py:87:80: E501 line too long (82 > 79 characters) -./src/utils/key_derivation.py:101:80: E501 line too long (82 > 79 characters) -./src/utils/key_derivation.py:151:80: E501 line too long (80 > 79 characters) -./src/utils/key_derivation.py:172:80: E501 line too long (84 > 79 characters) -./src/utils/key_derivation.py:175:80: E501 line too long (86 > 79 characters) -./src/utils/key_derivation.py:178:80: E501 line too long (86 > 79 characters) -./src/utils/key_derivation.py:188:80: E501 line too long (87 > 79 characters) -./src/utils/key_derivation.py:205:80: E501 line too long (85 > 79 characters) -./src/utils/key_derivation.py:240:80: E501 line too long (85 > 79 characters) -./src/utils/key_derivation.py:266:80: E501 line too long (80 > 79 characters) -./src/utils/key_derivation.py:286:80: E501 line too long (80 > 79 characters) -./src/utils/key_derivation.py:297:80: E501 line too long (86 > 79 characters) -./src/utils/key_validation.py:26:80: E501 line too long (83 > 79 characters) -./src/utils/password_prompt.py:6:80: E501 line too long (94 > 79 characters) -./src/utils/password_prompt.py:7:80: E501 line too long (90 > 79 characters) -./src/utils/password_prompt.py:8:80: E501 line too long (94 > 79 characters) -./src/utils/password_prompt.py:9:80: E501 line too long (86 > 79 characters) -./src/utils/password_prompt.py:11:80: E501 line too long (87 > 79 characters) -./src/utils/password_prompt.py:17:1: F401 'sys' imported but unused -./src/utils/password_prompt.py:39:80: E501 line too long (80 > 79 characters) -./src/utils/password_prompt.py:73:80: E501 line too long (88 > 79 characters) -./src/utils/password_prompt.py:75:80: E501 line too long (93 > 79 characters) -./src/utils/password_prompt.py:76:80: E501 line too long (96 > 79 characters) -./src/utils/password_prompt.py:80:80: E501 line too long (96 > 79 characters) -./src/utils/password_prompt.py:81:80: E501 line too long (98 > 79 characters) -./src/utils/password_prompt.py:87:80: E501 line too long (99 > 79 characters) -./src/utils/password_prompt.py:106:80: E501 line too long (88 > 79 characters) -./src/utils/password_prompt.py:116:80: E501 line too long (99 > 79 characters) -./src/utils/password_prompt.py:121:80: E501 line too long (93 > 79 characters) -./src/utils/password_prompt.py:129:80: E501 line too long (86 > 79 characters) -./src/utils/password_prompt.py:154:80: E501 line too long (85 > 79 characters) -./src/utils/password_prompt.py:159:80: E501 line too long (81 > 79 characters) -./src/utils/password_prompt.py:168:80: E501 line too long (80 > 79 characters) -./src/utils/password_prompt.py:169:80: E501 line too long (83 > 79 characters) -./src/utils/password_prompt.py:170:80: E501 line too long (80 > 79 characters) -./src/utils/password_prompt.py:192:80: E501 line too long (88 > 79 characters) -./src/utils/password_prompt.py:223:80: E501 line too long (96 > 79 characters) -./src/utils/password_prompt.py:226:80: E501 line too long (89 > 79 characters) -./src/utils/password_prompt.py:261:80: E501 line too long (90 > 79 characters) -./src/utils/password_prompt.py:263:80: E501 line too long (101 > 79 characters) -./src/utils/seed_prompt.py:109:80: E501 line too long (84 > 79 characters) -./src/utils/seed_prompt.py:121:80: E501 line too long (82 > 79 characters) -./src/utils/seed_prompt.py:177:80: E501 line too long (80 > 79 characters) -./src/utils/seed_prompt.py:180:80: E501 line too long (87 > 79 characters) -./src/utils/seed_prompt.py:189:80: E501 line too long (88 > 79 characters) -./src/utils/seed_prompt.py:192:80: E501 line too long (85 > 79 characters) -./src/utils/seed_prompt.py:200:80: E501 line too long (81 > 79 characters) -./src/utils/terminal_utils.py:23:80: E501 line too long (80 > 79 characters) -./src/utils/terminal_utils.py:41:80: E501 line too long (83 > 79 characters) -./src/utils/terminal_utils.py:45:80: E501 line too long (123 > 79 characters) -./src/utils/terminal_utils.py:80:80: E501 line too long (81 > 79 characters) -./src/utils/terminal_utils.py:85:80: E501 line too long (123 > 79 characters) -./tests/perf/test_bip85_cache.py:12:80: E501 line too long (88 > 79 characters) -./tests/perf/test_bip85_cache.py:41:5: F841 local variable 'uncached_time' is assigned to but never used -./tests/perf/test_bip85_cache.py:48:5: F841 local variable 'cached_time' is assigned to but never used -./torch/node_modules/torch-lock/find_dead_code.py:3:1: F401 'sys' imported but unused -./torch/node_modules/torch-lock/find_dead_code.py:5:1: E302 expected 2 blank lines, found 1 -./torch/node_modules/torch-lock/find_dead_code.py:8:80: E501 line too long (93 > 79 characters) -./torch/node_modules/torch-lock/find_dead_code.py:15:1: E302 expected 2 blank lines, found 1 -./torch/node_modules/torch-lock/find_dead_code.py:41:14: E111 indentation is not a multiple of 4 -./torch/node_modules/torch-lock/find_dead_code.py:41:14: E117 over-indented -./torch/node_modules/torch-lock/find_dead_code.py:44:13: E122 continuation line missing indentation or outdented -./torch/node_modules/torch-lock/find_dead_code.py:62:17: E261 at least two spaces before inline comment -./torch/node_modules/torch-lock/find_dead_code.py:64:1: E302 expected 2 blank lines, found 1 -./torch/node_modules/torch-lock/find_dead_code.py:68:80: E501 line too long (81 > 79 characters) -./torch/node_modules/torch-lock/find_dead_code.py:72:25: E261 at least two spaces before inline comment -./torch/node_modules/torch-lock/find_dead_code.py:73:31: E261 at least two spaces before inline comment -./torch/node_modules/torch-lock/find_dead_code.py:101:1: E305 expected 2 blank lines after class or function definition, found 1 -``` - -### pytest Output -``` -============================= test session starts ============================== -platform linux -- Python 3.12.12, pytest-8.4.1, pluggy-1.6.0 -rootdir: /app -configfile: pytest.ini -testpaths: src/tests, tests -plugins: anyio-4.9.0, hypothesis-6.136.7, cov-6.2.1, xdist-3.8.0 -created: 4/4 workers -4 workers [661 items] - -scheduling tests via LoadScheduling - -src/tests/test_bip85_vectors.py::test_bip85_mnemonic_12 -src/tests/test_api_new_endpoints.py::test_vault_lock_endpoint[asyncio] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-totp Email --secret JBSW...] -src/tests/test_add_new_fingerprint_words.py::test_add_new_fingerprint_word_entry_exits -[gw0] [ 0%] PASSED src/tests/test_add_new_fingerprint_words.py::test_add_new_fingerprint_word_entry_exits -src/tests/test_add_tags_from_retrieve.py::test_add_tags_from_retrieve -[gw2] [ 0%] PASSED src/tests/test_bip85_vectors.py::test_bip85_mnemonic_12 -src/tests/test_bip85_vectors.py::test_bip85_mnemonic_24 -[gw2] [ 0%] PASSED src/tests/test_bip85_vectors.py::test_bip85_mnemonic_24 -src/tests/test_bip85_vectors.py::test_bip85_symmetric_key -[gw2] [ 0%] PASSED src/tests/test_bip85_vectors.py::test_bip85_symmetric_key -src/tests/test_bip85_vectors.py::test_derive_totp_secret -[gw2] [ 0%] PASSED src/tests/test_bip85_vectors.py::test_derive_totp_secret -src/tests/test_bip85_vectors.py::test_derive_ssh_key -[gw2] [ 0%] PASSED src/tests/test_bip85_vectors.py::test_derive_ssh_key -src/tests/test_bip85_vectors.py::test_derive_seed_phrase -[gw3] [ 1%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-totp Email --secret JBSW...] -[gw2] [ 1%] PASSED src/tests/test_bip85_vectors.py::test_derive_seed_phrase -src/tests/test_bip85_vectors.py::test_invalid_params -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry archive 1] -[gw2] [ 1%] PASSED src/tests/test_bip85_vectors.py::test_invalid_params -src/tests/test_checksum_utils.py::test_json_checksum -[gw2] [ 1%] PASSED src/tests/test_checksum_utils.py::test_json_checksum -src/tests/test_checksum_utils.py::test_calculate_checksum -[gw2] [ 1%] PASSED src/tests/test_checksum_utils.py::test_calculate_checksum -src/tests/test_checksum_utils.py::test_calculate_checksum_missing -[gw2] [ 1%] PASSED src/tests/test_checksum_utils.py::test_calculate_checksum_missing -src/tests/test_checksum_utils.py::test_verify_and_update -[gw2] [ 1%] PASSED src/tests/test_checksum_utils.py::test_verify_and_update -src/tests/test_checksum_utils.py::test_initialize_checksum -[gw2] [ 2%] PASSED src/tests/test_checksum_utils.py::test_initialize_checksum -src/tests/test_cli_clipboard_flag.py::test_entry_get_handles_missing_clipboard -[gw3] [ 2%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry archive 1] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry export-totp --file totp.json] -[gw2] [ 2%] PASSED src/tests/test_cli_clipboard_flag.py::test_entry_get_handles_missing_clipboard -src/tests/test_cli_clipboard_flag.py::test_entry_get_no_clipboard_flag -[gw3] [ 2%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry export-totp --file totp.json] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry get "GitHub"] -[gw2] [ 2%] PASSED src/tests/test_cli_clipboard_flag.py::test_entry_get_no_clipboard_flag -src/tests/test_cli_config_set_extra.py::test_config_set_variants[secret_mode_enabled-true-set_secret_mode_enabled-True] -[gw3] [ 2%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry get "GitHub"] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry list] -[gw0] [ 3%] PASSED src/tests/test_add_tags_from_retrieve.py::test_add_tags_from_retrieve -src/tests/test_additional_backup.py::test_entry_manager_additional_backup -[gw2] [ 3%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[secret_mode_enabled-true-set_secret_mode_enabled-True] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[clipboard_clear_delay-10-set_clipboard_clear_delay-10] -[gw3] [ 3%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry list] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry list --sort label] -[gw2] [ 3%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[clipboard_clear_delay-10-set_clipboard_clear_delay-10] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[additional_backup_path--set_additional_backup_path-None] -[gw3] [ 3%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry list --sort label] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry modify 1 --key new --value updated] -[gw2] [ 3%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[additional_backup_path--set_additional_backup_path-None] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[backup_interval-5-set_backup_interval-5.0] -[gw3] [ 3%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry modify 1 --key new --value updated] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry search "GitHub"] -[gw2] [ 4%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[backup_interval-5-set_backup_interval-5.0] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[kdf_iterations-123-set_kdf_iterations-123] -[gw3] [ 4%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry search "GitHub"] -[gw0] [ 4%] PASSED src/tests/test_additional_backup.py::test_entry_manager_additional_backup -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry totp-codes] -src/tests/test_api.py::test_token_hashed[asyncio] -[gw2] [ 4%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[kdf_iterations-123-set_kdf_iterations-123] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[kdf_mode-argon2-set_kdf_mode-argon2] -[gw2] [ 4%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[kdf_mode-argon2-set_kdf_mode-argon2] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[quick_unlock-true-set_quick_unlock-True] -[gw2] [ 4%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[quick_unlock-true-set_quick_unlock-True] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[nostr_max_retries-3-set_nostr_max_retries-3] -[gw2] [ 4%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[nostr_max_retries-3-set_nostr_max_retries-3] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[nostr_retry_delay-1.5-set_nostr_retry_delay-1.5] -[gw3] [ 5%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry totp-codes] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry unarchive 1] -[gw2] [ 5%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[nostr_retry_delay-1.5-set_nostr_retry_delay-1.5] -src/tests/test_cli_config_set_extra.py::test_config_set_variants[relays-wss://a.com, wss://b.com-set_relays-expected9] -[gw3] [ 5%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry unarchive 1] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[fingerprint add] -[gw2] [ 5%] PASSED src/tests/test_cli_config_set_extra.py::test_config_set_variants[relays-wss://a.com, wss://b.com-set_relays-expected9] -src/tests/test_cli_core_services.py::test_cli_vault_unlock -[gw3] [ 5%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[fingerprint add] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[fingerprint list] -[gw2] [ 5%] PASSED src/tests/test_cli_core_services.py::test_cli_vault_unlock -src/tests/test_cli_core_services.py::test_cli_entry_add_search_sync -[gw3] [ 6%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[fingerprint list] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[nostr get-pubkey] -[gw3] [ 6%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[nostr get-pubkey] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[nostr sync] -[gw3] [ 6%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[nostr sync] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util generate-password] -[gw2] [ 6%] PASSED src/tests/test_cli_core_services.py::test_cli_entry_add_search_sync -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[api start] -[gw3] [ 6%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util generate-password] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util generate-password --length 24 --special-mode safe --exclude-ambiguous] -[gw2] [ 6%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[api start] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[api start --host 0.0.0.0 --port 8000] -[gw3] [ 6%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util generate-password --length 24 --special-mode safe --exclude-ambiguous] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util update-checksum] -[gw2] [ 7%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[api start --host 0.0.0.0 --port 8000] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config get kdf_iterations] -[gw3] [ 7%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util update-checksum] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util verify-checksum] -[gw2] [ 7%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config get kdf_iterations] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config set backup_interval 3600] -[gw3] [ 7%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[util verify-checksum] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault change-password] -[gw2] [ 7%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config set backup_interval 3600] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config set kdf_iterations 200000] -[gw3] [ 7%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault change-password] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault lock] -[gw2] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config set kdf_iterations 200000] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config toggle-offline] -[gw3] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault lock] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault reveal-parent-seed] -[gw2] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config toggle-offline] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config toggle-secret-mode] -[gw3] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault reveal-parent-seed] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault reveal-parent-seed --file backup.enc] -[gw2] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[config toggle-secret-mode] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry --help] -[gw3] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault reveal-parent-seed --file backup.enc] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault stats] -[gw2] [ 8%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry --help] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add Example --length 16 --no-special --exclude-ambiguous] -[gw3] [ 9%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[vault stats] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-add_entry-cli_args0-expected_args0-expected_kwargs0-1] -[gw1] [ 9%] PASSED src/tests/test_api_new_endpoints.py::test_vault_lock_endpoint[asyncio] -src/tests/test_api_new_endpoints.py::test_secret_mode_endpoint[asyncio] -[gw2] [ 9%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add Example --length 16 --no-special --exclude-ambiguous] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-key-value "API Token" --key api --value abc123] -[gw3] [ 9%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-add_entry-cli_args0-expected_args0-expected_kwargs0-1] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-totp-add_totp-cli_args1-expected_args1-expected_kwargs1-otpauth://uri] -[gw2] [ 9%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-key-value "API Token" --key api --value abc123] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-managed-account Trading] -[gw3] [ 9%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-totp-add_totp-cli_args1-expected_args1-expected_kwargs1-otpauth://uri] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-ssh-add_ssh_key-cli_args2-expected_args2-expected_kwargs2-3] -[gw2] [ 9%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-managed-account Trading] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-nostr Chat] -[gw3] [ 10%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-ssh-add_ssh_key-cli_args2-expected_args2-expected_kwargs2-3] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-pgp-add_pgp_key-cli_args3-expected_args3-expected_kwargs3-4] -[gw2] [ 10%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-nostr Chat] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-pgp Personal --user-id me@example.com] -[gw3] [ 10%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-pgp-add_pgp_key-cli_args3-expected_args3-expected_kwargs3-4] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-nostr-add_nostr_key-cli_args4-expected_args4-expected_kwargs4-5] -[gw2] [ 10%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-pgp Personal --user-id me@example.com] -[gw0] [ 10%] PASSED src/tests/test_api.py::test_token_hashed[asyncio] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-seed Backup --words 24] -src/tests/test_api.py::test_cors_and_auth[asyncio] -[gw3] [ 10%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-nostr-add_nostr_key-cli_args4-expected_args4-expected_kwargs4-5] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-seed-add_seed-cli_args5-expected_args5-expected_kwargs5-6] -[gw2] [ 11%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-seed Backup --words 24] -src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-ssh Server --index 0] -[gw3] [ 11%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-seed-add_seed-cli_args5-expected_args5-expected_kwargs5-6] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-key-value-add_key_value-cli_args6-expected_args6-expected_kwargs6-7] -[gw2] [ 11%] PASSED src/tests/test_cli_doc_examples.py::test_doc_cli_examples[entry add-ssh Server --index 0] -src/tests/test_cli_subcommands.py::test_get_command -[gw2] [ 11%] PASSED src/tests/test_cli_subcommands.py::test_get_command -src/tests/test_cli_subcommands.py::test_totp_command -[gw2] [ 11%] PASSED src/tests/test_cli_subcommands.py::test_totp_command -src/tests/test_cli_subcommands.py::test_search_command_no_results -[gw3] [ 11%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-key-value-add_key_value-cli_args6-expected_args6-expected_kwargs6-7] -src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-managed-account-add_managed_account-cli_args7-expected_args7-expected_kwargs7-8] -[gw2] [ 11%] PASSED src/tests/test_cli_subcommands.py::test_search_command_no_results -src/tests/test_cli_subcommands.py::test_get_command_multiple_matches -[gw2] [ 12%] PASSED src/tests/test_cli_subcommands.py::test_get_command_multiple_matches -src/tests/test_cli_subcommands.py::test_get_command_wrong_type -[gw2] [ 12%] PASSED src/tests/test_cli_subcommands.py::test_get_command_wrong_type -src/tests/test_cli_subcommands.py::test_totp_command_multiple_matches -[gw2] [ 12%] PASSED src/tests/test_cli_subcommands.py::test_totp_command_multiple_matches -src/tests/test_cli_subcommands.py::test_totp_command_wrong_type -[gw2] [ 12%] PASSED src/tests/test_cli_subcommands.py::test_totp_command_wrong_type -src/tests/test_cli_subcommands.py::test_main_fingerprint_option -[gw2] [ 12%] PASSED src/tests/test_cli_subcommands.py::test_main_fingerprint_option -src/tests/test_cli_toggle_offline_mode.py::test_toggle_offline_updates -[gw3] [ 12%] PASSED src/tests/test_cli_entry_add_commands.py::test_entry_add_commands[add-managed-account-add_managed_account-cli_args7-expected_args7-expected_kwargs7-8] -src/tests/test_cli_export_import.py::test_cli_export_creates_file -[gw2] [ 13%] PASSED src/tests/test_cli_toggle_offline_mode.py::test_toggle_offline_updates -src/tests/test_cli_toggle_offline_mode.py::test_toggle_offline_keep -[gw2] [ 13%] PASSED src/tests/test_cli_toggle_offline_mode.py::test_toggle_offline_keep -src/tests/test_cli_toggle_secret_mode.py::test_toggle_secret_mode_updates -[gw2] [ 13%] PASSED src/tests/test_cli_toggle_secret_mode.py::test_toggle_secret_mode_updates -src/tests/test_cli_toggle_secret_mode.py::test_toggle_secret_mode_keep -[gw3] [ 13%] PASSED src/tests/test_cli_export_import.py::test_cli_export_creates_file -src/tests/test_cli_export_import.py::test_cli_import_round_trip -[gw2] [ 13%] PASSED src/tests/test_cli_toggle_secret_mode.py::test_toggle_secret_mode_keep -src/tests/test_cli_vault_stats.py::test_vault_stats_command -[gw2] [ 13%] PASSED src/tests/test_cli_vault_stats.py::test_vault_stats_command -src/tests/test_clipboard_utils.py::test_copy_to_clipboard_clears -[gw2] [ 13%] PASSED src/tests/test_clipboard_utils.py::test_copy_to_clipboard_clears -src/tests/test_clipboard_utils.py::test_copy_to_clipboard_does_not_clear_if_changed -[gw2] [ 14%] PASSED src/tests/test_clipboard_utils.py::test_copy_to_clipboard_does_not_clear_if_changed -src/tests/test_clipboard_utils.py::test_copy_to_clipboard_missing_dependency -[gw2] [ 14%] PASSED src/tests/test_clipboard_utils.py::test_copy_to_clipboard_missing_dependency -src/tests/test_concurrency_stress.py::test_concurrency_stress[0-5] -[gw3] [ 14%] PASSED src/tests/test_cli_export_import.py::test_cli_import_round_trip -src/tests/test_cli_export_import.py::test_cli_export_import_unencrypted -[gw3] [ 14%] PASSED src/tests/test_cli_export_import.py::test_cli_export_import_unencrypted -src/tests/test_cli_integration.py::test_cli_integration -[gw2] [ 14%] PASSED src/tests/test_concurrency_stress.py::test_concurrency_stress[0-5] -src/tests/test_concurrency_stress.py::test_concurrency_stress[0-20] -[gw2] [ 14%] SKIPPED src/tests/test_concurrency_stress.py::test_concurrency_stress[0-20] -src/tests/test_concurrency_stress.py::test_concurrency_stress[1-5] -[gw1] [ 14%] PASSED src/tests/test_api_new_endpoints.py::test_secret_mode_endpoint[asyncio] -src/tests/test_api_new_endpoints.py::test_vault_export_endpoint[asyncio] -[gw2] [ 15%] PASSED src/tests/test_concurrency_stress.py::test_concurrency_stress[1-5] -src/tests/test_concurrency_stress.py::test_concurrency_stress[1-20] -[gw2] [ 15%] SKIPPED src/tests/test_concurrency_stress.py::test_concurrency_stress[1-20] -src/tests/test_concurrency_stress.py::test_concurrency_stress[2-5] -[gw0] [ 15%] PASSED src/tests/test_api.py::test_cors_and_auth[asyncio] -src/tests/test_api.py::test_invalid_token[asyncio] -[gw2] [ 15%] PASSED src/tests/test_concurrency_stress.py::test_concurrency_stress[2-5] -src/tests/test_concurrency_stress.py::test_concurrency_stress[2-20] -[gw2] [ 15%] SKIPPED src/tests/test_concurrency_stress.py::test_concurrency_stress[2-20] -src/tests/test_config_manager.py::test_config_defaults_and_round_trip -[gw0] [ 15%] PASSED src/tests/test_api.py::test_invalid_token[asyncio] -src/tests/test_api.py::test_get_entry_by_id[asyncio] -[gw2] [ 16%] PASSED src/tests/test_config_manager.py::test_config_defaults_and_round_trip -src/tests/test_config_manager.py::test_pin_verification_and_change -[gw1] [ 16%] PASSED src/tests/test_api_new_endpoints.py::test_vault_export_endpoint[asyncio] -src/tests/test_api_new_endpoints.py::test_backup_parent_seed_endpoint[asyncio] -[gw3] [ 16%] PASSED src/tests/test_cli_integration.py::test_cli_integration -src/tests/test_cli_invalid_input.py::test_empty_and_non_numeric_choice -[gw3] [ 16%] PASSED src/tests/test_cli_invalid_input.py::test_empty_and_non_numeric_choice -src/tests/test_cli_invalid_input.py::test_out_of_range_menu -[gw3] [ 16%] PASSED src/tests/test_cli_invalid_input.py::test_out_of_range_menu -src/tests/test_cli_invalid_input.py::test_invalid_add_entry_submenu -[gw3] [ 16%] PASSED src/tests/test_cli_invalid_input.py::test_invalid_add_entry_submenu -src/tests/test_cli_invalid_input.py::test_inactivity_timeout_loop -[gw3] [ 16%] PASSED src/tests/test_cli_invalid_input.py::test_inactivity_timeout_loop -src/tests/test_cli_relays.py::test_cli_relay_crud -[gw3] [ 17%] PASSED src/tests/test_cli_relays.py::test_cli_relay_crud -src/tests/test_cli_subcommands.py::test_search_command -[gw3] [ 17%] PASSED src/tests/test_cli_subcommands.py::test_search_command -src/tests/test_config_manager.py::test_backup_interval_round_trip -[gw3] [ 17%] PASSED src/tests/test_config_manager.py::test_backup_interval_round_trip -src/tests/test_config_manager.py::test_quick_unlock_round_trip -[gw0] [ 17%] PASSED src/tests/test_api.py::test_get_entry_by_id[asyncio] -src/tests/test_api.py::test_get_config_value[asyncio] -[gw3] [ 17%] PASSED src/tests/test_config_manager.py::test_quick_unlock_round_trip -src/tests/test_config_manager.py::test_nostr_retry_settings_round_trip -[gw3] [ 17%] PASSED src/tests/test_config_manager.py::test_nostr_retry_settings_round_trip -src/tests/test_config_manager.py::test_special_char_settings_round_trip -[gw3] [ 18%] PASSED src/tests/test_config_manager.py::test_special_char_settings_round_trip -src/tests/test_config_manager.py::test_password_policy_extended_fields -[gw3] [ 18%] PASSED src/tests/test_config_manager.py::test_password_policy_extended_fields -src/tests/test_core_api_services.py::TestVaultService::test_export_vault -[gw3] [ 18%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_export_vault -src/tests/test_core_api_services.py::TestVaultService::test_import_vault -[gw3] [ 18%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_import_vault -src/tests/test_core_api_services.py::TestVaultService::test_export_profile -[gw3] [ 18%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_export_profile -src/tests/test_core_api_services.py::TestVaultService::test_import_profile -[gw3] [ 18%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_import_profile -src/tests/test_core_api_services.py::TestVaultService::test_change_password -[gw3] [ 18%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_change_password -src/tests/test_core_api_services.py::TestVaultService::test_unlock -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_unlock -src/tests/test_core_api_services.py::TestVaultService::test_lock -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_lock -src/tests/test_core_api_services.py::TestVaultService::test_backup_parent_seed -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_backup_parent_seed -src/tests/test_core_api_services.py::TestVaultService::test_stats -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestVaultService::test_stats -src/tests/test_core_api_services.py::TestProfileService::test_list_profiles -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestProfileService::test_list_profiles -src/tests/test_core_api_services.py::TestProfileService::test_add_profile -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestProfileService::test_add_profile -src/tests/test_core_api_services.py::TestProfileService::test_remove_profile -[gw3] [ 19%] PASSED src/tests/test_core_api_services.py::TestProfileService::test_remove_profile -src/tests/test_core_api_services.py::TestProfileService::test_switch_profile -[gw3] [ 20%] PASSED src/tests/test_core_api_services.py::TestProfileService::test_switch_profile -src/tests/test_core_api_services.py::TestSyncService::test_sync -[gw3] [ 20%] PASSED src/tests/test_core_api_services.py::TestSyncService::test_sync -src/tests/test_core_api_services.py::TestSyncService::test_sync_none -[gw3] [ 20%] PASSED src/tests/test_core_api_services.py::TestSyncService::test_sync_none -src/tests/test_core_api_services.py::TestSyncService::test_start_background_sync -[gw3] [ 20%] PASSED src/tests/test_core_api_services.py::TestSyncService::test_start_background_sync -src/tests/test_core_api_services.py::TestSyncService::test_start_background_vault_sync -[gw3] [ 20%] PASSED src/tests/test_core_api_services.py::TestSyncService::test_start_background_vault_sync -src/tests/test_core_api_services.py::TestEntryService::test_list_entries -[gw3] [ 20%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_list_entries -src/tests/test_core_api_services.py::TestEntryService::test_search_entries -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_search_entries -src/tests/test_core_api_services.py::TestEntryService::test_retrieve_entry -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_retrieve_entry -src/tests/test_core_api_services.py::TestEntryService::test_generate_password_default -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_generate_password_default -src/tests/test_core_api_services.py::TestEntryService::test_generate_password_custom -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_generate_password_custom -src/tests/test_core_api_services.py::TestEntryService::test_get_totp_code -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_get_totp_code -src/tests/test_core_api_services.py::TestEntryService::test_add_entry -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_entry -src/tests/test_core_api_services.py::TestEntryService::test_add_totp -[gw3] [ 21%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_totp -src/tests/test_core_services.py::test_vault_service_unlock -[gw3] [ 22%] PASSED src/tests/test_core_services.py::test_vault_service_unlock -src/tests/test_core_services.py::test_entry_service_add_entry_and_search -[gw3] [ 22%] PASSED src/tests/test_core_services.py::test_entry_service_add_entry_and_search -src/tests/test_core_services.py::test_sync_service_sync -[gw3] [ 22%] PASSED src/tests/test_core_services.py::test_sync_service_sync -src/tests/test_custom_fields_display.py::test_retrieve_entry_shows_custom_fields -[gw1] [ 22%] PASSED src/tests/test_api_new_endpoints.py::test_backup_parent_seed_endpoint[asyncio] -src/tests/test_api_new_endpoints.py::test_backup_parent_seed_path_traversal_blocked[asyncio] -[gw3] [ 22%] PASSED src/tests/test_custom_fields_display.py::test_retrieve_entry_shows_custom_fields -src/tests/test_decrypt_messages.py::test_wrong_password_message -[gw3] [ 22%] PASSED src/tests/test_decrypt_messages.py::test_wrong_password_message -src/tests/test_decrypt_messages.py::test_legacy_file_requires_migration_message -[gw3] [ 22%] PASSED src/tests/test_decrypt_messages.py::test_legacy_file_requires_migration_message -src/tests/test_decrypt_messages.py::test_corrupted_data_message -[gw3] [ 23%] PASSED src/tests/test_decrypt_messages.py::test_corrupted_data_message -src/tests/test_default_encryption_mode.py::test_default_encryption_mode -[gw3] [ 23%] PASSED src/tests/test_default_encryption_mode.py::test_default_encryption_mode -src/tests/test_delta_merge.py::test_merge_modified_ts -[gw0] [ 23%] PASSED src/tests/test_api.py::test_get_config_value[asyncio] -src/tests/test_api.py::test_list_fingerprint[asyncio] -[gw3] [ 23%] PASSED src/tests/test_delta_merge.py::test_merge_modified_ts -src/tests/test_duplicate_seed_profile_creation.py::test_duplicate_seed_profile_creation -[gw3] [ 23%] PASSED src/tests/test_duplicate_seed_profile_creation.py::test_duplicate_seed_profile_creation -src/tests/test_edit_tags_from_retrieve.py::test_edit_tags_from_retrieve -[gw3] [ 23%] PASSED src/tests/test_edit_tags_from_retrieve.py::test_edit_tags_from_retrieve -src/tests/test_encryption_checksum.py::test_encryption_checksum_workflow -[gw3] [ 24%] PASSED src/tests/test_encryption_checksum.py::test_encryption_checksum_workflow -src/tests/test_encryption_checksum.py::test_update_checksum_removes_legacy -[gw3] [ 24%] PASSED src/tests/test_encryption_checksum.py::test_update_checksum_removes_legacy -src/tests/test_encryption_files.py::test_json_save_and_load_round_trip -[gw3] [ 24%] PASSED src/tests/test_encryption_files.py::test_json_save_and_load_round_trip -src/tests/test_encryption_files.py::test_encrypt_and_decrypt_file_binary_round_trip -[gw3] [ 24%] PASSED src/tests/test_encryption_files.py::test_encrypt_and_decrypt_file_binary_round_trip -src/tests/test_encryption_files.py::test_encrypt_file_rejects_traversal -[gw3] [ 24%] PASSED src/tests/test_encryption_files.py::test_encrypt_file_rejects_traversal -src/tests/test_encryption_fuzz.py::test_encrypt_decrypt_roundtrip -[gw1] [ 24%] PASSED src/tests/test_api_new_endpoints.py::test_backup_parent_seed_path_traversal_blocked[asyncio] -src/tests/test_api_new_endpoints.py::test_relay_management_endpoints[asyncio] -[gw0] [ 24%] PASSED src/tests/test_api.py::test_list_fingerprint[asyncio] -src/tests/test_api.py::test_get_nostr_pubkey[asyncio] -[gw2] [ 25%] PASSED src/tests/test_config_manager.py::test_pin_verification_and_change -src/tests/test_config_manager.py::test_config_file_encrypted_after_save -[gw3] [ 25%] PASSED src/tests/test_encryption_fuzz.py::test_encrypt_decrypt_roundtrip -src/tests/test_encryption_fuzz.py::test_corrupted_ciphertext_fails -[gw2] [ 25%] PASSED src/tests/test_config_manager.py::test_config_file_encrypted_after_save -src/tests/test_config_manager.py::test_set_relays_persists_changes -[gw3] [ 25%] PASSED src/tests/test_encryption_fuzz.py::test_corrupted_ciphertext_fails -src/tests/test_encryption_validate_seed.py::test_validate_seed_valid_mnemonic -[gw3] [ 25%] PASSED src/tests/test_encryption_validate_seed.py::test_validate_seed_valid_mnemonic -src/tests/test_encryption_validate_seed.py::test_validate_seed_invalid_mnemonic -[gw3] [ 25%] PASSED src/tests/test_encryption_validate_seed.py::test_validate_seed_invalid_mnemonic -src/tests/test_entries_empty.py::test_list_entries_empty -[gw2] [ 26%] PASSED src/tests/test_config_manager.py::test_set_relays_persists_changes -src/tests/test_config_manager.py::test_set_relays_requires_at_least_one -[gw3] [ 26%] PASSED src/tests/test_entries_empty.py::test_list_entries_empty -src/tests/test_entry_add.py::test_add_and_retrieve_entry -[gw2] [ 26%] PASSED src/tests/test_config_manager.py::test_set_relays_requires_at_least_one -src/tests/test_config_manager.py::test_inactivity_timeout_round_trip -[gw3] [ 26%] PASSED src/tests/test_entry_add.py::test_add_and_retrieve_entry -src/tests/test_entry_add.py::test_round_trip_entry_types[add_entry-password] -[gw2] [ 26%] PASSED src/tests/test_config_manager.py::test_inactivity_timeout_round_trip -src/tests/test_config_manager.py::test_password_hash_migrates_from_file -[gw3] [ 26%] PASSED src/tests/test_entry_add.py::test_round_trip_entry_types[add_entry-password] -src/tests/test_entry_add.py::test_round_trip_entry_types[add_totp-totp] -[gw0] [ 26%] PASSED src/tests/test_api.py::test_get_nostr_pubkey[asyncio] -src/tests/test_api.py::test_create_modify_archive_entry[asyncio] -[gw3] [ 27%] PASSED src/tests/test_entry_add.py::test_round_trip_entry_types[add_totp-totp] -src/tests/test_entry_add.py::test_round_trip_entry_types[add_ssh_key-ssh] -[gw3] [ 27%] PASSED src/tests/test_entry_add.py::test_round_trip_entry_types[add_ssh_key-ssh] -src/tests/test_entry_add.py::test_round_trip_entry_types[add_seed-seed] -[gw3] [ 27%] PASSED src/tests/test_entry_add.py::test_round_trip_entry_types[add_seed-seed] -src/tests/test_entry_add.py::test_round_trip_entry_types[add_key_value-key_value] -[gw3] [ 27%] PASSED src/tests/test_entry_add.py::test_round_trip_entry_types[add_key_value-key_value] -src/tests/test_entry_add.py::test_round_trip_entry_types[add_managed_account-managed_account] -[gw2] [ 27%] PASSED src/tests/test_config_manager.py::test_password_hash_migrates_from_file -src/tests/test_config_manager.py::test_additional_backup_path_round_trip -[gw3] [ 27%] PASSED src/tests/test_entry_add.py::test_round_trip_entry_types[add_managed_account-managed_account] -src/tests/test_entry_add.py::test_legacy_entry_defaults_to_password -[gw2] [ 27%] PASSED src/tests/test_config_manager.py::test_additional_backup_path_round_trip -src/tests/test_config_manager.py::test_secret_mode_round_trip -[gw3] [ 28%] PASSED src/tests/test_entry_add.py::test_legacy_entry_defaults_to_password -src/tests/test_entry_add.py::test_add_default_archived_false[add_entry-args0] -[gw2] [ 28%] PASSED src/tests/test_config_manager.py::test_secret_mode_round_trip -src/tests/test_config_manager.py::test_kdf_iterations_round_trip -[gw3] [ 28%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_entry-args0] -src/tests/test_entry_add.py::test_add_default_archived_false[add_totp-args1] -[gw2] [ 28%] PASSED src/tests/test_config_manager.py::test_kdf_iterations_round_trip -src/tests/test_core_api_services.py::TestEntryService::test_add_ssh_key -[gw2] [ 28%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_ssh_key -src/tests/test_core_api_services.py::TestEntryService::test_add_pgp_key -[gw2] [ 28%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_pgp_key -src/tests/test_core_api_services.py::TestEntryService::test_add_nostr_key -[gw2] [ 29%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_nostr_key -src/tests/test_core_api_services.py::TestEntryService::test_add_seed -[gw2] [ 29%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_seed -src/tests/test_core_api_services.py::TestEntryService::test_add_key_value -[gw2] [ 29%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_key_value -src/tests/test_core_api_services.py::TestEntryService::test_add_managed_account -[gw2] [ 29%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_add_managed_account -src/tests/test_core_api_services.py::TestEntryService::test_modify_entry -[gw2] [ 29%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_modify_entry -[gw3] [ 29%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_totp-args1] -src/tests/test_entry_add.py::test_add_default_archived_false[add_ssh_key-args2] -src/tests/test_core_api_services.py::TestEntryService::test_archive_entry -[gw2] [ 29%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_archive_entry -src/tests/test_core_api_services.py::TestEntryService::test_restore_entry -[gw2] [ 30%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_restore_entry -src/tests/test_core_api_services.py::TestEntryService::test_export_totp_entries -[gw2] [ 30%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_export_totp_entries -src/tests/test_core_api_services.py::TestEntryService::test_display_totp_codes -[gw2] [ 30%] PASSED src/tests/test_core_api_services.py::TestEntryService::test_display_totp_codes -src/tests/test_core_api_services.py::TestConfigService::test_get -[gw2] [ 30%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_get -src/tests/test_core_api_services.py::TestConfigService::test_set_simple -[gw2] [ 30%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_set_simple -src/tests/test_core_api_services.py::TestConfigService::test_set_bool -[gw2] [ 30%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_set_bool -src/tests/test_core_api_services.py::TestConfigService::test_set_relays -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_set_relays -src/tests/test_core_api_services.py::TestConfigService::test_set_invalid_key -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_set_invalid_key -src/tests/test_core_api_services.py::TestConfigService::test_get_secret_mode_enabled -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_get_secret_mode_enabled -src/tests/test_core_api_services.py::TestConfigService::test_get_clipboard_clear_delay -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_get_clipboard_clear_delay -src/tests/test_core_api_services.py::TestConfigService::test_set_secret_mode -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_set_secret_mode -src/tests/test_core_api_services.py::TestConfigService::test_get_offline_mode -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_get_offline_mode -src/tests/test_core_api_services.py::TestConfigService::test_set_offline_mode -[gw2] [ 31%] PASSED src/tests/test_core_api_services.py::TestConfigService::test_set_offline_mode -src/tests/test_core_api_services.py::TestUtilityService::test_generate_password -[gw3] [ 32%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_ssh_key-args2] -[gw2] [ 32%] PASSED src/tests/test_core_api_services.py::TestUtilityService::test_generate_password -src/tests/test_entry_add.py::test_add_default_archived_false[add_pgp_key-args3] -src/tests/test_core_api_services.py::TestUtilityService::test_verify_checksum -[gw2] [ 32%] PASSED src/tests/test_core_api_services.py::TestUtilityService::test_verify_checksum -src/tests/test_core_api_services.py::TestUtilityService::test_update_checksum -[gw2] [ 32%] PASSED src/tests/test_core_api_services.py::TestUtilityService::test_update_checksum -src/tests/test_core_api_services.py::TestNostrService::test_get_pubkey -[gw1] [ 32%] PASSED src/tests/test_api_new_endpoints.py::test_relay_management_endpoints[asyncio] -[gw2] [ 32%] PASSED src/tests/test_core_api_services.py::TestNostrService::test_get_pubkey -src/tests/test_core_api_services.py::TestNostrService::test_list_relays -src/tests/test_api_new_endpoints.py::test_generate_password_no_special_chars[asyncio] -[gw2] [ 32%] PASSED src/tests/test_core_api_services.py::TestNostrService::test_list_relays -src/tests/test_core_api_services.py::TestNostrService::test_add_relay -[gw2] [ 33%] PASSED src/tests/test_core_api_services.py::TestNostrService::test_add_relay -src/tests/test_core_api_services.py::TestNostrService::test_remove_relay -[gw2] [ 33%] PASSED src/tests/test_core_api_services.py::TestNostrService::test_remove_relay -src/tests/test_get_entry_summaries_archived_view.py::test_get_entry_summaries_excludes_archived_and_view_handler -[gw2] [ 33%] PASSED src/tests/test_get_entry_summaries_archived_view.py::test_get_entry_summaries_excludes_archived_and_view_handler -src/tests/test_get_entry_summaries_updates.py::test_get_entry_summaries_updates_label -[gw3] [ 33%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_pgp_key-args3] -src/tests/test_entry_add.py::test_add_default_archived_false[add_nostr_key-args4] -[gw2] [ 33%] PASSED src/tests/test_get_entry_summaries_updates.py::test_get_entry_summaries_updates_label -src/tests/test_get_entry_summaries_updates.py::test_get_entry_summaries_updates_archive_restore -[gw3] [ 33%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_nostr_key-args4] -src/tests/test_entry_add.py::test_add_default_archived_false[add_seed-args5] -[gw2] [ 34%] PASSED src/tests/test_get_entry_summaries_updates.py::test_get_entry_summaries_updates_archive_restore -src/tests/test_gui_features.py::test_relay_manager_add_remove -[gw2] [ 34%] SKIPPED src/tests/test_gui_features.py::test_relay_manager_add_remove -src/tests/test_gui_features.py::test_status_bar_updates_and_lock -[gw2] [ 34%] SKIPPED src/tests/test_gui_features.py::test_status_bar_updates_and_lock -src/tests/test_gui_features.py::test_totp_viewer_refresh_on_sync -[gw2] [ 34%] SKIPPED src/tests/test_gui_features.py::test_totp_viewer_refresh_on_sync -src/tests/test_gui_headless.py::test_unlock_creates_main_window -[gw3] [ 34%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_seed-args5] -src/tests/test_entry_add.py::test_add_default_archived_false[add_key_value-args6] -[gw2] [ 34%] PASSED src/tests/test_gui_headless.py::test_unlock_creates_main_window -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[password-expect0] -[gw2] [ 34%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[password-expect0] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[totp-expect1] -[gw2] [ 35%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[totp-expect1] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[ssh-expect2] -[gw2] [ 35%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[ssh-expect2] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[seed-expect3] -[gw2] [ 35%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[seed-expect3] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[pgp-expect4] -[gw3] [ 35%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_key_value-args6] -src/tests/test_entry_add.py::test_add_default_archived_false[add_managed_account-args7] -[gw2] [ 35%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[pgp-expect4] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[nostr-expect5] -[gw2] [ 35%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[nostr-expect5] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[key_value-expect6] -[gw2] [ 36%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[key_value-expect6] -src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[managed_account-expect7] -[gw2] [ 36%] PASSED src/tests/test_gui_headless.py::test_entrydialog_add_calls_service[managed_account-expect7] -src/tests/test_gui_headless.py::test_entrydialog_edit_calls_service[password-expected0] -[gw2] [ 36%] PASSED src/tests/test_gui_headless.py::test_entrydialog_edit_calls_service[password-expected0] -src/tests/test_gui_headless.py::test_entrydialog_edit_calls_service[key_value-expected1] -[gw2] [ 36%] PASSED src/tests/test_gui_headless.py::test_entrydialog_edit_calls_service[key_value-expected1] -src/tests/test_gui_headless.py::test_entrydialog_edit_calls_service[totp-expected2] -[gw2] [ 36%] PASSED src/tests/test_gui_headless.py::test_entrydialog_edit_calls_service[totp-expected2] -src/tests/test_gui_sync.py::test_start_vault_sync_schedules_task -[gw2] [ 36%] PASSED src/tests/test_gui_sync.py::test_start_vault_sync_schedules_task -src/tests/test_gui_sync.py::test_status_updates_on_bus_events -[gw3] [ 36%] PASSED src/tests/test_entry_add.py::test_add_default_archived_false[add_managed_account-args7] -[gw2] [ 37%] PASSED src/tests/test_gui_sync.py::test_status_updates_on_bus_events -src/tests/test_handle_switch_fingerprint.py::test_handle_switch_fingerprint_active_profile -[gw2] [ 37%] PASSED src/tests/test_handle_switch_fingerprint.py::test_handle_switch_fingerprint_active_profile -src/tests/test_inactivity_lock.py::test_inactivity_triggers_lock -[gw2] [ 37%] PASSED src/tests/test_inactivity_lock.py::test_inactivity_triggers_lock -src/tests/test_inactivity_lock.py::test_input_timeout_triggers_lock -[gw2] [ 37%] PASSED src/tests/test_inactivity_lock.py::test_input_timeout_triggers_lock -src/tests/test_inactivity_lock.py::test_update_activity_checks_timeout -src/tests/test_entry_management_checksum_path.py::test_update_checksum_writes_to_expected_path -[gw2] [ 37%] PASSED src/tests/test_inactivity_lock.py::test_update_activity_checks_timeout -src/tests/test_index_cache.py::test_index_caching -[gw3] [ 37%] PASSED src/tests/test_entry_management_checksum_path.py::test_update_checksum_writes_to_expected_path -src/tests/test_entry_management_checksum_path.py::test_backup_index_file_creates_backup_in_directory -[gw2] [ 37%] PASSED src/tests/test_index_cache.py::test_index_caching -src/tests/test_index_import_export.py::test_index_export_import_round_trip -[gw1] [ 38%] PASSED src/tests/test_api_new_endpoints.py::test_generate_password_no_special_chars[asyncio] -src/tests/test_api_new_endpoints.py::test_generate_password_allowed_chars[asyncio] -[gw0] [ 38%] PASSED src/tests/test_api.py::test_create_modify_archive_entry[asyncio] -src/tests/test_api.py::test_update_config[asyncio] -[gw3] [ 38%] PASSED src/tests/test_entry_management_checksum_path.py::test_backup_index_file_creates_backup_in_directory -src/tests/test_entry_policy_override.py::test_entry_policy_override_changes_password -[gw2] [ 38%] PASSED src/tests/test_index_import_export.py::test_index_export_import_round_trip -src/tests/test_index_import_export.py::test_get_encrypted_index_missing_file -[gw2] [ 38%] PASSED src/tests/test_index_import_export.py::test_get_encrypted_index_missing_file -src/tests/test_invalid_password_message.py::test_invalid_password_shows_friendly_message_once -[gw3] [ 38%] PASSED src/tests/test_entry_policy_override.py::test_entry_policy_override_changes_password -src/tests/test_export_totp_codes.py::test_handle_export_totp_codes -[gw2] [ 39%] PASSED src/tests/test_invalid_password_message.py::test_invalid_password_shows_friendly_message_once -src/tests/test_kdf_iteration_fallback.py::test_kdf_iteration_fallback -[gw3] [ 39%] PASSED src/tests/test_export_totp_codes.py::test_handle_export_totp_codes -src/tests/test_file_lock.py::test_exclusive_lock_blocks_until_released -[gw0] [ 39%] PASSED src/tests/test_api.py::test_update_config[asyncio] -src/tests/test_api.py::test_update_config_quick_unlock[asyncio] -[gw1] [ 39%] PASSED src/tests/test_api_new_endpoints.py::test_generate_password_allowed_chars[asyncio] -src/tests/test_api_notifications.py::test_notifications_endpoint[asyncio] -[gw2] [ 39%] PASSED src/tests/test_kdf_iteration_fallback.py::test_kdf_iteration_fallback -src/tests/test_kdf_modes.py::test_setup_encryption_manager_kdf_modes -[gw3] [ 39%] PASSED src/tests/test_file_lock.py::test_exclusive_lock_blocks_until_released -src/tests/test_file_locking.py::test_concurrent_shared_and_exclusive_lock -[gw3] [ 39%] PASSED src/tests/test_file_locking.py::test_concurrent_shared_and_exclusive_lock -src/tests/test_fingerprint_encryption.py::test_generate_fingerprint_deterministic -[gw3] [ 40%] PASSED src/tests/test_fingerprint_encryption.py::test_generate_fingerprint_deterministic -src/tests/test_fingerprint_encryption.py::test_encryption_round_trip -[gw3] [ 40%] PASSED src/tests/test_fingerprint_encryption.py::test_encryption_round_trip -src/tests/test_fingerprint_manager_utils.py::test_add_and_remove_fingerprint -[gw3] [ 40%] PASSED src/tests/test_fingerprint_manager_utils.py::test_add_and_remove_fingerprint -src/tests/test_fingerprint_manager_utils.py::test_remove_nonexistent_fingerprint -[gw3] [ 40%] PASSED src/tests/test_fingerprint_manager_utils.py::test_remove_nonexistent_fingerprint -src/tests/test_full_sync_roundtrip.py::test_full_sync_roundtrip -[gw0] [ 40%] PASSED src/tests/test_api.py::test_update_config_quick_unlock[asyncio] -src/tests/test_api.py::test_change_password_route[asyncio] -[gw1] [ 40%] PASSED src/tests/test_api_notifications.py::test_notifications_endpoint[asyncio] -src/tests/test_api_notifications.py::test_notifications_endpoint_clears_queue[asyncio] -[gw2] [ 40%] PASSED src/tests/test_kdf_modes.py::test_setup_encryption_manager_kdf_modes -src/tests/test_kdf_modes.py::test_kdf_param_round_trip -[gw2] [ 41%] PASSED src/tests/test_kdf_modes.py::test_kdf_param_round_trip -src/tests/test_kdf_modes.py::test_vault_kdf_migration -[gw2] [ 41%] PASSED src/tests/test_kdf_modes.py::test_vault_kdf_migration -src/tests/test_kdf_strength_slider.py::test_kdf_strength_slider_persists -[gw3] [ 41%] PASSED src/tests/test_full_sync_roundtrip.py::test_full_sync_roundtrip -src/tests/test_full_sync_roundtrip_new.py::test_full_sync_roundtrip -[gw2] [ 41%] PASSED src/tests/test_kdf_strength_slider.py::test_kdf_strength_slider_persists -src/tests/test_key_derivation.py::test_pbkdf2_fingerprint_affects_key -[gw2] [ 41%] PASSED src/tests/test_key_derivation.py::test_pbkdf2_fingerprint_affects_key -src/tests/test_key_derivation.py::test_derive_key_empty_password_error -[gw2] [ 41%] PASSED src/tests/test_key_derivation.py::test_derive_key_empty_password_error -src/tests/test_key_derivation.py::test_seed_only_key_deterministic -[gw2] [ 42%] PASSED src/tests/test_key_derivation.py::test_seed_only_key_deterministic -src/tests/test_key_derivation.py::test_derive_index_key_seed_only -[gw2] [ 42%] PASSED src/tests/test_key_derivation.py::test_derive_index_key_seed_only -src/tests/test_key_derivation.py::test_argon2_fingerprint_affects_key -[gw2] [ 42%] PASSED src/tests/test_key_derivation.py::test_argon2_fingerprint_affects_key -src/tests/test_key_hierarchy.py::test_kd_distinct_infos -[gw2] [ 42%] PASSED src/tests/test_key_hierarchy.py::test_kd_distinct_infos -src/tests/test_key_hierarchy.py::test_derive_index_key_matches_hierarchy -[gw2] [ 42%] PASSED src/tests/test_key_hierarchy.py::test_derive_index_key_matches_hierarchy -src/tests/test_key_manager_helpers.py::test_key_manager_getters -[gw2] [ 42%] PASSED src/tests/test_key_manager_helpers.py::test_key_manager_getters -src/tests/test_key_validation_failures.py::test_add_totp_invalid_secret -[gw2] [ 42%] PASSED src/tests/test_key_validation_failures.py::test_add_totp_invalid_secret -src/tests/test_key_validation_failures.py::test_add_ssh_key_validation_failure -[gw3] [ 43%] PASSED src/tests/test_full_sync_roundtrip_new.py::test_full_sync_roundtrip -src/tests/test_fuzz_key_derivation.py::test_fuzz_key_round_trip -[gw2] [ 43%] PASSED src/tests/test_key_validation_failures.py::test_add_ssh_key_validation_failure -src/tests/test_key_validation_failures.py::test_add_pgp_key_validation_failure -[gw2] [ 43%] PASSED src/tests/test_key_validation_failures.py::test_add_pgp_key_validation_failure -src/tests/test_key_validation_failures.py::test_add_nostr_key_validation_failure -[gw0] [ 43%] PASSED src/tests/test_api.py::test_change_password_route[asyncio] -src/tests/test_api.py::test_update_config_unknown_key[asyncio] -[gw2] [ 43%] PASSED src/tests/test_key_validation_failures.py::test_add_nostr_key_validation_failure -src/tests/test_list_entries_sort_filter.py::test_sort_by_updated -[gw2] [ 43%] PASSED src/tests/test_list_entries_sort_filter.py::test_sort_by_updated -src/tests/test_list_entries_sort_filter.py::test_filter_by_type -[gw2] [ 44%] PASSED src/tests/test_list_entries_sort_filter.py::test_filter_by_type -src/tests/test_load_global_config.py::test_load_global_config_invalid_toml -[gw2] [ 44%] PASSED src/tests/test_load_global_config.py::test_load_global_config_invalid_toml -src/tests/test_managed_account.py::test_add_managed_account_fields_and_dir -[gw2] [ 44%] PASSED src/tests/test_managed_account.py::test_add_managed_account_fields_and_dir -src/tests/test_managed_account.py::test_load_and_exit_managed_account -[gw1] [ 44%] PASSED src/tests/test_api_notifications.py::test_notifications_endpoint_clears_queue[asyncio] -src/tests/test_api_notifications.py::test_notifications_endpoint_does_not_clear_current[asyncio] -[gw2] [ 44%] PASSED src/tests/test_managed_account.py::test_load_and_exit_managed_account -src/tests/test_managed_account_entry.py::test_add_and_get_managed_account_seed -[gw2] [ 44%] PASSED src/tests/test_managed_account_entry.py::test_add_and_get_managed_account_seed -src/tests/test_managed_account_entry.py::test_load_and_exit_managed_account -[gw0] [ 44%] PASSED src/tests/test_api.py::test_update_config_unknown_key[asyncio] -src/tests/test_api.py::test_shutdown[asyncio] -[gw2] [ 45%] PASSED src/tests/test_managed_account_entry.py::test_load_and_exit_managed_account -src/tests/test_manager_add_password.py::test_handle_add_password -[gw2] [ 45%] PASSED src/tests/test_manager_add_password.py::test_handle_add_password -src/tests/test_manager_add_password.py::test_handle_add_password_secret_mode -[gw2] [ 45%] PASSED src/tests/test_manager_add_password.py::test_handle_add_password_secret_mode -src/tests/test_manager_add_password.py::test_handle_add_password_quick_mode -[gw2] [ 45%] PASSED src/tests/test_manager_add_password.py::test_handle_add_password_quick_mode -src/tests/test_manager_add_totp.py::test_handle_add_totp -[gw1] [ 45%] PASSED src/tests/test_api_notifications.py::test_notifications_endpoint_does_not_clear_current[asyncio] -src/tests/test_api_profile_stats.py::test_profile_stats_endpoint[asyncio] -[gw2] [ 45%] PASSED src/tests/test_manager_add_totp.py::test_handle_add_totp -src/tests/test_manager_checksum_backup.py::test_handle_verify_checksum_success -[gw2] [ 45%] PASSED src/tests/test_manager_checksum_backup.py::test_handle_verify_checksum_success -src/tests/test_manager_checksum_backup.py::test_handle_verify_checksum_failure -[gw2] [ 46%] PASSED src/tests/test_manager_checksum_backup.py::test_handle_verify_checksum_failure -src/tests/test_manager_checksum_backup.py::test_handle_verify_checksum_missing -[gw2] [ 46%] PASSED src/tests/test_manager_checksum_backup.py::test_handle_verify_checksum_missing -src/tests/test_manager_checksum_backup.py::test_backup_and_restore_database -[gw2] [ 46%] PASSED src/tests/test_manager_checksum_backup.py::test_backup_and_restore_database -src/tests/test_manager_current_notification.py::test_notify_sets_current -[gw2] [ 46%] PASSED src/tests/test_manager_current_notification.py::test_notify_sets_current -src/tests/test_manager_current_notification.py::test_get_current_notification_ttl -[gw2] [ 46%] PASSED src/tests/test_manager_current_notification.py::test_get_current_notification_ttl -src/tests/test_manager_display_totp_codes.py::test_handle_display_totp_codes -[gw2] [ 46%] PASSED src/tests/test_manager_display_totp_codes.py::test_handle_display_totp_codes -src/tests/test_manager_display_totp_codes.py::test_display_totp_codes_excludes_archived -[gw2] [ 47%] PASSED src/tests/test_manager_display_totp_codes.py::test_display_totp_codes_excludes_archived -src/tests/test_manager_edit_totp.py::test_edit_totp_period_from_retrieve -[gw0] [ 47%] PASSED src/tests/test_api.py::test_shutdown[asyncio] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/entry/1] -[gw2] [ 47%] PASSED src/tests/test_manager_edit_totp.py::test_edit_totp_period_from_retrieve -src/tests/test_manager_import_database.py::test_import_non_backup_file -[gw2] [ 47%] PASSED src/tests/test_manager_import_database.py::test_import_non_backup_file -src/tests/test_manager_import_database.py::test_import_missing_file -[gw2] [ 47%] PASSED src/tests/test_manager_import_database.py::test_import_missing_file -src/tests/test_manager_list_entries.py::test_handle_list_entries -[gw2] [ 47%] PASSED src/tests/test_manager_list_entries.py::test_handle_list_entries -src/tests/test_manager_list_entries.py::test_list_entries_show_details -[gw2] [ 47%] PASSED src/tests/test_manager_list_entries.py::test_list_entries_show_details -src/tests/test_manager_list_entries.py::test_show_entry_details_by_index -[gw1] [ 48%] PASSED src/tests/test_api_profile_stats.py::test_profile_stats_endpoint[asyncio] -src/tests/test_api_rate_limit.py::test_rate_limit_exceeded[asyncio] -[gw2] [ 48%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_by_index -src/tests/test_manager_list_entries.py::test_show_seed_entry_details -[gw2] [ 48%] PASSED src/tests/test_manager_list_entries.py::test_show_seed_entry_details -src/tests/test_manager_list_entries.py::test_show_ssh_entry_details -[gw2] [ 48%] PASSED src/tests/test_manager_list_entries.py::test_show_ssh_entry_details -src/tests/test_manager_list_entries.py::test_show_pgp_entry_details -[gw0] [ 48%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/entry/1] -[gw2] [ 48%] PASSED src/tests/test_manager_list_entries.py::test_show_pgp_entry_details -src/tests/test_manager_list_entries.py::test_show_nostr_entry_details -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/config/k] -[gw2] [ 49%] PASSED src/tests/test_manager_list_entries.py::test_show_nostr_entry_details -src/tests/test_manager_list_entries.py::test_show_managed_account_entry_details -[gw2] [ 49%] PASSED src/tests/test_manager_list_entries.py::test_show_managed_account_entry_details -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[password] -[gw2] [ 49%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[password] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[seed] -[gw2] [ 49%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[seed] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[ssh] -[gw2] [ 49%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[ssh] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[pgp] -[gw2] [ 49%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[pgp] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[nostr] -[gw0] [ 49%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/config/k] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/fingerprint] -[gw1] [ 50%] PASSED src/tests/test_api_rate_limit.py::test_rate_limit_exceeded[asyncio] -src/tests/test_api_reload_relays.py::test_reload_relays_logs_errors -[gw1] [ 50%] PASSED src/tests/test_api_reload_relays.py::test_reload_relays_logs_errors -src/tests/test_archive_from_retrieve.py::test_archive_entry_from_retrieve -[gw2] [ 50%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[nostr] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[totp] -[gw1] [ 50%] PASSED src/tests/test_archive_from_retrieve.py::test_archive_entry_from_retrieve -src/tests/test_archive_from_retrieve.py::test_restore_entry_from_retrieve -[gw2] [ 50%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[totp] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[key_value] -[gw1] [ 50%] PASSED src/tests/test_archive_from_retrieve.py::test_restore_entry_from_retrieve -src/tests/test_archive_nonpassword.py::test_archive_nonpassword_list_search -[gw2] [ 50%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[key_value] -src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[managed_account] -[gw1] [ 51%] PASSED src/tests/test_archive_nonpassword.py::test_archive_nonpassword_list_search -src/tests/test_archive_restore.py::test_archive_restore_affects_listing_and_search -[gw2] [ 51%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_sensitive[managed_account] -src/tests/test_manager_list_entries.py::test_show_entry_details_with_enum_type[password] -[gw1] [ 51%] PASSED src/tests/test_archive_restore.py::test_archive_restore_affects_listing_and_search -src/tests/test_archive_restore.py::test_view_archived_entries_cli -[gw2] [ 51%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_with_enum_type[password] -src/tests/test_manager_list_entries.py::test_show_entry_details_with_enum_type[totp] -[gw1] [ 51%] PASSED src/tests/test_archive_restore.py::test_view_archived_entries_cli -src/tests/test_archive_restore.py::test_view_archived_entries_view_only -[gw2] [ 51%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_with_enum_type[totp] -src/tests/test_manager_list_entries.py::test_show_entry_details_with_enum_type[key_value] -[gw1] [ 52%] PASSED src/tests/test_archive_restore.py::test_view_archived_entries_view_only -src/tests/test_archive_restore.py::test_view_archived_entries_removed_after_restore -[gw2] [ 52%] PASSED src/tests/test_manager_list_entries.py::test_show_entry_details_with_enum_type[key_value] -src/tests/test_manager_retrieve_totp.py::test_handle_retrieve_totp_entry -[gw0] [ 52%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/fingerprint] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/nostr/pubkey] -[gw1] [ 52%] PASSED src/tests/test_archive_restore.py::test_view_archived_entries_removed_after_restore -src/tests/test_archive_restore.py::test_archived_entries_menu_hides_active -[gw2] [ 52%] PASSED src/tests/test_manager_retrieve_totp.py::test_handle_retrieve_totp_entry -src/tests/test_manager_search_display.py::test_search_entries_prompt_for_details -[gw1] [ 52%] PASSED src/tests/test_archive_restore.py::test_archived_entries_menu_hides_active -src/tests/test_atomic_write.py::test_atomic_write_concurrent -[gw2] [ 52%] PASSED src/tests/test_manager_search_display.py::test_search_entries_prompt_for_details -src/tests/test_manager_seed_setup.py::test_validate_bip85_seed_invalid_word -[gw2] [ 53%] PASSED src/tests/test_manager_seed_setup.py::test_validate_bip85_seed_invalid_word -src/tests/test_manager_seed_setup.py::test_validate_bip85_seed_checksum_failure -[gw2] [ 53%] PASSED src/tests/test_manager_seed_setup.py::test_validate_bip85_seed_checksum_failure -src/tests/test_manager_seed_setup.py::test_setup_existing_seed_words -[gw2] [ 53%] PASSED src/tests/test_manager_seed_setup.py::test_setup_existing_seed_words -src/tests/test_manager_seed_setup.py::test_setup_existing_seed_paste -[gw2] [ 53%] PASSED src/tests/test_manager_seed_setup.py::test_setup_existing_seed_paste -src/tests/test_manager_seed_setup.py::test_setup_existing_seed_with_args -[gw2] [ 53%] PASSED src/tests/test_manager_seed_setup.py::test_setup_existing_seed_with_args -src/tests/test_manager_warning_notifications.py::test_handle_search_entries_no_query -[gw2] [ 53%] PASSED src/tests/test_manager_warning_notifications.py::test_handle_search_entries_no_query -src/tests/test_manager_workflow.py::test_manager_workflow -[gw1] [ 54%] PASSED src/tests/test_atomic_write.py::test_atomic_write_concurrent -src/tests/test_audit_logger.py::test_audit_logger_records_events -[gw1] [ 54%] PASSED src/tests/test_audit_logger.py::test_audit_logger_records_events -src/tests/test_audit_logger.py::test_audit_log_tamper_evident -[gw1] [ 54%] PASSED src/tests/test_audit_logger.py::test_audit_log_tamper_evident -src/tests/test_auto_sync.py::test_auto_sync_triggers_post -[gw1] [ 54%] PASSED src/tests/test_auto_sync.py::test_auto_sync_triggers_post -src/tests/test_background_error_reporting.py::test_start_background_sync_error -[gw1] [ 54%] PASSED src/tests/test_background_error_reporting.py::test_start_background_sync_error -src/tests/test_background_error_reporting.py::test_start_background_relay_check_error -[gw1] [ 54%] PASSED src/tests/test_background_error_reporting.py::test_start_background_relay_check_error -src/tests/test_background_relay_check.py::test_background_relay_check_runs_async -[gw2] [ 54%] PASSED src/tests/test_manager_workflow.py::test_manager_workflow -src/tests/test_manifest_id_privacy.py::test_published_events_no_fingerprint -[gw2] [ 55%] PASSED src/tests/test_manifest_id_privacy.py::test_published_events_no_fingerprint -src/tests/test_manifest_state_restore.py::test_manifest_state_restored -[gw1] [ 55%] PASSED src/tests/test_background_relay_check.py::test_background_relay_check_runs_async -src/tests/test_background_relay_check.py::test_background_relay_check_warns_when_unhealthy -[gw1] [ 55%] PASSED src/tests/test_background_relay_check.py::test_background_relay_check_warns_when_unhealthy -src/tests/test_background_sync_always.py::test_switch_fingerprint_triggers_bg_sync -[gw1] [ 55%] PASSED src/tests/test_background_sync_always.py::test_switch_fingerprint_triggers_bg_sync -src/tests/test_background_sync_always.py::test_exit_managed_account_triggers_bg_sync -[gw1] [ 55%] PASSED src/tests/test_background_sync_always.py::test_exit_managed_account_triggers_bg_sync -src/tests/test_backup_interval.py::test_backup_interval -[gw2] [ 55%] PASSED src/tests/test_manifest_state_restore.py::test_manifest_state_restored -src/tests/test_memory_protection.py::test_inmemory_secret_round_trip_bytes_and_str -[gw2] [ 55%] PASSED src/tests/test_memory_protection.py::test_inmemory_secret_round_trip_bytes_and_str -src/tests/test_memory_protection.py::test_inmemory_secret_invalid_type -[gw2] [ 56%] PASSED src/tests/test_memory_protection.py::test_inmemory_secret_invalid_type -src/tests/test_memory_protection.py::test_inmemory_secret_wipe_clears_attributes -[gw2] [ 56%] PASSED src/tests/test_memory_protection.py::test_inmemory_secret_wipe_clears_attributes -src/tests/test_menu_navigation.py::test_navigate_all_main_menu_options -[gw2] [ 56%] PASSED src/tests/test_menu_navigation.py::test_navigate_all_main_menu_options -src/tests/test_menu_notifications.py::test_display_menu_prints_notifications -[gw2] [ 56%] PASSED src/tests/test_menu_notifications.py::test_display_menu_prints_notifications -src/tests/test_menu_notifications.py::test_display_menu_reuses_notification_line -[gw2] [ 56%] PASSED src/tests/test_menu_notifications.py::test_display_menu_reuses_notification_line -src/tests/test_menu_options.py::test_menu_totp_option -[gw2] [ 56%] PASSED src/tests/test_menu_options.py::test_menu_totp_option -src/tests/test_menu_options.py::test_menu_settings_option -[gw2] [ 57%] PASSED src/tests/test_menu_options.py::test_menu_settings_option -src/tests/test_menu_search.py::test_menu_search_option -[gw2] [ 57%] PASSED src/tests/test_menu_search.py::test_menu_search_option -src/tests/test_migrations.py::test_migrate_v0_to_v4 -[gw1] [ 57%] PASSED src/tests/test_backup_interval.py::test_backup_interval -src/tests/test_backup_restore.py::test_backup_restore_workflow -[gw0] [ 57%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-get-/api/v1/nostr/pubkey] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/shutdown] -[gw2] [ 57%] PASSED src/tests/test_migrations.py::test_migrate_v0_to_v4 -src/tests/test_migrations.py::test_migrate_v1_to_v4 -[gw3] [ 57%] PASSED src/tests/test_fuzz_key_derivation.py::test_fuzz_key_round_trip -src/tests/test_generate_test_profile.py::test_initialize_profile_creates_directories -[gw1] [ 57%] PASSED src/tests/test_backup_restore.py::test_backup_restore_workflow -src/tests/test_backup_restore.py::test_additional_backup_location -[gw2] [ 58%] PASSED src/tests/test_migrations.py::test_migrate_v1_to_v4 -src/tests/test_migrations.py::test_migrate_v2_to_v4 -[gw1] [ 58%] PASSED src/tests/test_backup_restore.py::test_additional_backup_location -src/tests/test_backup_restore_startup.py::test_cli_flag_restores_before_init -[gw1] [ 58%] PASSED src/tests/test_backup_restore_startup.py::test_cli_flag_restores_before_init -src/tests/test_backup_restore_startup.py::test_menu_option_restores_before_init -[gw1] [ 58%] PASSED src/tests/test_backup_restore_startup.py::test_menu_option_restores_before_init -src/tests/test_bip85_derivation_path.py::test_derivation_paths_for_entropy_lengths -[gw1] [ 58%] PASSED src/tests/test_bip85_derivation_path.py::test_derivation_paths_for_entropy_lengths -src/tests/test_bip85_derivation_path.py::test_default_word_count_from_entropy_bytes -[gw1] [ 58%] PASSED src/tests/test_bip85_derivation_path.py::test_default_word_count_from_entropy_bytes -src/tests/test_bip85_init.py::test_init_with_seed_bytes -[gw1] [ 59%] PASSED src/tests/test_bip85_init.py::test_init_with_seed_bytes -src/tests/test_bip85_init.py::test_init_with_xprv -[gw1] [ 59%] PASSED src/tests/test_bip85_init.py::test_init_with_xprv -src/tests/test_modify_ssh_managed_entries.py::test_modify_ssh_entry -[gw2] [ 59%] PASSED src/tests/test_migrations.py::test_migrate_v2_to_v4 -src/tests/test_migrations.py::test_error_on_future_version -[gw2] [ 59%] PASSED src/tests/test_migrations.py::test_error_on_future_version -src/tests/test_migrations.py::test_schema_migration_persisted_once -[gw1] [ 59%] PASSED src/tests/test_modify_ssh_managed_entries.py::test_modify_ssh_entry -src/tests/test_modify_ssh_managed_entries.py::test_modify_managed_account_entry -[gw2] [ 59%] PASSED src/tests/test_migrations.py::test_schema_migration_persisted_once -src/tests/test_nostr_client.py::test_check_relay_health_runs_async -[gw2] [ 59%] PASSED src/tests/test_nostr_client.py::test_check_relay_health_runs_async -src/tests/test_nostr_client.py::test_ping_relay_accepts_eose -[gw2] [ 60%] PASSED src/tests/test_nostr_client.py::test_ping_relay_accepts_eose -src/tests/test_nostr_client.py::test_update_relays_reinitializes_pool -[gw2] [ 60%] PASSED src/tests/test_nostr_client.py::test_update_relays_reinitializes_pool -src/tests/test_nostr_client.py::test_retrieve_json_sync_backoff -[gw3] [ 60%] PASSED src/tests/test_generate_test_profile.py::test_initialize_profile_creates_directories -[gw2] [ 60%] PASSED src/tests/test_nostr_client.py::test_retrieve_json_sync_backoff -src/tests/test_nostr_client.py::test_client_methods_run_in_event_loop -src/tests/test_generate_test_profile_sync.py::test_generate_test_profile_sync -[gw2] [ 60%] PASSED src/tests/test_nostr_client.py::test_client_methods_run_in_event_loop -src/tests/test_nostr_contract.py::test_publish_and_retrieve -[gw2] [ 60%] PASSED src/tests/test_nostr_contract.py::test_publish_and_retrieve -src/tests/test_nostr_dummy_client.py::test_manifest_generation -[gw1] [ 60%] PASSED src/tests/test_modify_ssh_managed_entries.py::test_modify_managed_account_entry -src/tests/test_modify_totp_entry.py::test_modify_totp_entry_period_digits_and_archive -[gw2] [ 61%] PASSED src/tests/test_nostr_dummy_client.py::test_manifest_generation -src/tests/test_nostr_dummy_client.py::test_retrieve_multi_chunk_snapshot -[gw2] [ 61%] PASSED src/tests/test_nostr_dummy_client.py::test_retrieve_multi_chunk_snapshot -src/tests/test_nostr_dummy_client.py::test_publish_and_fetch_deltas -[gw2] [ 61%] PASSED src/tests/test_nostr_dummy_client.py::test_publish_and_fetch_deltas -src/tests/test_nostr_dummy_client.py::test_fetch_snapshot_fallback_on_missing_chunk -[gw2] [ 61%] PASSED src/tests/test_nostr_dummy_client.py::test_fetch_snapshot_fallback_on_missing_chunk -src/tests/test_nostr_dummy_client.py::test_fetch_snapshot_uses_event_ids -[gw2] [ 61%] PASSED src/tests/test_nostr_dummy_client.py::test_fetch_snapshot_uses_event_ids -src/tests/test_nostr_dummy_client.py::test_publish_delta_aborts_if_outdated -[gw2] [ 61%] PASSED src/tests/test_nostr_dummy_client.py::test_publish_delta_aborts_if_outdated -src/tests/test_nostr_entry.py::test_nostr_key_determinism -[gw0] [ 62%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/shutdown] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/entry] -[gw1] [ 62%] PASSED src/tests/test_modify_totp_entry.py::test_modify_totp_entry_period_digits_and_archive -src/tests/test_modify_totp_entry.py::test_modify_totp_entry_invalid_field -[gw1] [ 62%] PASSED src/tests/test_modify_totp_entry.py::test_modify_totp_entry_invalid_field -src/tests/test_multiple_deltas_sync.py::test_sync_applies_multiple_deltas -[gw2] [ 62%] PASSED src/tests/test_nostr_entry.py::test_nostr_key_determinism -src/tests/test_nostr_index_size.py::test_nostr_index_size_limits -[gw2] [ 62%] SKIPPED src/tests/test_nostr_index_size.py::test_nostr_index_size_limits -src/tests/test_nostr_legacy_decrypt_fallback.py::test_legacy_password_only_fallback -[gw2] [ 62%] PASSED src/tests/test_nostr_legacy_decrypt_fallback.py::test_legacy_password_only_fallback -src/tests/test_nostr_legacy_key_fallback.py::test_fetch_snapshot_legacy_key_fallback -[gw2] [ 62%] PASSED src/tests/test_nostr_legacy_key_fallback.py::test_fetch_snapshot_legacy_key_fallback -src/tests/test_nostr_qr.py::test_show_qr_for_nostr_keys -[gw3] [ 63%] PASSED src/tests/test_generate_test_profile_sync.py::test_generate_test_profile_sync -src/tests/test_key_validation_failures.py::test_add_seed_validation_failure -[gw2] [ 63%] PASSED src/tests/test_nostr_qr.py::test_show_qr_for_nostr_keys -src/tests/test_nostr_qr.py::test_show_private_key_qr -[gw1] [ 63%] PASSED src/tests/test_multiple_deltas_sync.py::test_sync_applies_multiple_deltas -src/tests/test_multiple_deltas_sync.py::test_initial_sync_applies_multiple_deltas -[gw3] [ 63%] PASSED src/tests/test_key_validation_failures.py::test_add_seed_validation_failure -src/tests/test_key_validation_failures.py::test_add_managed_account_validation_failure -[gw2] [ 63%] PASSED src/tests/test_nostr_qr.py::test_show_private_key_qr -src/tests/test_nostr_qr.py::test_qr_menu_case_insensitive -[gw3] [ 63%] PASSED src/tests/test_key_validation_failures.py::test_add_managed_account_validation_failure -src/tests/test_key_value_entry.py::test_add_and_modify_key_value -[gw2] [ 63%] PASSED src/tests/test_nostr_qr.py::test_qr_menu_case_insensitive -src/tests/test_nostr_real.py::test_nostr_publish_and_retrieve -[gw2] [ 64%] SKIPPED src/tests/test_nostr_real.py::test_nostr_publish_and_retrieve -src/tests/test_nostr_restore_flow.py::test_restore_flow_from_snapshot -[gw1] [ 64%] PASSED src/tests/test_multiple_deltas_sync.py::test_initial_sync_applies_multiple_deltas -[gw3] [ 64%] PASSED src/tests/test_key_value_entry.py::test_add_and_modify_key_value -src/tests/test_multiple_fingerprint_prompt.py::test_prompt_when_multiple_fingerprints -src/tests/test_last_used_fingerprint.py::test_last_used_fingerprint -[gw1] [ 64%] PASSED src/tests/test_multiple_fingerprint_prompt.py::test_prompt_when_multiple_fingerprints -src/tests/test_new_seed_profile_creation.py::test_generate_new_seed_creates_profile -[gw3] [ 64%] PASSED src/tests/test_last_used_fingerprint.py::test_last_used_fingerprint -src/tests/test_legacy_format_exception.py::test_decrypt_data_raises_legacy_exception -[gw3] [ 64%] PASSED src/tests/test_legacy_format_exception.py::test_decrypt_data_raises_legacy_exception -src/tests/test_legacy_format_exception.py::test_vault_handles_legacy_exception -[gw1] [ 65%] PASSED src/tests/test_new_seed_profile_creation.py::test_generate_new_seed_creates_profile -src/tests/test_nonce_uniqueness.py::test_nonce_uniqueness -[gw1] [ 65%] PASSED src/tests/test_nonce_uniqueness.py::test_nonce_uniqueness -src/tests/test_noninteractive_init_unlock.py::test_init_with_password -[gw0] [ 65%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/entry] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-put-/api/v1/entry/1] -[gw2] [ 65%] PASSED src/tests/test_nostr_restore_flow.py::test_restore_flow_from_snapshot -src/tests/test_nostr_sdk_workflow.py::test_nostr_sdk_send_receive -[gw3] [ 65%] PASSED src/tests/test_legacy_format_exception.py::test_vault_handles_legacy_exception -src/tests/test_legacy_migration.py::test_legacy_index_migrates -[gw3] [ 65%] PASSED src/tests/test_legacy_migration.py::test_legacy_index_migrates -src/tests/test_legacy_migration.py::test_failed_migration_restores_legacy -[gw1] [ 65%] PASSED src/tests/test_noninteractive_init_unlock.py::test_init_with_password -src/tests/test_noninteractive_init_unlock.py::test_unlock_with_password -[gw3] [ 66%] PASSED src/tests/test_legacy_migration.py::test_failed_migration_restores_legacy -src/tests/test_legacy_migration.py::test_migrated_index_has_v3_prefix -[gw3] [ 66%] PASSED src/tests/test_legacy_migration.py::test_migrated_index_has_v3_prefix -src/tests/test_legacy_migration.py::test_legacy_index_migration_removes_strays -[gw2] [ 66%] PASSED src/tests/test_nostr_sdk_workflow.py::test_nostr_sdk_send_receive -src/tests/test_nostr_snapshot.py::test_prepare_snapshot_roundtrip -[gw2] [ 66%] PASSED src/tests/test_nostr_snapshot.py::test_prepare_snapshot_roundtrip -src/tests/test_nostr_snapshot.py::test_fetch_latest_snapshot -[gw2] [ 66%] PASSED src/tests/test_nostr_snapshot.py::test_fetch_latest_snapshot -src/tests/test_offline_mode_behavior.py::test_sync_vault_skips_network -[gw2] [ 66%] PASSED src/tests/test_offline_mode_behavior.py::test_sync_vault_skips_network -src/tests/test_offline_mode_behavior.py::test_start_background_sync_offline -[gw3] [ 67%] PASSED src/tests/test_legacy_migration.py::test_legacy_index_migration_removes_strays -src/tests/test_legacy_migration.py::test_migration_syncs_when_confirmed -[gw2] [ 67%] PASSED src/tests/test_offline_mode_behavior.py::test_start_background_sync_offline -src/tests/test_offline_mode_default_enabled.py::test_offline_mode_default_enabled -[gw0] [ 67%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-put-/api/v1/entry/1] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-put-/api/v1/config/inactivity_timeout] -[gw2] [ 67%] PASSED src/tests/test_offline_mode_default_enabled.py::test_offline_mode_default_enabled -src/tests/test_offline_mode_profile_creation.py::test_toggle_offline_mode_after_profile_creation -[gw3] [ 67%] PASSED src/tests/test_legacy_migration.py::test_migration_syncs_when_confirmed -src/tests/test_legacy_migration.py::test_migration_declines_sync -[gw1] [ 67%] PASSED src/tests/test_noninteractive_init_unlock.py::test_unlock_with_password -src/tests/test_nostr_backup.py::test_backup_and_publish_to_nostr -[gw2] [ 67%] PASSED src/tests/test_offline_mode_profile_creation.py::test_toggle_offline_mode_after_profile_creation -src/tests/test_parent_seed_backup.py::test_handle_backup_reveal_parent_seed_confirm -[gw2] [ 68%] PASSED src/tests/test_parent_seed_backup.py::test_handle_backup_reveal_parent_seed_confirm -src/tests/test_parent_seed_backup.py::test_handle_backup_reveal_parent_seed_cancel -[gw2] [ 68%] PASSED src/tests/test_parent_seed_backup.py::test_handle_backup_reveal_parent_seed_cancel -src/tests/test_parent_seed_backup.py::test_is_valid_filename -[gw2] [ 68%] PASSED src/tests/test_parent_seed_backup.py::test_is_valid_filename -src/tests/test_password_change.py::test_change_password_triggers_nostr_backup -[gw3] [ 68%] PASSED src/tests/test_legacy_migration.py::test_migration_declines_sync -src/tests/test_legacy_migration.py::test_legacy_nostr_payload_syncs_when_confirmed -[gw1] [ 68%] PASSED src/tests/test_nostr_backup.py::test_backup_and_publish_to_nostr -src/tests/test_nostr_client.py::test_nostr_client_uses_custom_relays -[gw1] [ 68%] PASSED src/tests/test_nostr_client.py::test_nostr_client_uses_custom_relays -src/tests/test_nostr_client.py::test_initialize_client_pool_add_relays_used -[gw1] [ 68%] PASSED src/tests/test_nostr_client.py::test_initialize_client_pool_add_relays_used -src/tests/test_nostr_client.py::test_initialize_client_pool_add_relay_fallback -[gw1] [ 69%] PASSED src/tests/test_nostr_client.py::test_initialize_client_pool_add_relay_fallback -src/tests/test_password_prompt.py::test_prompt_new_password_retry -[gw3] [ 69%] PASSED src/tests/test_legacy_migration.py::test_legacy_nostr_payload_syncs_when_confirmed -src/tests/test_legacy_migration.py::test_legacy_index_reinit_syncs_once_when_confirmed -[gw2] [ 69%] PASSED src/tests/test_password_change.py::test_change_password_triggers_nostr_backup -src/tests/test_password_generation_policy.py::test_zero_policy_preserves_length -[gw2] [ 69%] PASSED src/tests/test_password_generation_policy.py::test_zero_policy_preserves_length -src/tests/test_password_generation_policy.py::test_custom_policy_applied -[gw2] [ 69%] PASSED src/tests/test_password_generation_policy.py::test_custom_policy_applied -src/tests/test_password_generation_policy.py::test_generate_password_respects_policy -[gw3] [ 69%] PASSED src/tests/test_legacy_migration.py::test_legacy_index_reinit_syncs_once_when_confirmed -src/tests/test_legacy_migration.py::test_schema_migration_no_sync_prompt -[gw2] [ 70%] PASSED src/tests/test_password_generation_policy.py::test_generate_password_respects_policy -src/tests/test_password_helpers.py::test_derive_password_entropy_length -[gw3] [ 70%] PASSED src/tests/test_legacy_migration.py::test_schema_migration_no_sync_prompt -src/tests/test_legacy_migration.py::test_declined_migration_no_sync_prompt -[gw2] [ 70%] PASSED src/tests/test_password_helpers.py::test_derive_password_entropy_length -src/tests/test_password_helpers.py::test_map_entropy_to_chars_only_uses_alphabet -[gw2] [ 70%] PASSED src/tests/test_password_helpers.py::test_map_entropy_to_chars_only_uses_alphabet -src/tests/test_password_helpers.py::test_enforce_complexity_minimum_counts -[gw3] [ 70%] PASSED src/tests/test_legacy_migration.py::test_declined_migration_no_sync_prompt -[gw2] [ 70%] PASSED src/tests/test_password_helpers.py::test_enforce_complexity_minimum_counts -src/tests/test_legacy_migration.py::test_failed_migration_no_sync_prompt -src/tests/test_password_helpers.py::test_shuffle_deterministically_repeatable -[gw2] [ 70%] PASSED src/tests/test_password_helpers.py::test_shuffle_deterministically_repeatable -src/tests/test_password_length_constraints.py::test_generate_password_too_short_raises -[gw2] [ 71%] PASSED src/tests/test_password_length_constraints.py::test_generate_password_too_short_raises -src/tests/test_password_notes_display.py::test_password_notes_shown -[gw0] [ 71%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-put-/api/v1/config/inactivity_timeout] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/entry/1/archive] -[gw3] [ 71%] PASSED src/tests/test_legacy_migration.py::test_failed_migration_no_sync_prompt -src/tests/test_legacy_migration_iterations.py::test_migrate_iterations[50000] -[gw2] [ 71%] PASSED src/tests/test_password_notes_display.py::test_password_notes_shown -src/tests/test_password_prompt.py::test_prompt_new_password -[gw2] [ 71%] PASSED src/tests/test_password_prompt.py::test_prompt_new_password -src/tests/test_password_special_modes.py::test_enforce_complexity_min_special_zero -[gw2] [ 71%] PASSED src/tests/test_password_special_modes.py::test_enforce_complexity_min_special_zero -src/tests/test_password_unlock_after_change.py::test_password_change_and_unlock -[gw3] [ 72%] PASSED src/tests/test_legacy_migration_iterations.py::test_migrate_iterations[50000] -src/tests/test_legacy_migration_iterations.py::test_migrate_iterations[100000] -[gw3] [ 72%] PASSED src/tests/test_legacy_migration_iterations.py::test_migrate_iterations[100000] -src/tests/test_legacy_migration_prompt.py::test_open_legacy_without_migrating -[gw3] [ 72%] PASSED src/tests/test_legacy_migration_prompt.py::test_open_legacy_without_migrating -src/tests/test_legacy_migration_prompt.py::test_migrate_legacy_sets_flag -[gw3] [ 72%] PASSED src/tests/test_legacy_migration_prompt.py::test_migrate_legacy_sets_flag -src/tests/test_legacy_migration_second_session.py::test_legacy_migration_second_session -[gw1] [ 72%] PASSED src/tests/test_password_prompt.py::test_prompt_new_password_retry -src/tests/test_password_prompt.py::test_prompt_existing_password -[gw1] [ 72%] PASSED src/tests/test_password_prompt.py::test_prompt_existing_password -src/tests/test_password_prompt.py::test_confirm_action_yes_no -[gw1] [ 72%] PASSED src/tests/test_password_prompt.py::test_confirm_action_yes_no -src/tests/test_password_properties.py::test_password_properties -[gw3] [ 73%] PASSED src/tests/test_legacy_migration_second_session.py::test_legacy_migration_second_session -src/tests/test_list_entries_all_types.py::test_cli_list_all_types -[gw0] [ 73%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/entry/1/archive] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/entry/1/unarchive] -[gw3] [ 73%] PASSED src/tests/test_list_entries_all_types.py::test_cli_list_all_types -src/tests/test_list_entries_all_types.py::test_menu_list_all_types -[gw3] [ 73%] PASSED src/tests/test_list_entries_all_types.py::test_menu_list_all_types -src/tests/test_list_entries_sort_filter.py::test_sort_by_label -[gw3] [ 73%] PASSED src/tests/test_list_entries_sort_filter.py::test_sort_by_label -src/tests/test_portable_backup.py::test_export_creates_additional_backup_and_import -[gw3] [ 73%] PASSED src/tests/test_portable_backup.py::test_export_creates_additional_backup_and_import -src/tests/test_post_sync_messages.py::test_handle_post_success -[gw3] [ 73%] PASSED src/tests/test_post_sync_messages.py::test_handle_post_success -src/tests/test_post_sync_messages.py::test_handle_post_failure -[gw3] [ 74%] PASSED src/tests/test_post_sync_messages.py::test_handle_post_failure -src/tests/test_post_sync_messages.py::test_handle_post_prints_all_ids -[gw3] [ 74%] PASSED src/tests/test_post_sync_messages.py::test_handle_post_prints_all_ids -src/tests/test_profile_cleanup.py::test_generate_seed_cleanup_on_failure -[gw3] [ 74%] PASSED src/tests/test_profile_cleanup.py::test_generate_seed_cleanup_on_failure -src/tests/test_profile_deletion_sync.py::test_profile_deletion_stops_sync -[gw3] [ 74%] PASSED src/tests/test_profile_deletion_sync.py::test_profile_deletion_stops_sync -src/tests/test_profile_export_import.py::test_profile_export_import_round_trip -[gw0] [ 74%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/entry/1/unarchive] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/change-password] -[gw3] [ 74%] PASSED src/tests/test_profile_export_import.py::test_profile_export_import_round_trip -src/tests/test_profile_init_integration.py::test_initialize_profile_and_manager -[gw2] [ 75%] PASSED src/tests/test_password_unlock_after_change.py::test_password_change_and_unlock -src/tests/test_pgp_entry.py::test_pgp_key_determinism -[gw0] [ 75%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/change-password] -src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/vault/lock] -[gw2] [ 75%] PASSED src/tests/test_pgp_entry.py::test_pgp_key_determinism -src/tests/test_pgp_entry.py::test_pgp_rsa_key_determinism -[gw3] [ 75%] PASSED src/tests/test_profile_init_integration.py::test_initialize_profile_and_manager -src/tests/test_profile_management.py::test_add_and_delete_entry -[gw3] [ 75%] PASSED src/tests/test_profile_management.py::test_add_and_delete_entry -src/tests/test_profiles.py::test_add_and_switch_fingerprint -[gw3] [ 75%] PASSED src/tests/test_profiles.py::test_add_and_switch_fingerprint -src/tests/test_profiles.py::test_sync_index_missing_bad_data -[gw3] [ 75%] PASSED src/tests/test_profiles.py::test_sync_index_missing_bad_data -src/tests/test_profiles.py::test_attempt_initial_sync_incomplete_data -[gw3] [ 76%] PASSED src/tests/test_profiles.py::test_attempt_initial_sync_incomplete_data -src/tests/test_publish_json_result.py::test_publish_snapshot_success -[gw3] [ 76%] PASSED src/tests/test_publish_json_result.py::test_publish_snapshot_success -src/tests/test_publish_json_result.py::test_publish_snapshot_failure -[gw3] [ 76%] PASSED src/tests/test_publish_json_result.py::test_publish_snapshot_failure -src/tests/test_pubsub.py::test_subscribe_and_publish -[gw3] [ 76%] PASSED src/tests/test_pubsub.py::test_subscribe_and_publish -src/tests/test_pubsub.py::test_unsubscribe -[gw3] [ 76%] PASSED src/tests/test_pubsub.py::test_unsubscribe -src/tests/test_quick_unlock_default.py::test_quick_unlock_default_off -[gw3] [ 76%] PASSED src/tests/test_quick_unlock_default.py::test_quick_unlock_default_off -src/tests/test_quick_unlock_default.py::test_quick_unlock_logs_event -[gw3] [ 77%] PASSED src/tests/test_quick_unlock_default.py::test_quick_unlock_logs_event -src/tests/test_quick_unlock_profile_creation.py::test_toggle_quick_unlock_after_profile_creation -[gw0] [ 77%] PASSED src/tests/test_api.py::test_invalid_token_other_endpoints[asyncio-post-/api/v1/vault/lock] -src/tests/test_api_new_endpoints.py::test_create_and_modify_totp_entry[asyncio] -[gw3] [ 77%] PASSED src/tests/test_quick_unlock_profile_creation.py::test_toggle_quick_unlock_after_profile_creation -src/tests/test_restore_from_nostr_setup.py::test_handle_new_seed_setup_restore_from_nostr -[gw3] [ 77%] PASSED src/tests/test_restore_from_nostr_setup.py::test_handle_new_seed_setup_restore_from_nostr -src/tests/test_restore_from_nostr_setup.py::test_handle_new_seed_setup_restore_from_local_backup -[gw3] [ 77%] PASSED src/tests/test_restore_from_nostr_setup.py::test_handle_new_seed_setup_restore_from_local_backup -src/tests/test_restore_from_nostr_setup.py::test_restore_from_nostr_warns -[gw0] [ 77%] PASSED src/tests/test_api_new_endpoints.py::test_create_and_modify_totp_entry[asyncio] -src/tests/test_api_new_endpoints.py::test_create_and_modify_ssh_entry[asyncio] -[gw3] [ 77%] PASSED src/tests/test_restore_from_nostr_setup.py::test_restore_from_nostr_warns -src/tests/test_restore_from_nostr_setup.py::test_restore_from_nostr_abort -[gw0] [ 78%] PASSED src/tests/test_api_new_endpoints.py::test_create_and_modify_ssh_entry[asyncio] -src/tests/test_api_new_endpoints.py::test_update_entry_error[asyncio] -[gw3] [ 78%] PASSED src/tests/test_restore_from_nostr_setup.py::test_restore_from_nostr_abort -src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-True0] -[gw3] [ 78%] PASSED src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-True0] -src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-True1] -[gw3] [ 78%] PASSED src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-True1] -src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-True2] -[gw0] [ 78%] PASSED src/tests/test_api_new_endpoints.py::test_update_entry_error[asyncio] -src/tests/test_api_new_endpoints.py::test_update_config_secret_mode[asyncio] -[gw3] [ 78%] PASSED src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-True2] -src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-False] -[gw3] [ 78%] PASSED src/tests/test_retrieve_pause_sensitive_entries.py::test_pause_before_entry_actions[-False] -src/tests/test_search_entries.py::test_search_by_website -[gw3] [ 79%] PASSED src/tests/test_search_entries.py::test_search_by_website -src/tests/test_search_entries.py::test_search_by_username -[gw2] [ 79%] PASSED src/tests/test_pgp_entry.py::test_pgp_rsa_key_determinism -src/tests/test_portable_backup.py::test_round_trip -[gw3] [ 79%] PASSED src/tests/test_search_entries.py::test_search_by_username -src/tests/test_search_entries.py::test_search_by_url -[gw2] [ 79%] PASSED src/tests/test_portable_backup.py::test_round_trip -src/tests/test_portable_backup.py::test_round_trip_unencrypted -[gw3] [ 79%] PASSED src/tests/test_search_entries.py::test_search_by_url -src/tests/test_search_entries.py::test_search_by_notes_and_totp -[gw2] [ 79%] PASSED src/tests/test_portable_backup.py::test_round_trip_unencrypted -src/tests/test_portable_backup.py::test_corruption_detection -[gw3] [ 80%] PASSED src/tests/test_search_entries.py::test_search_by_notes_and_totp -src/tests/test_search_entries.py::test_search_by_custom_field -[gw2] [ 80%] PASSED src/tests/test_portable_backup.py::test_corruption_detection -src/tests/test_portable_backup.py::test_import_over_existing -[gw3] [ 80%] PASSED src/tests/test_search_entries.py::test_search_by_custom_field -src/tests/test_search_entries.py::test_search_key_value_value -[gw0] [ 80%] PASSED src/tests/test_api_new_endpoints.py::test_update_config_secret_mode[asyncio] -[gw2] [ 80%] PASSED src/tests/test_portable_backup.py::test_import_over_existing -src/tests/test_portable_backup.py::test_checksum_mismatch_detection -src/tests/test_api_new_endpoints.py::test_totp_export_endpoint[asyncio] -[gw3] [ 80%] PASSED src/tests/test_search_entries.py::test_search_key_value_value -src/tests/test_search_entries.py::test_search_no_results -[gw2] [ 80%] PASSED src/tests/test_portable_backup.py::test_checksum_mismatch_detection -src/tests/test_portable_backup.py::test_export_import_seed_encrypted_with_different_key -[gw3] [ 81%] PASSED src/tests/test_search_entries.py::test_search_no_results -src/tests/test_search_entries.py::test_search_by_tag_password -[gw2] [ 81%] PASSED src/tests/test_portable_backup.py::test_export_import_seed_encrypted_with_different_key -src/tests/test_seed_import.py::test_seed_encryption_round_trip -[gw3] [ 81%] PASSED src/tests/test_search_entries.py::test_search_by_tag_password -src/tests/test_search_entries.py::test_search_by_tag_totp -[gw2] [ 81%] PASSED src/tests/test_seed_import.py::test_seed_encryption_round_trip -src/tests/test_seed_migration.py::test_parent_seed_migrates_from_fernet -[gw3] [ 81%] PASSED src/tests/test_search_entries.py::test_search_by_tag_totp -src/tests/test_search_entries.py::test_search_with_kind_filter -[gw2] [ 81%] PASSED src/tests/test_seed_migration.py::test_parent_seed_migrates_from_fernet -src/tests/test_seed_prompt.py::test_masked_input_posix_backspace -[gw2] [ 81%] PASSED src/tests/test_seed_prompt.py::test_masked_input_posix_backspace -src/tests/test_seed_prompt.py::test_masked_input_windows_space -[gw2] [ 82%] PASSED src/tests/test_seed_prompt.py::test_masked_input_windows_space -src/tests/test_seed_prompt.py::test_masked_input_posix_ctrl_c -[gw2] [ 82%] PASSED src/tests/test_seed_prompt.py::test_masked_input_posix_ctrl_c -src/tests/test_seed_prompt.py::test_masked_input_windows_ctrl_c -[gw2] [ 82%] PASSED src/tests/test_seed_prompt.py::test_masked_input_windows_ctrl_c -src/tests/test_seed_prompt.py::test_prompt_seed_words_valid -[gw2] [ 82%] PASSED src/tests/test_seed_prompt.py::test_prompt_seed_words_valid -src/tests/test_seed_prompt.py::test_prompt_seed_words_invalid_word -[gw3] [ 82%] PASSED src/tests/test_search_entries.py::test_search_with_kind_filter -src/tests/test_secret_mode.py::test_password_retrieve_secret_mode -[gw3] [ 82%] PASSED src/tests/test_secret_mode.py::test_password_retrieve_secret_mode -src/tests/test_secret_mode.py::test_totp_display_secret_mode -[gw3] [ 83%] PASSED src/tests/test_secret_mode.py::test_totp_display_secret_mode -src/tests/test_secret_mode.py::test_password_retrieve_no_secret_mode -[gw0] [ 83%] PASSED src/tests/test_api_new_endpoints.py::test_totp_export_endpoint[asyncio] -src/tests/test_api_new_endpoints.py::test_totp_codes_endpoint[asyncio] -[gw3] [ 83%] PASSED src/tests/test_secret_mode.py::test_password_retrieve_no_secret_mode -src/tests/test_secret_mode.py::test_totp_display_no_secret_mode -[gw3] [ 83%] PASSED src/tests/test_secret_mode.py::test_totp_display_no_secret_mode -src/tests/test_secret_mode_profile_creation.py::test_add_new_fingerprint_initializes_managers -[gw3] [ 83%] PASSED src/tests/test_secret_mode_profile_creation.py::test_add_new_fingerprint_initializes_managers -src/tests/test_secret_mode_profile_creation.py::test_toggle_secret_mode_after_profile_creation -[gw3] [ 83%] PASSED src/tests/test_secret_mode_profile_creation.py::test_toggle_secret_mode_after_profile_creation -src/tests/test_seed_entry.py::test_seed_phrase_determinism -[gw3] [ 83%] PASSED src/tests/test_seed_entry.py::test_seed_phrase_determinism -src/tests/test_seed_generation.py::test_generate_bip85_and_new_seed -[gw3] [ 84%] PASSED src/tests/test_seed_generation.py::test_generate_bip85_and_new_seed -src/tests/test_settings_menu.py::test_relay_and_profile_actions -[gw3] [ 84%] PASSED src/tests/test_settings_menu.py::test_relay_and_profile_actions -src/tests/test_settings_menu.py::test_settings_menu_additional_backup -[gw3] [ 84%] PASSED src/tests/test_settings_menu.py::test_settings_menu_additional_backup -src/tests/test_settings_menu.py::test_settings_menu_change_password -[gw3] [ 84%] PASSED src/tests/test_settings_menu.py::test_settings_menu_change_password -src/tests/test_settings_menu.py::test_settings_menu_change_password_incorrect -[gw0] [ 84%] PASSED src/tests/test_api_new_endpoints.py::test_totp_codes_endpoint[asyncio] -src/tests/test_api_new_endpoints.py::test_parent_seed_endpoint_removed[asyncio] -[gw2] [ 84%] PASSED src/tests/test_seed_prompt.py::test_prompt_seed_words_invalid_word -src/tests/test_seed_word_by_word_flow.py::test_prompt_seed_words_confirmation_loop -[gw2] [ 85%] PASSED src/tests/test_seed_word_by_word_flow.py::test_prompt_seed_words_confirmation_loop -src/tests/test_seed_word_by_word_flow.py::test_prompt_seed_words_invalid_word -[gw2] [ 85%] PASSED src/tests/test_seed_word_by_word_flow.py::test_prompt_seed_words_invalid_word -src/tests/test_seed_word_by_word_flow.py::test_add_new_fingerprint_words_flow_success -[gw2] [ 85%] PASSED src/tests/test_seed_word_by_word_flow.py::test_add_new_fingerprint_words_flow_success -src/tests/test_seed_word_by_word_flow.py::test_add_new_fingerprint_words_flow_invalid_phrase -[gw2] [ 85%] PASSED src/tests/test_seed_word_by_word_flow.py::test_add_new_fingerprint_words_flow_invalid_phrase -src/tests/test_seedqr_encoding.py::test_seedqr_standard_example -[gw2] [ 85%] PASSED src/tests/test_seedqr_encoding.py::test_seedqr_standard_example -src/tests/test_service_classes.py::test_entry_service_add_password -[gw3] [ 85%] PASSED src/tests/test_settings_menu.py::test_settings_menu_change_password_incorrect -src/tests/test_settings_menu.py::test_settings_menu_without_nostr_client -[gw3] [ 85%] PASSED src/tests/test_settings_menu.py::test_settings_menu_without_nostr_client -src/tests/test_ssh_entry.py::test_add_and_retrieve_ssh_key_pair -[gw2] [ 86%] PASSED src/tests/test_service_classes.py::test_entry_service_add_password -src/tests/test_service_classes.py::test_menu_handler_list_entries -[gw3] [ 86%] PASSED src/tests/test_ssh_entry.py::test_add_and_retrieve_ssh_key_pair -src/tests/test_ssh_entry_valid.py::test_ssh_private_key_corresponds_to_public -[gw2] [ 86%] PASSED src/tests/test_service_classes.py::test_menu_handler_list_entries -src/tests/test_service_classes.py::test_profile_service_switch -[gw2] [ 86%] PASSED src/tests/test_service_classes.py::test_profile_service_switch -src/tests/test_stats_screen.py::test_stats_display_resets_after_exit -[gw2] [ 86%] PASSED src/tests/test_stats_screen.py::test_stats_display_resets_after_exit -src/tests/test_stats_screen.py::test_stats_screen_breaks_on_enter -[gw2] [ 86%] PASSED src/tests/test_stats_screen.py::test_stats_screen_breaks_on_enter -src/tests/test_sync_race_conditions.py::test_sync_race_conditions -[gw3] [ 86%] PASSED src/tests/test_ssh_entry_valid.py::test_ssh_private_key_corresponds_to_public -src/tests/test_state_manager.py::test_state_manager_round_trip -[gw3] [ 87%] PASSED src/tests/test_state_manager.py::test_state_manager_round_trip -src/tests/test_stats_screen.py::test_live_stats_shows_message -[gw3] [ 87%] PASSED src/tests/test_stats_screen.py::test_live_stats_shows_message -src/tests/test_stats_screen.py::test_live_stats_shows_notification -[gw3] [ 87%] PASSED src/tests/test_stats_screen.py::test_live_stats_shows_notification -src/tests/test_stats_screen.py::test_live_stats_triggers_background_sync -[gw3] [ 87%] PASSED src/tests/test_stats_screen.py::test_live_stats_triggers_background_sync -src/tests/test_stats_screen.py::test_stats_display_only_once -[gw3] [ 87%] PASSED src/tests/test_stats_screen.py::test_stats_display_only_once -src/tests/test_terminal_utils_failure_handling.py::test_format_profile_reraises -[gw3] [ 87%] PASSED src/tests/test_terminal_utils_failure_handling.py::test_format_profile_reraises -src/tests/test_terminal_utils_failure_handling.py::test_clear_header_with_notification_reraises -[gw3] [ 88%] PASSED src/tests/test_terminal_utils_failure_handling.py::test_clear_header_with_notification_reraises -src/tests/test_totp.py::test_current_code_matches_pyotp -[gw0] [ 88%] PASSED src/tests/test_api_new_endpoints.py::test_parent_seed_endpoint_removed[asyncio] -src/tests/test_api_new_endpoints.py::test_fingerprint_endpoints[asyncio] -[gw3] [ 88%] PASSED src/tests/test_totp.py::test_current_code_matches_pyotp -src/tests/test_totp.py::test_time_remaining -[gw3] [ 88%] PASSED src/tests/test_totp.py::test_time_remaining -[gw2] [ 88%] PASSED src/tests/test_sync_race_conditions.py::test_sync_race_conditions -src/tests/test_tag_persistence.py::test_tags_persist_on_new_entry -src/tests/test_totp.py::test_print_progress_bar_terminates -[gw3] [ 88%] PASSED src/tests/test_totp.py::test_print_progress_bar_terminates -src/tests/test_totp_entry.py::test_add_totp_and_get_code -[gw3] [ 88%] PASSED src/tests/test_totp_entry.py::test_add_totp_and_get_code -src/tests/test_totp_entry.py::test_totp_time_remaining -[gw2] [ 89%] PASSED src/tests/test_tag_persistence.py::test_tags_persist_on_new_entry -src/tests/test_tag_persistence.py::test_tags_persist_after_modify -[gw3] [ 89%] PASSED src/tests/test_totp_entry.py::test_totp_time_remaining -src/tests/test_totp_entry.py::test_add_totp_imported -[gw3] [ 89%] PASSED src/tests/test_totp_entry.py::test_add_totp_imported -src/tests/test_totp_entry.py::test_add_totp_with_notes -[gw2] [ 89%] PASSED src/tests/test_tag_persistence.py::test_tags_persist_after_modify -src/tests/test_totp_uri.py::test_parse_otpauth_missing_prefix -[gw2] [ 89%] PASSED src/tests/test_totp_uri.py::test_parse_otpauth_missing_prefix -src/tests/test_totp_uri.py::test_parse_otpauth_missing_secret -[gw2] [ 89%] PASSED src/tests/test_totp_uri.py::test_parse_otpauth_missing_secret -src/tests/test_totp_uri.py::test_make_otpauth_uri_roundtrip -[gw2] [ 90%] PASSED src/tests/test_totp_uri.py::test_make_otpauth_uri_roundtrip -src/tests/test_typer_cli.py::test_entry_list -[gw3] [ 90%] PASSED src/tests/test_totp_entry.py::test_add_totp_with_notes -src/tests/test_totp_entry.py::test_legacy_deterministic_entry -[gw2] [ 90%] PASSED src/tests/test_typer_cli.py::test_entry_list -src/tests/test_typer_cli.py::test_entry_search -[gw2] [ 90%] PASSED src/tests/test_typer_cli.py::test_entry_search -src/tests/test_typer_cli.py::test_entry_get_password -[gw2] [ 90%] PASSED src/tests/test_typer_cli.py::test_entry_get_password -src/tests/test_typer_cli.py::test_vault_export -[gw3] [ 90%] PASSED src/tests/test_totp_entry.py::test_legacy_deterministic_entry -[gw2] [ 90%] PASSED src/tests/test_typer_cli.py::test_vault_export -src/tests/test_typer_cli.py::test_vault_import -src/tests/test_totp_uri.py::test_parse_otpauth_normal -[gw3] [ 91%] PASSED src/tests/test_totp_uri.py::test_parse_otpauth_normal -src/tests/test_typer_cli.py::test_vault_change_password -[gw2] [ 91%] PASSED src/tests/test_typer_cli.py::test_vault_import -[gw3] [ 91%] PASSED src/tests/test_typer_cli.py::test_vault_change_password -src/tests/test_typer_cli.py::test_vault_import_triggers_sync -src/tests/test_typer_cli.py::test_vault_lock -[gw3] [ 91%] PASSED src/tests/test_typer_cli.py::test_vault_lock -src/tests/test_typer_cli.py::test_root_lock -[gw2] [ 91%] PASSED src/tests/test_typer_cli.py::test_vault_import_triggers_sync -src/tests/test_typer_cli.py::test_nostr_get_pubkey -[gw3] [ 91%] PASSED src/tests/test_typer_cli.py::test_root_lock -[gw2] [ 91%] PASSED src/tests/test_typer_cli.py::test_nostr_get_pubkey -src/tests/test_typer_cli.py::test_vault_reveal_parent_seed -src/tests/test_typer_cli.py::test_fingerprint_list -[gw2] [ 92%] PASSED src/tests/test_typer_cli.py::test_fingerprint_list -src/tests/test_typer_cli.py::test_fingerprint_add -[gw3] [ 92%] PASSED src/tests/test_typer_cli.py::test_vault_reveal_parent_seed -src/tests/test_typer_cli.py::test_fingerprint_remove -[gw2] [ 92%] PASSED src/tests/test_typer_cli.py::test_fingerprint_add -[gw3] [ 92%] PASSED src/tests/test_typer_cli.py::test_fingerprint_remove -src/tests/test_typer_cli.py::test_fingerprint_switch -src/tests/test_typer_cli.py::test_config_set -[gw3] [ 92%] PASSED src/tests/test_typer_cli.py::test_fingerprint_switch -[gw2] [ 92%] PASSED src/tests/test_typer_cli.py::test_config_set -src/tests/test_typer_cli.py::test_config_set_unknown_key -src/tests/test_typer_cli.py::test_config_get -[gw3] [ 93%] PASSED src/tests/test_typer_cli.py::test_config_get -src/tests/test_typer_cli.py::test_generate_password -[gw2] [ 93%] PASSED src/tests/test_typer_cli.py::test_config_set_unknown_key -src/tests/test_typer_cli.py::test_nostr_sync -[gw3] [ 93%] PASSED src/tests/test_typer_cli.py::test_generate_password -[gw2] [ 93%] PASSED src/tests/test_typer_cli.py::test_nostr_sync -src/tests/test_typer_cli.py::test_api_start_passes_fingerprint -src/tests/test_typer_cli.py::test_entry_list_passes_fingerprint -[gw3] [ 93%] PASSED src/tests/test_typer_cli.py::test_api_start_passes_fingerprint -[gw2] [ 93%] PASSED src/tests/test_typer_cli.py::test_entry_list_passes_fingerprint -src/tests/test_typer_cli.py::test_entry_modify -src/tests/test_typer_cli.py::test_entry_add -[gw3] [ 93%] PASSED src/tests/test_typer_cli.py::test_entry_modify -src/tests/test_typer_cli.py::test_entry_modify_invalid -[gw2] [ 94%] PASSED src/tests/test_typer_cli.py::test_entry_add -src/tests/test_typer_cli.py::test_entry_archive -[gw3] [ 94%] PASSED src/tests/test_typer_cli.py::test_entry_modify_invalid -src/tests/test_typer_cli.py::test_entry_unarchive -[gw2] [ 94%] PASSED src/tests/test_typer_cli.py::test_entry_archive -src/tests/test_typer_cli.py::test_entry_export_totp -[gw3] [ 94%] PASSED src/tests/test_typer_cli.py::test_entry_unarchive -src/tests/test_typer_cli.py::test_entry_totp_codes -[gw2] [ 94%] PASSED src/tests/test_typer_cli.py::test_entry_export_totp -src/tests/test_typer_cli.py::test_verify_checksum_command -[gw3] [ 94%] PASSED src/tests/test_typer_cli.py::test_entry_totp_codes -src/tests/test_typer_cli.py::test_update_checksum_command -[gw2] [ 95%] PASSED src/tests/test_typer_cli.py::test_verify_checksum_command -src/tests/test_typer_cli.py::test_tui_forward_fingerprint -[gw3] [ 95%] PASSED src/tests/test_typer_cli.py::test_update_checksum_command -src/tests/test_typer_cli.py::test_gui_command -[gw2] [ 95%] PASSED src/tests/test_typer_cli.py::test_tui_forward_fingerprint -src/tests/test_typer_cli.py::test_gui_command_no_backend -[gw3] [ 95%] PASSED src/tests/test_typer_cli.py::test_gui_command -src/tests/test_typer_cli.py::test_gui_command_install_backend -[gw2] [ 95%] PASSED src/tests/test_typer_cli.py::test_gui_command_no_backend -src/tests/test_unlock_sync.py::test_unlock_triggers_sync -[gw3] [ 95%] PASSED src/tests/test_typer_cli.py::test_gui_command_install_backend -src/tests/test_unlock_sync.py::test_quick_unlock_background_sync -[gw3] [ 95%] PASSED src/tests/test_unlock_sync.py::test_quick_unlock_background_sync -src/tests/test_v2_prefix_fallback.py::test_v2_prefix_fernet_fallback -[gw3] [ 96%] PASSED src/tests/test_v2_prefix_fallback.py::test_v2_prefix_fernet_fallback -src/tests/test_v2_prefix_fallback.py::test_aesgcm_payload_too_short -[gw3] [ 96%] PASSED src/tests/test_v2_prefix_fallback.py::test_aesgcm_payload_too_short -src/tests/test_vault_initialization.py::test_save_and_encrypt_seed_initializes_vault -[gw2] [ 96%] PASSED src/tests/test_unlock_sync.py::test_unlock_triggers_sync -src/tests/test_unlock_sync.py::test_start_background_sync_running_loop -[gw2] [ 96%] PASSED src/tests/test_unlock_sync.py::test_start_background_sync_running_loop -src/tests/test_vault_lock_flag.py::test_lock_vault_sets_flag_and_keeps_objects -[gw2] [ 96%] PASSED src/tests/test_vault_lock_flag.py::test_lock_vault_sets_flag_and_keeps_objects -src/tests/test_vault_lock_flag.py::test_entry_service_requires_unlocked -[gw2] [ 96%] PASSED src/tests/test_vault_lock_flag.py::test_entry_service_requires_unlocked -src/tests/test_vault_lock_flag.py::test_unlock_vault_clears_locked_flag -[gw2] [ 96%] PASSED src/tests/test_vault_lock_flag.py::test_unlock_vault_clears_locked_flag -src/tests/test_verbose_timing.py::test_unlock_vault_logs_time -[gw2] [ 97%] PASSED src/tests/test_verbose_timing.py::test_unlock_vault_logs_time -src/tests/test_verbose_timing.py::test_publish_snapshot_logs_time -[gw2] [ 97%] PASSED src/tests/test_verbose_timing.py::test_publish_snapshot_logs_time -tests/perf/test_bip85_cache.py::test_bip85_cache_benchmark -[gw2] [ 97%] PASSED tests/perf/test_bip85_cache.py::test_bip85_cache_benchmark -[gw0] [ 97%] PASSED src/tests/test_api_new_endpoints.py::test_fingerprint_endpoints[asyncio] -src/tests/test_api_new_endpoints.py::test_checksum_endpoints[asyncio] -[gw3] [ 97%] PASSED src/tests/test_vault_initialization.py::test_save_and_encrypt_seed_initializes_vault -src/tests/test_vault_lock_event.py::test_lock_vault_publishes_event -[gw3] [ 97%] PASSED src/tests/test_vault_lock_event.py::test_lock_vault_publishes_event -[gw0] [ 98%] PASSED src/tests/test_api_new_endpoints.py::test_checksum_endpoints[asyncio] -src/tests/test_api_new_endpoints.py::test_vault_import_via_path[asyncio] -[gw0] [ 98%] PASSED src/tests/test_api_new_endpoints.py::test_vault_import_via_path[asyncio] -src/tests/test_api_new_endpoints.py::test_vault_import_via_upload[asyncio] -[gw0] [ 98%] PASSED src/tests/test_api_new_endpoints.py::test_vault_import_via_upload[asyncio] -src/tests/test_api_new_endpoints.py::test_vault_import_invalid_extension[asyncio] -[gw0] [ 98%] PASSED src/tests/test_api_new_endpoints.py::test_vault_import_invalid_extension[asyncio] -src/tests/test_api_new_endpoints.py::test_vault_import_path_traversal_blocked[asyncio] -[gw0] [ 98%] PASSED src/tests/test_api_new_endpoints.py::test_vault_import_path_traversal_blocked[asyncio] -[gw1] [ 98%] PASSED src/tests/test_password_properties.py::test_password_properties -src/tests/test_password_shuffle_consistency.py::test_password_generation_consistent_output -[gw1] [ 98%] PASSED src/tests/test_password_shuffle_consistency.py::test_password_generation_consistent_output -src/tests/test_password_special_chars.py::test_no_special_chars -[gw1] [ 99%] PASSED src/tests/test_password_special_chars.py::test_no_special_chars -src/tests/test_password_special_chars.py::test_allowed_special_chars_only -[gw1] [ 99%] PASSED src/tests/test_password_special_chars.py::test_allowed_special_chars_only -src/tests/test_password_special_chars.py::test_exclude_ambiguous_chars -[gw1] [ 99%] PASSED src/tests/test_password_special_chars.py::test_exclude_ambiguous_chars -src/tests/test_password_special_chars.py::test_safe_special_chars_mode -[gw1] [ 99%] PASSED src/tests/test_password_special_chars.py::test_safe_special_chars_mode -src/tests/test_password_special_modes.py::test_include_special_chars_false -[gw1] [ 99%] PASSED src/tests/test_password_special_modes.py::test_include_special_chars_false -src/tests/test_password_special_modes.py::test_safe_mode_uses_safe_chars -[gw1] [ 99%] PASSED src/tests/test_password_special_modes.py::test_safe_mode_uses_safe_chars -src/tests/test_password_special_modes.py::test_allowed_chars_override_special_mode -[gw1] [100%] PASSED src/tests/test_password_special_modes.py::test_allowed_chars_override_special_mode - -=============================== warnings summary =============================== -../home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/pgpy/constants.py:5 -../home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/pgpy/constants.py:5 -../home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/pgpy/constants.py:5 -../home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/pgpy/constants.py:5 -../home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/pgpy/constants.py:5 - /home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/pgpy/constants.py:5: DeprecationWarning: 'imghdr' is deprecated and slated for removal in Python 3.13 - import imghdr - -../home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/typer/params.py:206: 24 warnings -src/tests/test_cli_integration.py: 2 warnings - /home/jules/.pyenv/versions/3.12.12/lib/python3.12/site-packages/typer/params.py:206: DeprecationWarning: The 'is_flag' and 'flag_value' parameters are not supported by Typer and will be removed entirely in a future release. - return OptionInfo( - -src/tests/test_gui_headless.py: 14 warnings -src/tests/test_gui_sync.py: 4 warnings - :31: DeprecationWarning: Pack.padding_top is deprecated. Use Pack.margin_top instead. - -src/tests/test_gui_headless.py: 13 warnings -src/tests/test_gui_sync.py: 2 warnings - :30: DeprecationWarning: Pack.padding is deprecated. Use Pack.margin instead. - --- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html -================= 653 passed, 8 skipped, 64 warnings in 45.30s ================= -``` diff --git a/torch/torch-config.json b/torch/torch-config.json deleted file mode 100644 index e41366a3..00000000 --- a/torch/torch-config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nostrLock": { - "namespace": "torch-c00fb269", - "relays": [ - "wss://relay.damus.io", - "wss://nos.lol", - "wss://relay.primal.net" - ], - "ttlSeconds": 7200, - "queryTimeoutMs": 15000, - "publishTimeoutMs": 8000, - "minSuccessfulRelayPublishes": 2, - "minActiveRelayPool": 2, - "relayFallbacks": [ - "wss://relay.snort.social", - "wss://relay.nostr.band" - ] - }, - "dashboard": { - "defaultCadenceView": "daily", - "defaultStatusView": "active" - }, - "scheduler": { - "firstPromptByCadence": { - "daily": "scheduler-update-agent", - "weekly": "bug-reproducer-agent" - } - } -}