Repository navigation
Releases: s1crab/android-log-mcp
Release list
v0.10.0
Highlights: (1) Recording now resumes for previously wanted devices after the service process is killed or replaced (new run/wanted.json, #234); set ANDROID_LOG_MCP_LINGER=0 to opt out. (2) service stop --force and the automatic replacement of a hung service now wait for the old holder process to exit (Windows; the Unix wait is deferred), bounded by 5 s (#247, #226). (3) Crash and ANR identity (R2, #225): one crash or ANR stays one row across service restarts, ANR sources are paired by ordinal (#229, #235, #238, #245). (4) Redaction is about 30x faster (#177). The additive optional fields (inc, incw, ev, n in crashes.jsonl, the new run/wanted.json) do not change the format version: segment files are unchanged and older binaries ignore them.
Platform status: Windows and macOS are covered by CI only (unit tests with the fake adb); they were not tested with a real or emulator-attached adb for this release and are unverified. The emulator checks are summarised in validation/0.10.0/README.md.
Changed
- After a crash, a kill or an automatic replacement (a newer MCP or CLI process, a hung service process), the new service process resumes recording the devices that were still within their linger time (default 10 min to 1 h after the last log tool call), even if the MCP or CLI process that used them has gone (#226). Each device stops at the end of that linger as before; an idle exit,
service stopandservice stop --forcerestore nothing, nor does a cold start;ANDROID_LOG_MCP_LINGER=0disables it. The devices are carried in the new optional filerun/wanted.json(the writer's pid and start time, the write time, per device the idle time and linger; unknown keys and a garbage file are ignored, and older binaries never read it). - Performance: redaction (
report,logcat_export redact=true,export --redact) is several times faster on typical logs (#177). Each message is scanned once and a rule's regular expression only runs when the text can contain what it matches. On the real API 29 captures, per-line redaction went from about 1.6 MiB/s to about 49 MiB/s (about 15,600 to about 476,000 lines/s); logs with many sensitive values, and oneTextcall on a whole multi-line text that contains matches, gain less. The output is byte-identical (the old implementation is kept as a test reference), so the numbers of the[REDACTED:<type>#<n>]placeholders do not change.
Fixed
- Replacing or stopping a hung service process (
service stop --force, the automatic replacement) now waits until the old service process has really exited, not only until it released its lock, bounded by 5 s (#226). A reused pid is never waited for (the old process is identified by pid and start time), a zombie counts as exited, and the wait ending early is logged and is not an error. This makes sure a successor never starts beside a process that still holds the device directories (matters on Windows). Wait for exit after a kill of an adb child on Linux and macOS is unchanged. - A call made right after the service went away (for example an MCP process whose in-process service stopped) no longer fails with
write: broken pipe: a write to a closing service socket is treated as a lost connection, so the client reconnects, starts or takes over the service and retries the read-only call (#237). - A native crash is no longer listed twice when its tombstone lines are read after the log line
Fatal signalwas already stored: the tombstone,am_crashand the DropBox entry now attach to the existing crash (same id, same row) instead of creating a second one a moment later (#233, #225). - A crash or ANR is no longer listed twice when the service process restarts between two of its sources (version handoff, hung-service replacement,
set-root, linger exit): the detector now looks for the open crash in the stored crash list instead of its own memory, so a lateam_anr,ANR inblock,am_crash, tombstone or DropBox entry attaches to the crash it belongs to (#225, first of three changes). A DropBox entry fetched while the detector writes the same crash no longer drops the detector's update (the write now re-reads the latest stored version under one lock), and a pid reused by a new process lifetime inside the pairing window is two crashes, not one. The pairing windows themselves are unchanged.crashes.jsonlgains optional fields (inc,incw,ev) on crashes written by this version; no format version changes, older binaries ignore them and list the same crashes, and files written by 0.9.0 and 0.8.1 list identically. - The sources of one ANR (
am_anr, theANR inblock, the DropBox entry) are paired in the order they arrived, no longer by a 120 s window (#225, second of three changes). A dump that takes longer than two minutes (slow emulator, loaded device) no longer produces a second ANR; the pairing gives up only after 10 minutes. Two real ANRs of one process are never merged, whatever the gap and wherever the service process restarts, and a DropBox entry or block with another reason does not attach to an unrelated ANR.crashes.jsonlcrashes of kindanrwritten by this version carry the optional fieldn(ordinal within the process lifetime); older binaries ignore it. Crashes written by 0.9.x keep the old pairing windows. - A DropBox entry no longer attaches to the crash or ANR of an earlier process lifetime of the same pid (#225, third of three changes). The entry is placed in the process lifetime that owned its pid at its device time, and a crash from another lifetime is never its source, however close in time; a Java or native crash entry no longer attaches to "the same pid and process whatever the time" across a restart of the app. The remaining 15 s, 60 s and 120 s matching windows are gone for crashes written by this version: sources of one crash are paired by source (one per kind), process lifetime and, for an ANR, the 10 minute cap; a logcat crash that arrives after its DropBox-only twin is adopted by the same rules, so a DropBox entry written more than two minutes after the logcat lines of the ANR is still one crash. Crashes written by 0.9.x (no
evfield) keep the old windows, only for that data. No new field and no format change. - A crash or ANR that was already stored could lose its earlier sources and its ordinal
nwhencrashes.jsonlcould not be opened for a moment while a compaction replaced it (seen on Windows as a flaky race-gate; the service process runs both). The reader now retries a transient open failure, and attaching a source never overwrites a stored crash it cannot read (#244).
Full changelog: v0.9.1...v0.10.0
v0.9.1
Patch release: the on-disk store is declared an internal format, and meta.json is trimmed with retention (#173). No tool or CLI change; segment files are unchanged and older binaries read meta.json as before. Platform status: Windows and macOS are covered by CI only (unit tests with the fake adb) and are unverified with a real or emulator-attached adb. The emulator checks are summarised in validation/0.9.1/README.md.
Changed
- Docs: the on-disk store (JSONL segments,
meta.json) is declared an internal, versioned format; agents and users should usereportandlogcat_exportrather than parsing it (#173). - The
cursor_prunednote oflogcat_searchnow says which line "oldest" is:note code=cursor_pruned boot=<boot> first=<seq> using=oldest(#173). meta.jsonis trimmed together with retention (#173): when segments are pruned,seg_times, theclock_jumpsbefore the first retained seq and thesessionsthat ended before it are dropped, so a long-lived device no longer grows its meta forever. The session that covers the first retained seq is always kept, because the boot of a seq is the last session starting at or below it.meta.jsonis written compactly (no indentation). What tools read is unchanged; older binaries read the file as before.
Full changelog: v0.9.0...v0.9.1
v0.9.0
Highlights: the logcat_export tool and android-log-mcp export CLI (#172), and a round of store and device robustness fixes (failed writes, torn tails, follower takeover, retention after a clock correction, recorder restarts). Segment files are unchanged; meta.json gains an optional seg_times field that older binaries ignore.
Platform status: Windows and macOS are covered by CI only (unit tests with the fake adb); they were not tested with a real or emulator-attached adb for this release and are unverified. The emulator checks are summarised in validation/0.9.0/README.md.
Added
logcat_export(#172): writes the lines alogcat_searchselection matches to a text file for a human to review (logcat -v threadtime -v yearlines in device time, a#header,--------- clock jumpmarkers, a# endfooter) and returns only its path and counts, never log lines. Files go to<root>/export/(the tool has no output path). Limits:max_mb(default 64, max 512) and a 20 s time budget; a cut export says so (note code=truncated,truncated newer=N cap=max_mb|time_budget next: after=<cursor>) andafter=writes the rest. Not redacted by default (note code=unredacted);redact=truemasks the message text with the rules ofreport,redact_extraadds RE2 patterns. A redacted export of a large window is slow (about 15,000 lines/s) and usually ends at the time budget. The CLI command follows in a later change.android-log-mcp export(#172): the CLI side oflogcat_export, through a new service op. The same selection flags as the tool,--out FILE(an absolute path; refused inside the data root's or control directory's own entries),--forceto overwrite,--redact/--redact-extra/--keep-domain/--keep-private-ip;--offline DIR exportneeds--out. Not redacted unless--redact, the opposite ofreport(report --no-redact); both are documented side by side in docs/reference.md.logcat_statuslists the newest export file:export file= bytes= age_s= complete=1|0.- A data root now has an
export/directory (mode 0700, a protected ACL on Windows) for the export files of the cominglogcat_export(#172). It is bounded from the start: files older than 7 days, more than 20 files or more than 256 MiB in total are removed (oldest first), it counts toward the total cap, andset-root --delete-oldremoves it.
Changed
- Tool annotations:
logcat_exportis not read-only and not idempotent, andlogcat_statusis no longer annotated read-only (it starts recording, andrecord_forpins it). An MCP host that auto-approves read-only tools may now ask first. The tool definition carries the new optional fieldswritesandnot_idempotent; an older MCP process ignores them. - docs/GOALS.md principle 5 now reads: a report meant for other people is redacted by default; a log file exported for the developer's own review is written as recorded unless redaction is requested, and says so.
reportstill redacts by default. - The MCP process no longer retries
logcat_exportafter a lost connection to the service (a retry could write a second file); it answerserror code=service_unreachable. - The
logcat_exporttool description is about 1 KB shorter intools/list(details stay in docs/reference.md) and a repeated sentence oflogcat_searchwas folded:out.tools_list.bytes13,730 → 13,097. No behaviour change.
Fixed
- The sweep of device directories nobody is recording (at service start and every minute) no longer deletes segments by file mtime (#218). After a host clock correction the mtimes can be far in the past while the data is recent, and the sweep removed it although the store's own retention would have kept it (#210). The sweep now uses the store's rule: the
seg_timesstamps inmeta.json(a future or absurdly old stamp is set to now and persisted; the mtime is used only for a store that has no stamps yet), expires oldest first, removes the newest segment only when all have expired, and orders the total-size cap by stamp. A directory with a livewriter.lockis still left alone. - The same ANR record fed twice (same pid and seq) no longer opens a second, partial ANR in
logcat_crashes(#175): since the one-to-one pairing of #111 a repeat found no ANR lacking its source. The repeat now leaves the ANR unchanged; a genuine second ANR of the process stays separate. Repeats of the same text under a different seq are not recognised by this rule. Store.RewriteAuxnow refuses a store that is not the writer, likeAppendAuxandCompactAux, and a failed rewrite of an aux file (crashes,procs,dropbox_seen) removes its*.jsonl.tmpinstead of leaving it to eat disk space (#196).- A log line stamped after the year 2262 (a device clock set far ahead, or a corrupt line) is no longer turned into a negative time (#195): the epoch form (
seconds * 1e9wrapped) and the-v yeardate form are rejected as not a log line when the time does not fit 64-bit nanoseconds. - Store hardening (#188):
after=MaxUint64madeScan,Count,CountMatchandAggClassesreturn everything (after+1wrapped to 0); they now return nothing.nilclass ids meant "every class" for scans but "no class" for the block-count path ofCountMatch/AggClasses; they now mean "every class" everywhere (an empty set still matches nothing). Latent: the search tools already clamp cursors and pass a non-nil set. - Retention no longer trusts segment file mtimes or a future start (#184, #186, #189). After the host clock was set back (RTC correction), a segment stamped in the future was never rotated or deleted, and
OpenDircould prune with the wrong "now"; frequent restarts could also keep a segment pastMaxAge/4forever. The store now persists a host-time first/last stamp per segment inmeta.json(seg_times), clamps stamps to now (so a future segment ages from the correction), re-anchors stamps older than max(30 days, 10 × MaxAge) to now (never an immediate mass delete after a clock fix), and keeps the segment start across restarts so theMaxAge/4rotation holds. A store withoutseg_times(older version) gets its stamps filled from the file mtimes on first write. Segment files are unchanged and an older binary ignores the new meta field and keeps using mtimes. android-log-mcp export --outcould not be stopped from writing into the data root's own entries (v<N>/,run/,tmp/,bin/,log/,export/,config.json) by spelling them in another letter case on a case-insensitive file system (Windows, macOS) (#205). The guard now compares by file identity after resolving symlinks (and by name ignoring case on those systems).- A follower store re-reads
meta.jsonwhenever its mtime or size changed, not only when the mtime is newer, so a host clock set back no longer leaves read-only consumers and promoted followers on stale clock jumps and sessions (#183). A failedmeta.jsonsave (a Windows follower holding the file, a full disk) now stays dirty andFlushretries it (#187). android-log-mcp export --outno longer creates its temporary file in your directory (#206): it is written in<root>/export/(where a leftover is swept after an hour) and renamed into place; when the rename crosses volumes the content is copied next to the target and renamed there. With--offline(no data root) the temporary file is still created next to--out.- Each in-session restart of the recorder (the logcat stream dies while the device stays attached) no longer leaves the previous iteration's
reconcileand side-work goroutines running (#194): everyrecordOncehas its own context, cancelled when it returns, so repeated restarts no longer pile up repair loops and adb work. - A search no longer races with the recording of new lines on the block index of a segment (#197): the index a search reads is now a snapshot taken under the history lock.
- Concurrent track-devices updates can no longer start two recording sessions for one device (#193): the check that no session runs and the publication of the new session are one critical section.
- A follower store promoted to writer now takes over what the old writer left (#180): it re-lists the segments, reloads the newest seq and the recent window, and cuts a torn last line before its first append. Before, it reused seqs the old writer had written (duplicate seqs on disk) and its first record joined a partial last line and was lost.
crashes.jsonl,procs.jsonlanddropbox_seen.jsonl: an entry appended after a torn last line (a hard kill or short write in the middle of a line) now starts a new line instead of joining the partial one and being lost (#181). Read...
v0.8.1
Platform status: Windows and macOS are covered by CI only (unit tests with the fake adb); they were not tested with a real or emulator-attached adb for this release and are unverified. The emulator checks ran on API 29 locally (software emulation / TCG); API 35 is noted in the release PR / validation README.
Added
- Exact filtered counts for tag / package / process / level / uid queries via per-block class counts in the history index (#37). Countable
logcat_searchqueries reportexact=1with no 100 ms count budget; blocks with zero matching classes are skipped;view=summarylevels / tags / processes are exact over the whole range. Index built lazily at query time (ingest unchanged). Cap: >4096 classes in a block falls back to decode so the result stays exact.
Fixed
CountMatch/AggClassesskipped decoding only for seq-range misses; blocks wholly outside a time window were still decoded, which slowedfrom/toandview=summaryqueries after #37. They now use the sameoutsidecheck asScanTime.
Full changelog: v0.8.0...v0.8.1
v0.8.0
Platform status: Windows and macOS are covered by CI only (unit tests with the fake adb); they were not tested with a real or emulator-attached adb for this release and are unverified. The emulator checks ran on API 29 and API 35 on the GitHub runners, and on API 29 locally (software emulation).
Output format fmt=1 (#85, agent-first output, docs/GOALS.md). The whole set ships in one breaking release (0.8.0); this version covers logcat_search, logcat_status, logcat_crashes, logcat_app_launches and the errors shared by all tools (the CLI in this part).
Upgrade notes
0.8.0 is one deliberate output break (#85): parse once, against fmt=1 (see docs/reference.md). Pre-1.0, so breaking is a minor version (AGENTS.md §4). The first token of every response is now ok, empty or error; read the header line first, then split rows on spaces / key=value.
| # | Breaking change | What to do |
|---|---|---|
| B1 | logcat_search has a summary sampled=N top3_share=P% tags=... msgs=... head line when more matches exist than shown (sampled is the number of matches the digest was computed from, at most 2000, not matched; ~ before a percentage means sampled < matched); the top tags in ... tail line is gone |
read the head line |
| B2 | Every truncation is one truncated <what>=N exact=0|1 ... next: param=value line (replaces [truncated: ...], [output cap: ...], more:, raise limit); crashes, launches and status gain markers |
follow next: |
| B3 | The clock: prose line is gone; clock_jumps=N ... next: after=... appears only when a jump splits the matches |
parse the one line |
| B4 | Header is key=value with ISO times; host now, format: and count: lines are gone (host_now, tz with verbose=true); fmt=1 |
match on the first token and keys |
| B5 | Rows: one-token process (uid:system, never ?(uid system)), | continuation lines, ^ rep=N, ASCII separators, no thousands separators, durations in seconds/ms |
split rows on whitespace |
| B6 | Empty results and errors are empty ... scanned= range= and error code=<code> ... next:; call maps JSON/usage mistakes to the same shape |
match code=, not message text |
| B7 | logcat_crashes detail shows 20 lines before a crash (was 40) |
pass before_lines=40 (max 100) for the old amount |
| B8 | CLI: call writes nothing to stderr normally; service status, record, report stderr and set-root print key=value lines; usage errors are one error code=usage ... next: line |
use --json (unchanged, numeric fields added) or parse key=value |
| B9 | logcat_search view="summary" is levels / tags / procs / msgs / crashes=N anrs=N next: logcat_crashes ... / latest_errors=N lines instead of the prose block (total ... · by level, top tags:, top processes:, top repeated messages, crashes in range) |
parse the key lines; counts are over the whole range |
| B10 | Header values: a string is bare unless it is empty or contains a space, control character, ", = or \; otherwise a JSON string (a Windows path is always "C:\\x"). next: commands quote paths for the host shell, one per line |
read quoted values with a JSON string parser |
| B11 | Status and error text uses the glossary terms (#40): service status --verbose / logcat_status print service: service process pid …, connected N (was service: background service pid …, clients N) and N MCP/CLI process(es) connected (was N client(s) connected); the client: version … line is note code=version_skew mcp_process=… service=… protocol=… compatible=1. The JSON clients field is unchanged |
match the new text (or code=version_skew), or read clients from service status --json |
Changed
- Breaking: text that agents and users read says MCP process, CLI process, service process and MCP host (see docs/glossary.md) instead of "client", "MCP client" and "background service":
service status,logcat_status(lifetime and linger lines), errors (version skew, handoff, repair),install, the--homeerror, usage texts, thereportREADME and the perf baseline descriptions. The version-skew note oflogcat_statusis structured:note code=version_skew mcp_process=0.7.0 service=0.8.0 protocol=2 compatible=1. Not renamed: the JSONclientsfield ofservice status --jsonandservice.json, Go identifiers and perf metric keys (#40). tools/list(sent at the start of every session) is smaller: 10,378 bytes on 0.7.x, 10,230 bytes now (-1%), althoughlogcat_search,logcat_crashesandlogcat_app_launchesgained theverboseparameter and a one-line description of the fmt=1 response (the full format is in docs/reference.md).- Breaking:
logcat_searchresponses start with one header lineok,emptyorerror, thenkey=valuefields (view device matched exact shown order range dev_now clock offset_s clock_age_s fmt=1). Times are ISO-8601 with offset, numbers are plain integers (no thousands separators), durations are seconds (_s). The old prose header (device … · device time … · device now … · host now …), theformat:line and thecount:line are gone.host nowis no longer printed by default; passverbose=trueto gethost_now,tzand the column legend. Upgrade: parse the first line asok|empty|error k=v …; read the count frommatched/exact(exact=0means a lower bound, withcounted_from/counted_to). - Breaking: rows of
logcat_searchuse one-token columnstime level pid/tid process tag: message: the process is never?(uid system)any more butuid:system(?= unknown); continuation lines of a multi-line message start with|;↳ ×N identical until Tbecame^ rep=N until=T; Java stack markers became^ frames_hidden=Kand^ same_frames=K as=T. Upgrade: split a row on whitespace into 5 tokens, the rest is the message. - Breaking: every cut is one line
truncated <what>=N exact=0|1 … next: param=value, replacing[truncated: …],[output cap: …],more:,more/wait:,stacks: … shortenedand the…[+N chars; …]per-line marker (now...[line_cut=N more_chars=M next: …]). The next step is always executable; the page cursors arenext: before=…(older) andnewer: after=…. - Breaking: clock jumps print one line
clock_jumps=N splitting=S seq=… delta_s=… matches_before=… matches_after=… next: after=…, and only when a jump splits the matches. The proseclock:line and theclock_jump:key=value line are gone (verbose=trueprints each jump asclock_jump seq=…). The same jump is no longer printed twice. - Breaking: empty results and errors are first-class:
empty … matched=0 scanned=N range=<stored first>/<stored last>withhint code=… next: …lines; errors areerror code=<code> … next: …with a closed set of codes (bad_param,bad_filter,bad_time,bad_cursor,host_time,empty_window,unknown_device,several_devices,unknown_tool,unknown_param,bad_type,bad_json,device_*…).unknown toollists the tools. Thefilter parsed as:echo isfilter parsed=…and is printed only whenfilteris set. - Breaking: the
top tags in …tail line of a widelogcat_searchmoved to the head: when more matches exist than are shown, onesummary sampled=N top3_share=P% tags=a:12%,b:6% msgs=515x"tag: message with #"line (at most 300 chars) follows the header;sampledis how many matches the digest was computed from (the first 2000, not the matched count), and the percentages carry a~whensampledis smaller thanmatched, so one call says what the hidden matches are (#85).view="summary"islevels/tags/procs/msgslines with counts, crash counts plus thelogcat_crashescall, and onenext:. logcat_searchgainsverbose(see above). The shared-uid package list (up to 24 packages for one query) isnote code=shares_uid … shares_uid=N next: verbose=trueby default.- Breaking:
logcat_status,logcat_crashesandlogcat_app_launchesuse the same header (ok|empty|error,view device … dev_now clock offset_s fmt=1), ISO-8601 times with offset and the sametruncated … next:,note code=…,hint code=… next: …anderror code=… next: …lines aslogcat_search;host_nowandtzare printed withverbose=trueonly.logcat_statusprints one fact per line (adb,store,device id=… state=… model=… tz=…, indentedrecording state=…,window,stored,memory,note), the cursor iscursor=…in the header (cursor=nonewhile the backfill runs or several devices are online), andpackage=addspackage id=… installed=1|0|unknown uid=… running=Nand oneproc pid=… name=… state=running|ended … reason=… [crash=cr3|anr=anr1] scope=this_boot|before_rebootrow per process (names are one token).logcat_crasheslistscrash id=… time=… kind=… pid=… process=…entries withsame_signature same_sig=N ids=… next: id=…; the detail hassignal:,crash_location:,tombstone,main_thread traces=…andbefore_crash pid=… lines=Nsections.logcat_app_launchesprints#N launch time=… kind=COLD|WARM|HOT component=… pid=… ttid_ms=…|ttid=none ttfd_ms=…|ttfd=nonewithstep name=… at_ms=… [took_ms=…]lines; the explanatory sentences arenote code=…codes now (see docs/reference.md). Upgrade: parse the header line first; split rows onkey=value. - Breaking:
logcat_crashesshows the 20 lines before a crash by default (was 40);before_lines(1-100) asks for more, and a cut says `truncated before_lines=20 exact=0 next: before_line...
v0.7.1
Fixed
- Correction to the 0.7.0 upgrade notes: the long-path fallback socket did not move. When
<control dir>/run/service.sockwould exceed the socket path limit (a very long home or cache path), the socket is, as in 0.6.0,/tmp/alm-<uid>/<hash>.sock(Windows:%TEMP%\alm-<hash>\s.sock). Nothing needs to be done when upgrading; the unreachable cache-dir fallback code was removed (#108). - One ANR is no longer reported twice when the
ANR inblock follows theam_anrevent by more than 30 s (slow ANR dumps on loaded devices and emulators): events, logcat block and DropBox entry are now matched per process over up to 120 s (#111). - An ANR whose DropBox entry arrives 60 to 120 s after the
am_anrevent (a slow ANR dump on a loaded device or emulator) is no longer listed twice when the DropBox entry was fetched before the logcat events were seen: the DropBox entry of an ANR now uses the same 120 s window as the other sources, in both orders. Other crash kinds keep their 60 s window (#129). - The first
logcat_status(or log tool call) that replaces a running service process (version handoff, or automatic replacement of a hung service process) now starts the requested device, like on a cold start, instead of answeringrecording: idle/not recording (device not connected)until the next call; a slow device identity no longer prevents the device from being started, also when the device is named by its adb serial (#109).
Full changelog: v0.7.0...v0.7.1
v0.7.0
Upgrade notes
android-log-mcp service stopexits 1 when a service holds the lock but does not answer, instead of printingservice: not runningand exiting 0. Scripts that treated that exit code as "stopped" should callservice stop --force(#83).- The long-path fallback socket moved (unix only; used when
<control dir>/run/service.sockwould exceed the socket path limit, e.g. a very long home path). It is now<user cache dir>/android-log-mcp/run/s-<hash>.sockinstead of/tmp/alm-<uid>/<hash>.sock. A service started by an older version keeps its old socket until it stops or is replaced (#83).
Added
android-log-mcp service stop --forceends a service that holds the lock but does not answer: SIGTERM first, SIGKILL after 10 s (Windows: a stop request, then TerminateProcess after 10 s; lines the service had not flushed yet can be lost, and the command says so). It only signals a process that is verified to be the service recorded inrun/service.json(same executable and start time), and it never kills a service that runs inside an MCP process (that would kill the MCP process): it refuses and says to end that MCP process instead (#83).android-log-mcp report --logs-onlymakes a small zip with no recorded device data: the service log (last 1 MiB),crash.log,service.json, a diagnosis of the service lock andadb devices -l, all redacted. It works with--offline DIRwhile the service is down (#83).- Every report now contains
diagnostics/(service log,crash.log,service.json,diagnose.txt, andadb devices -lwhen adb is available), also--offlinereports (they runadb devices -lonly;--no-adbskips it). The values go through the usual redaction, plus two new rules that apply to every report: user names in home paths (/home/<user>,/Users/<user>,C:\Users\<user>) and the home directory of the user who runs the report become[REDACTED:username#n]/[REDACTED:home_dir#n](also in the command line and warnings ofmanifest.json), and the serials inadb devices -lbecome[REDACTED:device_id#n]. ANDROID_LOG_MCP_REPORT_HINT=1(developer option, off by default): connect and start errors that mean android-log-mcp itself is broken then tell the agent to ask the user first and, if they agree, runreport --offline <root> --logs-only; the user reviews the zip and submits the issue by hand. It changes only error text. There is no network code in android-log-mcp: nothing is uploaded or sent.service statussays so when a service holds the lock but does not answer (exit 1;{"running":false,"unresponsive":true,…}with--json).run/service.jsonrecords the service's OS start time (start_time).
Changed
- The service watches its socket every 30 s and recreates it when the file was removed (for example by a tmp cleaner), logging a warning (#83).
- MCP process and CLI process messages are levelled: "using the service pid …", "replacing the older service …", "connection lost" and similar notes go to the service log only; only warnings and errors also reach stderr, which MCP hosts such as Cursor show as
[error](#84). - Documentation reorganised (#87): the README only covers install, a short tools list, filter syntax and FAQ. Details moved to docs/tools.md, docs/reference.md and docs/compatibility.md; the repository layout is in AGENTS.md. The README "Upgrading" section is removed: upgrade notes and breaking changes are only in this changelog.
logcat_crasheslists crashes newest first by arrival, no longer by device time (#55). After the device clock is set back, crashes that arrive later have older device times and used to sort as old, so the newest page could leave them out. Shown times and thefrom/towindow stay device time; ids are unchanged. A DropBox-only crash of an old event that is imported late appears at the top of the list.
Fixed
- An MCP process no longer gives up with "holds the lock but does not answer" or a vague
context deadline exceededwhen the service process is hung (#83):- A service counts as reachable only when it answers a hello, not when the socket merely accepts a connection.
- A service that is verified as this tool's, is not shutting down and has been silent for more than 10 s is replaced automatically (at most 3 replacements per minute). A service that is not recognised as ours, or that runs inside an MCP process, is never signalled; the error names the pid and the manual command.
- When several MCP processes start at once, one repairs the service and the others wait for its result (up to 60 s) instead of each waiting in turn.
- Connection errors name the service log and the commands to try.
- A lock that was just released could look held for a few milliseconds while a child process was being started (macOS and Linux) (#82). The recorder then logged "being written by another process … not recording it",
set-rootcould fail with "is being written by another process", and the service could report "already running for this user" or take a stopped service for a running one. The cause is that aflockbelongs to the open file description, and a forked child holds a copy of every open fd until itsexec. A lock that looks held is now retried for up to 200 ms (1 ms to 10 ms backoff) before it is reported held; the worst wait measured on a macOS CI runner was 23 ms. A lock that really is held is reported after the 200 ms. Windows is not affected and is unchanged. - After
set-root, lines that the old root already stored are no longer stored again by a later whole-buffer read (#56). The move now carries the hashes of the newest stored lines of the current boot (up to 500,000, in a newresume_dedup.binnext tometa.json, not inside it), and every resume in the new root uses them until the device reboots or that many new lines are stored. Before, only the first resume used a small set taken near the newest line's device time, so a device clock jump or set-back after the move could store old lines twice. - The crash detector follows device clock jumps (#55). Its matching windows (15 s Java, 30 s ANR, 60 s
am_crashnative) and its quiet period are counted on a time line without the jumps. A clock set no longer splits one crash into two (theam_crashevent and its stack, or the native crash and its tombstone), no longer merges two crashes of a reused pid, and no longer keeps a pending native crash open until the device clock catches up. A pending native crash now also ends when no more lines arrive: until now only the next line ended it. - Lines are attributed to the right device process after the device clock is set back (#55). A pid reused by another app after a back jump used to give its lines to the previous app, or to none, because lifetimes were compared by device time. Lifetimes now carry the sequence numbers of their start and end (
start_seq/end_seqinprocs.jsonl; older files load without them and keep the time rule), and the owner of a pid is the newest lifetime that started before the line in arrival order. Device time only gives the 2 s fork-race slack and the 5 s grace after an end, and only when no clock jump lies between the two records.
Full changelog: v0.6.0...v0.7.0
v0.6.0
Upgrade notes
logcat_searchreturns 50 lines by default (was 200). Passlimit: 200, or page with thebeforecursor, to get the previous amount (#23).- Java stacks inside lines are short by default. Pass
stack: "full"for whole stacks, as before.logcat_crashes(id=…)still shows a crash's whole stack (#23). - Device time vs host time wording (#48):
- Every response header now reads
device now … · host now … · clock offset …. logcat_statuslabels host times (recording since host time …,last seen at host time …), and its cursor line readscursor (newest line, <serial>).- Update anything that parses these lines.
- Query windows (
from/to,age:) are always device time.
- Every response header now reads
host:window values are rejected (from="host:5m",age:host:5m). Use device time windows. For "what happens from here on", take thelogcat_statuscursor and pass it asafter.- Relative windows end at device now + 2 s (
age:5m,from="5m", the default window oflogcat_app_launches). Lines dated later (logged before the device clock was set back) are counted in a note that gives the absolutefrom/tothat shows them. logcat_statusshows rarely changing lines once per connection. These areroot:,linger:,lifetime:,service log:,adb:andstore:. Passverbose: trueto always get them (#23).- Shared-uid device processes whose name matches none of the uid's packages (e.g.
android.process.acore) are attributed to no package instead of the uid's first package.package=still includes them for every package of the uid (#44).
Changed
- Breaking: A device process of a shared uid whose name matches none of the uid's packages, such as
android.process.acore(uid ofcom.android.providers.contacts,com.android.contactsandcom.android.calllogbackup), is attributed to no package instead of the uid's alphabetically first package (#44).package=inlogcat_search,logcat_crashesandlogcat_statusincludes such device processes for every package of the uid and adds anote:naming the packages. Thepackage:filter term is unchanged. Data recorded before the upgrade keeps its old attribution until it expires. - Searches over older history decode stored lines about 4× faster (#22). A hand-written decoder handles the stored record layout and hands anything else to
encoding/json, so results are unchanged. On the synthetic one-day history, a full scan reads 976k records/s instead of 259k (cold), a search with no match takes 0.73 s instead of 2.8 s, andlogcat_app_launchesover 24 h 0.41 s instead of 1.7 s. from/to/age:windows skip stored blocks wholly outside the device-time window instead of scanning back from the newest line (#21). On the 515k-line perf fixture:age:search 110 → 0.38 ms; an oldfrom/torange 1,536 → 0.5 ms (cold 1,549 → 89 ms). Other query timings are unchanged within noise.- Breaking: Device time and host time are named everywhere (#48, glossary updated). Every response header shows
device now … (observed | estimated, observed … ago | unknown) · host now … · clock offset …(about 115 chars more per response).logcat_statusshows the detailed form, plus auto_time (read once after connect or a clock change, on the next status call) and the stored clock jumps, and labels host times (recording since host time …,last seen at host time …). The status cursor line readscursor (newest line, <serial>). Device now comes from one device clock read at connect plus the live lines (device time − host arrival time); the 30 s device clock poll is gone, and no query reads the device clock. - Breaking: A relative window (
age:5m,from="5m", the 30 min default oflogcat_app_launches) ends at device now + 2 s. Lines dated later (logged before the device clock was set back) are left out and counted in a note that gives the absolutefrom/tothat shows them.logcat_search {}stays unbounded: newest lines by arrival, whatever their device time. - Breaking:
host:values (from="host:5m",age:host:5m) are rejected with an explanation: queries use device time only; for "what happens from here on" use thelogcat_statuscursor withafter. - Breaking:
logcat_searchanswers are much smaller by default (#23). Upgrade note: passlimit: 200andstack: "full"for the previous output.- The default
limitis 50 instead of 200.logcat_search {}on the API 29 fixture is 8.4k chars instead of 27.5k (about 2.1k tokens instead of 6.9k); the rest is onebeforecursor away, and a truncated page says thatview="summary"(about 2k chars for any range) shows what the whole range contains. - Java stacks inside a line are short by default (new parameter
stack,"short"or"full"). Each exception and eachCaused bykeeps its head and its first 3 frames; the rest becomes↳ K more frames(Java's... N moreand the frames of aSuppressed:exception are counted there). A stack whose frames were already shown in the same response becomes↳ same K frames as HH:MM:SS.mmm. A line holding a stack is cut at 2,000 chars, and everyCaused byhead and its first frame are kept first. Astacks: N shortened (M frames hidden) · stack="full" shows every frameline says when anything was left out. Ten warnings with the same 30-frame stack and aCaused bynow take 3.8k chars instead of 27.5k, and all ten fit (before, the 30k cap showed seven).stack: "full"shows the lines exactly as before. - The cut marker of a long line no longer suggests narrowing the query, which never showed more:
…[+N chars; lines are cut at 2,000 chars in every query, narrowing does not show more]. A response that hits the ~30k-char cap now says so ([output cap: only 7 of the 10 matches asked for fit in ~30k chars · …]).
- The default
logcat_searchanswers in milliseconds on large histories (#20). It used to keep counting matches for up to 1.5 s after the page was full (about 1.5 s per call on a one-day history). Now:- Without a filter or time window, the total is exact and comes from the stored record range, without scanning.
- With a filter, counting stops 100 ms after the page is full. The total is then a lower bound, and the response says which span was counted and how to get an exact count:
matched ≥14,043 lines (counted the newest 21 min only, since 23:32:09; older history not counted — narrow with from/to, e.g. from="1h", for an exact count). - Every lines response has a machine-readable
count:line:total,count_exactand, for a lower bound,counted_from/counted_to. - The top-tags line says when it covers only the counted matches, or only the page.
- On a one-day history,
logcat_search {}takes about 1 ms instead of 1.5 s, and filtered searches whose page fills quickly about 110 ms.
- Crash ingest and crash lists no longer slow down as crashes accumulate (#22).
- The recorder used to re-read and decode the whole
crashes.jsonlfor every new crash (twice).logcat_crashes,logcat_statusand the zero-result hints oflogcat_searchdid the same on every call. - Each device now has one in-memory crash index, shared by the recorder and the tool calls of the service process. It keeps a small summary of the newest version of each crash and the file offset of that version. It reads only the bytes appended since the last read, and rebuilds itself after the file is compacted or rewritten. A stack, ANR traces or DropBox text is decoded only for the crashes a response shows.
- On the synthetic one-day history, recording runs at 448k lines/s instead of 47k, and
logcat_crashes {}takes 0.7 ms instead of 17 ms. - With 2,000 stored crashes, recording a new crash takes 0.2 ms instead of 41 ms (35 KB allocated instead of 18 MB), and the crash list takes microseconds instead of 45 ms. Output is unchanged, including the order of the crash list.
- The recorder used to re-read and decode the whole
- Breaking:
logcat_statusno longer repeats the lines that rarely change on every call (#23).root:,linger:,lifetime:,service log:,adb:andstore:are shown on the first call of each MCP or CLI process's connection and again whenever one of them changes; otherwise they become one line,unchanged since shown earlier in this session: root, linger, lifetime, service log, adb, store (logcat_status(verbose=true) shows them). This saves about 500 characters (about 35 %) on every later call. Theservice:line (pid, version, uptime, connected MCP and CLI processes), recording state, pins, devices, adb problems and the cursor are always shown. The newverboseparameter always shows every line.android-log-mcp service statusis unchanged.
Fixed
- Crashes in
android.process.*orandroid.hardware.*device processes are no longer attributed to the framework packageandroid(#70).logcat_crashes(package="android")andreport --package androidonly include crashes ofsystem_server, also for crashes recorded before the upgrade, and the crash details print nopackage:line for such processes. logcat_app_launchespairs launch steps correctly when the device clock is set (#54). Steps ...
v0.5.2
Fixed
- A device that disconnects and reconnects quickly no longer has two recording sessions running at once (#30). The new session starts only after the previous session's recorder stopped (at most 10 s) and its background work returned (at most 5 s, after which its adb children are killed together with their own child processes, never the adb server). A tool call right after the reconnect again waits for the new session's backfill: the previous session could end that wait early. If the previous session does not stop in time, the new session starts anyway, and the service log and
logcat_statussay so for that device ("recording session N started … before session N-1 had stopped: …"). - When a device starts recording again, the read-only view of its directory used while it was idle is closed at once instead of after one minute.
Full changelog: v0.5.1...v0.5.2
v0.5.1
Fixed
- Stopping the recording of a device (idle stop, end of a pin,
record --stop) or stopping the service no longer leaves adb processes behind (#24). It cancels the device's background work (process table, packages, buffer span, DropBox fetches, device tracking, identity probes) and waits for it, at most 5 s, before the store is closed. Work still running then is named in the service log, and its adb processes are killed with their child processes (never the adb server). A cancelled adb call also stops waiting for output pipes that a child process of adb still holds open. A DropBox fetch can now be cancelled, and a fetch interrupted by a stop runs in the next recording session.
Full changelog: v0.5.0...v0.5.1