Skip to content

Releases: flagdizero/jenny-android-ai-agent

Jenny 1.0.0

Choose a tag to compare

@flagdizero flagdizero released this 02 Oct 10:08
3c28c87

The first stable release of Jenny. From here on, a change that breaks something you rely on gets a new major number.

What's new

  • A new home built around the conversation, with swipeable pages: Apps, Jenny, Notebooks and Settings. A Jenny App or a notebook can be pinned as a page of its own.
  • The workshop: the previous interface, regrouped into four drawers (Console, Brain, Hands, Memory) behind Settings.
  • Jenny outside the app: she floats above other apps and you can talk to her there, and you can reply to her from the notification shade.
  • Notebooks in the home, each with its own chat, pages and map, plus editing and reporting from the reader.
  • Synthwave '84 is the new default theme, and the onboarding and splash screen wear it.
  • Sturdier streaming from providers, tighter network and file guards, and every word in every theme readable at 4.5:1.

Over 800 changes since 0.11.0. The full list, including the changes you may notice, is in CHANGELOG.md.

Updating

Jenny 0.7.0 or newer offers this update on its own, and Android asks you to confirm the install. Older versions need the APK below installed by hand, once. The signing key is unchanged, so it installs over your current version and keeps your data.

Verify what you downloaded

sha256  6fa89bb2e6e08b260b1cbbd98def48585a1a38feb164b096d2564126cdf6ba43
size    75244909 bytes (71.8 MB)
shasum -a 256 jenny-1.0.0.apk

The hash proves you got this file whole; the signature is what proves it came from
this project, and Android checks it for you on an in-place update. To check it yourself:

apksigner verify --print-certs jenny-1.0.0.apk

The certificate is the same one published with
0.3.0 — if it
ever differs, the build did not come from this project.

Jenny 0.11.0

Choose a tag to compare

@flagdizero flagdizero released this 10 Sep 09:21

Settings can finally tell you what is scheduled — and a switch that was supposed to stop a background worker now actually stops it.

The switches that did not switch anything off

If you ever turned Dream off in Settings, it kept running. Every two hours: a snapshot, a real model turn, memory rewrites, session pruning — and it survived restarts. An off switch turned to stop Jenny from rewriting her own memory, that did not stop it. The same hole was open for the heartbeat, and for the update check it read a stale copy of your settings.

GatewayContainer registers a worker's scheduled job only when the worker is on, but nothing ever removes that job once it exists, and protected system jobs cannot be deleted. So a job written by a start where the worker was enabled stayed scheduled forever. The only thing that stops it is a check inside the handler itself, and now all four workers have one — reading your configuration from disk, so flipping a switch takes effect at the next tick instead of waiting for a restart.

Two things worth being plain about:

  • The turns already spent cannot be given back. If you switched Dream off weeks ago and kept paying for it, this release stops the next one and nothing more.
  • Memory it wrote in that time stays written. Dream's edits are ordinary edits to your memory files; you can read them and correct them, but there is no undo.

What is scheduled, and what only looks scheduled

A new Scheduling section in Settings, between Background activity and SSH. Read-only on purpose: reminders are created and removed by asking Jenny, and a "run now" button would spend tokens and possibly message you from a screen you opened to look at something.

It does not just list the store — it argues with it:

  • Armed but doing nothing. A worker switched off keeps a scheduled job that fires and returns immediately, recorded as a normal success. That state used to leave no trace anywhere; now the panel names it.
  • A heartbeat checking nothing. If HEARTBEAT.md has no active tasks, the cycle is skipped and recorded as fine — every 30 minutes, indefinitely. The panel says so, and lists the checks it can actually see, marking one that has not run for several cycles and whether Jenny has already told you.
  • Overdue instead of a date in the past. Next-run times are stored, not recomputed on demand, so after a long doze or with the scheduler stopped they sit in the past. The panel reads them as late rather than showing yesterday's time as if it were next.
  • Silence that is not failure. A monitor job that stayed quiet looked and had nothing to report. It is not coloured like an error, and "could not check" stays a third thing, distinct from a failure.
  • Your reminders, with their own times. A reminder created with an explicit timezone is shown in it, named only when it differs from the phone's — so the screen and what Jenny says in chat never disagree.

Tapping a card opens the reminder's full text and its last 20 runs with durations and errors. The panel reads once when you open it and has a Refresh button: it does not poll, because a screen that wakes the phone every few seconds would undo the work that keeps scheduled jobs punctual through doze.

Images from a tool reach the model as images

On providers using the OpenAI Responses API, asking Jenny to read an image sent her the whole data URI as text: no picture, and a token bill proportional to the file. It now travels as an image, natively. Nothing about it was visible before — no error, no retry, no log line.

Smaller

  • The gardener introduces itself in the scheduled-jobs list instead of showing up as "System-managed internal job."
  • The release page publishes the SHA-256 the README tells you to check. It had quietly stopped doing that after 0.3.0.

Verify what you downloaded

sha256  c884f9bf06a9d2cd473ce9947d7756231dd0c2d7722d7a0b9aafc5431189a2c7
size    75509560 bytes (72.0 MB)
shasum -a 256 jenny-0.11.0.apk

The hash proves you got this file whole; the signature is what proves it came from this project, and Android checks it for you on an in-place update. To check it yourself:

apksigner verify --print-certs jenny-0.11.0.apk

The certificate is the same one published with 0.3.0 — if it ever differs, the build did not come from this project.

Jenny 0.10.0

Choose a tag to compare

@flagdizero flagdizero released this 08 Sep 20:15
af3c70b

What you say about yourself inside a project now reaches Jenny's memory — plus a face for the mascot, projects as real conversations, and one settings screen instead of five.

Memory across projects

Until now, a fact you told Jenny about yourself inside a project went nowhere: not into her long-term memory, and into that project's journal in the wrong place. Measured on a real phone before the fix: 39 of 72 journal lines were facts about the person, and 18 of 23 sampled were in no memory file at all.

The boundary was guarding the right rule on the wrong axis — it asked where a sentence came from instead of what it was about. Now a project conversation feeds the diary with its own key, and a Dream run over project material can write to USER.md and nothing else: the cross-project inventory stays where it belongs, unreachable by construction rather than by instruction.

  • A project's idle conversation is read into the queue without being shortened — no message is lost.
  • The extraction prompt was measured over four revisions on real material: 45% → 87% of the reference facts recovered, zero false positives in every revision, and a pure work project produced nothing at all in four runs out of four.
  • A suspicion is recorded as a suspicion. "Wondering whether X explains it" no longer becomes "has X".
  • USER.md's budget goes 3000 → 4000 characters to make room for what was missing.

A face for the mascot

The mascot has drawn expressions now, and a mood that follows what Jenny is actually doing — thinking, working, replying — instead of sitting still. The black-and-white variant is retired.

Projects, apps and settings

  • A wiki is a conversation. Each project has its own chat, folder and journal, and creating or deleting one is a single deliberate action that takes the conversation with it.
  • Settings consolidated into one screen with the apps drawer, instead of scattered pages.
  • Telegram gets an enable toggle, a startup log line and backlog handling.
  • A custom CA bundle per provider, for self-hosted endpoints behind a private certificate.
  • Search history, an agentic browser, and nothing_to_report so a scheduled job can say "nothing" without spending a turn on prose.

Fixes

  • A finished subagent card expires on its own clock instead of lingering.
  • A failed tool call collapses like a successful one.
  • Synthetic rows no longer leak into the user profile.
  • Base URL falls back correctly when the provider omits /v1.

Installs over any previous release — same signature, upgrades in place. latest.json is the manifest the in-app updater reads.

Jenny 0.9.5

Choose a tag to compare

@flagdizero flagdizero released this 28 Aug 18:56
1cf5e1c

Slash commands you can actually find, and a /new that really starts a new
conversation. Closes #11.

What's new

  • A Commands menu above the composer, next to Writes: every command with a
    one-line description. Commands that take an argument (/model, /goal, …)
    fill the composer instead of being sent, so you can finish the line. /tidy
    and /init only show up inside a project, where they have a subject.
  • A New chat button in the composer, next to the paperclip, with a
    confirmation.
  • A delete button on each project in the scope chip — the place the project
    was created. The confirmation says how many messages of its conversation go
    with the folder.

What changed

  • /clear is gone. It never reached the server: it wiped the screen, printed
    "Chat cleared." and left the model's memory of the conversation completely
    intact. Typing it now just sends a message to the agent. The command you want
    is /new.
  • /new clears the screen too, and the model really does start empty. The
    summaries of the conversation you just discarded no longer come back into the
    next turn's prompt. Dream still consolidates them into long-term memory —
    /new resets what is in front of the model, it does not delete what she has
    learned.
  • The chat reopens at the last separator. This applies to conversations that
    already contain one, so after updating you may find a chat that starts later
    than it used to. Nothing was deleted: scrolling up loads the previous session,
    and if the chat is too short to scroll there is a "Show the previous
    conversation" button.
  • The ⋮ on workspace items is gone — it only appeared on hover, which on a
    phone means never. Long-press an item for its actions, as in the apps grid.

Under the hood

Verified on a Unihertz Titan 2 with the signed release build. Full test suite
green on 3.11 and 3.12.

Sideloading over an older build keeps your workspace — same signing key, same
package. The in-app updater will offer this one to anyone already on 0.9.0.

Jenny 0.9.0

Choose a tag to compare

@flagdizero flagdizero released this 27 Aug 21:23
f3b3aac

Projects become real conversations: every wiki gets its own chat, folder and journal, and Jenny keeps it tidy on her own.

Projects

Selecting a wiki changes conversation. It gets its own session, its own thread and its own folder: Jenny stays herself inside it, reads widely, and writes only in that folder. The confinement is mechanical rather than advisory — one function answers where may I write, and a test stops a second answer from ever appearing. Nothing that happens inside a project reaches the personal memory.

  • Inside a project cron refuses in all three actions and says where reminders live.
  • The wiki view and the graph show that project, and offer no way out to the others.
  • A conversation can be made read-only and mean it.
  • Renaming a folder takes its chat with it; deleting a project deletes more than the folder.

The journal, and the gardener

Every wiki gains an append-only journal, and the conversation itself counts as a source. The map enters the context of every turn, measured the way a turn actually receives it.

The gardener turns a project's journal into pages, and then into its map — cold, on a schedule, with a manual /gardener when you want it now. It takes a workspace checkpoint before it writes, records in the log when a pass read the journal and promoted nothing, and parks what it cannot settle instead of deciding on its own.

/tidy

Restructures the wiki you are in, with the real numbers in hand: how big the map is against its budget, and which pages are past the per-turn injection ceiling — read from disk by the same code that performs the injection, not estimated. It follows the recipe for the layout it finds, and it is allowed to change nothing: a tidy that moves nothing is a correct outcome.

A page's provenance is now held by every writer, not only the gardener.

Also in this release

  • Long-term memory demotes instead of deleting, and makes the fact the unit.
  • The personal diary is filtered out of Dream's read rather than its write.
  • A delivered message is no longer overwritten by what follows it.
  • Pending alerts clear when the chat is on screen, not when the app resumes.
  • Cron run records stop outliving the job and the disk.
  • A release built from a dirty working tree now says so out loud.

versionCode 13. 12 was never published — it is skipped deliberately, and the reason is recorded next to the constant.

Jenny 0.8.0

Choose a tag to compare

@flagdizero flagdizero released this 17 Aug 16:20

Recurring-check alerts speak up again when you reply, long-term memory keeps itself tidy, and the chat no longer loses a message after a network drop.

Jenny 0.7.1

Choose a tag to compare

@flagdizero flagdizero released this 15 Aug 20:05

Conversations no longer wedge when the assistant uses two tools at once, and files read in parallel no longer overwrite each other.

Jenny 0.7.0

Choose a tag to compare

@flagdizero flagdizero released this 14 Aug 09:34

Jenny updates herself. Plus: she recovers on her own if the gateway dies, her alerts arrive one by one and she remembers them, a goal waiting on you no longer nags, and apps get the whole theme.

Jenny 0.6.6

Choose a tag to compare

@flagdizero flagdizero released this 11 Aug 16:43
de02c6c

Jenny 0.6.6

Everything since v0.6.0. The work was developed on a branch called 0.6.5 and that number was never published, so it all ships here under one version.

The theme is time. A phone with the screen off suspends its processor, and a foreground service does not change that — it keeps the process alive, not the processor. Every timer Jenny was sleeping on stopped advancing along with the CPU, which is why a 30-minute job was measured firing between 30 and 83 minutes apart. That is fixed, and measured. Around it: reminders that no longer vanish quietly, background checks that are allowed to say nothing at all, a wiki graph you can search by what the pages say, and a back button that finally knows what is on top of what.

versionName 0.6.6, versionCode 8.


Scheduled work now fires on time

power.keepAwake (default turns) takes a partial wake lock around real work — an agent turn, a cron/Dream/Atlas/heartbeat job, an SSH command, a Telegram update being processed — and releases it immediately after. always holds one for the life of the gateway service, for a phone that lives on a charger; off is the previous behaviour, kept as an escape hatch. It is the one power key with a UI control, in Settings → Background activity.

The scheduler also asks Android to wake the phone at each job's real deadline (power.alarmDrivenCron, on by default). An OS alarm pierces Doze; a suspended timer does not.

For when the process is killed rather than merely frozen, six nets funnel through one GatewayStarter so they cannot diverge: a self-chaining watchdog alarm with a Doze-adaptive interval, a 15-minute WorkManager job, an 8-hourly alarm-clock wake-up, opportunistic restarts when the network returns or you open the app, and recovery from MY_PACKAGE_REPLACED — after an app update the gateway used to stay down until you opened Jenny by hand.

Settings → Background activity is new: whether the battery exemption is in force (and a button to request it), whether exact alarms are granted (and a button that opens Android's own screen for it), the wake-lock choice, and a list of recorded outages with manufacturer-specific guidance when downtime exceeds power.gapWarningMin.

The measurement

On a Unihertz Titan 2 (Android 16), unplugged and idle for a nine-hour night, both permissions granted:

  • The 30-minute heartbeat fired 19 times, every interval between 30m00s and 30m02s — worst deviation two seconds.
  • dumpsys deviceidle confirms deep Doze throughout, including an uninterrupted four-hour deep-idle block with zero maintenance windows, during which the heartbeat still fired eight times on schedule.
  • The gateway PID never changed: never killed, never restarted.
  • Battery 80% → 77% over 9.4 hours.

What it does not prove: nothing is known about a phone where neither permission is granted, and since nothing killed the gateway, the watchdog's repair path was never exercised. Your manufacturer's battery manager remains the variable no application code can control.

Two bugs found on the way, neither about Doze

  • startForeground() added the location foreground-service type whenever ACCESS_FINE_LOCATION read GRANTED — but that is a while-in-use permission, so a cold start from a broadcast was ineligible and threw SecurityException, killing the process 92 ms in. It fired on any cold service start from the background: boot, watchdog restarts, app updates. The recovery nets had been hiding it. The type now degrades to specialUse and self-heals.
  • The exact-alarm permission-change broadcast is not something an app can rely on: the system sends it only on the granting edge, never on revocation, and edge-triggers it off a cache that disagrees with canScheduleExactAlarms(). Jenny still listens, but now also re-checks and re-arms on the triggers she already has.

Reminders that no longer disappear

  • A one-shot reminder whose time passed while the app was dead now fires late, once. It used to be left enabled with no next run — indistinguishable from a job that had already fired, never retried, never reported. Hours late beats never.
  • A corrupt cron/jobs.json no longer takes the gateway down with it. It is handled the way config.json already was: the previous good copy is kept as .bak and promoted, and a file that cannot be read either way is set aside as .corrupt-<timestamp>. Either way the app comes online and Settings says which of the two happened.
  • Scheduled jobs now pick a mode. A reminder behaves as before and always speaks. A monitor runs in its own session, has its reply suppressed by default, and reaches you only by explicitly sending a message. Ask for a check every ten minutes and you no longer get 144 messages a day telling you nothing is wrong. The private session is not just hygiene: it stops the model remembering things it told nobody, and it holds the previous runs, which is what lets a monitor report a change instead of repeating the same alert every cycle. A quiet run is recorded as silenced, so the job list can tell "found nothing" apart from "never ran".

Internal work stays out of the conversation

Delivery used to be a property of the channel a message carried, so any turn addressed at the WebUI chat spoke — and three incompatible mechanisms had been bolted on to stop that. One of them, the turn a subagent announcement opens, had nothing at all, which is how "All clear." kept reaching the chat from a heartbeat check that had been correctly silenced.

Visibility is now a property of the turn, resolved once at its boundary. A silent turn reaches you through nothing — answer, progress, reasoning, spinner, its own error — unless the agent calls the message tool. The default comes from provenance: internal work running on a user channel is silent, which covers heartbeat, cron monitors and anything they spawn without a special case.

The LLM notification judge that used to guess at this is deleted rather than kept as a belt: with a 256-token ceiling and a reasoning model it routinely ran out of budget before deciding and fell back to its default.

Measured on the device: ten consecutive heartbeat cycles over ~4.5 hours delivered zero messages, and a cycle that spawned two subagents left nothing in the chat.

The wiki graph is searchable by what the pages say

A search box above a wiki's graph that searches page contents, not titles: matching nodes stay lit and ringed, everything else dims, letter by letter as you type.

The index rides on a pass the graph was already paying for — build_graph() read every page in full and threw the text away — and graph and index are cached together behind a fingerprint of (path, mtime, size), which is stat only. The common case, nothing changed since last time, is now faster than it was before the feature existed. They travel in one response because the postings are indices into the nodes array, and two endpoints would open a window for the wiki to change between the calls and light the wrong nodes.

Querying happens entirely in the WebView on a reused byte mask — no allocation, no GC while typing — and is coalesced on requestAnimationFrame rather than debounced, because with the index in memory a query costs less than a frame.

Deliberate limit: tokens are [0-9a-z]+ after NFKD folding, so accents fold away (citta finds Città) and a wiki in a non-Latin script is not searchable. It matches what JavaScript can reproduce exactly, which matters more here than reach — a "unicode-aware" class would diverge between the two tokenisers in silence.

The back button

It knew only about tabs. Everything layered on top of a view — dialogs, the drawer, the image lightbox, the mascot's minichat, an open mini-app — and everything nested inside one — workspace folders, the editor, onboarding steps, graph focus, the chat's session popover — did not exist for it, so a press navigated behind whatever you were looking at. Two sources also stacked twin history entries, and a press landing on one of those was swallowed in silence. That is where "it skips pages" came from.

There is now one ordered list of overlay layers, and the four things that used to describe "what is on top" in four diverging ways — hardware back, Home, the Escape shortcut, and the chat's type-ahead guard — all walk it. Every press produces exactly one visible change.

Three things found along the way that were not about the back button at all:

  • A relative link in a chat reply or a wiki page was a real main-frame navigation. It reloaded the SPA without its bootstrap fragment, leaving it authenticated against nothing — or, under /api/, replaced it outright, after which the native callback swallowed every press forever. Content links are delegated now, and the shell can tell a live SPA from a lost one.
  • The workspace editor discarded unsaved changes on every exit path. There is one teardown point now, and it asks.
  • Home closed dialogs with close(), walking over the one that refuses to close on purpose — the post-restore restart dialog — leaving a restore armed to fire at some later boot.

Smaller things

  • The thinking box follows the stream. It has a max-height, so it is its own scroll container: text grew below the fold while the box stayed pinned at the top, and re-rendering each frame reset the scroll anyway. It now tracks the last line while you are at the bottom, stops the moment you scroll up, and offers a pill to come back down.
  • Jenny settles on whichever edge she lands on. The mascot's side was a setting while the pegman throw always walked her back to the dock she started from — a setting the game contradicted. The choice is gone: she walks to the edge nearest where she falls and stays there across restarts.
  • SSH after a restore says why it broke. The private key and known_hosts live beside the workspace, not inside it, so a restore brings back config.json ...
Read more

Jenny 0.6.0

Choose a tag to compare

@flagdizero flagdizero released this 08 Aug 14:08
79b01c2

Jenny 0.6.0

This release covers everything since v0.4.0 — 0.5.0 was tagged internally and never published, so its work ships here too. Three things are new: the agent you talk to now delegates real work to background subagents instead of doing it in your conversation, Jenny can log into remote machines over SSH, and a second background job called Atlas keeps a directory of your wikis in the prompt. The rest is security work on how secrets are stored and displayed, and fixes to things that were silently wrong — including the fact that corrections to Jenny's own system prompts had never reached an existing install.

versionName 0.6.0, versionCode 6.


Agents: the assistant you talk to is now an orchestrator

The main agent keeps the tools that talk to you and the ones that start and steer work — spawn, the subagent controls, cron, message, ui_view, the long-task tools, and read-only file access. It loses the ones whose output would sit in your conversation forever: python_exec, write_file, edit_file, apply_patch, find_files, download_file, web_search, web_fetch, and the exec-session tools. That work now goes to subagents, whose tool output does not accumulate in your chat.

This is on by default (agents.defaults.orchestratorMode, default true). Setting it to false gives the main agent the old full toolset back.

Search came back as an index. The orchestrator has grep, but a reduced one: output_mode accepts only files_with_matches and count, results cap at 60 files, and asking for content returns the paths plus a note pointing at read_file. Without it, lookups on the phone degenerated into paging through files in 180-line slices — one measured lookup took twenty turns and pushed the context from 59k to 89k tokens in two minutes. Subagents and core mode keep the full tool.

Six agent types, each with its own tool allowlist. researcher (web + read + write_file), writer (read/write/patch, no network), coder (full filesystem, patch, python_exec, exec sessions, search), analyst (python_exec + read + write_file, no network), sysadmin (the SSH tools + read + write_file, no web, no local code execution), and operator (the catch-all, everything in the subagent scope). The researcher/coder and researcher/sysadmin splits are a security boundary, not a preference: whoever reads untrusted web pages is not whoever then runs code, locally or on your server. The SSH tools live in a scope of their own (remote) so that operator — defined as "everything in the subagent scope" — cannot inherit a remote shell by accident.

A Subagents panel in the chat. A strip above the message box that appears when background work starts and disappears when the turn ends. Each card shows the job label, its state, the agent type, elapsed and idle time side by side, the current phase, the iteration, and the last tool called. Stop ends a job; on a finished card the button becomes Relaunch. Tapping a card opens a detail sheet with the full task, the stop reason, and a live activity stream that labels its own health (live, paused, frozen, offline) so a quiet stream is never mistaken for a quiet job. The activity feed describes the action from Jenny's own metadata and never the content, with query strings always stripped. It subscribes when you open the sheet and unsubscribes when you close it. The panel polls every 5 seconds while something runs and stops polling in the background.

When a subagent finishes, a collapsible "What it actually did" block is appended under the reply in chat. It fetches the step-by-step digest only when you open it.

A subagent is a conversation, not a fire-and-forget. subagent_status, subagent_cancel, subagent_restart and subagent_send are available to the orchestrator and never to a subagent — a subagent cannot drive its siblings. You can send a correction mid-run ("restart the container instead of rebuilding it"). A stopped job now stays stopped across a gateway restart; it used to come back to life. The stall watchdog (180s without progress by default) only marks a job as stalled — cancelling stays a decision, yours or the orchestrator's.

Fixes found during extended use on the device:

  • A subagent no longer dies on its first recoverable mistake. fail_on_tool_error threw away jobs that had already finished their real work because of a mis-guessed read_file offset. It is now a budget, and the spilled-output reference carries a line count and a resume offset so the offset stops being a guess.
  • maxConcurrentSubagents was stuck at 1 on every existing install. The config is saved with all defaults expanded, so raising the default reached nobody. configVersion now tells a deliberate value apart from an old default and migrates once; the default is 3.
  • status_snapshot leaked other sessions. On an unknown session key it disabled its own filter and returned every session's running subagents.
  • Stopping a subagent left the orchestrator hanging for 307 seconds. The cancelled announcement left the pending queue with nothing to drain. Now 2.5 seconds on the device.
  • The anti-polling guard was dead twice over — keyed on a message_id the WebUI never sets, and counting a sequence the runner never advanced. Rekeyed on the turn id.
  • Snapshot frames with empty content wrote empty rows into the transcript.

SSH: Jenny can work on your remote machines

Off by default (tools.ssh.enable, default false) and with no hosts. Both gates are deliberate: this is the only capability in the app that acts on a computer that is not your phone.

Jenny herself has no SSH. Four tools — ssh_hosts, ssh_exec, ssh_job, ssh_transfer — reachable only by a sysadmin subagent, which in exchange has no web access and no python_exec. The orchestrator delegates and stays free to talk while the job runs; the subagent's activity streams to the panel, and subagent_send/subagent_cancel work on it like any other.

The agent can only name an alias you registered. No tool takes an address, a port, a username or a credential — there is no "connect to 10.0.0.7 as root". Hosts are added by hand under Settings → SSH, with an alias, host, port, user and a description that is shown to the model so it can pick the right machine.

Host keys are pinned and there is no trust-on-first-use. You tap Verify fingerprint, compare the SHA256 against what the server reports, and accept. Until a person has done that, every SSH call for that alias fails and tells the model to ask you. A host that later presents a different key is not updated silently: both fingerprints are shown side by side and a second explicit confirmation is required. Editing a host's address or port drops the accepted fingerprint, because a verification of the old address says nothing about the new one.

Key or password. The default is an ed25519 key generated on device, one per alias, whose private half is never displayed, never returned by any API, and lives outside the workspace where the agent's file tools cannot reach it. Password authentication is also available for servers where you cannot edit authorized_keys; the trade-offs are real and listed under Known limits. jsch is told to offer keyboard-interactive alongside password, so a PAM-authenticating Linux (PasswordAuthentication no, KbdInteractiveAuthentication yes) works on the phone and not only on a desktop. The keyboard-interactive handler answers only the unambiguous shape — a single non-echoed prompt; two prompts, or an echoed one, mean 2FA or a security question and get no answer.

Long jobs survive the connection. ssh_exec runs and returns immediately (60s default, 300s max, ~10,000 characters of output). ssh_job is never awaited: the command is detached, its output goes to a log file on the server, and Jenny reads it back incrementally from a byte cursor she keeps. The gateway is a foreground service with no wake lock, so with the screen off the CPU can suspend and a wifi-to-data handover kills the socket — a detached job does not care. Jobs survive a dropped connection, a gateway restart, and the app being killed, and resume at the exact byte.

ssh_transfer copies one file at a time in either direction over SFTP, capped at 50 MB and checked before anything is written. The local side is always inside the workspace.

A Tailscale host works with no extra configuration. Saving a *.ts.net host used to fail with "host refused by the network policy" — the validator borrowed the Jenny Apps blocklist, which refuses the carrier-grade-NAT range 100.64.0.0/10 that Tailscale hands out. SSH now has its own blocklist that allows RFC1918, IPv6 ULA and CGNAT, rather than requiring you to add CGNAT to the global security.ssrfWhitelist — which would also have opened it to web_fetch and Jenny Apps, the targets the model picks. Loopback and link-local stay blocked in every policy.

The client is native (jsch 2.28.6 + Bouncy Castle 1.85 behind a Kotlin bridge), not a Python library, which costs about 3.4 MB of APK and means SSH works in the Android app and nowhere else.


Atlas: a directory of your wikis, in every prompt

Dream turns your conversations into what Jenny knows about you. The wiki had no equivalent — pages could pile up for months and the agent stayed unaware of them until told to go look. Atlas is that half.

Every 6 hours it reads workspace/wikis/ and maintains one file, memory/WIKI.md, injected into every system prompt beside MEMORY.md: the wikis you have with a one-line scope, then the entities in the main one that matter operationally — people you deal with, projects you run, systems you operate — each a line plus a [[wikilink]]. What it buys you is that "what's the nickname of that plant I'm monitoring?" is answered from the prompt instead of costing several tool calls through the wiki.

On...

Read more