Skip to content

Requirements and their checks

Lens's requirements are the lists in the README: what is built, what is in progress (an open pull request) and what is planned. Each built item there links its section here, and each row of a section names the checks that fail when that requirement stops being true: a test, a clicks journey and the number it is held to, or the CI workflow that builds or tests it. A requirement nothing checks yet says so and links the issue that adds one.

How the checks are kept

make requirements runs ci/check_requirements.py, and pre-commit runs it on every pull request, so drift shows up in CI. It fails when:

  • a built item in the README links no section here, or a section here is linked by no item;
  • a check is gone: its file, a test function or class in a Python test, a quoted name (a test's title, a journey's id) in a script, a job or step name: in a workflow, or a journey in a clicks baseline;
  • a measured number is no longer met: journey ≤ N clicks (or keys) in ci/clicks/baseline.json (a computer) or baseline-phone.json (a phone), or the journey fails there;
  • a requirement with no check yet links no issue;
  • an In progress item links no pull request, or one the changelog already lists as released;
  • a Planned item links nowhere.

The script checks that a check exists, not that it passes: CI runs the tests, the Clicks workflow the journeys. A check named here runs in CI rather than skipping there (a test that needs a program the runners lack is not a check yet). When a pull request builds something, it adds a line to the README's built list and a row here, and takes its own line out of In progress. Renaming or removing a test that a row names means changing the row in the same pull request, to another check that proves the same thing; loosening a requirement or its number waits for the owner (AGENTS.md).

Install

Requirement Checks
One line (install.sh) opens a web installer that checks the computer install.yml Install from the web installer, Start the installer, as install.sh does on a first install; test_lens_installer.py test_plan_lists_docker_when_it_is_missing, test_only_the_link_opens_it
It installs with the defaults, using a model server it finds on the computer journeys.mjs install; baseline.json install ≤ 3 clicks, ≤ 2 keys
It installs what's missing, Docker included, starts Lens and opens its setup page install.yml A new computer, without Docker, Answer it and install, Lens answers, waits for its first admin, and has the installer's settings; test_lens_installer.py test_install_runs_install_sh_with_the_answers
Lens can run in a microsandbox microVM instead of Docker install.yml One-line install in a microsandbox; test_lens_installer.py test_lens_app_offers_microsandbox_first_where_it_runs
Running the line again updates Lens in place and keeps its secrets install.yml One-line install, Running it again updates in place and keeps the secrets
Windows installs with install.ps1 install.yml install.ps1 parses in Windows PowerShell 5.1, what Windows has; test_install_windows.py test_installs_and_hands_over_the_setup_address, test_a_failed_build_says_so
Double-click launchers for each system launchers.yml Build the launchers, Check them
Lens.app on a Mac with Apple silicon installs Lens and runs it without Docker desktop.yml Build Lens.app, The bundled msb runs, The app carries its helper and microsandbox
Packages for Synology, QNAP and Cloudron synology.yml Build; qnap.yml Build the package; cloudron.yml build
Each release publishes Lens's images for amd64 and arm64 to GHCR images.yml Build, One tag for both architectures, Both architectures are there
LENS_IMAGES=pull installs a release from its prebuilt images, without building install.yml Install a release from its images, Lens runs the downloaded images and keeps them named for later runs
A Proxmox container script No check yet (#486)
The first admin signs up with a passkey from the setup link, which fills in the setup code journeys.mjs first-admin; baseline.json first-admin ≤ 1 click; test_passkeys.py test_first_admin_with_a_passkey_and_no_password; setupForm.test.tsx fills in the code from the setup link and starts at the email

Connect your agents

Requirement Checks
An MCP server at /mcp: agents search, read assets' text and notes, and cite exact moments test_mcp.py test_search_and_cite, test_reading_recordings, test_note_tools
Agents find assets by face, voice, entity, topic, type, content type and date in one call, each with the moments that match test_find.py test_by_face_voice_entity_and_date, test_by_topic_type_content_type_and_text, test_find_for_agents
Agents classify an asset or a text against labels, content types or topics with the decision model, acting only on a sure answer test_classify.py test_labels_given_and_what_the_model_reads, test_vocabularies_and_applying_a_sure_answer, test_agents_classify
Agents find what was said about a day or a range, each date with its line and a link to that moment test_dates.py test_agents_find_what_was_said_about_a_day
Agents find similar content test_mcp.py test_search_and_cite; test_similar.py test_like_a_text_by_its_key_words, test_like_a_resource_by_words_topics_and_entities
Agents explore the graph with Cypher or SPARQL, or browse it as folders and files test_mcp.py test_graph_tools, test_sparql; test_file_tree.py test_agents_browse_it_over_mcp
Agents suggest topics and graph changes, and edit and merge entities and topics test_mcp.py test_topic_tools, test_editing_topics_and_entities
Agents write notes and make podcasts test_mcp.py test_note_tools; test_podcasts.py test_agents_make_episodes
Agents sign in with OAuth and see only what their person may read test_mcp.py test_clients_are_sent_to_sign_in, test_collection_roles; test_oauth.py test_an_app_gets_access_and_acts_with_the_persons_roles
The account menu copies the MCP address in one click; connecting an assistant takes a handful connect-assistant.test.tsx copies the address from the account menu in one click; journeys.mjs agent-connect; baseline.json agent-connect ≤ 5 clicks
A namespace's own MCP server at /mcp/<namespace>, off until its owner turns it on test_mcp_namespace.py test_off_until_an_owner_turns_it_on, test_it_sees_one_namespace
OpenAI's chat completions API per namespace, cited from that namespace alone, off until turned on test_completions.py test_off_until_an_owner_turns_it_on, test_answers_from_the_namespace_alone_with_citations, test_streams_as_openai_does
API keys that open one namespace and nothing else test_namespace_keys.py test_a_key_for_one_namespace_opens_its_servers_alone, test_it_has_the_persons_role_there_and_no_more
The graph's folders and files mounted read-only over S3, WebDAV, NFS and SMB test_s3.py test_people_read_their_namespaces_over_s3, test_what_s3_refuses; test_webdav.py test_mount_and_read, test_what_webdav_refuses; test_nfs_share.py test_the_runner_follows_the_setting; test_smb_share.py test_reads_the_tree_as_its_key_sees_it, test_nothing_can_be_written
S3 and WebDAV at the web app's address, through remote access, once an admin allows it test_s3.py test_at_the_web_apps_address_once_an_admin_allows_it; test_webdav.py test_at_the_web_apps_address_once_an_admin_allows_it, test_through_the_web_app_each_visitor_has_its_own_throttle
A file copied in over S3, WebDAV, SMB or NFS is imported where it was put, with an upload's checks and permissions test_s3_writes.py test_a_file_put_into_a_collection_is_imported_there, test_folders_become_collections, test_multipart_uploads, test_what_may_not_be_added_is_refused, test_files_added_at_the_web_apps_address; test_webdav_writes.py test_finder_copies_a_file, test_windows_copies_a_file, test_a_folder_dragged_in_becomes_collections, test_what_may_not_be_added_is_refused; test_smb_writes.py test_a_file_copied_into_a_collection_is_imported_there, test_new_folders_under_collections_are_new_collections, test_what_may_not_be_added_is_refused, test_the_share_is_read_only_while_add_files_is_off; test_s3_writes.py test_the_nfs_shares_key_follows_nfs_add_files (NFS adds through rclone's S3 client)
The web app runs on the same REST API, through a client generated from its OpenAPI schema ci.yml OpenAPI schema is up to date

Ask, and get cited answers

Requirement Checks
Questions about Lens itself answered from Lens's docs and this server's facts, both cited, for people and agents over MCP test_lens_help.py test_the_docs_are_found_by_words, test_the_assistant_answers_about_lens_itself, test_agents_ask_lens_over_mcp, test_without_a_question_lens_help_still_gives_the_server
A chat assistant on every page test_chats.py test_asking_from_a_page; page-chat.test.tsx; ask-place.test.tsx
By voice assistant-mode.test.tsx starts a voice chat from the mic; test_voice.py test_what_is_said_comes_back_as_text
A search result opens the asset with Find showing the words searched for, at the moment it linked to search-find.test.ts carries them on the links from the results, finds the words together, else the longest word there, on the match at the linked moment
It searches by words, and by meaning once search by meaning is set up test_chats.py test_tools_approvals_and_checking; test_semantic.py test_chat_retrieval_by_meaning, test_off_or_unconfigured
Word search takes "phrases", OR, and NOT or -word to leave lines out test_search_index.py test_not_and_minus_leave_lines_out, test_the_queries_parts
Answers cite their sources, and only what the person may read test_chats.py test_chat_cites_only_what_you_can_read; chat.test.tsx splits [n], [n, m] and [n][m]
It acts with tools, and changes wait for approval test_chats.py test_tools_approvals_and_checking; test_approvals.py test_the_assistant_lists_and_declines_but_never_approves
Questions about Lens itself answered from Lens's docs and this server's facts, both cited, for people and agents over MCP test_lens_help.py test_the_docs_are_found_by_words, test_the_assistant_answers_about_lens_itself, test_agents_ask_lens_over_mcp, test_without_a_question_lens_help_still_gives_the_server
A conversation, or one question, picks the tools an answer may use, a limit on its tool calls and the namespaces it reads test_chat_tools.py test_an_answer_is_offered_only_what_was_picked, test_at_most_so_many_tool_calls, test_a_scope_for_one_answer; chat-tools-picker.test.tsx picks groups, keeps single tools, and goes back to Auto, limits the tool calls, from 1 to the server's most
Web search and reading pages, cited like the archive, off until an admin turns it on test_web_search.py test_an_answer_cites_the_web_and_the_conversation_keeps_it, test_reading_pages_and_pdfs; chat-web-citations.test.tsx
An admin's assistant searches and reads connected mail, calendars and files, cites messages and events, and brings them in only once approved test_connection_tools.py test_only_admins_get_them_and_only_for_what_is_connected, test_searching_and_reading_gmail, test_calendar_events_between_two_times, test_finding_and_reading_files_in_storage, test_bringing_things_in_waits_for_the_persons_yes; chat-web-citations.test.tsx a message from a connected mailbox
Where an admin allows it, the assistant sends mail, adds events and writes files, each only after the person reads all of it and says yes, signed in or in the chat; an API key, an app or an agent only declines test_connection_tools.py test_sending_mail_waits_for_the_persons_yes, test_adding_an_event_waits_for_the_persons_yes, test_writing_a_file_waits_for_the_persons_yes; test_approvals.py test_only_the_person_approves_what_the_assistant_asks; approvals-page.test.tsx shows the assistant's whole message before it is sent; add-connection.test.tsx connects Gmail the same way, and asks a service account whose mail it reads
Python and bash in a sandbox of each conversation's own, off until an admin turns it on test_sandbox.py test_python_and_bash_share_one_sandbox_per_conversation, test_the_sandbox_is_off_until_an_admin_turns_it_on_for_the_people_it_names
Each run opens under the answer with its code and output, and a file the sandbox makes downloads from there test_sandbox.py test_a_run_shows_its_output_and_a_file_it_shares_downloads_from_the_answer; sandbox-steps.test.tsx shows the code collapsed, then the code and its output, downloads a file the sandbox shared from the answer
On a Docker install, Settings starts and stops the sandbox's gateway next to Lens, and says why a machine can't run it test_sandbox.py test_the_gateway_is_started_and_stopped_from_settings, test_a_machine_that_cant_run_the_gateway_says_why; sandbox-gateway.test.ts says what the machine is doing, and whether Lens reaches the gateway
Deep research: a goal gets a plan to approve, runs in the background and comes back as a cited report, saved as a note or a podcast episode test_research.py test_a_goal_gets_a_plan_that_waits_for_approval_and_can_be_revised, test_an_approved_plan_runs_and_its_report_comes_back_checked, test_a_report_becomes_a_note_and_a_podcast; chat-research.test.tsx approves the plan with Enter, sending the edits, saves it as a note, asking where when it could go in several namespaces, turns the next message into a research goal, and says when it can't; journeys.mjs chat-research; baseline.json chat-research ≤ 4 clicks
Regenerating an answer or editing a question makes a new version beside the old one test_chat_versions.py test_regenerate_edit_switch_and_go_on; chat-versions.test.tsx regenerating an answer, the version switch; journeys.mjs chat-regenerate; baseline.json chat-regenerate ≤ 4 clicks
A new conversation with nothing in its scope offers Import, and Change scope when namespaces are picked, instead of questions nothing can answer chat-empty.test.tsx with nothing imported, offers Import instead of questions nothing can answer, with an empty namespace in scope, offers Import and a wider scope, someone who can't import is told who can
A thumbs up or down on an answer is saved, shown when the conversation opens, carried in its JSON export and given by agents over MCP test_chat_ratings.py test_rating_an_answer_is_saved_shown_and_exported, test_someone_elses_answer_cant_be_rated, test_agents_rate_answers_over_mcp; chat-rating.test.tsx thumbs up saves the rating, and pressing it again takes it back
Each answer keeps a record of what produced it: the model, its tools, how long it took and what it cost test_chat_records.py test_an_answer_records_what_produced_it, test_a_regenerated_answer_has_its_own_record; chat-details.test.tsx says what produced it: model and host, scope, tools, use, tokens, cost, time and how it ended
A conversation exported as Markdown, PDF or JSON, its citations as footnotes linking back to Lens, by people and by agents over MCP test_chat_export.py test_markdown, test_json, test_pdf_is_printed_by_what_the_server_has, test_agents_read_and_export_over_mcp; chat-copy-export.test.tsx downloads what the server makes, named as the server names it, prints in the browser when the server can't make the PDF
Answers, questions and code blocks copied from the web app, an answer as Markdown or plain text chat-copy-export.test.tsx gives Markdown with citations as footnotes linking back to Lens, or plain text, copies a question, copies a code block and says Copied
An answer is finished, saved and shown even when the person leaves before it's done; only Stop ends it early test_chats.py test_an_answer_is_saved_when_the_client_leaves, test_stopping_an_answer; chat-fresh-answer.test.tsx shows the answer even when the conversation's first read left before it was saved
Answers read aloud by a speech provider, macOS say or StyleTTS2 (opt-in) test_voice.py test_answers_are_read_aloud_by_a_speech_model; test_speech.py test_macos_say_reads_aloud_on_a_mac, test_styletts2_reads_aloud_from_its_own_service; styletts2.yml Read a sentence aloud
Answers and podcasts read aloud by Kokoro v1.0 on this machine, in 54 voices, fetched once it's picked and checked against its SHA-256 (opt-in) test_kokoro.py test_kokoro_reads_answers_aloud_on_this_machine, test_the_model_size_is_a_setting, test_kokoro_is_fetched_once_it_is_picked, test_every_voice_is_listed_before_it_is_fetched, test_a_podcast_is_read_by_kokoro; settings-kokoro.test.tsx reads on this machine, with the model size to pick; kokoro.yml Lens fetches Kokoro and reads aloud with each model
Any message read aloud in one click, one at a time, its audio kept so a replay costs nothing chat-read-aloud.test.tsx plays an answer by the speech model in one click, stops on the next, and keeps its audio, reads one message at a time: starting another stops the first, reads a long answer piece by piece, asking for the next while one plays
Conversations title themselves after their first answer, and a name you give is kept test_chat_titles.py test_a_first_answer_titles_the_conversation, test_a_title_you_give_is_kept, test_agents_rename_and_retitle_over_mcp
Long conversations summarised before they fill the model's context, and /clear and /compact test_compaction.py test_older_messages_are_folded_into_a_summary, test_the_context_window_is_the_model_servers, test_a_long_conversation_is_folded_in_runs_that_fit_the_summary_model, test_a_conversation_that_doesnt_fit_is_folded_in_and_asked_again, test_what_tools_returned_is_cut_to_fit_the_model, test_clear_starts_afresh_and_compact_folds_everything_now, test_commands_in_the_app; chat-summary-mark.test.tsx says how many messages the summary stands for, and shows it on request
Questions about Lens itself answered from its docs and this server, citing both, for people and agents over MCP test_lens_help.py test_the_assistant_answers_about_lens_itself, test_admins_see_where_each_setting_is, test_agents_ask_lens_over_mcp
A namespace's own assistant, with its own name, instructions and memory test_ns_assistant.py test_off_by_default_and_owners_turn_it_on, test_remembers_across_conversations_with_citations; ns-assistant.test.tsx saving a name and forgetting a memory
Telegram and Slack built in, with voice notes and files test_telegram.py test_it_answers_private_chats_and_what_is_said_to_it_in_groups, test_voice_notes_are_heard, test_files_are_held_for_the_assistant_to_import; test_slack.py test_it_answers_direct_messages_mentions_and_the_slash_command, test_voice_clips_are_heard_and_files_held
Other chat rooms through Matterbridge test_bridge.py test_it_answers_what_is_said_to_it_and_nothing_else; test_matterbridge_run.py test_chat_networks_are_set_in_the_app_and_become_matterbridges_config, test_the_bridge_talks_to_the_matterbridge_lens_runs; matterbridge.yml Lens writes a config, and Matterbridge starts with it
By email, at an address of its own test_mailbox.py test_mail_is_answered_by_email_in_its_thread, test_the_mailbox_is_set_up_and_checked_in_the_app, test_answers_list_the_excerpts_they_cite

Answer your email

Requirement Checks
A reply drafted to an imported email from its own namespace, each fact cited, what the archive doesn't say marked to confirm, a citation to an excerpt it wasn't given taken out and a link of the model's making broken up, addressed and threaded as a reply; nothing sent test_replies.py test_a_cited_threaded_draft
Drafts edited, set aside and brought back, the text before kept; the mail that waits for an answer listed, not mailing lists' test_replies.py test_editing_setting_aside_and_listing
A pipeline's reply step drafts each new email, skipping mailing lists, no-reply senders and the person's own mail without a model call test_replies.py test_a_pipeline_drafts_only_mail_to_answer
Agents list what waits, draft with instructions, read and edit replies through MCP test_replies.py test_agents_draft_read_and_edit_replies
The Replies page drafts the first email's reply with Enter from its focused field, shows a draft with its sources and what's left to confirm, and opens it in your mail app without citations replies-page.test.tsx drafts a reply from the first field with Enter, saying what it should say; replies-page.test.tsx shows a draft with its sources and what's left to confirm, and opens it in your mail app
Drafts edited on the page (Ctrl+Enter saves, addresses checked) and set aside at once with Undo instead of a question replies-page.test.tsx saves an edit with Ctrl+Enter, and won't save an address that isn't one; replies-page.test.tsx sets a draft aside at once, with Undo instead of a question
A reply sent, or kept in Gmail's drafts, through the Gmail connection its email came in on, in its thread and without citations: only an admin signed in, only from a connection let send, never with something left to confirm, and never twice test_replies.py test_sending_after_the_persons_yes
Agents ask to send a reply through MCP; the ask waits in Approvals showing all of it, an agent can only decline it, and approved it sends the text it showed, never a later edit test_replies.py test_agents_ask_to_send_and_the_person_approves
The Replies page sends after a few seconds with Undo instead of a question, saving an edit first, and keeps a draft in Gmail at once; no Send without a connection that sends replies-page.test.tsx sends through Gmail after a few seconds unless undone, once nothing is left to confirm; replies-page.test.tsx keeps a draft in Gmail at once, and offers no Send without a connection that sends or to members

Work toward a goal in projects

Requirement Checks
A project with a goal, instructions and namespaces, a coordinator conversation and threads scoped to its namespaces, marked done, brought in or taken out; deleting it keeps its conversations test_projects.py test_a_project_with_a_goal_namespaces_and_threads, test_changing_its_namespaces_moves_its_conversations
A project is its owner's alone and draws only on namespaces they can read test_projects.py test_projects_are_their_owners_and_draw_on_readable_namespaces
Every answer in a project reads its goal and instructions; the coordinator is told about the threads, as data test_projects.py test_every_answer_in_a_project_reads_what_it_is_for
The coordinator starts threads with a brief, reads them and marks them done; a thread can't start threads; it asks first after reading outside text, and starts a few per answer at most test_projects.py test_the_coordinator_starts_threads_with_a_brief, test_starting_threads_after_reading_outside_text_asks_first
A brief from the person, an agent or the coordinator is answered in the background as the person; one cut off by a stopped process is marked, never left waiting test_projects.py test_a_brief_from_the_api_or_an_agent_is_answered_in_the_background, test_a_brief_cut_off_by_a_stop_is_marked_by_the_sweep
The Projects page starts a project with just a name, starts threads with a brief on Enter, shows which are working and marks them done in one click; projects sit under Chat projects.test.tsx starts one in the current namespace with just a name, then opens it, starts a thread with a brief on Enter and keeps the page, marks a thread done with one click, are under Chat in the navigation
A schedule asks its prompt as a brief in a new thread each time or always the same one, and leaves a thread still answering to finish test_projects.py test_a_schedule_asks_its_prompt_in_a_new_thread_or_the_same_one
Schedules wait while switched off or the project is archived, are their owner's alone, end with the project, and agents set them over MCP test_projects.py test_schedules_wait_while_off_or_archived_and_agents_set_them
The project page adds a schedule from a list in the person's time zone, switches one off in a click and removes one with Undo projects.test.tsx asks on a schedule picked from a list, in the person's time zone, shows when each runs, switches one off in a click and removes one with Undo
Agents list, read, start and change projects over MCP test_projects.py test_agents_use_projects_over_mcp

Extend the assistant

Requirement Checks
Tools: a prompt, a web request, a graph drawn on the canvas, or sandboxed Python test_extensions.py test_tools_and_skills_in_a_conversation, test_tools_drawn_on_the_canvas, test_python_tools_run_apart_and_only_admins_write_them
Skills, hooks and MCP servers, by code, on the canvas or by asking test_extensions.py test_written_as_code_shared_and_versioned, test_change_tools_ask_first_and_hooks, test_made_in_chat_behind_an_approval; journeys.mjs extension-add; baseline.json extension-add ≤ 3 clicks
Remote MCP servers connect by address test_mcp_servers.py test_an_admin_adds_a_server_and_the_assistant_uses_it
MCP servers that run as a program (npx, uvx) run in a sandboxed container of their own test_mcp_servers.py test_a_server_that_runs_as_a_program_runs_in_the_runner; mcp-runner.yml Runs an npm server and a PyPI server
Ready-made servers: the browser (a headless Chromium in a sandbox of its own, one session for everyone) and GitHub added in a click; the MCP runner, the browser and monitoring started and stopped from the web app through the upgrade agent, or the line to run shown without one test_services.py test_each_profile_is_turned_on_and_off_on_its_own, test_no_agent_shows_the_line_to_run, test_only_admins; test_mcp_servers.py test_the_browser_preset_keeps_one_session; mcp-server-ui.test.tsx adds the browser with one click once its container runs, turns the browser's container on first, when it isn't running, sets GitHub up with the token typed in its place
Settings › AI assistant › MCP servers lists every server with its tools and the containers' switches; agents list and add servers over MCP test_mcp_servers.py test_agents_list_and_add_servers; mcp-server-ui.test.tsx lists each server with its tools, and the containers' switches
An MCP server added by form or a pasted config, its tokens sealed and each tool switched on or off test_mcp_servers.py test_a_pasted_config_reads_as_servers_to_add, test_its_secrets_are_sealed_and_never_shown, test_each_tool_is_switched_and_says_whether_it_asks; mcp-server-ui.test.tsx adding an MCP server
Extensions kept as files: lens ext lists, pulls, checks and pushes manifests, here or over the API, losing neither side's changes test_cli_ext.py test_pull_edit_and_push_on_this_install, test_over_the_api_with_a_token
Skills and plugins from marketplaces in the open plugin format, a repository or an archive test_marketplaces.py test_a_marketplace_an_admin_adds, test_imported_from_a_link_or_a_file
Plugins found across marketplaces, updates shown, and installed by agents over MCP, the API or chat test_marketplaces.py test_found_updated_and_installed_by_agents; marketplace-ui.test.tsx an installed plugin with another version listed offers the update

Bring in everything

Requirement Checks
Watched folders on storage connections (S3, Dropbox, Google Drive, OneDrive, SFTP, SMB, WebDAV, local disks) test_sources.py test_sources_and_watches_without_rclone, test_importing_chosen_files_of_a_source
A local folder in four clicks journeys.mjs source-folder; baseline.json source-folder ≤ 4 clicks; watch-folder.test.tsx
A watched folder on each kind of storage connection, run against rclone in CI No check yet (#486)
IMAP mail, Gmail and Google Workspace test_mail_calendar_sources.py test_an_imap_account_as_a_source, test_an_imap_message_is_an_email_document_with_its_attachments; test_gmail_source.py test_a_workspace_mailbox_through_the_gmail_api, test_a_connected_google_account
A shared inbox's mail routed to namespaces, or skipped, by rules on a watch and shared rules on the email account that every watch on it tries after its own; agents read and change them over MCP test_mail_routes.py test_a_shared_inbox_routed_to_namespaces, test_an_accounts_shared_rules_apply_to_every_watch, test_agents_read_and_change_routing_rules
iCal calendars and Google Calendar test_mail_calendar_sources.py test_a_calendar_feed_as_a_source; test_google_calendar_source.py test_a_shared_calendar_read_as_a_service_account, test_a_connected_google_account
RSS and Atom news feeds: a feed or a site's page as a source, each post imported as text with its link, author and date; private networks only when allowed test_news_feed_source.py test_a_news_feed_as_a_source, test_feed_addresses, test_what_a_feed_holds_stays_data, test_a_feed_file_imported_as_text
Agents follow news feeds in one step: MCP subscribe_feed checks a feed and watches it into a namespace, news_feeds lists them with addresses sealed, unsubscribe_feed stops one; the assistant's tools ask first test_news_feed_source.py test_subscribing_in_one_step; test_setup_assistant.py test_the_assistant_follows_news_feeds
A built-in MQTT broker, syslog and webhook streams test_sensors.py test_hub_takes_devices_in_and_organises_them, test_syslog_senders_become_sensors, test_webhooks_and_one_list_of_sensors; journeys.mjs sensor-webhook; baseline.json sensor-webhook ≤ 3 clicks
Import webhooks for scanners and phone shortcuts test_import_hooks.py test_pushing_files_urls_and_text, test_owners_manage_hooks; import-hooks.test.tsx
Import webhooks hand back the PDFs of documents a light server can't convert test_import_hooks.py test_a_hook_hands_back_the_pdfs_of_documents_this_server_cant_convert
Connect with Google; Dropbox, OneDrive and Google Drive sign in through rclone on the server test_gmail_source.py test_connecting_the_account_in_the_browser; test_drive_google.py test_drive_connects_with_google_for_rclone; test_rclone_signin.py test_drive_signs_in_through_rclone_with_no_google_app, test_a_browser_on_the_server_reaches_rclone_itself, test_onedrive_finds_the_accounts_own_drive
Uploads of audio, PDFs, Office files, photos, JSON and archives test_uploads.py test_uploading_audio_in_chunks, test_json_uploaded_is_read_as_records; test_documents.py test_a_pdf_becomes_a_document_with_its_pages_and_text, test_scans_and_images_are_read_by_ocr; test_conversion.py test_office_and_text_files_become_documents; test_anytopdf.py test_an_uploaded_zip_keeps_its_files; test_archives.py test_reading_an_archive; journeys.mjs import-file, import-paste
Transcripts made elsewhere (text, Markdown, Word, PDF, SRT, WebVTT, JSON) imported with each line credited to the speaker the file names, WebVTT voice spans (<v Name>, <v.loud Name>) included test_importing.py test_formats_and_speakers_by_name, test_webvtt_voice_spans_name_each_line
A transcript imported without times says its times are estimated, and is lined up with its audio only when asked (API, MCP and one button): its words matched to the words the speech engine hears, each line taking their times and the rest placed between their neighbours, the text and speakers unchanged test_align.py test_lines_take_the_times_of_their_words_and_the_rest_fill_in_between, test_a_long_transcript_is_matched_as_well_as_word_by_word_and_quickly, test_imports_say_their_times_are_estimated_and_older_ones_are_recognised, test_lining_up_a_transcript_with_its_audio, test_agents_line_up_a_transcript_through_mcp; align.test.tsx is offered only for estimated times with audio, and not while a job is at it, lines it up in one click, then gets out of the way
Media browsers can't play (an AVI, HEVC, AMR) gets a copy they can, for the player only, off by default; files they play get none test_playable.py test_a_camera_clip_gets_a_copy_every_browser_plays, test_a_file_browsers_play_gets_no_copy
Photos and videos dated by when they were taken, from the file itself (a photo's EXIF date and GPS fix as place, a video's creation time) when no name or source dates them; a date already set is kept and none is invented test_file_dates.py test_a_photo_is_dated_and_placed_by_its_exif, test_a_date_already_set_is_kept, test_a_video_is_dated_by_its_creation_time, test_a_scanned_video_is_dated_by_its_creation_time
Assets placed by their file's GPS fix (a photo's EXIF, a video's ISO 6709 location) on the Library's map, listed by API (located, bbox) and MCP to members only, on tiles from a setting test_photo_map.py test_located_assets_are_listed_inside_a_box, test_an_agent_finds_what_was_taken_in_a_place, test_a_video_is_placed_by_its_location, test_the_map_tiles_are_a_setting; library-map.test.tsx placed assets are dots on the tiles from Settings, framed to show them all
An uploaded video, end to end No check yet (#486)
Apache Tika (opt-in) reads documents nothing else here can test_tika.py test_off_by_default_nothing_changes, test_what_nothing_here_can_convert_tika_reads_as_text
Web pages captured one at a time, from a link list or a bookmarks file, by API or by asking; private networks only when allowed test_webcapture.py test_a_web_page_becomes_a_document, test_an_admin_allows_a_private_network_in_settings, test_private_addresses_are_refused; test_web_links.py test_one_or_several_addresses_as_a_list, test_a_bookmarks_file_becomes_pages_to_capture; test_setup_assistant.py test_the_assistant_captures_web_pages
A content type chosen as a file or a page comes in, in Import, by API or to the assistant, runs that content type's pipeline from the start import-content-type.test.tsx offers the content types of the file's base type, and none chosen means recognised from the file; import-web.test.tsx captures pages as the content type chosen for them; test_uploads.py test_a_content_type_chosen_at_upload, test_a_held_file_is_placed_as_a_content_type; test_imports.py test_choosing_the_content_type_at_import; test_web_links.py test_pages_captured_as_a_content_type; test_setup_assistant.py test_the_assistant_imports_files_as_the_content_type_said
A file, page or video the namespace already has links to the one there instead of being imported again: a copy in a watched folder on this machine, a page or video address imported twice test_uploads.py test_a_copy_in_a_folder_on_this_machine_links_to_the_one_there; test_video_urls.py test_an_address_imported_again_links_to_what_the_namespace_has
A video's own captions, written by people, become its transcript instead of transcribing it, off by default test_video_urls.py test_a_videos_own_captions_become_its_transcript
Videos and podcasts imported by their address, off by default test_video_urls.py test_off_by_default, test_a_video_by_its_address_is_downloaded_then_transcribed; import-web.test.tsx imports a video or podcast by its address, when asked to
A bulk import (a folder scan, an archive's files, a link list, a source's files) is one run, listed and undone whole by its owners, by API, the Import screen or the assistant test_import_runs.py test_a_link_import_is_one_run_undone_whole, test_an_uploaded_archive_and_its_files_are_one_run, test_a_folder_scan_is_one_run, test_chosen_files_of_a_source_are_one_run, test_the_assistant_lists_and_undoes_an_import_with_approval; import-runs.test.tsx

Clip from the browser

Requirement Checks
A page, a selection or a part you pick becomes Markdown, looked over and sent to a namespace clip.test.js a full page becomes its article, with front matter, the selection, a picked element, with the selection template; clipper-edge.yml Smoke test in Edge
The clipper signs in to Lens, or takes your API token or an import webhook's, and says why a token is refused lens.test.js with the person's own token, the clip names its namespace, errors say what to do, by the kind of token, the popup offers each namespace a signed-in Lens can take, the picked one chosen; test_import_hooks.py test_people_push_with_their_own_token, test_the_clipper_signed_in_as_the_person
Built for Chrome, Edge, Firefox and Safari browser-extension.yml Test and build the web clipper, Build every target; browser-extension-firefox.yml Lint and package the Firefox clipper; safari-clipper.yml Build the Safari clipper; clipper-edge.yml Clip a page in Edge
New releases reach installed copies through a store or a self-hosted update file release.test.js update manifests
In Chrome and Edge, the side panel is a chat with the assistant assistant.test.js; clipper-edge.yml Smoke test in Edge
The assistant uses that browser on allowed sites, asking before anything that sends, deletes, buys or posts act.test.js; test_browser.py test_a_connected_side_panel_gives_the_assistant_the_browser, test_after_reading_a_page_the_browser_asks_before_what_the_person_didnt_write

Understand it

Requirement Checks
Transcription test_speech.py test_an_openai_compatible_server_transcribes, test_deepgram_transcribes_with_speakers_and_sentiment
Transcription that expects the names the namespace knows (custom vocabulary, named people, defined entities, names people fixed), off by default test_speech.py test_the_namespaces_own_names_go_to_the_engine
Transcription by a local engine (SenseVoice, Whisper), run in CI No check yet (#486)
One asset's voice split off from the person it was matched to, with Undo, and a voice never matched or suggested again as someone it was said not to be test_speakers.py test_detach_one_assets_voice; test_voices.py test_a_voice_never_goes_to_someone_it_was_said_not_to_be
How sure transcription was of each word kept (Whisper, Deepgram, AssemblyAI, ElevenLabs), words from before still loading, the unsure ones marked in the transcript with how sure, a threshold in Settings (0 for none), and unsure words per line over MCP test_speech.py test_providers_keep_how_sure_they_were_of_each_word, test_whisper_keeps_how_sure_it_was_of_each_word; test_split_merge.py test_words_keep_how_sure_transcription_was, test_words_from_before_how_sure_was_kept_still_load; test_mcp.py test_get_transcript_says_which_words_transcription_was_unsure_of; recording-split-words.test.ts keeps how sure transcription was of each word, and words from before it was kept, marks the words below the threshold, and none when it's off; recording-unsure-words.test.tsx are underlined with how sure, and a click still plays the line
The language transcription hears kept on the asset, and a person sets or corrects it as a BCP 47 tag with a region (en-GB, pt-BR), checked and written the usual way, kept over the detected one when it is transcribed again; through the Details tab, the API and MCP test_language.py test_tags_are_written_the_usual_way, test_what_isnt_a_tag_is_refused, test_editors_set_and_correct_an_assets_language, test_transcribing_again_keeps_the_language_a_person_set, test_a_pipeline_condition_on_en_takes_its_regions, test_agents_set_it_over_mcp; test_speech.py test_whisper_keeps_the_language_it_heard; recording-language.test.tsx sends a tidy tag, null to forget it, and nothing when it's the same, lets an editor correct it with Enter, and refuses what isn't a tag
People recognised by voice; long recordings sent to a speech provider in pieces that fit its limits test_voices.py test_match_suggest_merge_undo_link, test_merging_keeps_who_they_are_not, test_upgrade_gives_pairs_lost_to_merges_to_who_they_were_merged_into; test_speech.py test_a_long_recording_goes_in_pieces_and_comes_back_whole, test_a_piece_too_large_to_send_is_halved
An owner turns voice recognition off for a namespace: voices told apart in each recording only, its voiceprints deleted test_voice_mode.py test_voices_can_be_told_apart_without_being_recognised
Text on screen and in scans test_video.py test_shots_text_search_chat_and_iiif; test_documents.py test_scans_and_images_are_read_by_ocr; test_ocr.py test_a_video_read_by_doctr_and_without_it
Faces, off by default test_video.py test_people_on_screen, test_shots_text_search_chat_and_iiif; test_video.py test_face_modes
The faces nobody has named yet, the largest group first with crops, the assets they're in and a name to offer, named one after another (Enter for the next), a name someone has joining that person, with Undo; through the API, MCP and the People page test_unnamed_faces.py test_unnamed_faces_largest_first_named_in_a_row_with_undo, test_only_where_faces_are_recognised, test_agents_list_and_name_faces; jest name-faces.test.tsx focuses the largest group first; Enter names it and moves on to the next, with Undo, joins a face to the person who has the name, and Undo takes the merge back
NSFW pictures flagged by a model on your own server, off by default, per namespace test_nsfw.py test_off_by_default_nothing_is_fetched_or_classified, test_a_namespace_classifies_flags_blurs_and_hides; nsfw.test.tsx
Low quality images enhanced before they're read, off by default test_enhance.py test_off_by_default_the_original_is_read, test_a_low_quality_image_is_read_from_its_enhanced_copy, test_a_good_image_is_left_as_it_is, test_agents_and_automations_enhance_too, test_the_copy_is_encrypted_where_the_files_are; enhance.test.tsx
Entities and topics in a knowledge graph to explore and query with Cypher or SPARQL test_graph.py test_query; test_rdf.py test_sparql; journeys.mjs graph-find
The graph as it was at any version test_graph_history.py test_the_explorer_and_cypher_as_of_a_version, test_the_canvas_as_of_a_version; test_graph_history.py test_every_curation_is_an_event_and_every_version_replays
Sources, sensors, people and agents as actors in the graph, with ADDED links to what they brought in, on the canvas, in the explorer and in Cypher test_graph.py test_sources_are_actors_in_the_graph; jest graph-explorer.test.ts shows a source only while something it brought in is visible
A For you order of the timeline, learned from each person's own reactions, comments, marks and opens, each item saying why, with Latest the default; through the API, MCP, the assistant and the page test_timeline_for_you.py test_for_you_learns_from_reactions_comments_and_marks_and_says_why, test_opening_an_item_counts_for_you_a_little_each_time_up_to_a_few, test_a_persons_affinity_is_reused_for_a_minute_until_a_signal_changes, test_agents_read_for_you_too; jest timeline-model.test.ts keep the For you order as a view, not a filter, timeline-ui.test.tsx For you: the toggle goes in the address, each placed line says why, and Latest stays the default
Muting a source, a person or a kind on the timeline, for good or until a time, for the person alone, with nothing deleted; through the API, MCP, the assistant (asking first after outside text) and the page test_timeline_mutes.py test_a_mute_hides_an_actor_or_a_kind_for_the_person_alone, test_agents_mute_too_and_the_assistant_asks_first_after_outside_text; jest timeline-ui.test.tsx muting a source takes everything from it out, lists it as muted, and Undo or the chip brings it back, the menu beside Mute snoozes a source for an hour, a day or a week, or mutes the item's kind, a snooze can end at a time you pick, nine tomorrow to start with, and Enter mutes, timeline-model.test.ts a snooze ends an hour, a day or a week from now, a snooze to a time you pick starts at nine tomorrow, in the browser's own time
An asset moved to another namespace is asset.moved, a Move seen where it is now, and a sensor a person pauses, ignores, resumes or sets back to new is sensor.changed, an Update with the status it had before, seen in its namespace or by admins; a change that changes nothing logs nothing test_timeline_verbs.py test_moving_an_asset_is_a_move_seen_where_it_is_now, test_switching_a_sensor_is_an_update_with_what_it_was
A file a watched folder finds changed in its source and reads again is file.changed, an Update linking the asset it was before, not a second file.added; it is still a child of asset.added, so triggers on new assets fire on it as before test_events_content.py test_a_file_changed_in_its_source_is_file_changed_linked_to_the_asset_it_was_before
An opened timeline item shows what it caused, the runs it started of the automations the person manages and the approvals those asked for, with Approve and Decline on waiting ones; GET /activities/{id}/caused, approvals?activity=, MCP activity_caused and the assistant test_timeline_caused.py test_an_item_lists_what_it_caused_and_the_waiting_approval_is_decided_there, test_agents_read_what_an_item_caused_too; jest timeline-ui.test.tsx an opened item shows what it caused, and a waiting approval is approved there
One timeline item by its id in the address (/timeline?item=<id>), opened above the timeline with its actions and a way back, which is where a comment on an item, a reaction and an approval's "because" link jest timeline-ui.test.tsx the address can name one item, opened above the timeline, with a way back; test_timeline_social.py test_comments_on_a_logged_activity_follow_its_audience
Saved views of the timeline: its filters and order under a name, each person's own, picked from the page's Views menu and through the API and MCP test_timeline_views.py test_timeline_views_are_saved_renamed_changed_and_deleted_by_their_maker_alone, test_agents_list_save_and_delete_timeline_views; jest timeline-model.test.ts a saved view keeps the filters and order as they are, and offers a name from them, timeline-ui.test.tsx Views: one click shows a saved view, and the filters shown now are saved under a guessed name, Views: the view the filters match is named on the button, and deleting it can be undone
Every entity's own page at /wiki/<namespace>/<Name> test_entities.py test_wiki_addresses; wiki-page.test.tsx; journeys.mjs entity-find
Entities, topics and people edited and merged from their lists, the graph or an asset, with Undo instead of a confirm test_entities.py test_suggestions_and_merge_undo; entities-curate.test.tsx; topics-curate.test.tsx; recording-entities-edit.test.tsx; journeys.mjs entity-merge, graph-merge, speaker-rename
Names get their real types (people, places, organisations, events, works) by rules, without a model; older Terms are typed on upgrade test_name_types.py test_names_get_the_type_their_words_give, test_analysis_types_people_places_and_events, test_the_upgrade_types_the_old_terms_and_keeps_what_people_chose
A decision model (Jev or Laya) types the names the rules leave as Terms, in one call, keeping only types it is sure of; Settings › Analysis can leave them Terms test_name_types.py test_the_decision_model_types_what_the_rules_cant, test_a_type_the_model_isnt_sure_of_stays_a_term, test_by_default_the_language_model_isnt_asked, test_with_typing_rules_nothing_is_asked
What is most like a passage, an asset, a topic or an entity test_similar.py test_like_a_text_by_its_key_words, test_like_a_resource_by_words_topics_and_entities, test_like_a_topic_and_an_entity; similar.test.tsx
Search finds and filters moments by the sound heard in them (laughter, applause, music), typed any way, and counts each test_search.py test_sound_events_filter_and_count; test_semantic.py test_search_by_meaning_keeps_to_a_sound; search.test.ts reads sound events typed any way
Search keeps to the assets recorded between two days, with a tag, from a source or in a language, by the API, MCP and the search page (empty until picked, kept in the link and in saved searches) test_search.py test_dates_tags_origins_and_languages_narrow_search; search-asset-filters.test.tsx starts with none set, so the search is as it was, and picking one puts it in the address, asks with the ones in the address, shows each as a chip, and says which to clear when nothing matches; search.test.ts reads dates, tags, sources and languages, and keeps them in the address
With anytopdf, photos found by what they show test_photos.py test_photos_found_by_what_they_show, test_off_by_default
Pages and shots described by the provider's vision model or, off by default, by Florence-2 on this server, fetched pinned and checked when picked test_florence.py test_florence_describes_pictures_without_a_model_provider, test_florence_is_fetched_pinned_and_checked, test_admins_and_agents_choose_what_describes_pictures, test_a_picture_is_read_as_the_model_was_trained
Or by Moondream on this server, off by default, fetched at a pinned revision only where the chosen model can run, and saying why where it can't test_moondream.py test_moondream_describes_pictures_without_a_model_provider, test_where_each_moondream_can_run, test_on_a_cpu_moondream_3_is_never_fetched_and_the_step_says_why, test_moondream_is_fetched_pinned, test_admins_and_agents_choose_moondream
A recording's sound cleaned up before transcription in a pipeline that has the step (noise, gain, echo), with silence left out of what's transcribed and the transcript's times kept on the recording's timeline, optionally only for low quality recordings test_audio_cleanup.py test_speech_is_found_and_silence_left_out_of_transcription, test_echo_of_the_far_side_is_taken_out, test_without_the_step_or_with_every_option_off_it_is_transcribed_as_it_is, test_an_agent_reads_what_the_step_did, test_quality_gate_leaves_a_clean_recording_as_it_is, test_quality_gate_still_cleans_a_low_quality_recording
Transcripts cleaned up before analysis (stutters, "um"s, spacing), off by default, each change in the edit history with Revert and people's corrections left alone test_cleanup.py test_off_by_default_changes_nothing, test_clean_step_tidies_lines_and_keeps_them_in_the_history, test_people_s_corrections_outrank_the_rules
Punctuation put back in transcripts that lack it by a local model, fetched only when picked test_cleanup.py test_punctuation_put_back_where_it_is_missing, test_without_the_model_the_rules_run_alone, test_the_model_is_fetched_only_when_it_is_asked_for
Grammar fixed by a LanguageTool server you run, when its address is set: grammar fixes only, never spelling or a word with a capital test_cleanup.py test_grammar_fixes_from_languagetool_leave_names_and_spelling, test_without_its_server_the_rules_run_alone, test_the_grammar_server_is_an_address_without_a_password
Entities found by spaCy, fetched the first time it's picked, with the defined entities first and the rules until it's there test_spacy_entities.py test_defined_entities_come_first_then_spacy_s_names, test_the_analyze_step_and_workflows_use_it, test_without_spacy_the_rules_find_them, test_spacy_is_fetched_only_when_it_is_picked
Dates said in an asset read as days ("next Tuesday", "last week"), off by default, with dates that read two ways left out test_dates.py test_dates_are_read_as_days, test_what_could_be_two_dates_is_left_out, test_off_by_default_reads_none
A transcript translated into another language only when someone asks, the estimate first: in batches of lines that keep their times and speaker, the transcript marked as data, every call in the activity ledger and within budgets, kept as a translation file like an uploaded one; through the web app's Files tab, the API, MCP and the assistant (asking first) test_translations.py test_translating_writes_a_translation_file, test_the_transcript_reaches_the_model_as_data, test_lines_go_in_batches_and_keep_their_place, test_a_spent_budget_refuses, test_the_estimate_comes_first, test_what_stops_a_translation, test_agents_translate, test_the_assistant_asks_first; recording-translate.test.tsx is the first the browser reads that the transcript isn't in, translates into the browser's language with Enter, after showing the estimate
A date said in an asset is proposed as a calendar event in one click, prefilled from its line (the day, a time said, the line as its title, who it names), and added only once the person approves it, signed in; declined, nothing is added; offered only to admins with a calendar connection that may add events, and agents propose over MCP but can't approve test_event_proposals.py test_a_date_said_is_proposed_and_added_only_once_approved, test_a_declined_proposal_adds_nothing, test_offered_only_to_admins_with_a_calendar_that_adds_events, test_agents_propose_over_mcp_but_cannot_approve, test_the_time_of_day_is_read_from_the_line; said-dates.test.tsx proposes an event from a date in one click, and says it waits in Approvals, offers no Propose event without a calendar that adds events, and shows nothing without dates
Videos play their transcript and timed supplementary captions and translations as WebVTT tracks with signed links, in the workspace (drawn by the browser only where the overlay can't be seen) and on the public page (off until a button turns them on), with only what the viewer may read test_caption_tracks.py test_the_player_offers_the_transcript_and_timed_files_as_tracks, test_the_public_page_offers_what_the_visitor_may_read_as_tracks; jest caption-tracks.test.tsx stay undrawn while the page's own overlay shows the captions, and are drawn full screen, plays its text as captions the browser draws once the visitor turns them on

Learn from it

Requirement Checks
A two-host podcast episode, every claim cited and checked against its source, in two voices test_podcasts.py test_episode_is_made_by_a_job, test_fact_check_rewrites_or_drops, test_episode_gets_two_voices; journeys.mjs podcast-create; baseline.json podcast-create ≤ 5 clicks
A technical spec from a transcript, in a note linked to it: decisions, nos and constraints in the speaker's words linked to when they were said, gaps left to decide flagged, quotes checked against the transcript; through the web app's More actions menu, the API, MCP, the assistant (asking first) and pipelines test_specs.py test_writing_a_spec, test_quotes_are_found_word_for_word, test_writing_it_again_keeps_one_note, test_long_transcripts_say_where_they_were_cut, test_agents_write_specs, test_the_assistant_asks_first, test_the_template_is_there_to_edit; recording-spec.test.ts offers to write a spec until there is one, opens the spec and writes it again once it's written
Notes beside every asset, entity and topic, written by you or the assistant test_notebook.py test_pages_of_things, test_links_and_backlinks, test_assistant_keeps_notes; journeys.mjs note-create
A tree of notes renamed, moved and pinned (pins are your own), today's note one click away, and a search test_notebook.py test_tree, test_pins, test_search; notes-tree.test.tsx; notes-today.test.tsx is one click from the Notes page
A note, or all of a namespace's notes, downloaded as Markdown in folders that follow the tree test_notebook.py test_markdown_export; notes-tree.test.tsx downloads a note as Markdown; notes-find.test.tsx downloads every note as Markdown from its menu
Templates a note starts from, and a copy of any note test_notebook.py test_templates_and_copies; notes-templates.test.tsx; notes-tree.test.tsx makes a copy beside the note and opens it
A note shows the notes it's inside and the notes inside it, to people and agents test_notebook.py test_path_and_children, test_assistant_keeps_notes; test_mcp.py test_note_tools; note-page.test.tsx
Undo and Recently deleted instead of a confirm test_notebook.py test_recently_deleted; notes-delete.test.tsx
A namespace publishes a private RSS feed or a podcast feed (Apple Podcasts' tags, audio with byte ranges) of resources, episodes, timeline activities and text, holding only what its readers can already read; through the API and MCP test_feeds.py test_making_filling_and_reading_a_feed, test_a_podcast_feed_lists_what_has_audio, test_timeline_activities_in_a_feed, test_vaults_and_settings, test_agents_use_feeds
A saved collection is a private podcast feed of its assets with audio or video: one address per person who may read it, listing only what they can read, from the collection's page in one click with Copy, rotated or stopped like a namespace's feed; through the API and MCP test_feeds.py test_a_collection_feed_lists_its_assets_with_media, test_a_collection_feed_token_rotates_and_stops, test_agents_get_a_collection_feed; collection-feed.test.ts
A routine fills a feed on a schedule, or makes a podcast episode about what's new: graph workflow nodes pick the resources or timeline activities since the last run, add them to a feed, or make an episode about them; Try says what they would do test_feed_nodes.py test_a_routine_publishes_new_resources_to_a_feed, test_a_routine_publishes_the_timeline_and_text, test_a_routine_makes_an_episode_and_the_feed_lists_it_once_made, test_try_says_what_it_would_do_and_bad_settings_are_refused
Feeds in the web app: a Feeds page, reached from the Podcasts page and under Explore once there is a feed; New feed with the title focused and Enter making it in the current namespace; a feed's page copies the address in one click, gets a new one, edits what readers show, and takes items out or deletes the feed with Undo instead of a confirm; the publishing nodes on the canvas, checked before Publish feeds-ui.test.tsx the Feeds page lists each feed with its kind, namespace, items and whether an app reads it, New feed opens on the title, defaults to the current namespace, and Enter makes it and opens its page, a feed's page copies the address, gets a new one, and takes an item out with Undo, deleting a feed is undone by making it again with its items, at a new address, editors change what readers show, and Enter in a field saves; workflow-flow.test.ts a Select node into Publish to feed or Podcast episode is a whole graph workflow, says what to fix: a feed to choose, the limits, and a graph that publishes nothing, names the selection and the feed on the canvas; nav-groups.test.tsx feeds are under Explore, after Podcasts, for everyone, Feeds is in the nav only once there is a feed, so a phone's nav stays as short as it was
A moment of any resource kept as a clip (a title, a note) in one click from the transcript or a highlight, in the playlist used last; playlists play clips back to back, reorder and remove with Undo, and each reader gets only the clips of resources they can read; through the API and MCP test_playlists.py test_playlist_crud_and_clips, test_order_reorder_remove_and_undo, test_last_used_is_the_default, test_readers_get_only_clips_of_resources_they_can_read, test_clips_of_highlights, test_clips_go_with_their_resource_and_private_playlists_with_their_maker, test_agents_list_playlists_and_add_moments; playlists-model.test.ts; playlist-page.test.tsx reorders with Up and Down and saves the new order, removes a clip at once, and Undo puts it back in its place; recording-comments.test.tsx keeps a highlight as a clip in the latest playlist in one click, with Undo, makes a first playlist called Clips when there is none; nav-groups.test.tsx Playlists is in the nav only once there is one, under Explore after Feeds
An asset or a collection exported as an evidence PDF, whole or the parts you pick test_evidence.py test_a_collection_as_one_evidence_pdf, test_everything_lens_found_goes_in, test_picking_the_parts; evidence-parts.test.tsx

Automate it

Requirement Checks
Pipelines and workflows on a canvas test_workflows.py test_workflows_run_in_pipelines_drawn_as_graphs; workflow-canvas.test.ts; journeys.mjs pipeline-create, workflow-create
What a pipeline's model step or a workflow's model node writes about an asset is found by search, by the API and MCP, under the step's name; running it again replaces those words, a step can opt out, deleting the asset removes them, and outputs written before are made searchable on upgrade test_generated_search.py test_a_pipelines_model_step_is_found_by_search, test_searchable_is_an_llm_steps_setting, test_a_workflows_model_output_is_found_by_search, test_outputs_a_model_wrote_before_become_searchable; search.test.ts says where what wasn't said was found: on screen, on a page, seen, what a shot or page shows, or a model wrote; pipelines-templates.test.ts keeps an llm step's words in search unless it opts out
Convert steps make new assets: a video's soundtrack as audio, a document's pages as images, each run through its own pipeline test_conversions.py test_a_videos_soundtrack_becomes_an_audio_asset, test_a_documents_pages_become_image_assets, test_settings_are_checked; pipeline-convert.test.ts is added from the library ready to run: to audio unless images are picked
A pipeline's Read step picks its own converter (auto, anytopdf or Lens's own) in place of the server's, checked as it's saved test_anytopdf.py test_a_read_step_picks_its_own_converter; pipeline-convert.test.ts is kept only on a read step, and said in its summary
A Classify node in workflows chooses from labels, content types or topics, and acting on a sure answer keeps something test_classify.py test_a_workflow_classifies; workflow-flow.test.ts chooses from labels, content types or topics, and acting on a sure answer keeps something
An Entity filter node keeps a list's entities by type, stop list, length, pattern or how often they're said test_flow.py test_entity_filter_keeps_the_entities_asked_for; workflow-flow.test.ts an entity filter says what it keeps and refuses lowercase types
An HTTP call node calls a web address through the web tools' guard, with its host written out, its credentials sealed to their host, and a try that only reads test_http_nodes.py test_it_calls_the_address_and_passes_the_answer_on, test_where_it_goes_is_written_out_and_guarded, test_a_try_only_reads; workflow-flow.test.ts an HTTP call node's host is written out
Routines on a schedule test_routines.py test_routines_are_checked_and_run_when_due, test_schedules; journeys.mjs routine-create; baseline.json routine-create ≤ 2 clicks
Graph workflows find look-alikes to merge or link, wrong types, noise and topics to group, make the sure changes and propose the rest, each undoable and not made again once taken back test_routines.py test_graph_workflows_organise_entities, test_rules_read_types_and_noise, test_graph_workflows_retype_and_hide, test_graph_workflows_group_topics; graph-changes.test.tsx say a type change in words and make it, show a group of topics under a new broader one
Everything that runs on a clock (routines, automations' schedule and at triggers, the backup) in one Schedules list, switched, re-timed and run from it, by people and by agents over the API and MCP test_schedules.py test_one_list_holds_every_routine_automation_schedule_and_the_backup, test_schedules_are_changed_switched_and_run_from_the_list, test_agents_list_change_and_run_schedules_over_mcp; schedules-page.test.tsx lists every routine, automation schedule and the backup, each linking to its own page, switches one off, runs one now and changes when one runs, from the list
Batch runs over a collection test_batches.py test_collections_and_batches
Notifications to webhooks, Slack, Discord and Matterbridge test_notifications.py test_owners_manage_targets_and_a_test_is_signed, test_runs_that_end_are_sent_once_to_matterbridge
A routine's run that fails or leaves graph changes to review is told through Matterbridge and webhooks, to each namespace it runs over test_notifications.py test_routine_runs_that_fail_or_leave_changes_to_review_are_told
Everything that happens is kept as W3C Activity Streams activities, read by agents through the API, MCP and the assistant test_events.py test_activities_have_one_shape; test_activities.py test_the_timeline_lists_new_assets_newest_first, test_agents_read_the_timeline_too; test_news_feed_source.py test_a_news_feed_as_a_source
Ask the assistant about a timeline item from the item (its button, or a on its line), in the chat beside the page, with the item's id, line and fields as what the question is about timeline-ui.test.tsx asking about an item hands the page chat its id, line and fields, by button or the a key; page-chat.test.tsx opens with what a page's own Ask about this hands it, and sends it as the highlighted part
The Timeline page and Latest on Home, marking what you've seen test_timeline_marks.py test_marks_are_set_cleared_and_filter_the_timeline, test_a_tidy_drops_marks_and_reactions_left_on_activities_that_are_gone; timeline-ui.test.tsx Home's Latest card lists what you haven't reviewed
Reactions and comments on timeline activities, shared by everyone who sees them, each reaction saying who gave it, comments edited and threads resolved in place, through the API, MCP and the page test_timeline_social.py test_reactions_are_shared_counted_and_taken_back, test_comments_on_an_activity_are_a_thread_everyone_who_sees_it_joins, test_comments_on_a_logged_activity_follow_its_audience, test_agents_react_and_comment_too, test_a_reaction_names_who_else_gave_it_earliest_first_up_to_five; timeline-social-model.test.ts a reaction's hover says who gave it: you first, then up to five names, then how many more; timeline-ui.test.tsx reactions and comments: a line sums its items', a 👍 goes to the server, and Enter sends a comment, your comment is edited in place, and a thread you can resolve is resolved and reopened from the timeline
Tagging @lens in a comment on the timeline or an asset gets the agent's answer in the thread, as the person who tagged it, with every change waiting for their approval, off with ai.answer_comments test_comment_agent.py test_the_agent_answers_a_tagged_comment_in_the_thread, test_without_a_model_or_within_a_used_up_budget_the_reply_says_so; timeline-social-model.test.ts the agent's reply links its addresses
Automations act on the stream, on a schedule, at a time or when their own webhook is called test_automation_runs.py test_each_new_activity_runs_each_automation_it_matches_once, test_a_schedule_runs_its_automation_when_its_timer_fires, test_a_call_to_a_webhook_runs_its_automation; test_timer_triggers.py test_an_at_trigger_fires_once_then_reads_done
They test conditions, then tell you, ask the model or the agent, use a tool, call a web address with sealed credentials, run Lens work, or change assets and notes test_automation_runs.py test_the_if_limits_and_dry_runs; test_actions_messages.py test_notify_tells_me_in_my_chat_app; test_actions_ai.py test_ask_model_answers_in_text_or_as_the_fields_a_schema_names, test_ask_agent_works_on_what_happened_and_saves_the_conversation, test_call_tool_reads_as_the_person_and_changes_only_by_asking_them; test_actions_web.py test_credentials_in_headers_are_sealed_to_the_automation_and_the_host_and_never_shown; test_actions_run.py test_run_workflow_queues_it_on_the_asset_as_the_automation, test_run_pipeline_processes_the_asset_again; test_actions_lens.py test_set_field_is_kept_in_the_assets_history_by_the_automation, test_write_note_writes_a_page_adds_to_a_things_page_or_notes_an_asset
What reaches outside Lens, or changes more than what set it off, asks first: on the Approvals page or with a yes in chat; agents decline but never approve test_automation_spec.py test_risk_follows_the_effect; test_approvals.py test_approve_carries_the_action_out_and_the_run_records_it, test_agents_over_mcp_list_and_decline_but_never_approve; test_approvals_chat.py test_a_yes_in_my_chat_decides_an_action_inside_lens; approvals-page.test.tsx groups what waits, puts the first in focus, and Enter approves it
Conditions test text, patterns, numbers, lists, times, days and whether a value changed, checked as they're saved and read in words test_match_ops.py test_each_test_passes_what_it_should, test_checks_refuse_values_a_test_cant_use, test_a_pattern_reads_a_long_value_only_so_far_and_gives_up_when_slow, test_changed_compares_with_the_last_value_seen, test_the_added_tests_read_in_words
Automations fire when an entity is mentioned: any entity of a type, a name with * and ? wildcards, or an id, among up to fifty entities a processed asset is tagged with; one that names neither doesn't fire, and conditions saved before pass as they did test_automation_mentions.py test_a_wildcard_a_type_and_an_id_find_a_mention, test_a_glob_is_linear_and_checks_refuse_what_it_cant_use, test_trees_saved_before_still_pass_as_they_did, test_an_automation_fires_on_any_entity_of_a_type_or_a_name_pattern_and_not_on_others, test_a_processed_asset_is_tagged_with_up_to_fifty_entities
Conditions look back over what happened in the automation's namespaces, as its person may see it, and compare each activity with the last one; a dry run keeps nothing test_match_seen.py test_seen_finds_what_happened_in_the_window, test_an_if_looks_back_and_reads_the_context, test_seen_is_only_for_an_automations_if; test_automation_conditions.py test_the_if_looks_back_in_the_automations_namespaces, test_changed_in_a_where_compares_with_the_last_activity, test_the_if_and_a_condition_node_agree_and_dry_runs_keep_nothing
Joins wait for an automation's triggers together: all of them, a number of times, one that doesn't come in time, or a batch, on timers that outlast a restart test_match_joins.py test_all_waits_for_every_trigger_within_the_window_on_the_same_key, test_count_fires_once_per_crossing_then_counts_again, test_absent_waits_with_a_timer_and_the_missing_one_cancels_it, test_batch_collects_then_runs_once, test_two_writers_at_once_lose_nothing; test_automation_conditions.py test_an_absent_join_waits_on_a_timer_and_switching_off_drops_it
Each automation has limits and budgets test_automation_runs.py test_the_if_limits_and_dry_runs, test_a_run_over_budget_is_held_for_the_persons_pick
Agents make, try and run automations over MCP and in the assistant, or draft one from a sentence test_automation_tools.py test_agents_save_try_and_run_automations_over_mcp, test_the_assistant_saves_at_once_and_asks_to_switch_on_or_run; test_automations.py test_an_automation_is_drafted_from_a_sentence
Starter automations come switched off, each one saving as it is, and changing nothing until someone turns it on; in the builder one fills it in to save and turn on test_automation_starters.py test_every_starter_saves_as_it_is_and_stays_off, test_the_school_starter_tells_me_at_once_and_asks_before_posting_to_the_family; automations-ui.test.tsx a starter fills the builder, and Save and turn on saves it switched on; journeys.mjs automation-starter; baseline.json automation-starter ≤ 4 clicks, 0 keys
From an email to a yes: an automation tells you at once, asks before posting to the family, carries the post out on approval, and its run shows on the timeline test_automation_journey.py test_an_email_runs_an_automation_that_tells_me_asks_before_posting_and_shows_on_the_timeline
The automations guide reads from a five-minute walkthrough to the reference, and asked "how do I make Lens tell me when the school emails?", the assistant and agents find that walkthrough test_lens_help.py test_the_automations_guide_answers_how_to_be_told_when_the_school_emails
An automation's workflow is drawn and tried on the canvas: its actions in the palette, their settings drawn from each action's params, Decide's ports and the leaf rule workflow-canvas.test.ts give a decide node a port per option, then unsure, keep the leaf rule: nothing after an action that reaches outside Lens or waits, say what the backend would reject in their settings; automation-nodes-ui.test.tsx sets an action up from the catalog, its params drawn from its schema; schema-form.test.tsx opens the variable picker on {{ and puts the pick in with Enter
In the web app, automations are listed with what sets each off in words, and built in When, If and Then with a test on a recent activity, problems next to their fields automations-ui.test.tsx the list reads each automation, and Delete is undone from its toast, the builder adds a trigger with a condition and an action, then saves it switched off, the test panel runs the draft on a recent activity and says what it would do, API problems go next to what they are about; test_automations.py test_lists_read_the_triggers_in_words_and_say_when_it_runs_next
An automation's page shows its runs a page at a time with each one's log, what's coming up, its variables to edit and its costs automation-detail.test.tsx Runs pages back with before, and a run opens to what it did and its log, a run held over a budget can run once or be skipped, Coming up lists its times, and says why when there are none, Variables set from the keyboard, and Clear is undone from its toast

Share with the people close to you

Requirement Checks
Circles of people on the same Lens are each person's own, and hold only people they may add test_circles.py test_circles_are_your_own
Sharing an activity or a resource with a circle and a note shows it in each member's own timeline and the sharer's, with the item or without, undone with its permissions, never from a vault test_circles.py test_sharing_with_a_circle
Automations follow what was shared with the person; removing someone takes their circles and what was shared with them test_circles.py test_automations_follow_shares
Agents make circles and share over MCP; the assistant shares only after the person's yes test_circles.py test_agents_share_too
Circles live under Profile, a suggested circle takes one click and deleting one is undone, not confirmed circles-ui.test.tsx makes a suggested circle in one click; circles-ui.test.tsx deletes a circle with Undo instead of a confirm
Share on a timeline item or a resource picks the only circle, takes a note and is undone from its toast circles-ui.test.tsx picks the only circle, shares with a note, and undoes from the toast
A rule shares what comes in with a circle and a note, asking first until the person lets it run on its own test_circles.py test_rules_share_on_their_own_only_when_let
A circle sees when you're free only once you let it, only when and never what, each ask in your timeline; agents and the assistant find a time test_circles.py test_when_people_are_free; circles-ui.test.tsx lets a circle see when you're free in one click
Circles reach people on other servers only once an admin and the person turn it on: shares go to them signed, and theirs are taken only signed, recent and from someone in a circle test_federation.py test_off_until_turned_on_and_findable, test_people_elsewhere_get_shares_and_send_them, test_notes_from_people_in_a_circle_only; circles-ui.test.tsx makes you findable, and adds someone elsewhere by handle with Enter
Someone's agent answers a question only from the namespaces they let that circle ask about, citing them, and they see it test_circles.py test_asking_their_agent; circles-ui.test.tsx lets a circle ask your agent about the namespaces you pick

See what it costs

Requirement Checks
An activity ledger of every model call, with reports test_activity.py test_a_routines_calls_count_for_it_its_run_and_the_jobs_it_queued, test_chats_and_people_see_only_their_own
Models priced by tokens or by the hour, and speech providers by the minute heard or the character read test_activity.py test_models_are_costed_by_tokens_by_time_or_not_at_all; test_speech.py test_speech_calls_are_costed_per_minute_heard_and_per_character_read; settings-prices.test.ts round-trips every unit
One overall budget, asked after a cloud model, with a soft limit that warns and a hard limit that stops paid calls test_budgets.py test_one_budget_for_everything_with_a_soft_and_a_hard_limit; test_setup.py test_the_wizard_asks_only_what_has_no_answer
A meter in the top bar; the Costs page by day, model and what cost most budget-meter.test.tsx the top bar's budget meter; test_budgets.py test_the_meter_follows_the_namespace_and_its_budget
The assistant and agents over MCP answer where budgets stand (budgets), each person seeing only their namespaces test_budgets.py test_agents_ask_where_budgets_stand
The Costs page's charts No check yet (#486)
Routine choices go to small decision models, the LLM only when it adds value test_decisions.py test_the_decision_model_answers_when_it_has_a_key, test_the_language_model_decides_without_one_and_when_it_fails

Scale out

Requirement Checks
Off until an admin turns it on; off, every unit is let go and the scaler writes nothing test_scaling.py test_scaling_is_off_and_lists_its_drivers, test_the_settings_section, test_this_machine_runs_more_workers_at_no_cost
Providers keep their credentials sealed, shown only as set, and prove them with a read-only call test_scaling.py test_a_providers_credentials_are_sealed_and_checked
A group launches nothing until a person signed in to the web app approves its terms; agents never approve, and widening the terms takes the approval away test_scaling.py test_only_a_person_in_the_web_app_approves_a_groups_terms; test_scaling_rules.py test_what_widens_an_approval
Target, step and schedule rules pick the number of units, within min and max test_scaling_rules.py test_a_target_rule_keeps_its_metric_at_the_target, test_a_step_rule_fires_once_its_condition_has_held, test_a_schedule_rule_sets_the_bounds_when_its_time_comes, test_the_largest_proposal_wins; test_scaling.py test_rules_scale_on_waiting_jobs_and_on_a_schedule
The scaler launches units, sees them join, drains busy workers before letting them go, and fails and pauses on units that don't come up test_scaling.py test_the_scaler_launches_joins_drains_and_terminates, test_a_unit_that_never_joins_fails_and_failures_pause_the_group, test_protected_and_terminated_units
Each unit's cost goes into the activity ledger as an estimate, and a group stops at its budget's cap test_scaling.py test_the_scaler_launches_joins_drains_and_terminates, test_at_its_budgets_cap_a_group_stops
Settings › Auto scaling shows each group's terms in words and approves them in one click settings-scaling.test.tsx shows each group's terms and approves them in one click, puts the terms and the state in words
Settings › Auto scaling adds a provider from its driver's fields (credentials sent apart, never shown), shows its check in words, removes it with Undo, and makes a group, with a budget when the provider charges settings-scaling.test.tsx adds a provider from its driver's fields, with the secret sent apart, shows a failed check in words, after saving and in the list, makes a group with a budget on a priced provider, asks no budget on a free provider and ticks a list's options, removes a provider with Undo instead of a question
Units a provider runs with Lens's tag that Lens has no unit for show on the provider and in the activity ledger once; Lens lets none go itself, a person signed in to the web app does (with Undo), and the assistant only asks test_scaling.py test_units_lens_lost_track_of_show_on_their_provider, test_a_person_lets_a_stray_go_and_an_agent_cant; test_scaling_agents.py test_the_assistant_asks_to_let_a_stray_go_and_the_person_says_yes; settings-scaling.test.tsx shows the units a provider runs that Lens lost track of, and why it couldn't ask, lets a stray go with Undo instead of a question
A unit that goes (let go, failed, or never came up) leaves the mesh: its unspent join token is withdrawn and the node that joined with it is removed, its worker stopped and its database user gone test_scaling.py test_a_unit_let_go_or_failed_leaves_the_mesh; test_mesh_workers.py test_a_scaling_unit_let_go_leaves_the_mesh_with_its_token
This machine runs more workers at no cost test_scaling.py test_this_machine_runs_more_workers_at_no_cost
DigitalOcean and Hetzner Cloud: credentials checked, list prices, machines launched with a one-use join token, watched, found by tag and deleted; a group waits for the mesh's install line before launching test_scaling_clouds.py test_a_digitalocean_group_waits_for_the_install_line_then_launches_droplets, test_hetzner_prices_in_euros_and_its_errors_read_as_hetzners, test_the_cloud_drivers_are_listed_and_need_the_mesh
Lambda: GPU instances launched with the account's SSH key and a one-use join token in their user-data, tagged with Lens's tag and the unit, priced from Lambda's list, watched (an instance Lambda takes back counts as gone), found by tag across pages and terminated; every request and answer held to Lambda's published API document test_scaling_clouds.py test_a_lambda_group_launches_gpu_instances_that_join_over_the_mesh, test_lambda_needs_an_ssh_key_and_pages_through_instances, test_lens_reads_only_fields_the_clouds_answer_with
A group of machines elsewhere counts only the jobs a worker node takes, and never launches one for others test_scaling_clouds.py test_a_group_elsewhere_counts_only_the_jobs_a_node_takes
Proxmox VE: clones of a VM template, started and booted through the guest agent, found by tag, stopped and deleted; a clone whose agent never answers says so; every request and answer held to Proxmox's own API schema test_scaling_proxmox.py test_proxmox_clones_boots_through_the_guest_agent_and_deletes, test_a_clone_whose_agent_never_answers_says_so, test_a_clone_still_booting_is_pending, test_lens_reads_only_fields_proxmox_answers_with
Linux hosts over SSH: host keys trusted on first use and held to, workers started per host with a script that removes itself, found, counted and stopped test_scaling_ssh.py test_host_specs, test_ssh_trusts_host_keys_on_first_use_and_holds_to_them, test_ssh_starts_counts_and_stops_workers
AWS EC2 units launch as spot or on-demand instances, hardened (IMDSv2, encrypted disk, terminate on shutdown), with only the mesh's one-time join line in their user-data test_scaling_aws.py test_a_spot_unit_launches_hardened_and_priced_in_its_zone, test_the_driver_is_offered_and_its_settings_are_checked
A spot unit falls back to on demand when AWS has none; without a price or the mesh, nothing launches test_scaling_aws.py test_without_spot_capacity_it_falls_back_to_on_demand_unless_told_not_to, test_a_unit_without_a_price_or_the_mesh_launches_nothing
When AWS takes a spot instance back, the scaler says why and launches another test_scaling_aws.py test_describe_says_when_aws_took_a_spot_instance_back, test_the_scaler_replaces_a_spot_unit_aws_took_back, test_terminate_check_and_listing
RunPod: GPU pods serving a model with vLLM, each with an API key of its own sealed on the unit, running once vLLM answers, deleted when stopped; no mesh needed test_scaling_models.py test_a_runpod_group_serves_a_model_on_gpus_it_rents, test_runpod_says_why_it_made_no_pod
Hugging Face: Inference Endpoints serving a model with vLLM on hardware picked from Hugging Face's list (price, cloud, GPUs), each with a key of its own as an endpoint secret, never the Hugging Face token; requests and answers held to Hugging Face's published API document; units that never come up pause the group after three test_scaling_models.py test_a_hugging_face_group_serves_a_model_on_an_inference_endpoint, test_endpoints_that_never_come_up_pause_the_group, test_lens_reads_only_fields_hugging_face_answers_with
Amazon SageMaker: real-time endpoints serving a model with AWS's vLLM container, each a model, an endpoint configuration and an endpoint tagged with the unit and all three deleted when it goes, priced from the AWS Price List; Lens's model calls to it are signed with AWS Signature Version 4, never carry the secret key, stream as the container writes (InvokeEndpointWithResponseStream, read with botocore's own event stream decoder; whole when the key may only invoke), and embeddings and speech never borrow it test_scaling_sagemaker_moto.py test_a_unit_is_a_model_a_configuration_and_an_endpoint_and_goes_as_all_three, test_an_endpoint_that_fails_says_sagemakers_reason, test_without_a_price_or_with_a_bad_setting_nothing_is_made, test_a_sagemaker_group_serves_lenss_model_calls_signed_until_it_scales_in; test_llm_sagemaker.py test_a_chat_goes_to_invoke_endpoint_signed_and_never_carries_the_secret, test_a_streamed_answer_comes_as_the_container_writes_it, test_a_model_error_mid_stream_says_so, test_keys_that_may_only_invoke_get_the_answer_whole, test_an_endpoint_serves_its_one_model_and_is_never_asked_for_a_list, test_a_key_that_isnt_an_aws_key_says_what_it_should_be, test_other_servers_still_get_a_bearer_token_at_chat_completions, test_embeddings_and_speech_never_borrow_the_aws_key
Amazon Bedrock as the LLM provider: chat, tool calls and their results, images, JSON answers and summaries go to Bedrock's Converse API signed with the AWS key, Bedrock's answers and refusals read as an OpenAI-compatible server's (a context too long, tools a model doesn't take), chat answers stream as they are written (ConverseStream; whole when the key may only invoke), and embeddings and speech never borrow the key; each request and answer is held to botocore's model of the API test_llm_bedrock.py test_a_chat_is_a_converse_call_and_reads_as_an_openai_reply, test_tool_calls_and_their_results_go_as_converse_turns, test_a_json_answer_asks_for_the_schema_in_the_system_prompt, test_a_summary_goes_to_bedrock_too, test_a_streamed_answer_is_converse_stream_as_it_is_written, test_a_model_error_mid_stream_says_so, test_keys_that_may_only_invoke_get_the_answer_whole, test_bedrocks_refusals_read_as_lenss, test_bedrock_lends_its_key_to_nothing_and_lists_the_model_it_was_given
Amazon Bedrock provisioned throughput: made with no commitment, named and tagged with the unit, running once in service, failed ones gone with Bedrock's reason, found by name across pages and deleted when it goes; nothing launches without the admin's price; while it serves, Lens's model calls go to it signed; every request and answer held to AWS's model of Bedrock's API and its name patterns test_scaling_bedrock.py test_a_unit_is_a_provisioned_throughput_held_until_it_goes, test_one_that_fails_says_bedrocks_reason_and_none_launch_without_a_price, test_a_long_name_keeps_its_number_and_bedrocks_pattern, test_a_bedrock_group_serves_lenss_model_calls_until_it_scales_in
A running model unit is Lens's LLM provider until none runs, then the provider from before comes back; a provider a person picks meanwhile stays test_scaling_models.py test_a_running_model_unit_serves_lenss_model_calls_until_none_runs, test_a_provider_a_person_picks_while_a_unit_serves_stays
Model calls go to a group's running units in turn (one key per group, sealed on it); a unit that can't be reached hands its call on, and a spread left from another provider sends nothing elsewhere test_scaling_models.py test_model_calls_go_to_a_groups_running_units_in_turn, test_llm_spread_is_checked
Lambda as a model host: GPU instances run vLLM over HTTPS with a certificate the group's own CA signed (its key sealed on the group), behind a firewall ruleset made once per region and the group's key; Lens's calls trust only that CA, and fail with none or another group's, tried against a real HTTPS server test_scaling_clouds.py test_a_lambda_model_group_serves_https_with_a_certificate_lens_signed, test_lens_reads_only_fields_the_clouds_answer_with
Provider fakes take only the requests the real API would, answer only what it could, and Lens reads only fields the provider documents; AWS runs from launch to scale-in on moto test_scaling_clouds.py test_lens_reads_only_fields_the_clouds_answer_with; test_scaling_models.py test_lens_reads_only_fields_runpod_answers_with; test_scaling_aws_moto.py test_a_spot_instance_launches_tagged_hardened_and_is_found_by_its_tag, test_an_instance_someone_stopped_is_terminated_so_its_disk_stops_costing, test_the_scaler_runs_a_group_on_moto_from_launch_to_scale_in
Agents manage scaling in the assistant and over MCP: proposals wait for the person, actions within the terms happen at once test_scaling_agents.py test_the_assistant_proposes_and_the_person_approves_in_the_web_app, test_a_setup_conversation_makes_a_group_that_still_waits_for_the_person, test_only_admins_get_the_scaling_tools, test_mcp_clients_manage_scaling_but_never_approve

Run on more machines

Requirement Checks
Off until an admin adds a node, which makes the mesh's certificate authority and turns it on; nothing goes in .env test_mesh.py test_adding_a_node_makes_the_ca_a_token_and_turns_the_mesh_on; settings-mesh.test.tsx is off until a node is added
A machine joins with the install line and a one-time token that never crosses the wire, and its answer is sealed for that token; wrong or spent tokens are refused and repeated wrong joins turned away test_mesh.py test_a_machine_joins_without_sending_the_token, test_a_join_answer_is_only_for_the_token_it_was_sealed_with, test_too_many_wrong_joins_from_one_address_are_turned_away; test_mesh_node.py test_a_machine_joins_and_follows_the_primary, test_a_spent_or_wrong_token_is_not_tried_again, test_the_install_line_names_this_install_and_the_token
After joining, a node talks to this install over the mesh only: it follows its config, renews its certificate and reports every minute, and a removed node is blocked everywhere until an undo test_mesh.py test_a_node_reports_renews_and_follows_its_config; test_mesh_node.py test_a_machine_joins_and_follows_the_primary, test_a_primary_out_of_reach_is_tried_again_later, test_the_primarys_mesh_address_is_a_host_only_for_nodes_calls
Nebula runs in its own container, fetched once with its checksum checked against the one kept in Lens, and is started again when it stops test_mesh_service.py test_the_sidecar_pins_the_nebula_lens_does, test_the_sidecar_downloads_nebula_only_with_its_checksum, test_the_keeper_and_the_sidecar_bring_up_the_primary, test_a_stopped_nebula_is_started_again_after_a_while; test_mesh.py test_nebula_is_fetched_once_with_its_checksum_checked
A worker node runs jobs as its own user of Lens's database, never root and never with the archive's secret, reading only its namespaces' files over the mesh, never a vault's test_mesh_workers.py test_a_worker_node_runs_lens_worker_as_its_own_user_of_the_database, test_its_jobs_read_files_from_here_over_the_mesh, test_a_node_serving_some_namespaces_takes_only_their_jobs, test_only_a_worker_node_gets_the_database, test_a_node_runs_only_the_steps_a_node_can
A compute node runs the assistant's code on a gateway of its own, which Lens reaches over the mesh with mutual TLS, holding its certificate sealed test_mesh_compute.py test_a_compute_nodes_install_line_starts_its_gateway, test_a_compute_node_hands_over_its_gateway_and_the_assistant_may_run_code_there, test_only_a_compute_node_gives_a_gateway_and_only_a_certificate, test_lens_reaches_the_nodes_gateway_with_its_certificate_checked_by_name
Settings › Nodes adds a machine with Enter, shows its install line once, lists each machine with how it's doing, renames in place and removes with Undo; Settings › Network shows the mesh and its settings settings-mesh.test.tsx adds a node with Enter and shows the install line once, with its terms, lists each machine with how it's doing, removes a node without asking, and undoes it, renames a node with Enter, says whether a compute node's sandbox gateway runs, shows that it runs, and what Nebula said, checks the network and the hosts as the server does
Agents read the mesh and add, rename, remove, restore and withdraw over MCP and in the assistant; adding or removing a machine in the assistant waits for the admin's yes, and a join token's secret never reaches the model test_mesh_agents.py test_mcp_clients_add_and_manage_mesh_nodes, test_the_assistant_adds_and_removes_machines_once_the_person_approves, test_the_assistant_renames_restores_and_withdraws_at_once, test_a_setup_conversation_still_asks_before_a_machine_joins_or_goes, test_only_admins_get_the_mesh_tools; approval-install-line.test.tsx shows its install line this once, until the person is done with it

Keep it up to date and backed up

Requirement Checks
A daily check for a new release, told to admins by email or a notification test_updates.py test_the_daily_check_runs_when_due_and_can_be_turned_off, test_a_new_release_is_announced_once
One click (or one API call) upgrades through the machine's agent, showing each step test_updates.py test_one_click_upgrade_hands_the_release_to_the_agent; settings-updates.test.tsx shows each step of the upgrade as the machine goes; install.yml An upgrade asked for in the web app is installed by the machine's agent
A new version that doesn't start is rolled back test_upgrade_agent.py test_a_release_that_doesnt_start_is_rolled_back
The same agent changes whether other devices reach Lens and which edition runs test_install_settings.py test_the_agent_sets_up_the_version_running_with_the_change, test_the_agent_passes_the_change_to_install_sh; install-settings.test.tsx
Backups of the database and files: take, list, download and restore; unchanged files take no new room test_backups.py test_take_download_and_restore, test_backups_keep_and_restore_the_files_too
Backups on a schedule, each copied, encrypted, to a storage connection test_backups.py test_backups_on_a_schedule, test_copies_are_sealed_with_secret_key
A scheduled backup's copy reaching a storage connection, in CI No check yet (#486)
Each release is first checked by upgrading the one before it, holding data: nothing lost, each pending step run once after a backup, a second run changes nothing upgrade.yml Upgrade and check; release.yml Upgrade from the last release; test_cli.py test_a_worker_logs_the_upgrade_it_runs_as_it_starts

Watch it run

Requirement Checks
Structured logs, read in Settings › Logs, and optionally sent to a syslog server test_logs.py test_viewer_reads_the_file_newest_first_and_filters, test_syslog_off_by_default_then_sent_over_udp
Logs optionally sent straight to Grafana Loki or another HTTP endpoint, with sealed headers test_logs.py test_http_sends_to_loki_in_batches_with_the_sealed_headers, test_http_test_line_as_ndjson_and_headers_dropped_for_a_new_host, test_unreachable_http_endpoint_never_blocks
Client certificates (mutual TLS) and a private CA for syslog over TLS and https log endpoints test_logs.py test_client_certificates_for_syslog_over_tls_and_https
OpenTelemetry traces and metrics, off until turned on test_telemetry.py test_off_by_default_sends_nothing, test_traces_and_metrics_go_to_the_endpoint
A Prometheus /metrics endpoint, off until turned on test_prometheus.py test_off_by_default, test_needs_its_token
One switch starts Prometheus, node-exporter and Grafana next to Lens test_monitoring.py test_turning_monitoring_on_and_off, test_the_agent_changes_compose_profiles
The job queue: clear it, reorder it by drag and drop, retry failed runs in bulk, in Activity and over the API and MCP test_jobs.py test_clearing_reordering_and_retrying_the_queue, test_agents_work_the_queue_over_mcp; queue-controls.test.tsx; activity-model.test.ts
The Analytics page: active people, churn, retention and feature use, worked out on this machine test_analytics.py test_metrics_active_churn_retention; analyticsPage.test.tsx
The same daily events to a Google Analytics 4 property of your own, off unless turned on test_analytics.py test_events_kept_locally_with_ga_off, test_daily_events_once_a_day_counts_only
The space each source takes, its assets' own files and what Lens made of them, through the API and on System health test_recording_list.py test_the_space_each_origin_takes

Stay private and safe

Requirement Checks
Passkeys instead of passwords test_passkeys.py test_first_admin_with_a_passkey_and_no_password, test_sign_in_with_a_passkey
A passkey at another of Lens's addresses (remote access, HTTPS) added from a signed-in one: Open signed in, then one fingerprint test_passkeys.py test_taking_your_sign_in_to_another_address, test_the_addresses_lens_opens_at; passkeys-panel.test.tsx opens an address you have no passkey for signed in, to add one there; webauthn.test.ts points a failed sign-in to the address the passkey was made at
Encryption at rest, with a key per namespace test_keyring.py test_each_namespace_gets_its_own_key_kept_only_wrapped; test_encryption.py test_a_new_archive_encrypts_and_still_plays_seeks_and_processes
Secret fields that refer to a Vaultwarden or Bitwarden vault, read by Lens and never shown to anyone, agents included test_secrets.py test_a_secret_field_reads_its_value_from_the_vault, test_the_vault_is_read_again_and_what_vanishes_stops_being_used, test_the_vault_is_set_up_and_checked_in_the_app, test_agents_refer_to_secrets_and_never_read_them; settings-secrets.test.tsx is picked from the vault's items, and only the reference is saved, shows its reference and whether it was read, never a value
Roles per namespace: viewer, editor, owner, or custom roles made of permissions test_roles.py test_builtin_roles_keep_their_meaning, test_custom_roles_allow_exactly_their_permissions; journeys.mjs person-add
Access that ends on a date: a role in a namespace or on a collection, or permission on an asset, opens nothing after its end and lists show it as expired test_grant_ends.py test_asset_permission_holds_until_it_ends, test_namespace_role_holds_until_it_ends, test_collection_role_holds_until_it_ends; test_setup_assistant.py test_the_assistant_gives_a_role_until_a_date; access-until.test.ts sends the end of the day picked, and nothing for an empty field
A downloads switch per asset: off, only its owners save its files (its own file, its PDF, supplementary files) while everyone who sees it still plays it and reads its text test_downloads.py test_downloads_off_refuses_a_viewers_download_but_still_plays, test_downloads_off_keeps_iiif_playing; access-model.test.ts sends the downloads switch only when it changes, on by default, says why the files can't be saved, only when they can't
A video's copy with faces blurred for sharing, made by ffmpeg over the faces' tracks only when asked (all, unnamed or chosen faces; API, MCP and the People tab), kept with its files, and played to visitors instead of the original only when an owner picks that test_face_blur.py test_regions_cover_each_track_between_and_around_its_samples, test_the_filter_graph_blurs_each_region_for_its_time, test_blurring_faces_for_sharing; face-blur.test.ts asks for every face when all are ticked, else for the ones ticked in order, says how far it got, why it waits and why it failed
Email invitations, and removing someone for good test_users.py test_invites_are_emailed_when_mail_is_set_up, test_admins_remove_people_for_good
An audit log test_settings.py test_settings_are_editable_and_secret; test_files.py test_change_a_file_and_the_audit_log
Outside text reaches the model marked as data; text that reads like instructions is flagged, by patterns or an optional local classifier test_prompt_injection.py test_what_tools_read_is_marked_as_data, test_a_result_that_reads_like_instructions_is_flagged; test_injection_model.py test_until_the_model_is_here_the_patterns_stand_alone
Once an answer has read outside text, sending anything out, or the browser acting on what the assistant wrote, waits for a yes test_prompt_injection.py test_a_web_tool_asks_first_once_the_answer_read_outside_text; test_browser.py test_after_reading_a_page_the_browser_asks_before_what_the_person_didnt_write
Chat-room answers have the links they built broken up; agents over MCP get the archive as data test_prompt_injection.py test_a_room_gets_the_links_the_answer_built_broken_up; test_mcp.py test_what_the_archive_says_reaches_agents_as_data
HTTPS set up in Settings, with certificates that renew, only for its own domains and optionally only for client certificates test_https.py test_https_is_set_up_in_the_app, test_caddys_configuration_follows_the_settings, test_strict_sni_client_certificates_and_tls_13
Remote access through a Cloudflare tunnel test_tunnel.py test_remote_access_is_set_up_in_the_app, test_a_quick_tunnel_runs_in_one_process_and_stops_when_turned_off, test_a_managed_tunnel_is_made_routed_and_given_a_dns_record
The S3 and WebDAV shares and the MQTT hub at the web app's address (/s3, /dav, /mqtt over WebSockets), each off until turned on test_s3.py test_at_the_web_apps_address_once_an_admin_allows_it; test_webdav.py test_at_the_web_apps_address_once_an_admin_allows_it; test_sensors.py test_broker_over_websockets, test_mqtt_over_websockets_takes_hub_logins_only; lens-relay.test.ts relays any method, path and body to the share, and its answer back, passes the handshake on with the visitor and the prefix, then joins the two connections
The threat model's three files agree, and a change to the prose comes with its companions, without a line every change edits threat-model.yml validate, Validate the threat model, Test the validator; validate_threat_model.py; test_validate_threat_model.py test_prose_change_without_companions_fails, test_two_changes_to_the_prose_merge_cleanly
Owners make many assets public, restricted or private at once from the Library, with Undo putting each back library-catalogue.test.ts touches only those not there yet, and remembers how to put each back; test_access_settings.py test_access_endpoint_and_defaults

Use open standards

Requirement Checks
RDF and Dublin Core for every record test_rdf.py test_recording_uri_public_and_members, test_import_round_trip_and_dublin_core
OAuth for apps and OpenID Connect sign-in test_oauth.py test_discovery_says_where_everything_is, test_an_app_gets_access_and_acts_with_the_persons_roles; test_external_login.py test_signing_in_connects_the_account_with_the_same_verified_email
MQTT: a built-in broker, and a client for another one test_sensors.py test_broker_is_an_ordinary_local_broker, test_bridge_client_reads_another_broker
IIIF for publishing test_iiif.py test_manifests_collections_search_state_discovery, test_authorization_flow
Shared notes and comments as W3C Web Annotations in IIIF, for people with a role on the asset only (never unshared notes, never the public) test_iiif.py test_notes_and_comments_as_web_annotations; test_documents.py test_notes_on_a_documents_pages
OpenTelemetry and Prometheus for monitoring test_telemetry.py test_traces_and_metrics_go_to_the_endpoint; test_prometheus.py test_what_it_measures
An optional Fedora mirror test_fedora.py test_off_by_default, test_sync_sends_changes_and_deletes
A CSV catalogue of the assets selected in the Library, one row each with a link to its page, safe to open in a spreadsheet library-catalogue.test.ts has a row per asset with its people, tags and a link to its page, quotes what needs it and never lets a cell run as a formula

Light by default

Requirement Checks
Heavy parts stay off or are fetched on first use: speech engines, a local LLM, document conversion, Apache Tika, the NSFW model, Fedora test_components.py test_engines_are_fetched_on_first_use; test_local_llm.py test_settings_take_a_listed_model_or_any_gguf_on_hugging_face; test_convert.py test_libreoffice_and_chromium_are_installed_the_first_time_theyre_needed; test_tika.py test_off_by_default_nothing_changes; test_nsfw.py test_off_by_default_nothing_is_fetched_or_classified; test_fedora.py test_off_by_default
The MCP runner is off until turned on test_mcp_servers.py test_a_server_that_runs_as_a_program_runs_in_the_runner
Setup asks only what has no default: the model, and a budget for a cloud model journeys.mjs setup-wizard; baseline.json setup-wizard ≤ 1 click, ≤ 0 keys; test_setup.py test_the_wizard_asks_only_what_has_no_answer, test_a_paid_model_from_the_environment_still_asks_for_a_budget
Picking a cloud provider fills in its address and model; only the key is left to paste journeys.mjs cloud-model; baseline.json cloud-model ≤ 3 clicks; test_setup.py test_offers_cloud_model_providers; settings-llm-presets.test.tsx switches to a cloud provider with one click and a pasted key
Settings are in named groups, a search finds any one by name, and rarely changed options stay folded settings-find.test.tsx puts every section in exactly one group, finds a field by its label, and opens the section on it; settings-save.test.tsx are folded under a line saying what's in them, the main one left in view
A thin bar along the top of the web app runs while a page or its first data loads, not for a quick answer or a background refresh top-progress.test.tsx runs along the top while a page's first data loads, doesn't flash for a quick answer, or show for refreshing what's on screen, runs from a page change until Next writes the new address, finishes a page change that lands back on the page showing, as a redirect can
The web app works on a phone: every journey is surveyed on one, and none may take more than its baseline clicks.yml Survey on a phone; baseline-phone.json; phone-layout.test.tsx
Heavy steps kept to their hours and to so many at a time, off by default test_quiet.py test_the_window_and_its_checks, test_heavy_steps_wait_for_their_hours_and_their_turn
The default install runs on a Raspberry Pi (arm64) with a cloud model No check yet (#486)