SDK
AO_AppMgr
The Active Object that owns the apps region: installs an app from a file, a URL, the console or the mailbox, runs and erases apps, updates kodeOS.
What it is
An app on a Kode Dot is a whole firmware image, written into the apps region, a part of the flash. AO_AppMgr owns that region. It installs an app, starts an installed app by name, and erases one by name. It also writes the inactive kodeOS slot, which is how install dst=os updates kodeOS.
It reads no file itself: AO_Storage reads and lends the bytes in chunks (sdk/kaos/storage). It does not decide which firmware starts either. AO_Boot does (sdk/kaos/boot), and the two AOs write different regions of the flash.
An image can come from four places: a file on a medium, a URL, the console, or the mailbox, the app_pending partition that a computer writes with esptool. Every install ends the same way. The directory, the list of installed apps, is updated in one commit after the written image is verified. When an install must evict apps to make room, AO_AppMgr first copies each victim to the card, then commits the eviction, then erases.
How it is connected
flowchart LR file["File on a medium"] --> sto["AO_Storage"] url["URL"] --> net["AO_Net"] sto --> ao["AO_AppMgr"] net --> ao con["Console"] --> ao mbx["Mailbox<br/>app_pending"] --> ao ao -- "install, erase" --> region["Apps region"] ao -- "install dst=os" --> slot["Inactive<br/>kodeOS slot"] ao -- "run request" --> boot["AO_Boot"] class ao,sto,net,boot ao class region,slot,mbx store
stateDiagram-v2 [*] --> idle : claim, kdrv_apps_init, mailbox check idle --> opening : FLASH_REQ_SIG from a file, card or URL opening --> flashing : source open, both buffers primed flashing --> idle : eof verified and installed, or a failure idle --> receiving : APP_RECV_REQ_SIG, console armed first receiving --> launching : installed, run requested idle --> adopting : APP_ADOPT_SIG, mailbox holds an image flashing --> keeping : the plan evicts apps the card lacks adopting --> keeping : the same, for the mailbox keeping --> idle : copies done, the request is posted again idle --> asking : APP_EVICT_REQ_SIG launching --> idle : APP_RECV_LAUNCH_SIG
sequenceDiagram autonumber box "kodeOS" participant A as AO_AppMgr participant S as AO_Storage end A->>S: STORAGE_OPEN_REQ_SIG S-->>A: STORAGE_OPEN_RSP_SIG, size A->>S: READ_REQ x2, both buffers S-->>A: STORAGE_DATA_SIG, buffer 0 Note over A: writes chunk N to flash while S fills buffer 1 A->>S: STORAGE_NEXT_SIG, buffer 0 back
AO_AppMgr in numbers:
| Thing | Value |
|---|---|
| Priority | 9 |
| Queue | 16 events |
| Stack | 8192 bytes, the default tier |
| Claims | KBSP_RES_APPS_REGION, KBSP_RES_APP_PENDING, KBSP_RES_OTA_SLOT |
| Compiled under | KODEOS_APPMGR |
| Flash access | kdrv_apps, kdrv_slot and kdrv_kfw (sdk/kdrv/apps) |
The stack is that large because the install path holds two directory copies of about 1 KB on it, and at 4 KB a streamed install overflowed and hung with no panic.
The hierarchy
idle is the only state that accepts work. Every other working state sits inside a parent, AppMgr_busy, which answers a question about the next install ("what would you evict?") with a busy end marker and settles late answers from AO_Storage. A new write, run, erase or list request that arrives in a working state is refused and never queued, so two images cannot mix in one place of the flash.
opening, flashing, adopting and keeping log the refusal. receiving and launching ignore it silently. app_cat is the exception: the console waits for its answer, so a refused one is answered with KODE_DENIED.
The copy
FLASH_REQ_SIG carries three things:
- a source kind:
KFW_SRC_FS,_SD,_HTTPor_PENDING; - a destination:
KFW_TARGET_APP,_OS, orKODE_FLASH_DST_PENDING, the mailbox; - a
launchflag.
A URL moves the whole conversation from AO_Storage to the stream loan of AO_Net; the protocol is the same. Each chunk goes to the decoder of kdrv_kfw, which tells a bare 0xE9 image from a KFW1 container and inflates compressed ones, so progress counts the image and not the bytes that carried it. A URL holds up to 256 characters and travels whole. The progress events and the log of AO_AppMgr name it by its last path segment, and no log line prints its query, where a token usually rides. A file name holds up to 63.
The image is placed in the decoder's header callback, the only moment its true length is known. For dst=app that is kdrv_apps_plan() on a copy of the directory, told which installed app the header names. When there is no room it evicts that app's old copy first, then the oldest apps by sequence number, so a new version costs no other app while it fits the room of the old one. For dst=os it is a size check against the slot, and launch is forced off because the switch of AO_Boot is the launch. dst=c5 never reaches AO_AppMgr: the console sends it to AO_Radio.
The pacer
After a chunk is written, AO_AppMgr waits one 10 ms tick before it hands the buffer back. Erasing flash holds the cache down and nothing in flash runs, the GUI included, so the gap gives the interface a slot.
The returned buffers collect in the to_return bitmask and one pacer returns them all. Arming it once per buffer would produce two "next" requests for one loan, AO_Storage would refill the same buffer twice, and the image would be corrupt with a byte count that still adds up. The gap costs about 1.6 s on a 5 MB image.
Verify, then commit
xfer_end() verifies the written extent in place with the real image verifier. For dst=app, region_install() then runs these steps:
- Zero the entry of an app with the same name (installing replaces).
- Take the next sequence number, before compacting.
- Compact the directory.
- Evict the oldest entry when the directory holds
KAPPS_MAX_ENTRIES(64). - Append the new entry.
- Make one
kdrv_apps_dir_commit().
AO_AppMgr then publishes APP_FLASH_DONE_SIG with the project name. When launch is set, region_launch() resolves the extent out of the committed directory by name, the same path APP_RUN_REQ_SIG takes, and asks AO_Boot for a one-shot run.
Keeping an evicted app
A victim that came from a URL, the console, the mailbox or the internal FAT has no other copy. Before AO_AppMgr commits an eviction it copies each victim to /sdcard/<project>.bin, byte for byte. The extent is a verified, bootable image with its own manifest, so the launcher shows the file as an app to install again. A victim is already kept when that file exists, whatever its content.
The install runs in a round. The first plan that names victims abandons the transfer and enters keeping, which checks the card with a STAT and, if the file is absent, reads the extent from the region into PSRAM 32 KB per tick and hands it to AO_Storage as a whole-file write. When every victim is tried, AO_AppMgr posts the same request again at the front of its queue, and the second plan only evicts.
A failed copy decides by origin:
- From the internal FAT, a URL or the mailbox, the install goes ahead with a warning.
- From the card, it is cancelled before anything is evicted, with
APP_FLASH_FAIL_SIGand reasonKFAIL_KEEP. A failed write on a counterfeit card poisons later reads, and both apps would be lost. - From the console, nothing is copied.
Install from the console
install src="terminal" is its own request, APP_RECV_REQ_SIG. In idle AO_AppMgr checks that the region or the slot exists and fits, then asks AO_Log for raw mode first, before any flash work, because an erase stops the reader task and an arming that lands inside it cannot tell the command's newline from the first payload byte.
In receiving, the transfer runs in four steps:
- AO_AppMgr makes room, opens the sink, tightens the watchdog from the 30 s long-stop to the 5 s gap and publishes
RECV_READY_SIG. - The host sends 4096-byte chunks (
RECV_DATA_SIG, posted by the reader task). - AO_AppMgr stages each chunk, takes its CRC, writes it, and publishes
RECV_ACK_SIGfor each whole chunk. - At the full count it compares the CRC, verifies, installs and publishes one of three static outcome events:
RECV_DONE_SIG,RECV_FAIL_SIGorRECV_STALL_SIG.
The room is made before the first byte arrives, so the plan cannot read the image's name. The request carries it instead, from install … name="<project>", which kcli and kble send, and the old copy of that app goes first. Without name the plan treats the image as a new app. Each app evicted is published as APP_EVICTED_SIG before RECV_READY_SIG, and the console prints it as >|evicted.
The acks are the entire flow control, because the USB driver drops bytes silently when its ring is full. A failed write stops neither the draining nor the acks, since the host still owes the full count.
A failure after the 0 % report also publishes APP_FLASH_FAIL_SIG, so the Opening card ends as a failure. The launching state waits 50 ms so the >ok leaves the console before the board resets.
Mailbox adoption
At boot, and for install src="pending", AO_AppMgr checks kdrv_apps_pending_present() and posts APP_ADOPT_SIG to itself, because a copy of several MB must not run inside an initial transition. It reads the destination from the OS-intent sector (unless dst= was typed), reads the project name from the head of the mailbox, plans with it, and copies 4 KB per tick in adopting.
A deterministic failure (the image does not verify, the directory is full and nothing can be evicted) consumes the mailbox so no later boot adopts it again. A directory commit failure or an I/O error leaves it, and the next boot tries again. On success the mailbox is consumed after the commit.
A boot-time adoption then starts the app with KBOOT_RUN_STICKY, the one sticky request kodeOS writes, so the app survives the reset that the host's port opening causes. A typed install src="pending" starts it once, and only with run=on.
Resting with the board
With CONFIG_KODE_LOWPOWER, AO_AppMgr joins the rounds of AO_LowPower as appmgr (sdk/kaos/lowpower). It owns no device that sleeps. It owns work that must not be cut: an image being written, kept or received, a launch on its way and the asks of an eviction are every state but idle. Each of those answers a round busy (app write), and idle answers ready. Nothing is restored when the board wakes.
Specifications
Requests
| Signal | Event | From | What AO_AppMgr does |
|---|---|---|---|
FLASH_REQ_SIG | FlashReqEvt (src_kind, dst_kind, launch, src) | kode_flash(), the console, the launcher | The only way to ask for a copy. appmgr_flash_request(file, launch) is the form src="fs:…" dst=app |
APP_RECV_REQ_SIG | AppRecvReqEvt (dst, size, crc32, launch, src, name) | appmgr_recv_request() | An image arrives on the console |
APP_RUN_REQ_SIG | AppFlashEvt (file) | appmgr_run_request() | Resolves the name in the directory and asks AO_Boot for a one-shot run. Logs '<name>' is not installed otherwise |
APP_ERASE_REQ_SIG | AppFlashEvt | appmgr_erase_request() | Zeroes the entry, compacts, commits, then kdrv_apps_erase_header() clears the first 4 KB of the extent |
APP_CAT_REQ_SIG | AppCatReqEvt (project, cats, clear) | appmgr_cat_request() | Sets or clears the category override in NVS. AO_AppMgr is the only writer of that table |
APP_LIST_REQ_SIG | AppListReqEvt (skip, who) | appmgr_list_page() | Posts up to 24 APP_ENTRY_SIG and an end marker to who |
APP_EVICT_REQ_SIG | AppEvictReqEvt (size, project, who) | appmgr_evict_query() | Plans an install on a copy of the directory and posts up to 8 APP_EVICT_ENTRY_SIG, then APP_EVICT_END_SIG (victims, fits, busy) |
STORAGE_OPEN_RSP_SIG, STORAGE_DATA_SIG, NET_STREAM_OPEN_RSP_SIG, NET_STREAM_DATA_SIG | Storage and stream events | AO_Storage, AO_Net | The loan |
STORAGE_PATH_RSP_SIG, STORAGE_WRITE_RSP_SIG | StoragePathRspEvt, StorageWriteRspEvt | AO_Storage | The answers of keeping and asking |
CONSOLE_RAW_RSP_SIG, RECV_DATA_SIG | ConsoleRawRspEvt, RecvDataEvt | AO_Log, the reader task | The console install |
LOWPOWER_ROUND_SIG | LowPowerRoundEvt | AO_LowPower, subscribed with CONFIG_KODE_LOWPOWER | Answers ready in idle, busy app write in every other state |
Private: APP_ADOPT_SIG, and the QTimeEvts APP_FLASH_CHUNK_SIG, APP_RECV_IDLE_SIG and APP_RECV_LAUNCH_SIG.
What it sends
| Signal | Event | Range | Meaning |
|---|---|---|---|
APP_FLASH_PROGRESS_SIG | AppProgressEvt (percent, what, file) | publish | 0 % when a write starts, then each whole percent. what is KWRITE_APP, _APP_RUN, _OS or _C5. The Opening card and the LED pulse of AO_Power follow it. The log keeps a 10 % step |
APP_FLASH_DONE_SIG | AppFlashDoneEvt (name) | publish | The app installed; name is empty when the descriptor could not be read, and a static event goes out when the pool is dry |
APP_EVICTED_SIG | AppEvictedEvt (name, kept) | publish | One per app an install evicts, before the write starts. kept says a copy is on the card. The old copy of the app being replaced is not one |
APP_FLASH_FAIL_SIG | AppFlashFailEvt (why) | publish | KFAIL_WRITE, or KFAIL_KEEP when the card could not keep an app. One static event per reason |
APP_CAT_RSP_SIG | AppCatRspEvt | publish | The outcome of APP_CAT_REQ_SIG |
RECV_READY_SIG, RECV_ACK_SIG, RECV_DONE_SIG, RECV_FAIL_SIG, RECV_STALL_SIG | none | publish | The receive protocol. The three outcomes are static and cannot fail to allocate |
APP_ENTRY_SIG, APP_LIST_END_SIG, APP_EVICT_ENTRY_SIG, APP_EVICT_END_SIG | AppEntryEvt, ListEndEvt, AppEvictEntryEvt, AppEvictEndEvt | post to the asker | Pages and eviction answers |
BOOT_APP_RUN_SIG, BOOT_OS_SWITCH_SIG | BootRunEvt, none | post to AO_Boot | After a verified write, app_run, or an adoption |
CONSOLE_RAW_REQ_SIG, CONSOLE_RAW_END_SIG | ConsoleRawReqEvt, none | post to AO_Log | Arm raw mode; abandon it on a stall |
Timers and limits
| Name | Value | Bounds |
|---|---|---|
pacer (APPMGR_CHUNK_GAP_TICKS) | 10 ms | The pause before a buffer goes back, and each 4 KB or 32 KB step of adoption and keeping |
idle, APPMGR_RECV_IDLE_TICKS | 5 s (KODE_RECV_IDLE_MS) | The gap between two chunks of a console install |
idle, APPMGR_RECV_LONGSTOP_TICKS | 30 s | The wait for the answer that arms raw mode |
idle, APPMGR_OPEN_TICKS | 30 s | opening, and the STAT answer in keeping |
idle, APPMGR_CHUNK_TICKS | 30 s | The gap between chunks in flashing. Expiry is KFAIL_WRITE |
idle, APPMGR_KEEP_WRITE_TICKS | 60 s plus 1 s per 128 KB | One copy to the card |
pacer, APPMGR_KEEP_BEAT_TICKS | 1 s | Re-publishes the percent while the card writes, so AO_Power keeps the LED pulsing |
answer, APPMGR_ANSWER_DRAIN_TICKS | 50 ms | From "installed" to the reset |
KAPPS_MAX_ENTRIES | 64 | Apps in the directory. A full listing is three pages |
KODE_APP_EVICT_MAX | 8 | Victims in one eviction answer |
KODE_RECV_CHUNK | 4096 | Console chunk, and the static adoption buffer |
KODE_FLASH_SRC_MAX | 257 | A source with its terminator: a URL of up to 256 characters. A console line carries fewer |
The idle timer is shared by opening, flashing, receiving and keeping. These states exclude each other and each disarms it on exit. The timer idle and the state idle are two things.
Console verbs, calls and settings
app_list, app_run name="…", app_erase name="…", app_cat, install src=… dst=os|app|c5|pending, with name="<project>" for src="terminal" dst=app only. The facade call is kode_flash(src, dst, run): src is "fs:<file>", "sd:<file>", a bare name or an http:// or https:// URL, dst is "os" or "app", and it posts and returns. AO_AppMgr owns the category override table of kapp_cat, an NVS blob. The format of the region and the directory is on internals/apps-region.
Gotchas
Example code
Install an app from the card and start it. With blinky.bin on the card, the board writes it, resets and runs it.
/*
* install_from_card.c - 2026-10-28 - from Kode with love
*
* Asks AO_AppMgr to install /sdcard/blinky.bin into the apps region and run
* it. kode_flash() posts the request and returns; AO_AppMgr reads the file
* through AO_Storage, verifies it, commits the directory and resets into the
* new app. On a Kode Dot the Opening card shows, then the app starts.
*/
#include "kode.h"
#define TAG "install"
/* Post the install, then wait for the reset. */
void app_main(void)
{
kode_init();
if (kode_flash("sd:blinky.bin", "app", true) != KODE_OK) {
KLOG_E(TAG, "no installer in this firmware, or a bad argument");
return;
}
KLOG_I(TAG, "install requested");
for (;;) {
kode_delay_ms(1000);
}
}Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.
In the SDK
components/kaos/appmgr:appmgr_ao.c, the states, the copy and the adoption.components/kdrv/apps: the directory format (kapps_fmt.h), the sinks and the decoder (sdk/kdrv/apps).kaos.h:appmgr_flash_request(),appmgr_list_page()and the other request helpers.