)]}'
{
  "log": [
    {
      "commit": "eb60b0be53732aa016c79b7774b497770ad1e4f3",
      "tree": "a4a5a258fd95377f3a1dc8b3d5df094a44534f50",
      "parents": [
        "5ede20d2c19d9ae3b51bc4f296d48a93e6f5bfd5",
        "6f16df3ddc297590785e078f175053a64e350707"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 21 15:19:52 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 21 15:19:52 2026"
      },
      "message": "Merge pull request #2993 from square/py/referrers-tool\n\nAdd a referrers tool listing every object pointing at an object"
    },
    {
      "commit": "6f16df3ddc297590785e078f175053a64e350707",
      "tree": "a4a5a258fd95377f3a1dc8b3d5df094a44534f50",
      "parents": [
        "2e9a4cf191e3c8d5899753e8d75881a36bd3e522"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:07:30 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "List an object\u0027s GC roots from Shark\u0027s provider, once per local variable\n\nThe referrers of an object read its GC roots straight off the heap\ndump, which still has every root Shark moved onto a thread. So a local\nvariable of a thread the dump has was listed twice: as the thread\u0027s\nvirtual reference, and as a JAVA_FRAME, JNI_LOCAL or NATIVE_STACK root\nholding nothing. Telling the two apart took a copy of Shark\u0027s rule in\nReferenceStrengthReader.\n\nTreeGcRootProvider now indexes what MatchingGcRootProvider keeps by\nobject, and both the referrers and the root a path starts at read that.\nA local variable is listed once, as its thread\u0027s reference, and the copy\nof the rule is gone. On large-dump.hprof and on a 722 MB app dump every\none of the 3,085 and 7,602 stack roots was on a thread the dump has.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "2e9a4cf191e3c8d5899753e8d75881a36bd3e522",
      "tree": "85b12767d11d45c4b8dc0633b903eaa64b216e3b",
      "parents": [
        "f903bbdd82e34041c51d826646195be662285692"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:07:30 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "Move the device tools out of AgentTools\n\nThe referrers tool and the investigation states from main put AgentTools\npast detekt\u0027s LargeClass threshold, and the config says the next tool to\ntrip it should split the registry rather than raise the number again.\nlist_devices, list_processes and dump_heap only need the open heap dumps\nand the key a new one is open under, so they are a class of their own,\nlisted last as before.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "f903bbdd82e34041c51d826646195be662285692",
      "tree": "743c54b70a098c211d44df87b078667e2c8c24a1",
      "parents": [
        "13f6ac5ef382a828917dd78cfa456afe391231ca"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 16:19:45 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "Leave a thread\u0027s native roots to Shark\n\nShark now reads a thread\u0027s JNI_LOCAL and NATIVE_STACK roots as LOCAL\nreferences from the Thread, through the JavaLocalReferenceReader this already\nreads frames with, and MatchingGcRootProvider drops the roots. So\nReferenceStrengthReader\u0027s own reading of them would hand each one to the tree\ntwice, and it goes.\n\nisReadAsAReference stays: the list of every GC root on an object comes from\nthe heap dump rather than from the root provider, and it is what says such a\nroot holds nothing.\n\nThe tree is byte for byte what it was. On the production heap dump and the\neight here, every node\u0027s dominator, strength and retained size match, and so\ndo both referrer indexes, down to the order of each object\u0027s referrers and\nwhich of them are low priority.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "13f6ac5ef382a828917dd78cfa456afe391231ca",
      "tree": "e947aea5a45627ef9e33cb3a92893ab4f3fb0660",
      "parents": [
        "d777a2ae1cace70d936f583ca791e941f0707074"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 15:18:49 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "Read a thread\u0027s JNI_LOCAL and NATIVE_STACK roots as its references\n\nA thread running native code holds objects through JNI_LOCAL and NATIVE_STACK\nroots, which name the thread by serial number the way a JAVA_FRAME root does.\nShark reads a named thread\u0027s frames as LOCAL references from the Thread and\ndrops the roots, and reads neither native kind that way. So an object a frame\nand native code of one thread both held had two holders, the thread and the\nnative root, and was drawn under the whole heap dump.\n\nReferenceStrengthReader now reads both native kinds as virtual LOCAL\nreferences from their thread, and the tree drops those roots the way Shark\ndrops the frames. On large-dump.hprof that puts the 7 objects the previous\nchange moved under the whole heap dump back under their thread, plus 3\nStrings only a JNI_LOCAL root holds. Every dump here moves objects from under\nthe whole heap dump to under a thread, and none the other way.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "d777a2ae1cace70d936f583ca791e941f0707074",
      "tree": "c405f6d3a6aa73af1137ebbb38401e8201d8c096",
      "parents": [
        "c8d58e127c6c9e92c254c248892f238cda82f941"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 11:52:15 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "Say what moved seven stack frame objects under the whole heap dump\n\nThe note said they were held by the main thread alone. Three are on\nHandlerThreads, and one has no native root: it is held by another of the\nseven. What they share is a JNI_LOCAL or NATIVE_STACK root that the wrong\nstrength used to hide.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "c8d58e127c6c9e92c254c248892f238cda82f941",
      "tree": "5f4b2487db705f732edaf022c481fcd995893d84",
      "parents": [
        "c01954f06238075f6f4c042ed8a4c860f5336e1c"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 10:30:45 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "Hold nothing through a thread\u0027s local variable that a field holds, and mark virtual references\n\nShark reads a local variable of a thread the heap dump names twice: as a\nJAVA_FRAME root, and as a LOCAL reference from the Thread object. The tree\nfollowed the reference as a retaining one, so every such local was strong\nand an object a field also held had two holders, the thread and the field\u0027s\nowner. It was drawn under the whole heap dump. On large-dump.hprof that was\n160 objects retaining 45 KB.\n\nThe reference is now a weakening one at LOCAL, like the root, and the tree\ntakes no JAVA_FRAME root a thread reads. An object only a stack holds is\nunder its thread, and an object a field also holds is under the field.\n\nEvery reference an agent is answered with now carries isVirtual, Shark\u0027s\nflag for a reference read off a structure rather than recorded in the dump.\nAn ArrayList element has two referrers, the list through a virtual entry\nand the backing array through a real one, and the flag is what tells them\napart.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "c01954f06238075f6f4c042ed8a4c860f5336e1c",
      "tree": "90382caaa5faa75dd44d1a2c455713f910853916",
      "parents": [
        "37044ac49b1a860c8ee7193dd438230ef6b4414d"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 11:39:05 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "Page referrers, and list a GC root on the object as one of them\n\nA GC root on the object is a referrer like any object pointing at it, so it\ngoes in the same list rather than a list of its own. holdingReferrerCount then\ncounts a root that holds the object too, which is what makes it answer whether\nanything else keeps it in memory.\n\nThe whole list for the running MainActivity of large-dump.hprof was 505,394\ncharacters, well past what Claude Code hands a model inline. So referrers\nanswers 30 at a time, limit up to 500, and nextOffset says where the next page\nstarts. The order is the same on every call: holders first, a GC root ahead of\nan object, then the largest retained size.\n\nSorting reads whether a referrer holds the object off the tree\u0027s own index, so\nonly the objects of the page are read.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "37044ac49b1a860c8ee7193dd438230ef6b4414d",
      "tree": "7334cdf5b233e2b7d0565369a2463c1a6924d153",
      "parents": [
        "43d7ee9097e02c6eca91fce1d35eedc67d01670b"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 10:55:35 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:19:05 2026"
      },
      "message": "List every referrer and every GC root, as object list rows\n\nA referrer list capped at a limit left an agent no call that would hand over\nthe rest, so referrers now lists every object pointing at the object, uncut.\n\nA single gcRootType said one root of an object that can have several, some\nholding it and some not. gcRoots lists each of them with its own holds, read\nthe way a reference\u0027s is.\n\nA referrer is no step of a path, so it no longer carries a verdict or an\nisSuspect. It is the row find_objects answers with, plus the references it\npoints through, which also means no inspector runs per referrer.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "43d7ee9097e02c6eca91fce1d35eedc67d01670b",
      "tree": "63b56acd554c807c90c6c04c006a09ea3f2f5bfc",
      "parents": [
        "5ede20d2c19d9ae3b51bc4f296d48a93e6f5bfd5"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 08:06:18 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:18:46 2026"
      },
      "message": "Add a referrers tool listing every object pointing at an object\n\n`ways_held` walks to the GC roots and keeps paths that share no object, so\ntwo holders under one parent come back as one way, and there was no way to\nask an agent-side question as simple as \"what points at this?\".\n\n`referrers` answers it from `HeapDominatorTreemap.referrersOf`, which reads\na second `ReferrerIndex` built over every reference rather than the ones\nthe tree follows. The tree\u0027s index alone would have been wrong in the case\nthat matters most: on large-dump.hprof the running MainActivity has 951\nobjects pointing at it and one in that index, because its views, context\nwrappers and window all lose to the ActivityThread that owns it. Each\nreference says whether the tree holds the object through it (`holds`), and\nthe answer carries how many referrers hold it beside how many point at it.\n\nThe second index is built on the first `referrersOf` and nothing the window\ndraws asks for it. notes/referrer-index.md has its cost on eight dumps.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "5ede20d2c19d9ae3b51bc4f296d48a93e6f5bfd5",
      "tree": "27b3933f3362388a23e44a5379f81543c3ec750d",
      "parents": [
        "23b2275e0a359dead9a2e5739be4c5d21a20dabb",
        "53b15d36da42e8f565af2c610c923f57a69ab39b"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Fri Oct 16 09:12:45 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 09:12:45 2026"
      },
      "message": "Merge pull request #2992 from square/leak-trace-object-ids\n\nRead object ids unsigned, and print each leak trace object\u0027s id"
    },
    {
      "commit": "53b15d36da42e8f565af2c610c923f57a69ab39b",
      "tree": "27b3933f3362388a23e44a5379f81543c3ec750d",
      "parents": [
        "5a769610766e20bd34129f1ea0fd619184de8099"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 08:59:30 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:01:45 2026"
      },
      "message": "Say why the WeakHashMap tests don\u0027t use System.runFinalization()\n\nOn ART it would wait for the daemon that enqueues cleared references,\nbecause its finalization sentinel goes through that daemon. On HotSpot,\nwhich these tests run on, it only runs the finalizers already queued.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "5a769610766e20bd34129f1ea0fd619184de8099",
      "tree": "a49f655940a62c1618a441210dfd76b0ab3d60c8",
      "parents": [
        "fb51029ccf5de95d52d8a5482040ee7b933d6451"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 08:21:43 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:01:45 2026"
      },
      "message": "Name related leaking objects the way a leak trace names its objects\n\nThe labels naming another leaking object read `Also retains leaking object\n0x12d36d08 (android.app.ContextImpl)`, a second spelling of what the leak\ntrace above them writes as `android.app.ContextImpl instance 0x12d36d08`.\nThey now use that one: `Also retains android.app.ContextImpl instance\n0x12d36d08`, and `Also retained by … instance 0x…, which has its own leak\ntrace`. The labels that group several objects of a class keep their wording.\n\nBoth go through one internal function, so the object line and the labels\ncan\u0027t drift apart again.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "fb51029ccf5de95d52d8a5482040ee7b933d6451",
      "tree": "b14def34ceb65262db824071671e3cd4bcc8df85",
      "parents": [
        "d5d75d93877717ed4685567d29ced5bb948f4b4b"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 16:03:31 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:01:45 2026"
      },
      "message": "Print each object\u0027s id on its line of a leak trace\n\nA leak trace names an object by its class and the fields that lead to it,\nwhich is enough to read the leak but not to find that object again in the\nheap dump it came from. Each object\u0027s line now ends with its id,\n`├─ java.util.ArrayList instance 0x12d4cd48`, so that it can be handed\nstraight to Shark Dive: in a `shark://\u003cheap dump\u003e/object?id\u003d…` link, or as\nthe `object` of a `shark-dive --cli` command. It\u0027s the same spelling ahat,\nMAT, jhat and Android Studio show for that object. That\u0027s in\nLeakTrace.toString(), the LeakCanary activity and the leakcanary-app leak\nscreen, so every leak trace Shark builds has it.\n\nIt\u0027s display only. LeakTraceObject carries the id in a new objectId\nproperty, and only the rendering reads it: the leak fingerprint is still\nbuilt from class and reference names, so it doesn\u0027t change. The\nserialVersionUID is bumped so that an analysis serialized before this fails\nto deserialize, rather than reading back an id of 0.\n\nThe leakcanary-app leak screen printed the leaking object on every row of\nthe path instead of that row\u0027s own object, which would have printed the same\nid down the whole trace. The LeakCanary activity shares its hex helper with\nits heap explorer, which had a private copy.\n\nAndroid Studio\u0027s on-device parser of leak traces expects the last word of\nthat line to be the type, so it needs updating for this as it already does\nfor `Verdict:`.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "d5d75d93877717ed4685567d29ced5bb948f4b4b",
      "tree": "67d1b392bef26a0daa1aa346e13cedc0aa0fb466",
      "parents": [
        "894e70effea5f166778b12d95d51f364fbb18bb0"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 10:07:31 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:00:10 2026"
      },
      "message": "Write the remaining object ids in hex\n\nMain already writes object ids in hex in leak traces, HeapObject.toString(), the\nnot found errors, the Shark CLI and the heap explorer. This covers what was left:\n\n- ObjectDominators.renderDominatorTree() named objects by their position in\n  Shark\u0027s index, which nothing else can look up. Nothing in the repo called it\n  except HprofRetainedHeapPerfTest, to explain a failure, so it moves into that\n  test and names objects by their hex id instead.\n- Two consistency checks of HprofInMemoryIndex spelled ids by hand, which for a\n  class above the 2 GB mark came out as 0x-61544530 in a production failure\n  before ids were read unsigned. They use the shared helper now.\n- Two Shark Dive tree errors wrote node ids in decimal.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "894e70effea5f166778b12d95d51f364fbb18bb0",
      "tree": "784d696e4257a31985b545aad92a50576e83f798",
      "parents": [
        "23b2275e0a359dead9a2e5739be4c5d21a20dabb"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 16:02:44 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Fri Oct 16 09:00:10 2026"
      },
      "message": "Read 4 byte object ids unsigned, like every other heap dump tool\n\nAn object id is the address the heap dump recorded the object at. Shark\nhas read a 4 byte id as a signed Int widened to a Long since its first\nparser, copied from perflib with the comment \"As long as we don\u0027t\ninterpret IDs, reading signed values here is fine\". So every object\nabove the 2 GB mark of an Android heap dump got a negative id,\n0xffffffff82000010 for the object at 0x82000010.\n\nNothing else spells it that way. Android Studio, ahat, MAT, jhat and\nVisualVM all show that object as 0x82000010, and ahat and MAT find\nnothing given the 16 digits. Several workarounds grew around the\nnegative ids: HeapObject.positiveObjectId, hex helpers that mask the\nlow 32 bits back off, and in Shark Dive a second, exact spelling for\nits files, two readings of every address typed into a note, and a\nrange check instead of a sign check to tell a pile from an object.\n\nEvery reader of an id now reads it unsigned, which removes all of\nthose. The in-memory index stores ids as offsets from the smallest one,\nso its keys are unchanged and so are the numbers of the perf tests.\nHprofWriter now refuses an id that doesn\u0027t fit the heap dump\u0027s id size\ninstead of writing its low bytes, which would read back as a different\nid now.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "23b2275e0a359dead9a2e5739be4c5d21a20dabb",
      "tree": "62af9a57a5c2a00efde012cf93d003b5bd53210f",
      "parents": [
        "e5b54cd29c36456b1abf07b7e871f9caa3d3cd6e",
        "a42f9c170e59cb3d77ba30b4ce1bff2cc1bc36f4"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Fri Oct 16 08:55:43 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 08:55:43 2026"
      },
      "message": "Merge pull request #3011 from square/py/investigation-unsolved-state\n\nFold Shark Dive\u0027s investigation states into UNREACHABLE, NOTHING_STUCK, UNSOLVED and SOLVED"
    },
    {
      "commit": "e5b54cd29c36456b1abf07b7e871f9caa3d3cd6e",
      "tree": "519c99fd4b18df58746ae44e749b9e99b714e54d",
      "parents": [
        "68f9526bb8820586bacb441941ee25a0dd31a229",
        "d5be06c03c6b7fec50f01d657916abe8c1a03e35"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Fri Oct 16 08:50:26 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 08:50:26 2026"
      },
      "message": "Merge pull request #3017 from square/py/native-local-label\n\nPrint a thread\u0027s native roots as \u003cNative Local\u003e in a leak trace"
    },
    {
      "commit": "68f9526bb8820586bacb441941ee25a0dd31a229",
      "tree": "f0590dc0b03b91c1d982679a42a251b76bb22996",
      "parents": [
        "2013bd8aa0038ca249f3a509cde585cd22274414",
        "1bb238fa003ee4926569bb6d704031ae8b1de252"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Fri Oct 16 08:45:42 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 08:45:42 2026"
      },
      "message": "Merge pull request #3018 from square/py/weakhashmap-test-flake\n\nWait for cleared WeakHashMap entries to leave the pending list before dumping"
    },
    {
      "commit": "2013bd8aa0038ca249f3a509cde585cd22274414",
      "tree": "42f5f3c399f922f3742eb218c2eff7fd40563d2d",
      "parents": [
        "9589bcd1f4f5a22ac9e704162c868698a32304ff",
        "4aee22337687cb5da0c26b1149128b92aed61c34"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Fri Oct 16 08:23:57 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 08:23:57 2026"
      },
      "message": "Merge pull request #2711 from saket/saket/sep17/bytesize\n\nMigrate to me.saket.bytesize"
    },
    {
      "commit": "4aee22337687cb5da0c26b1149128b92aed61c34",
      "tree": "42f5f3c399f922f3742eb218c2eff7fd40563d2d",
      "parents": [
        "3329f3a91508918c1f96dfea41f83a3ad729926b"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Jul 28 22:43:35 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 21:33:12 2026"
      },
      "message": "Use bytesize 2.3.0, type Retained.heapSize as DecimalByteSize\n\n#2711 pinned the 2.0.0-beta04 prerelease because every stable bytesize\n2.x release carries Kotlin 2.2 metadata, which the Kotlin 1.9.25 that\nLeakCanary built with then could not read. LeakCanary builds with Kotlin\n2.4 since #2841, so this moves to 2.3.0. Since beta04, bytesize also\nstopped rendering sizes below -1000 bytes in scientific notation\n(\"-1.7949673E9 B\" rather than \"-1.79 GB\"), which a negative\n`retainedIncrease` could reach.\n\n`Retained.heapSize` is typed as the concrete `DecimalByteSize` rather\nthan the `ByteSize` sealed interface. Shark only ever means decimal\nbytes, so the signature says so, and a value class whose static type is\nan interface is boxed on every read.\n\nThat typing is also what lets 2.3.0\u0027s same-precision overloads apply\n(saket/byte-size#24). `DecimalByteSize - DecimalByteSize` resolves to an\ninline subtraction of two longs instead of the `ByteSize` operator,\nwhich boxed both operands. HotSpot scalar replaces those boxes, ART\ndoesn\u0027t: on an Android 14 arm64 device the interface operator allocated\n32 bytes per call. With 2.3.0, the compiled shark classes contain no\n`DecimalByteSize.box-impl` outside the `sortedByDescending` comparator\nover reported growing nodes, which boxed the old `shark.ByteSize` the\nsame way.\n\n`shark.api` is regenerated: `shark.ByteSize` is gone, and `Retained`\u0027s\nmangled names change, so this breaks binary compatibility even for\ncallers that never named `ByteSize`.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "3329f3a91508918c1f96dfea41f83a3ad729926b",
      "tree": "3c4d14d0134b1c266c17804754dd9ab8c770651f",
      "parents": [
        "9589bcd1f4f5a22ac9e704162c868698a32304ff"
      ],
      "author": {
        "name": "Saket Narayan",
        "email": "saket@saket.me",
        "time": "Wed Sep 18 17:24:52 2024"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 21:22:20 2026"
      },
      "message": "Migrate to me.saket.bytesize\n\n(cherry picked from commit 1f9b9397cbc13e003a4be2db6fd62626a797c61d)\n"
    },
    {
      "commit": "1bb238fa003ee4926569bb6d704031ae8b1de252",
      "tree": "bfc01884814e7f63a671093ee930d9dc39117053",
      "parents": [
        "9589bcd1f4f5a22ac9e704162c868698a32304ff"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 18:12:00 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 18:12:43 2026"
      },
      "message": "Wait for cleared WeakHashMap entries to leave the pending list before dumping\n\n`WeakHashMap with cleared key doesn\u0027t take over the path to its value` failed\non CI three times today, on three branches, with the leak trace going from a\nJNI global root through `Reference.discovered` to the cleared entry, or\nstarting at the entry itself, and from there to its value.\n\nThat root is the VM\u0027s reference pending list. A collection puts everything it\ncleared on that list, and the Reference Handler thread moves each reference to\nits queue some time later. HotSpot dumps the VM\u0027s global handles as JNI global\nroots, the head of that list among them, so a heap dump taken in between can\nreach the entry\u0027s value from the root in one or two references, where the\npath the test expects takes seven. clearWeakKeys() was a bare System.gc(), so\nwhether the handler got there before the dump came down to scheduling, which a\nloaded CI runner loses.\n\nHolding the Reference Handler back for 300 ms reproduces it every time: 10\nrounds out of 10 failed, with both shapes seen on CI. clearWeakKeys() now waits\nfor the handler: 10 rounds out of 10 passed, each waiting about 320 ms for a\nhandler held back 300 ms.\n\nIt waits for two sentinels, one per collection, because the handler takes a\ncollection\u0027s whole list at once. The first sentinel coming out of its queue\nonly says the list it was on has been taken. The second can only come out\nonce the handler is through the first list, so by then the entries have gone\nto the map\u0027s own queue, which is a longer path from the root than the one the\ntest expects.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "d5be06c03c6b7fec50f01d657916abe8c1a03e35",
      "tree": "8f17c6e62c7b139b2421428d6c90d7b10b16ac22",
      "parents": [
        "9589bcd1f4f5a22ac9e704162c868698a32304ff"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 18:06:24 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 18:06:24 2026"
      },
      "message": "Print a thread\u0027s native roots as \u003cNative Local\u003e in a leak trace\n\nShark reads a thread\u0027s JNI_LOCAL and NATIVE_STACK roots as references from\nthat thread, the way it reads its JAVA_FRAME roots, and they all printed as\n\u003cJava Local\u003e. Someone reading a trace through native code was told a Java\nmethod held the object, and would go looking for a variable that isn\u0027t there.\n\nSo ReferenceLocationType and LeakTraceReference.ReferenceType each get a\nNATIVE_LOCAL constant, which JavaLocalReferenceReader uses for the native\nroots, and which prints as \u003cNative Local\u003e. A Java frame is still LOCAL and\nstill \u003cJava Local\u003e, so no existing leak signature changes. The native roots\nonly started reaching a leak trace through their thread since the last\nrelease, so no released signature changes either.\n\nThe new constants are appended rather than placed beside LOCAL, which keeps\nevery existing ordinal. Nothing in the repo reads one, but a consumer might\nhave persisted one.\n\nAn exhaustive `when` over either enum needs a branch for the new constant,\nwhich is the breaking part. Android Studio\u0027s on-device LeakCanary task parses\nreference lines and only recognizes \u003cJava Local\u003e, so it can\u0027t parse a trace\nwith \u003cNative Local\u003e in it. It already fails on every analysis from this\nrelease because of the Verdict rename, so that costs it nothing more.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "9589bcd1f4f5a22ac9e704162c868698a32304ff",
      "tree": "c0aaa8ca66f668cd0239f67f8dda23650007defd",
      "parents": [
        "3a3331409637b9b1feb8962a83b5e09c8afd61b0",
        "c2f8c057dda2335f5da9595b8373157687e4ea11"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Thu Oct 15 17:50:45 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 15 17:50:45 2026"
      },
      "message": "Merge pull request #3015 from square/py/thread-native-locals\n\nRead a thread\u0027s JNI_LOCAL and NATIVE_STACK roots as references from it"
    },
    {
      "commit": "c2f8c057dda2335f5da9595b8373157687e4ea11",
      "tree": "c0aaa8ca66f668cd0239f67f8dda23650007defd",
      "parents": [
        "3a3331409637b9b1feb8962a83b5e09c8afd61b0"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 16:11:49 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 16:11:49 2026"
      },
      "message": "Read a thread\u0027s JNI_LOCAL and NATIVE_STACK roots as references from it\n\nShark reads what Java code running on a thread holds, its JAVA_FRAME roots, as\nlow priority LOCAL references from the Thread, and drops those roots, so a leak\ntrace goes through any other path first and names the thread when it doesn\u0027t.\nJNI_LOCAL and NATIVE_STACK roots name their thread by serial number the same\nway, and were read as GC roots with no priority, so a leak trace started at one\nwhenever that was the shortest path.\n\nProduction leak reports show it: over 90 days of a large app, 14 reports\nstarted at a NATIVE_STACK root holding the PathClassLoader, then went through\nthe class loader to an app class and its static field. In the one heap dump of\nthose still stored, the root is on the JIT compiler thread, which held the\nclass loader while compiling a class. The leak trace now starts at\nConnectivityThread.contextClassLoader and keeps its fingerprint, since every\nobject in the part that changed is expected.\n\nAcross that dump and every heap dump in the repo, leak and unreachable counts\nare the same, and every JNI_LOCAL and NATIVE_STACK root has a thread object.\n\nA javaLocal() matcher now covers native code on its thread too, so the main\nthread\u0027s native roots are ignored by default, as its Java frames are. That is\nthe whole of the HprofIOPerfTest change: 1 to 6 fewer reads per dump, and\nreading the main thread\u0027s native roots as references again gives back the old\nnumbers exactly. It is also why the MessageQueue leak of\ngc_root_in_non_primary_heap.hprof now starts at\nActivityThread.sMainThreadHandler instead of at the queue the main thread holds\nwhile it waits in nativePollOnce, so that test now checks the reference into\nthe message.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "a42f9c170e59cb3d77ba30b4ce1bff2cc1bc36f4",
      "tree": "3e5caf8899208e56553db46c96f3f5a115e09ed9",
      "parents": [
        "2cbd81be8bd963e2ef9470321db4066b1dcc9353"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 15:40:18 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 15:40:18 2026"
      },
      "message": "Refuse ways_held for uncollected garbage\n\nways_held is every way an object is held, and nothing holds garbage in\nmemory: the ways it found for one started at pieces of garbage, the same\nnon-path path_from_gc_roots stopped showing. So it refuses, with or\nwithout `from`, since only garbage holds garbage.\n\nThe sentence for an object no GC root holds is now one function, which\npath_from_gc_roots, ways_held and a garbage solvingLeakOf all say.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "2cbd81be8bd963e2ef9470321db4066b1dcc9353",
      "tree": "caa98fb742e81e94c213da1483d72594f36b11cc",
      "parents": [
        "0813e372d8b8b5a1963e018fad01c161b80ac484"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 11:56:20 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 15:37:55 2026"
      },
      "message": "Answer path_from_gc_roots on garbage with no path\n\nAn UNREACHABLE object is one no GC root holds, so there is no path from\na GC root to show. What the walk finds for it starts at a piece of\ngarbage, and it came back as a leak trace with candidate references\ncounted on it, suspectReferenceCount 1 on a real garbage leak: the\nnumbers of an investigation about no leak.\n\nSo the answer is the investigation alone, with nothing counted.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "0813e372d8b8b5a1963e018fad01c161b80ac484",
      "tree": "f37def791d1f91a505566185a5b3783bd6e9c9bd",
      "parents": [
        "c2e5bc3314514d753ab245ff5354ef7181e14b8b"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 10:27:47 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 15:37:55 2026"
      },
      "message": "Rename Shark Dive\u0027s NO_PATH investigation state to UNREACHABLE\n\nNO_PATH was for a path with no steps, and no agent tool could ever be\nanswered with one. The only ids rootPathTo returns no steps for are the\nones the tree has no node for: the whole heap dump, a pile, an object\nfolded into another one. requireOneObjectOf refuses all of them before a\npath is read. Every node has a path. Measured over every object of the\neight heap dumps in the repo: no node had an empty path, and no node was\nanything but an object.\n\nSo the two branches of the path walk that logged and returned nothing\nnow throw: a node that is no object, and a node the search can\u0027t reach\nfrom a root. Both would mean the tree and the referrer index were built\ndifferently. The window\u0027s \"No path from a GC root reaches it.\" goes too,\nsince it could only be shown for a path with no steps.\n\nUNREACHABLE is what the state means now: no GC root holds the object, so\nit is uncollected garbage, which subsumes the special case UNSOLVED had\nfor a garbage path whose top object is stuck. set_verdict refuses a\nsolvingLeakOf of garbage the same way, since there is no leak to solve.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "c2e5bc3314514d753ab245ff5354ef7181e14b8b",
      "tree": "4f6c8f4db0099266dd431c2f8127302a7bcb7848",
      "parents": [
        "3a3331409637b9b1feb8962a83b5e09c8afd61b0"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 16:38:56 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 15:37:55 2026"
      },
      "message": "Fold Shark Dive\u0027s unsolved investigation states into one UNSOLVED\n\nAn agent\u0027s `investigation` had six states, and three of them said the same\nthing or could not happen.\n\nNOTHING_EXPECTED_ABOVE and NARROWED both mean the verdicts don\u0027t name the\nfaulty reference yet, and an agent does the same thing next in both: settle the\nobjects between the last expected one and the first stuck one, from the top of\nthe path when nothing is expected. Since a GC root holding a stuck object\nsolves a leak on its own, nothing expected above a stuck object is no reason to\nlook further up either, which was the case for keeping it apart. And NARROWED\nwas an odd name for where every investigation starts, before any verdict has\nnarrowed anything. Both are UNSOLVED now.\n\nREFERENCE_UNREADABLE was a path whose stuck object was reached through no\nreference, which `stepTo` handed back when reading the referrer again didn\u0027t\nfind the reference the walk went through. It can\u0027t: the referrer index the\nwalk follows was built with the same reader `stepTo` reads, so the reference is\nalways there. It was never seen in a run log. `stepTo` now throws instead, and\nthe fallbacks downstream of it are gone.\n\nThe one unsolved path with nothing to settle is a stuck object at the top of\nuncollected garbage, which no GC root reaches. It says so instead of listing no\nobjects to decide about.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "3a3331409637b9b1feb8962a83b5e09c8afd61b0",
      "tree": "ef6054c13b474cb88980cbcc25563b0e4dce2fa7",
      "parents": [
        "bf1cd37aec4a5dc0c43a90544943fb225f603f7d",
        "dae62e76b5ad443cde3f7fd5cefa67ff3a071baa"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Thu Oct 15 15:18:59 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 15 15:18:59 2026"
      },
      "message": "Merge pull request #3014 from square/py/library-leak-trace-test\n\nCompare Shark Dive\u0027s library leak traces with LeakCanary\u0027s"
    },
    {
      "commit": "bf1cd37aec4a5dc0c43a90544943fb225f603f7d",
      "tree": "de6cfd986ad12e90c0bdb7136dbfcb0139dcc3d7",
      "parents": [
        "85ca19957fc753c10aadbf22912d4668c15220f7",
        "ab361337f9ebcb72dd1500a2da16f8eab0faeaeb"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Thu Oct 15 15:18:08 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 15 15:18:08 2026"
      },
      "message": "Merge pull request #3013 from square/nexus/task-59d75ee8\n\nCount an array\u0027s elements without reading them"
    },
    {
      "commit": "dae62e76b5ad443cde3f7fd5cefa67ff3a071baa",
      "tree": "6cb026cfde2ee6d395a30c26aa4116ae7978592f",
      "parents": [
        "85ca19957fc753c10aadbf22912d4668c15220f7"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 11:57:35 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 11:57:35 2026"
      },
      "message": "Compare Shark Dive\u0027s library leak traces with LeakCanary\u0027s\n\nLeakFingerprintTest compared application leaks only. A library leak\u0027s\ntrace carries a `Library leak match: \u003cpattern\u003e` label that Shark writes\nin RealLeakTracerFactory and Shark Dive writes again in toLeakTrace, so\na rewording on either side would have left the two traces one line apart\nwith nothing failing.\n\nThe comparison now runs over application and library leaks each on their\nown, and a library leak dump is one of its cases. Checked by rewording\nShark Dive\u0027s copy: the new case fails on exactly that line.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "ab361337f9ebcb72dd1500a2da16f8eab0faeaeb",
      "tree": "de6cfd986ad12e90c0bdb7136dbfcb0139dcc3d7",
      "parents": [
        "85ca19957fc753c10aadbf22912d4668c15220f7"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 11:55:44 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 15 11:55:44 2026"
      },
      "message": "Count an array\u0027s elements without reading them\n\nShark Dive read every element of an object array to draw the one line\nsaying how many it has, and shark-cli did the same for both kinds of\narray. The index already has each array\u0027s record size, and every element\nis one identifier or one primitive of a known size, so the length is a\ndivision.\n\nChecked against reading the record for every array of the seven Android\ndumps in the repo and of a JVM dump with 8 byte identifiers: the two\nagree on all of them, and every byte size divides exactly.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "85ca19957fc753c10aadbf22912d4668c15220f7",
      "tree": "8191737dacdc23c9457a3be2256feb6a68c60bc1",
      "parents": [
        "769f7273d3a5d624580d3cc24b2311cfc147107d",
        "4f4b43cf57d60772ba9a280be19085e37b4ee4bf"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Tue Oct 13 16:21:27 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 16:21:27 2026"
      },
      "message": "Merge pull request #3010 from square/py/decor-context-narrow-catches\n\nNarrow the exceptions DECOR_CONTEXT catches"
    },
    {
      "commit": "769f7273d3a5d624580d3cc24b2311cfc147107d",
      "tree": "9d5bda91df96360e2b1a9e0190386b309fd3b385",
      "parents": [
        "f0eb5ae84cdc9ba047a2615675ccdda61bcb20d9",
        "41ab67343a856984b9117036d1a4ecacc236049e"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Tue Oct 13 16:21:10 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 16:21:10 2026"
      },
      "message": "Merge pull request #3009 from square/py/decor-context-comment\n\nCorrect when DECOR_CONTEXT expects the DecorView to be created"
    },
    {
      "commit": "4f4b43cf57d60772ba9a280be19085e37b4ee4bf",
      "tree": "82ccdf62f65ddb0914b6129cd56e34cc43444c8a",
      "parents": [
        "f0eb5ae84cdc9ba047a2615675ccdda61bcb20d9"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 15:30:37 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 15:30:37 2026"
      },
      "message": "Narrow the exceptions DECOR_CONTEXT catches\n\ngetDeclaredField() can only fail with NoSuchFieldException here, and\nField#get() and Field#set() with IllegalAccessException. Catching Throwable\nalso swallowed the NonSdkApiUsedViolation that a StrictMode penalty listener\ncan throw from getDeclaredField(), so in an app whose debug builds fail on\nnon-SDK API usage the fix silently did nothing instead of failing.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "41ab67343a856984b9117036d1a4ecacc236049e",
      "tree": "9d5bda91df96360e2b1a9e0190386b309fd3b385",
      "parents": [
        "f0eb5ae84cdc9ba047a2615675ccdda61bcb20d9"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 14:45:38 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 14:45:38 2026"
      },
      "message": "Correct when DECOR_CONTEXT expects the DecorView to be created\n\nA comment said an activity might not have created its DecorView yet by\nonActivityPostCreated(). On Android 10 it always has, even when it doesn\u0027t\nset its content view in onCreate(): Activity#performCreate() calls\nActivityTransitionState#setEnterActivityOptions(), which creates it, right\nbefore dispatching that callback. A code reviewer read the comment as a gap\nin the fix.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "f0eb5ae84cdc9ba047a2615675ccdda61bcb20d9",
      "tree": "a71258c732873b2885fbf6d72e2e21d126090963",
      "parents": [
        "d45bdeb956106e20752919df987bc2fcae43addc",
        "e93172f653b02bc5cc909bb764cf43181a9f4930"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Tue Oct 13 14:23:05 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 14:23:05 2026"
      },
      "message": "Merge pull request #3007 from square/py/gc-trigger-wait-for-enqueue\n\nWait for the reference queue daemon after the last GC"
    },
    {
      "commit": "d45bdeb956106e20752919df987bc2fcae43addc",
      "tree": "70a92b7844b33285fc72f3f32e1ce7cb9cec0afe",
      "parents": [
        "0d3cb08ace85b8c10735c1086be9f8b75b4a84a1",
        "6bcd72cbb82820be5b2184235adb3e7bfb8b6712"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Tue Oct 13 14:08:59 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 14:08:59 2026"
      },
      "message": "Merge pull request #3001 from square/py/gitignore-kotlin-dir\n\nIgnore the Kotlin Gradle plugin\u0027s .kotlin directory"
    },
    {
      "commit": "e93172f653b02bc5cc909bb764cf43181a9f4930",
      "tree": "4c20e056b657a996d18a114f580f1ebd795177a5",
      "parents": [
        "bd3807cb7190cc6889b2c266880a2563a5a8a3c7"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 19:17:01 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 14:08:59 2026"
      },
      "message": "Skip the reference queue sleep on ART\n\nFinalizingInProcessGcTrigger slept 100 ms between its first GC and\nrunFinalization(), copied from AOSP\u0027s FinalizationTester. That sleep gave\nthe thread that enqueues cleared references time to run, so that\nrunFinalization() would find something to finalize.\n\nART hasn\u0027t needed it since Android 5 (libcore 2cc3d41, b/17381967):\nrunFinalization() puts its sentinel on ReferenceQueueDaemon\u0027s list behind\nthe references the GC already handed over, so it waits for the daemon on\nits own. LeakCanary\u0027s minSdk is 24. FinalizationTester added the sleep in\n2011, before that change, and never dropped it.\n\nThe JVM still needs it. Runtime.runFinalization() there only drains the\nfinalizer queue and doesn\u0027t wait for the Reference Handler thread, and\nHeapDumper.withGc and RepeatingJvmInProcessScenario run this trigger on\nthe JVM.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "bd3807cb7190cc6889b2c266880a2563a5a8a3c7",
      "tree": "42dcd150cb1066f8180e3611abb523456946428d",
      "parents": [
        "0d3cb08ace85b8c10735c1086be9f8b75b4a84a1"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 11:39:57 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Tue Oct 13 14:08:59 2026"
      },
      "message": "Wait for the reference queue daemon after the last GC\n\nFinalizingInProcessGcTrigger.runGc() ended with a System.gc() and\nreturned right away. ART clears weak references during a GC but only\nenqueues them afterwards, from ReferenceQueueDaemon. HeapDumpTrigger\npolls the KeyedWeakReference queue as soon as runGc() returns, so it\ncounted objects that last GC had just collected as still retained, and\ndumped a heap that had nothing left to report.\n\nWhat the first GC clears is already waited for: System.runFinalization()\nputs a sentinel on the daemon\u0027s list behind those references and returns\nonce it\u0027s finalized. Calling it again after the last GC does the same\nfor what that GC clears, without running another GC.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "0d3cb08ace85b8c10735c1086be9f8b75b4a84a1",
      "tree": "73b1df6c8d821affe9c8e09d677af7b02cd7555d",
      "parents": [
        "6fb67c46f147c618242138bd0b0e9346b86e586b",
        "f7210dfdcc791864cad379cdd81141e4ebc216eb"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Tue Oct 13 13:52:04 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 13:52:04 2026"
      },
      "message": "Merge pull request #3008 from square/py/decor-context-preserved-window\n\nFix DecorContext keeping the activity a preserved window was created for"
    },
    {
      "commit": "f7210dfdcc791864cad379cdd81141e4ebc216eb",
      "tree": "73b1df6c8d821affe9c8e09d677af7b02cd7555d",
      "parents": [
        "110d24594cfe8709f1739d6a3839ddddcde63a32"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 20:17:30 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 20:17:30 2026"
      },
      "message": "Add a changelog entry for the DecorContext fix\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "110d24594cfe8709f1739d6a3839ddddcde63a32",
      "tree": "21b1e4b0800335d45a14cd3efa29c0673115b1b6",
      "parents": [
        "6fb67c46f147c618242138bd0b0e9346b86e586b"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 20:16:36 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 20:16:36 2026"
      },
      "message": "Fix DecorContext keeping the activity a preserved window was created for\n\nOn Android 10, an activity relaunched with its window preserved reuses\nthe DecorView of the destroyed activity, and that DecorView\u0027s\nDecorContext keeps a weak reference to the destroyed activity, which\nDecorContext.setPhoneWindow() doesn\u0027t update. DecorView.getResources()\nreads that reference on every call, and a reference read while the GC\nis marking keeps its referent alive, so the destroyed activity survives\ngarbage collections while the window draws. LeakCanary reports it as\nretained, then finds no strong path to it in the heap dump. Android 11\nfixed it by replacing the reference in setPhoneWindow().\n\nAndroid 10 preserves the window when an overlay the app uses changes,\nwhich relaunches every activity of the app (navigation mode, accent\ncolor, or an overlay a device enables at runtime), and when an activity\nis resized in multi-window mode.\n\nDecorContext\u0027s fields are on the Android 10 blocklist, but PhoneWindow\nbuilds the DecorContext from Window.getContext(), and the field behind\nit, Window.mContext, is greylisted. The new DECOR_CONTEXT fix sets that\nfield to a ContextWrapper of the activity while the activity is created,\nwhich is when the DecorView is created, then sets it back and keeps the\nwrapper reachable from a DecorView tag. When an activity is created with\na preserved window, the fix points the wrapper\u0027s base context, also\ngreylisted, at the new activity.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "6fb67c46f147c618242138bd0b0e9346b86e586b",
      "tree": "4a045d42a090cebc37b202c2846ad4a2fbdb34a3",
      "parents": [
        "00880a8fb66eb4046a0124bff8303a04874bc07e",
        "425834da5d88ed8fb9685c6d0541c53eb349939f"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Mon Oct 12 10:19:38 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 12 10:19:38 2026"
      },
      "message": "Merge pull request #3006 from square/py/imm-ime-insets-consumer-fix\n\nFix InputMethodManager.mImeInsetsConsumer holding a removed window"
    },
    {
      "commit": "425834da5d88ed8fb9685c6d0541c53eb349939f",
      "tree": "4a045d42a090cebc37b202c2846ad4a2fbdb34a3",
      "parents": [
        "548b1bae8af46a8966dd96e2ae03f7c37eb1d23e"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 09:22:47 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 09:22:47 2026"
      },
      "message": "Add a changelog entry for the InputMethodManager.mImeInsetsConsumer fix\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "548b1bae8af46a8966dd96e2ae03f7c37eb1d23e",
      "tree": "135dec6fdbc3f43c15b0a62d6ac27a17f7376d80",
      "parents": [
        "00880a8fb66eb4046a0124bff8303a04874bc07e"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 09:12:23 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 12 09:12:23 2026"
      },
      "message": "Fix InputMethodManager.mImeInsetsConsumer holding a removed window\n\nFrom Android 11 to Android 15, InputMethodManager.mImeInsetsConsumer is\nset to a window\u0027s IME insets consumer when that window gains focus.\nViewRootImpl.handleWindowFocusChanged() makes that call before checking\nwhether the window is still added, and removing a window doesn\u0027t remove\nits queued MSG_WINDOW_FOCUS_CHANGED. So a dialog dismissed while its\nfocus gain is still queued, which happens when it\u0027s shown and dismissed\nwhile the main thread is busy, gets its consumer registered after it\u0027s\ngone. When the activity\u0027s own focus gain ran first, the dialog\u0027s wins,\nand InputMethodManager holds the removed ViewRootImpl until another\nwindow gains focus.\n\nThe new IMM_IME_INSETS_CONSUMER fix removes the queued focus change\nmessages of a window as its root view is detached. Android 16 doesn\u0027t\nregister these consumers anymore, as its release configuration enables\nthe refactor_insets_controller flag, so the fix only applies to API 30\nto 35.\n\nThe library leak description said the field isn\u0027t cleared when the\nactivity is destroyed, which isn\u0027t what happens: the activity is still\nalive, and what\u0027s held is a dialog that was dismissed.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "00880a8fb66eb4046a0124bff8303a04874bc07e",
      "tree": "39c9d1b83ba2d56204ccafb094f20b8ec8854db1",
      "parents": [
        "e907144aa506d8b3690bf7d88563b83183d0bf92",
        "7866f30d3edefae092ce4f12fddca60474db2ace"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Thu Oct 08 04:10:32 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 08 04:10:32 2026"
      },
      "message": "Merge pull request #3005 from square/py/link-context-menu\n\nGive links in notes and reasons the menu every way to an object has"
    },
    {
      "commit": "7866f30d3edefae092ce4f12fddca60474db2ace",
      "tree": "39c9d1b83ba2d56204ccafb094f20b8ec8854db1",
      "parents": [
        "e907144aa506d8b3690bf7d88563b83183d0bf92"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 08 03:40:19 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Thu Oct 08 03:40:19 2026"
      },
      "message": "Give links in notes and reasons the menu every way to an object has\n\nAn address or a class name in a verdict reason was a link a plain click\nfollowed and nothing else: no right click menu, no middle click, no ⌘\nclick, unlike every other place in the window that leads to an object.\nThe same was true of every link Shark Dive draws from markdown, since\nnotes, reference pages, library leak descriptions, verdict and agent\nreasons and inspector labels all go through one renderer.\n\nThat renderer now reads the other gestures off the pointer and matches\nthem to the link under it through the laid out text, because a\nLinkAnnotation only hears a plain click. A right click on an object\noffers \"Open in a new tab\" and \"Copy link\"; on a URL or a shark:// link\nit offers \"Copy link\", which copies it as written. A middle click or a ⌘\nclick opens an object behind. A press that isn\u0027t over a link is left\nalone, so a reason drawn inside a row that leads somewhere still opens\nthat row\u0027s menu.\n\nThe verdicts a new one disagrees with, listed before it is set, were\nrows that led to their object through a bare clickable. They now use\nOpenTarget and openable like every other row naming an object.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "e907144aa506d8b3690bf7d88563b83183d0bf92",
      "tree": "bd16f71cb744bf65890d2d4d345094dc63776926",
      "parents": [
        "9e6e9420d22c9dfa0d6085f45d182c4f646d7f81",
        "bc20c31f1e6e37eb49e618203f0511efd0ae2228"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 22:23:41 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 22:23:41 2026"
      },
      "message": "Merge pull request #3004 from square/py/cleanup42\n\nPrint markdown links as their text in the human leak trace"
    },
    {
      "commit": "9e6e9420d22c9dfa0d6085f45d182c4f646d7f81",
      "tree": "95ebaf56e4d3c64674dd10f42886b4ede9b114dd",
      "parents": [
        "584e45817b98e1a86f6bd1728da05d417496d6a1",
        "f6b43c95be63bbc1effc61222830b944445ce669"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 22:08:27 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 22:08:27 2026"
      },
      "message": "Merge pull request #3003 from square/nexus/task-de92a356\n\nList Shark Dive\u0027s commands under --cli --help"
    },
    {
      "commit": "bc20c31f1e6e37eb49e618203f0511efd0ae2228",
      "tree": "3bc453cf611687da3de9fa33b3ebc7faa154030a",
      "parents": [
        "d2b63e2827c05903da60e2960fbb663749c91135"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 22:07:58 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 22:07:58 2026"
      },
      "message": "Print markdown links as their text in the human leak trace\n\nA verdict\u0027s `why` links each source behind it as\n`[Owner.kt:42](idea://open?file\u003d/absolute/path/to/Owner.kt\u0026line\u003d42)`, and the leak trace prints the\n`why` beside its object. `humanLeakTrace` goes into a code block in front of a person, where nothing\nrenders a link, so each one came out as its whole target. `markdownLinksAsText` reduces a link to its\ntext, reading links the way a note does: a link inside backticks is code and stays as typed, and a link\nwith no text is its target. `leakTrace`\u0027s `verdictReason` keeps the full links.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "d2b63e2827c05903da60e2960fbb663749c91135",
      "tree": "7522f0d8f986d5e1dfb7d0358c889cd52707d300",
      "parents": [
        "584e45817b98e1a86f6bd1728da05d417496d6a1"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 22:02:27 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 22:07:58 2026"
      },
      "message": "Ask for the human leak trace verbatim in the leak method\n\nSteps 2 and 5 now say to paste `humanLeakTrace` in the final reply in a code block, character for\ncharacter, with no ellipsis, even when it is large.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "f6b43c95be63bbc1effc61222830b944445ce669",
      "tree": "95ebaf56e4d3c64674dd10f42886b4ede9b114dd",
      "parents": [
        "584e45817b98e1a86f6bd1728da05d417496d6a1"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:58:00 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:58:00 2026"
      },
      "message": "List Shark Dive\u0027s commands under --cli --help\n\nAn agent handed the app reads its help the way agents read any unfamiliar\nprogram\u0027s help: `--help 2\u003e\u00261 | head -50`. The help was 94 lines, so the cut\nfell at find_objects, the tenth of nineteen commands, and took the agent\ninstructions under the list with it, including the line that sends an\nagent to --leak-investigation-help before it works on a leak.\n\nSo `--help` is now the options and the agent instructions, 47 lines, and\nthe commands are their own text behind `--cli --help`, which `--help` names\ntwice: as a row of the option column and as the first agent instruction.\n`--cli` alone prints the list too, and `--cli \u003ccommand\u003e --help` prints one\ncommand, like `--help \u003ccommand\u003e`. The two reads an agent has to make come\nfirst in the instructions, so a cut further down keeps them.\n\nCliOptionsTest holds `--help` to 50 lines.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "584e45817b98e1a86f6bd1728da05d417496d6a1",
      "tree": "cb0369beecf6409d4263295b7f3ba8df45ce9750",
      "parents": [
        "570f6e2538118f9ef723ad26c0753f232da2fc22",
        "938fba460e8c91a4c679bc09f4f1ca2d378cd711"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 21:51:13 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 21:51:13 2026"
      },
      "message": "Merge pull request #3002 from square/py/app-version-code\n\nReport the app\u0027s version code in the heap dump metadata"
    },
    {
      "commit": "938fba460e8c91a4c679bc09f4f1ca2d378cd711",
      "tree": "cb0369beecf6409d4263295b7f3ba8df45ce9750",
      "parents": [
        "570f6e2538118f9ef723ad26c0753f232da2fc22"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:40:38 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:41:06 2026"
      },
      "message": "Report the app\u0027s version code in the heap dump metadata\n\nShark Dive said the app\u0027s own version number is in no heap dump, and\nthat BuildConfig constants never reach the heap. Neither is true. Every\nheap dump in this repository has the bound ApplicationInfo at\nActivityThread.sCurrentActivityThread.mBoundApplication.appInfo, from\nAPI 19 to API 31, and on every one from API 22 on it carries the app\u0027s\nversion code: gcroot_unknown_object.hprof reads 1564579130. Five of the\nten load a BuildConfig class too, three of them the app\u0027s own.\n\nApplicationInfo.versionCode is a hidden field added in API 21. API 28\nadded longVersionCode, which also carries the manifest\u0027s\nversionCodeMajor, and since then setVersionCode() keeps versionCode as\nits low 32 bits. So the new ApplicationInfo.versionCode key reads\nlongVersionCode when the heap dump has it. A test checks that on a built\ndump: the one real dump from API 28 or later, safe_iterable_map.hprof,\nis in shark\u0027s tests, which can\u0027t see shark-android.\n\nThe version name is not part of ApplicationInfo. It\u0027s only in a heap\ndump the app kept a PackageInfo in or loaded its own BuildConfig into,\nwhich is five of the ten (two of them with the strings stripped), so the\nhelp now says it isn\u0027t in the metadata rather than that it\u0027s in no heap\ndump, and still tells agents to ask which release a version code is.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "6bcd72cbb82820be5b2184235adb3e7bfb8b6712",
      "tree": "f3e6dd66edcdfa3437d9228dda93cb8e7aed3883",
      "parents": [
        "570f6e2538118f9ef723ad26c0753f232da2fc22"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:34:31 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:34:31 2026"
      },
      "message": "Ignore the Kotlin Gradle plugin\u0027s .kotlin directory\n\nThe Kotlin Gradle plugin writes compiler session files to .kotlin/ at\nthe root of the build, such as .kotlin/sessions/kotlin-compiler-\u003cn\u003e.salive.\nNothing ignored that directory, so a `git add -A` after a build committed\none of those files into a pull request.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "570f6e2538118f9ef723ad26c0753f232da2fc22",
      "tree": "27ef38b76a314d46e0e8e810df10185c3c79c2b0",
      "parents": [
        "34acba1d95a04a59534047494ecef43ac242f708",
        "9d78e4281a18a38e2948f01050eb66694d06cd91"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 21:32:55 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 21:32:55 2026"
      },
      "message": "Merge pull request #3000 from square/nexus/add-the-app-s-applicationinfo-to-the-heap-dump-m-c8cb8268\n\nReport the app\u0027s ApplicationInfo in the heap dump metadata"
    },
    {
      "commit": "9d78e4281a18a38e2948f01050eb66694d06cd91",
      "tree": "27ef38b76a314d46e0e8e810df10185c3c79c2b0",
      "parents": [
        "34acba1d95a04a59534047494ecef43ac242f708"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:07:40 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:17:48 2026"
      },
      "message": "Report the app\u0027s ApplicationInfo in the heap dump metadata\n\nAndroidMetadataExtractor now reads ApplicationInfo.sourceDir, dataDir,\ntargetSdkVersion, minSdkVersion and whether FLAG_DEBUGGABLE is set in\nflags. They come from the same ApplicationInfo instance as the\nApp process name, ActivityThread.sCurrentActivityThread.mBoundApplication\n.appInfo, so the walk to it is done once for all six keys.\n\nsourceDir is where the APK is on the device, which is where to pull it\nfrom to decompile it when there is no source to read, and what Shark\nDive\u0027s leak investigation method already points agents at.\ntargetSdkVersion decides which side of the framework\u0027s many\ntargetSdkVersion checks the app is on, which the device\u0027s API level\nalone doesn\u0027t say, so it gets a bullet of its own in that method.\nminSdkVersion was added in API 24, and api-versions.xml agrees with the\ntest dumps: the field is absent from the API 19, 22 and 23 ones, so\nit\u0027s Unknown there.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "34acba1d95a04a59534047494ecef43ac242f708",
      "tree": "4d23f43972b7797b5773d62e6245ee555a3bc36f",
      "parents": [
        "7f305d1fdc437fa9fd8953b5302507e988792b5e",
        "3252cad42ae6c2a6bc486de8b01234e4d1b0b82b"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 21:14:27 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 21:14:27 2026"
      },
      "message": "Merge pull request #2999 from square/py/method\n\nShorten the leak investigation method, and solve a leak a GC root holds"
    },
    {
      "commit": "3252cad42ae6c2a6bc486de8b01234e4d1b0b82b",
      "tree": "4d23f43972b7797b5773d62e6245ee555a3bc36f",
      "parents": [
        "ab9602c04dbe6551304352c71a9b8abd9251db03"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:02:29 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:02:29 2026"
      },
      "message": "Read a stuck object a GC root holds as a leak of that root\n\nA path whose first object is stuck read as unsolved, with no candidate\nreferences left: faultyReferenceIndexOrNull needs an expected object\nabove the first stuck one, and nothing is above a GC root. So an agent\nthat set the GC rooted object STUCK was told the fault could be further\nup than the path reaches and to find an EXPECTED object above the top.\n\nA GC root is a reference like the fields under it, the one holding the\nfirst object. It is now one of the candidates while nothing above the\nfirst stuck object is expected, and the faulty reference once the\nobject it holds is stuck: RootPath.isGcRootFaulty, named by the root\u0027s\nlabel in the window\u0027s Leak solved section, on the path\u0027s first line,\nand in faultyReference, where objectIndex is absent because no object\nof the path holds the root. leakTrace gains gcRootIsSuspect beside\ngcRootType. The root also counts as a reference of the path, so a\nprogress ratio can\u0027t go negative and inspectors ruling it out shows.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "ab9602c04dbe6551304352c71a9b8abd9251db03",
      "tree": "91d163d0f1b5ee1997ce4039c27bf90a1dfef1a2",
      "parents": [
        "c2c7b0dc9b23130c41483c6ddf1f2fd26affee78"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:02:29 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 21:02:29 2026"
      },
      "message": "Point the leak method at the metadata\u0027s ApplicationInfo.sourceDir\n\nThe APK path is going into the heap dump metadata the way the Build keys\ndid in #2997, under that name, so the method names the key rather than a\nfield of an object the agent would have had to find. Rewraps the\nverdict spreading paragraph to the 110 column while here.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "c2c7b0dc9b23130c41483c6ddf1f2fd26affee78",
      "tree": "feba4e43d34979e7f790f1c0c76cc0395826d263",
      "parents": [
        "f10dc95a7e9a0ff058cf38df44623d5e5da90d42"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 20:49:23 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 20:49:25 2026"
      },
      "message": "Introduce the verdict spreading list and drop the duplicate SDK sources line\n"
    },
    {
      "commit": "f10dc95a7e9a0ff058cf38df44623d5e5da90d42",
      "tree": "9cf0b457c94851366d62c4b985e33753e4a3819b",
      "parents": [
        "2a44d7936d47f7a3202f8cb89c4ad25290dea252"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 20:31:15 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 20:49:25 2026"
      },
      "message": "Indent and rewrap the leak method\u0027s new sections\n\nThe lines added at column 0 set the raw string\u0027s smallest indent to\nzero, so `trimIndent()` left four spaces on every other line and each\nlist in `--leak-investigation-help` printed as an indented code block.\nEverything is back at the string\u0027s indent, rewrapped at 110 columns,\nwith list continuations hanging under the item text.\n\nThe printed words are unchanged apart from typos: EXPECT and STUCk,\nUNKONWN, \"it\u0027s verdict\", \"other the other\", a stray \"that\", and the\ncasing of GC, JSON, CLI and GitHub.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "2a44d7936d47f7a3202f8cb89c4ad25290dea252",
      "tree": "925270bbc80c11a42e51d61f814e4f77750eccf6",
      "parents": [
        "7f305d1fdc437fa9fd8953b5302507e988792b5e"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 15:04:44 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 20:49:25 2026"
      },
      "message": "update help guidance\n"
    },
    {
      "commit": "7f305d1fdc437fa9fd8953b5302507e988792b5e",
      "tree": "0f06991e4a39d7ea4aadf1fccd45224ce48eaeee",
      "parents": [
        "86164c9faac2a4c5f9e0a7302883aed282d16b3d",
        "e85639a6a0962f41921e6c5a4d5e24568090cea8"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 20:32:48 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 20:32:48 2026"
      },
      "message": "Merge pull request #2998 from square/py/object-animator-inspector\n\nLabel mAnimationEndRequested only on heap dumps that have it"
    },
    {
      "commit": "86164c9faac2a4c5f9e0a7302883aed282d16b3d",
      "tree": "6a99b43979fa4b2841587b6d638038829cbc8951",
      "parents": [
        "0b5f47484b509140095ed0b7bfca34a2158e3001",
        "712fba9d726d473c5fbb72ac850a72f2cf6ef5e0"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 17:10:53 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 17:10:53 2026"
      },
      "message": "Merge pull request #2997 from square/nexus/task-ed02c3b5\n\nReport the Android build in the heap dump metadata"
    },
    {
      "commit": "e85639a6a0962f41921e6c5a4d5e24568090cea8",
      "tree": "a842a512cb3b23e4e5377bebb577645cc6f188f5",
      "parents": [
        "0b5f47484b509140095ed0b7bfca34a2158e3001"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 15:04:01 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 15:04:03 2026"
      },
      "message": "Label mAnimationEndRequested only on heap dumps that have it\n\nValueAnimator.mAnimationEndRequested was added in Android 7.0. OBJECT_ANIMATOR read it with `!!`,\nso on a heap dump from Android 6.0 or older every ObjectAnimator threw a NullPointerException. An\nanalysis with one in a leak trace or among the unreachable objects failed. The other fields it\nreads are on every test heap dump, from API 19 to API 31.\n\nLeakCanary\u0027s minSdk is 24, so this only reached tools that read older heap dumps: the Shark CLI,\nShark Dive, and HeapAnalyzer used directly.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "712fba9d726d473c5fbb72ac850a72f2cf6ef5e0",
      "tree": "6a99b43979fa4b2841587b6d638038829cbc8951",
      "parents": [
        "0b5f47484b509140095ed0b7bfca34a2158e3001"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 14:57:38 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 15:03:33 2026"
      },
      "message": "Report the Android build in the heap dump metadata\n\nAndroidMetadataExtractor now reads Build.VERSION.RELEASE, CODENAME and\nSECURITY_PATCH, Build.MODEL and Build.FINGERPRINT next to the SDK_INT and\nMANUFACTURER it already had. The API level alone doesn\u0027t say which source\nto read a framework class at: a preview reports the API level of the\nrelease before it (leak_asynctask_o.hprof is SDK_INT 25 with CODENAME O),\nand two devices on one API level can be years of security patches apart.\n\nShark Dive\u0027s leak investigation help used to explain how to dig these out\nwith find_objects and describe_object on the Build classes. They are now\nlines of heap_dump_metadata, so the help says what they mean instead.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "0b5f47484b509140095ed0b7bfca34a2158e3001",
      "tree": "5b644ec2b8dc77f9f6587b8c2be7100931811981",
      "parents": [
        "70b60bfde4526868a45de4e859f2cfa2447715e2",
        "f9ca56584ec54d044bffd406150ac4eeb53d3a87"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 15:02:44 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 15:02:44 2026"
      },
      "message": "Merge pull request #2996 from square/py/remove-shark-dive-no-ui\n\nRemove Shark Dive\u0027s --no-ui run mode"
    },
    {
      "commit": "70b60bfde4526868a45de4e859f2cfa2447715e2",
      "tree": "5ec9a788f1cf787ca9387e8c64211cc739012c09",
      "parents": [
        "d1859dc1c9b9da003fff0497965d04d2afca787a",
        "f13df9cc312b20a69c36814d7af8726a68914ae2"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 14:04:59 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 14:04:59 2026"
      },
      "message": "Merge pull request #2994 from square/nexus/task-5f5b4260\n\nRead reasons and inspector labels the way a note is, link notes to source, write ids in hex"
    },
    {
      "commit": "f13df9cc312b20a69c36814d7af8726a68914ae2",
      "tree": "5ec9a788f1cf787ca9387e8c64211cc739012c09",
      "parents": [
        "9deed0c42e9ee27c6e5a82097bee3a642538e7ab"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 12:56:35 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 13:30:45 2026"
      },
      "message": "Write object ids in hex wherever Shark writes one\n\nLeak trace labels, HeapObject.toString(), error messages, the CLI and LeakCanary\u0027s heap explorer\nwrote an object id in decimal: `instance @314615480 of com.example.Foo`. Heap dump viewers write an\naddress in hex, and so does Shark Dive, which turns `0x12c0a6b8` into a link to the object. A\ndecimal id was a number neither of them could find.\n\nA negative id is written as its low 32 bits. A 32 bit heap dump records 4 byte ids and shark widens\nthem by sign, so an object above the 2 GB mark would otherwise read `0x-7deb3000`. Shark Dive\u0027s\nhexObjectId already follows that rule. Each module has its own copy of the few lines, rather than a\npublic shark API for them.\n\nThe CLI\u0027s interactive mode reads the id after `@` as hex, since that is how it now prints one.\n\nLeak signatures don\u0027t change. An id only reaches the name of an array entry, and a signature uses\n`[x]` for those.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "9deed0c42e9ee27c6e5a82097bee3a642538e7ab",
      "tree": "a4d57b838e9fa70a76cb2e305debb03d60a224fa",
      "parents": [
        "3e543faae6ee9a053fa1cac44edde2cccb7d6203"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 11:40:07 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 12:56:38 2026"
      },
      "message": "Let a note link to a line of source on this machine\n\nA markdown link of any scheme is now handed to the OS, so `[Owner.kt:42](idea://open?file\u003d…\u0026line\u003d42)`\nopens that line in Android Studio, and `vscode://` opens VS Code for someone who prefers it. Agents\nare told to link the sources they read this way.\n\nOn macOS this goes through /usr/bin/open rather than Desktop.browse. The JDK\u0027s browse asks\nLaunchServices for the app that opens https:// and hands every URL to it, so an idea:// link never\nreaches Studio. It also returns as if it had worked. Measured on Temurin 17.0.19 with a scheme\nregistered to a probe app: open reached it and browse did not.\n\nA link to a bare path stays text, because nothing in it says which app opens it at which line.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "3e543faae6ee9a053fa1cac44edde2cccb7d6203",
      "tree": "61c5512767787f3d242607631bbc7d3dd3581adf",
      "parents": [
        "40a3f5465b7a847598ff0f0cd1eadd229825acca"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 11:40:07 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 12:56:38 2026"
      },
      "message": "Read inspector labels the way a note is\n\nWhat Shark\u0027s inspectors write about an object names classes, and the class an object is an instance of\nis the first thing worth opening from it. The labels in a leak trace and in the details panel were\nplain text, so none of that could be clicked. They now go through the same reader as verdict reasons\nand agent reasons, which is the same renderer as notes.\n\nReasonText becomes ProseText, since a label is not a reason.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "40a3f5465b7a847598ff0f0cd1eadd229825acca",
      "tree": "57125ed306992cc29ecd0b5211ea4c0e4e52cc8b",
      "parents": [
        "d1859dc1c9b9da003fff0497965d04d2afca787a"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 10:57:33 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 12:56:38 2026"
      },
      "message": "Read verdict and agent reasons the way a note is\n\nA reason was plain text everywhere it was drawn, so an address typed into\none was a number and a URL was text to copy out by hand. Notes already\nread markdown and resolve class names and addresses against the heap\ndump; reasons now go through the same `Note` pipeline, in the details\npanel, on every step of a path, on the Leaks screen, in the verdict\nsetter and its list of conflicts, and under each row of an agent log.\n\nThe names a reason mentions are asked once per window and kept, through\none `NoteReader` per heap dump, because the same reason is drawn on many\nsurfaces at once and each redraw would otherwise be a read of the heap\ndump. An agent call about another heap dump is drawn unresolved: its\naddresses are that dump\u0027s objects.\n\nA reason on the Leaks screen is no longer cut at three lines, since a\nlink or an address past the ellipsis would lead nowhere.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "f9ca56584ec54d044bffd406150ac4eeb53d3a87",
      "tree": "4d54c72a37015d97b1fd9f4f93d4a906945c4a55",
      "parents": [
        "d1859dc1c9b9da003fff0497965d04d2afca787a"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 12:44:11 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 12:44:11 2026"
      },
      "message": "Remove Shark Dive\u0027s --no-ui run mode\n\n--no-ui started a run that drew no window and answered agents over the\nsame socket. A run for a machine with no screen is to come back in\nanother form, probably a binary of its own, so the mode goes rather than\nstaying as a second kind of run the command line has to tell apart.\n\nWith it go the parts that only existed to support two kinds of run:\nHeadlessAgentHeapDumps, the window\u003d property every run published and the\ncheck that refused to open a dump in a run of the other kind, and\nShownPlace, whose only case beyond a link was a run with nowhere to show\none. show now answers with the link. --no-ui is an unknown option again,\nand the help, docs/shark-dive.md, the agent guide and the notes no\nlonger describe it as something that exists.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "d1859dc1c9b9da003fff0497965d04d2afca787a",
      "tree": "58d245009d49b0a3199a44c2ca0287d39a140a6a",
      "parents": [
        "ee29519d333af0a338be2cf276d09a910e538ed5",
        "2d88aa7550ea3ab739a2ecd575870dc26646c962"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Wed Oct 07 12:26:18 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 12:26:18 2026"
      },
      "message": "Merge pull request #2995 from square/py/gradle-daemon-jdk-17\n\nRun the Gradle daemon on JDK 17 whatever JAVA_HOME says"
    },
    {
      "commit": "2d88aa7550ea3ab739a2ecd575870dc26646c962",
      "tree": "58d245009d49b0a3199a44c2ca0287d39a140a6a",
      "parents": [
        "ee29519d333af0a338be2cf276d09a910e538ed5"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 11:20:32 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Wed Oct 07 11:20:32 2026"
      },
      "message": "Run the Gradle daemon on JDK 17 whatever JAVA_HOME says\n\nA build on the machine\u0027s default JDK drifts from CI, which builds on 17. On 25 it fails outright:\nthe Kotlin compiler and detekt reject the JVM target, and the pre-push hook reports that as\n\"Static analysis found violations!\" with no violation anywhere, so pushes from a shell without\nJAVA_HOME set fail for a reason the message hides. On 21 it\u0027s worse because nothing fails: lint\npassed locally and then crashed CI on 17.\n\ngradle/gradle-daemon-jvm.properties makes the wrapper find an installed JDK 17 and run the daemon\non it, whatever JVM the wrapper itself starts on.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "ee29519d333af0a338be2cf276d09a910e538ed5",
      "tree": "8a993e7b8ea9e20e3795c1e1fbecf3a6fe685307",
      "parents": [
        "7adade6557b1fd6793045582332e1c949b0acc3c",
        "86102f722130f88dd72693c8157c7a6398f7620a"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Tue Oct 06 06:56:01 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 06 06:56:01 2026"
      },
      "message": "Merge pull request #2991 from square/py/manual_edits\n\nSay verdict throughout Shark Dive"
    },
    {
      "commit": "86102f722130f88dd72693c8157c7a6398f7620a",
      "tree": "8a993e7b8ea9e20e3795c1e1fbecf3a6fe685307",
      "parents": [
        "9129d26ca331ffae249ae7e0fbee89b162611115"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 21:58:50 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 21:58:50 2026"
      },
      "message": "Say verdict throughout Shark Dive\n\n`LeakStatus`, `leakStatus` and `leakStatusReason` are now `Verdict`,\n`verdict` and `verdictReason`, in the enum, the window, the agent surface\nand the files on disk. One concept had two spellings depending on which\nside of the process it was read from, and a person watching an agent work\nhas to be able to say the same thing about the same object as the agent.\n\nThe files moved with the name: verdicts are in `~/.shark-dive/verdicts` as\n`\u003cdump\u003e.verdicts.tsv`, where they were `~/.shark-dive/leak-statuses` and\n`.leak-statuses.tsv`. A file written under the old name goes unread, which\na rename brings back — this app is an alpha and these are three columns of\ntext.\n\nThree more things came out of reading the same surface.\n\nA leak group no longer names a representative object. It was documented as\nthe object whose own path produced the group\u0027s references, and it was\n`objects[0]` every time: the candidates are sorted by retained size before\nany path is walked and Kotlin\u0027s sort is stable. So it cost an agent a\ndecision with one answer. The tool description now says any object of the\ngroup solves the group, and `AgentToolsTest` asserts that sentence is there\nrather than only that the field is gone.\n\n`ObjectContent` replaces the `headline: String?` that four model types\ncarried. A string\u0027s characters, a bitmap\u0027s dimensions and recycled flag and\na thread\u0027s name are facts, while \"420 × 467 pixels, recycled\" is a window\u0027s\nsentence on a surface a program also reads. The model carries the facts and\n`ObjectContent.headline` draws them for the window.\n\nEvery help page this command line prints now wraps at one width, with every\nline of a multi-paragraph description indented. The synopsis stays long: it\nis the line somebody copies.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "9129d26ca331ffae249ae7e0fbee89b162611115",
      "tree": "648e330d361fcf651a16e80e12b458853564fb1b",
      "parents": [
        "2831a50dee98a07b73a4f3cd1d5f05cae003dd9e"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 10:12:59 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 10:12:59 2026"
      },
      "message": "Name the tell that got through the pass against tells\n\nThe section heading added in the previous commit was \"The command line\nhas a house style, and it is not this file\u0027s\", which breaks three of the\nrules the section goes on to state: negative parallelism, a strawman the\nreader never proposed, and a sentence about the document rather than\nabout heap dumps. The source documenting the shape was already in that\nfile\u0027s link list.\n\nThe rules named `not just X, Y` and `not X but Y` but missed the trailing\nform, where a claim is followed by a coordinate clause rating the claim:\n\", and that is not an oversight\", \", and that is the point\". Counting it\nacross the module gives 186, against 18 in the whole of the rest of\nLeakCanary. Per word that is 6.3 times the rate, with a decade of prose\nnobody generated as the control, so it is this module\u0027s habit rather than\nthe repository\u0027s.\n\nFixed here: both headings that carried it, the section\u0027s own prose, and\none printed string the previous pass created by compressing a tricolon\ninto a two-part negation instead of deleting the negation. The remaining\ninstances are in KDoc and notes, and the section now says to fix those on\ntouch rather than in a sweep across seventy files.\n\nThe rules about length stay exempt for KDoc and notes. The rules about\nshape no longer do: a formula repeated 186 times reads as generated\nwherever it sits.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "2831a50dee98a07b73a4f3cd1d5f05cae003dd9e",
      "tree": "2549445146f122a36a4d323a27df8c38b6968573",
      "parents": [
        "ce2e7bf03f4a63408f15d16e03ccebdd0cee6361"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:59:36 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:59:36 2026"
      },
      "message": "Edit every string this command line prints against rules about shapes\n\nThe text printed from the agent surface had drifted into a register that\nreads as machine-written: 80 Markdown emphasis pairs that nothing in a\nterminal renders, triadic negations (\"Not a path, not a subsystem, not\n...\"), epigrams closing paragraphs (\"A root cause nobody can trigger is a\nhypothesis.\"), and sentences about the text itself rather than about heap\ndumps. An agent reading help written that way learns the house style of\nthe surface it is about to answer on, so it is not only a reading cost.\n\nThe problem turned out not to be lexical. A 49,688 character corpus of\nevery string this module prints contains zero instances of delve, crucial,\nrobust, leverage, seamless, comprehensive, realm, landscape, testament or\nunderscore. So the guidance added here names shapes rather than words,\nwhich is also the half that holds across model eras: the vocabulary lists\ngo stale, and the one word with a published frequency study behind it did\nnot replicate across registers.\n\n--help is 5144 to 4984 characters, with Markdown emphasis 2 to 0 and em\ndashes 4 to 0. --leak-investigation-help is 11855 to 10652, with emphasis\n62 to 0, em dashes 17 to 3, triadic negations 3 to 0, and sentences over\n25 words 34 to 20. The three remaining em dashes are the definitions in\nthe EXPECTED, STUCK and UNKNOWN bullets, which is what the character is\nfor. Em dashes were reduced by rewriting the sentences that needed one,\nnot hunted as a character: the claim that they are a tell is weaker than\nit is usually stated, and the guidance says so with the source.\n\nOne test assertion moves, for a phrase that became the start of a\nsentence and so gained a capital.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "ce2e7bf03f4a63408f15d16e03ccebdd0cee6361",
      "tree": "5dad0a99821b71ca49d4c0164f08e64254cfbb26",
      "parents": [
        "78b237efc18bf7316f52063df9394d84955b6958"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:38:17 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:38:17 2026"
      },
      "message": "Fold --investigation-help into --help, and drop the option\n\nFour paragraphs is not a document. An option earns its own name when\nsomebody would go looking for it, and what an agent types at a program\nit has been told nothing about is --help — so a second option was one\nmore thing to find for a text short enough to live inside the first.\n\nWhat moved: the `reason` on every call, the `shark://` links to put in\na reply, the gap to admit, and the pointer at\n--leak-investigation-help. What was dropped on the way: the window a\nperson may be watching, which is not a fact an agent can act on, and\nthe second telling of `reason` — it was in the --session\u003d option row\nand in the paragraph under the command column, and now it is in the\nparagraph alone.\n\n--leak-investigation-help stays an option. It is eleven thousand\ncharacters, it is read once per investigation rather than once per\nsession, and the sessions that never have a leak in hand never need it.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "78b237efc18bf7316f52063df9394d84955b6958",
      "tree": "0fd18b0fcb564471ac9d776ee62065a9880b05e8",
      "parents": [
        "88042b2e3cdd103084ce9d25882e131b8c317301"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:32:35 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:32:35 2026"
      },
      "message": "Keep the printed command\u0027s env vars on the command\n\nSplitting them onto lines of their own makes them shell variables of\nwhoever pasted it rather than environment variables of the run: a bare\n`FOO\u003dx` on its own line is an assignment to the shell, and the command\ntwo lines down is started without it. Measured both spellings — the\ncontinued one is the one the client reads.\n\nWhich matters here more than it looks: dropping CLAUDE_CONFIG_DIR is a\nrun against the pasting user\u0027s own config directory and history, which\nis the whole thing the harness stages a scratch one to avoid.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "88042b2e3cdd103084ce9d25882e131b8c317301",
      "tree": "f328b087116c3112b48246ec4acb8225178328b8",
      "parents": [
        "7adade6557b1fd6793045582332e1c949b0acc3c"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:32:00 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Mon Oct 05 09:32:00 2026"
      },
      "message": "Say what the harness prints in plainer words\n\nThe lines a harness run prints are read once, by somebody about to paste\nthe command under them, and they were three sentences each where one\nwould do. The transcript line says what it is instead of what it is for,\nthe paragraph under the command says what the command does, and the two\nsentences explaining why --session-id is on it are gone: the reader\neither needs the command or doesn\u0027t. Same for the log line naming the\nprocess, which described the OS\u0027s naming rules rather than saying what\nwas set.\n"
    },
    {
      "commit": "7adade6557b1fd6793045582332e1c949b0acc3c",
      "tree": "1022f91e716243f015c05716393268d8b8d07417",
      "parents": [
        "1aa85dc0472d6e38966a649741a7ce30f828d162",
        "8f71614feef8b1bcd91d12f11b2a90356ae9b9ac"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Mon Oct 05 08:32:31 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 05 08:32:31 2026"
      },
      "message": "Merge pull request #2988 from square/nexus/conclude-mechanism-necessity-investigation-b35fc222\n\nDelete conclude, and let the heap dump say when a leak is solved"
    },
    {
      "commit": "8f71614feef8b1bcd91d12f11b2a90356ae9b9ac",
      "tree": "1022f91e716243f015c05716393268d8b8d07417",
      "parents": [
        "8f7326f2d91434a5babf1f79514083169fc8046b"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:00:40 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:02:01 2026"
      },
      "message": "Read the starred file on a click that beat the read, not drop the click\n\nHeapDumpStars.toggle refused to do anything while isRead was false. The\nreason was sound — the list is written whole, so saving one worked out\nfrom a file nobody has read yet takes the star off everything that was\nin it — but refusing is the wrong answer to it, because the thing that\nis unknown is knowable: take the lock, read, and then write what the\nread says.\n\nNothing but a test is fast enough to hit this. The window starts the\nread as a heap dump opens (HeapDumpDive.kt:622) and a person reaches a\nstar seconds later, so the click that lands first is one something drove.\nThat is how it showed up: DiveAppTest\u0027s \"taking the star off the last\nobject is the working set gone\" timed out on a loaded CI runner in run\n37191639482 and nowhere else, over three clean local runs and one under\neight CPU hogs. The click went to a HeapDumpStars that had not finished\nreading, the star never appeared, and the test waited ten seconds for a\nlabel that was never going to be drawn.\n\nLeakStatusSection.kt:85 already spells the alternative — the equivalent\ncontrol is disabled until isRead — so the star was the outlier either\nway.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "8f7326f2d91434a5babf1f79514083169fc8046b",
      "tree": "f93825450381e645427a9e6ff36b6d8428c43d08",
      "parents": [
        "0363e5a5c7bf15f0f4b99b65a09061ca8ab38af3"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:00:30 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:02:01 2026"
      },
      "message": "Rewrap the sentence a rename left running off the line\n\nRenaming the tool-outcome helper in this branch stretched one line of\nopenHeapDumpsOfTool\u0027s KDoc to 126 characters, past the 120 the rest of\nthe repo wraps at, and left it pointing at the helper by name instead of\nby link.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "0363e5a5c7bf15f0f4b99b65a09061ca8ab38af3",
      "tree": "62150205efce7ee0af6eed222978b1480852867e",
      "parents": [
        "69264b375c53b520f2c1bc3b7fdd22e35ea41632"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:16:35 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:02:01 2026"
      },
      "message": "Name the progress ratio a ratio, since it was read as a percentage\n\n0 to 1 and 0 to 100 are both ways to spell a share, so a field named only\n\"progress\" gets read as whichever the reader expects, and 0.2 taken for 0.2%\nis a fifth of a percent of the way through. That happened: the rounding\nfunction multiplied by a constant called PROGRESS_SCALE and divided by it\nagain, which is two decimal places and reads exactly like a conversion to\npercent.\n\nRatio is Prometheus\u0027 convention for a 0-to-1 value and it goes on every\nspelling of this number:\n\n  leakSolvingProgress            -\u003e leakSolvingProgressRatio\n  progressBefore / progressAfter -\u003e progressRatioBefore / progressRatioAfter\n  RootPath.leakSolvingProgress() -\u003e leakSolvingProgressRatio()\n  PathVerdicts.progress          -\u003e progressRatio\n  AgentJson.roundedProgress      -\u003e roundedRatio\n  PROGRESS_SCALE                 -\u003e TWO_DECIMAL_PLACES\n\nThe value itself is unchanged, and path_from_gc_root\u0027s description now says\nthe range rather than leaving it to the name alone.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "69264b375c53b520f2c1bc3b7fdd22e35ea41632",
      "tree": "49ab3a3d734cb13168111aecb565d17b900d2b31",
      "parents": [
        "a4084b089782aae113d92ec99989e6825719cadb"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 08:31:53 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:02:01 2026"
      },
      "message": "Print every line of a leak trace, not the ones a hash reads\n\nA leak trace is the one artefact Shark Dive hands a person, and the whole\nreason it is worth handing over is that it can be held beside a LeakCanary\nreport of the same dump. Two of its lines were missing, both because\nLeakFingerprint.kt was written to feed a hash and a hash reads neither: the\nRetaining … in … objects line, and the Library leak match: \u003cpattern\u003e label.\n\nA trace missing a line is not read as a tool that renders less. It is read as\na heap dump that says less, and what somebody does with it is go and find the\nfact in a report that has it.\n\nMeasured against shark-cli analyze on leak_asynctask_o.hprof, which is now the\ncomparison to repeat: identical fingerprint, identical steps and underlines,\nidentical inspector labels, identical statuses and reasons. LeakFingerprintTest\nasserts the same thing on both of its dumps character for character, with the\nretained numbers blanked — those come from two different dominator trees, the\nexact one under the dive and the approximation under an analysis, and agree to\nwithin it rather than to the byte.\n\nThe JSON half already carried all of this: inspectorLabels, retainedBytes,\nretainedObjects, verdictReason, and the matched pattern on the reference.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "a4084b089782aae113d92ec99989e6825719cadb",
      "tree": "481936414d2982193dc5b1c878960d2fd46bab2e",
      "parents": [
        "1aa85dc0472d6e38966a649741a7ce30f828d162"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sat Oct 03 14:01:28 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 21:01:35 2026"
      },
      "message": "Delete conclude, and let the heap dump say when a leak is solved\n\n`conclude` took the faulty reference as an argument and refused until the\nverdicts on the path left exactly one candidate, which read as the one refusal\nworth having. It wasn\u0027t. The reference it checked is one the surface had already\nhanded over — a path answers with `faultyReference` the moment the verdicts\nnarrow to one, and the refusal before that lists the candidates — so the check\ncompared the tool\u0027s own derivation against a copy of it, and the eval column it\nfed was scoring transcription.\n\nSo the job is restated as what it always was: an agent settles the state of the\nobjects it can settle, and Shark Dive says when one reference is left.\n`RootPath.isLeakSolved()` is that fact and `leakSolvingProgress()` is how far off\nit is, both read off the verdicts rather than claimed by anybody. What replaces\nthe refusal is the answer carrying the distance — `set_verdict` takes\n`solvingLeakOf`, the stuck object the verdict is in service of, and answers with\nthe candidate count and the progress on both sides of it, so a verdict that ruled\nnothing out and a verdict that halved the search no longer read the same.\n\n`list_leaks` becomes `list_leak_groups`, which is what it has answered with since\ngrouping went in, and each group now names the `representativeObject` to work it\nthrough and carries the leak trace LeakCanary prints, so an agent matches a trace\nit was handed rather than retelling one. `chain_from_gc_root` becomes\n`path_from_gc_root`, the industry word, and \"chain\" meaning a root path is now\n\"path\" everywhere — the window, the docs, the notes.\n\n`outcomeOfTool` reads the reference off the two answers that can carry one, and\nonly when the same answer says `leakSolved`, so a build that changed one half\nwithout the other records nothing rather than something wrong. The eval\u0027s\n`REFUSED` outcome goes with the tool; what it measured is `NOT_SOLVED` plus the\nrefusal count beside it.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "1aa85dc0472d6e38966a649741a7ce30f828d162",
      "tree": "a0a67e080c944c45ff04fdabb4d0289a3bdd5361",
      "parents": [
        "d73534748f9eb263cc3cca13304a398a28cce971",
        "75d17cfe75611f57a285392c0a60a47b1867ccbd"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Sun Oct 04 12:31:15 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Oct 04 12:31:15 2026"
      },
      "message": "Merge pull request #2990 from square/nexus/task-f84644e6\n\nSay Verdict: Stuck / Expected where a leak trace said Leaking: YES / NO"
    },
    {
      "commit": "d73534748f9eb263cc3cca13304a398a28cce971",
      "tree": "e313e5e6213b2d42f713a8c395aceff44093a5ce",
      "parents": [
        "fd1def6a4513f8e221f114bbc50d52a096104561",
        "c58c8136d37ca8e50f2284ec1d8ea9555de5f226"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Sun Oct 04 11:09:14 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Oct 04 11:09:14 2026"
      },
      "message": "Merge pull request #2989 from square/nexus/expose-harness-tool-call-reasoning-55be2670\n\nFollow a harness run\u0027s calls as it makes them"
    },
    {
      "commit": "75d17cfe75611f57a285392c0a60a47b1867ccbd",
      "tree": "507cb1e3139010b7131b5ffea1c0875c37c2d226",
      "parents": [
        "2db4ed7973325334f805d694c19d79d05cae61f4"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 10:09:17 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 10:09:17 2026"
      },
      "message": "Assert the leaks.db recreate rather than a migration that can\u0027t happen\n\nDatabaseMigrationTest opened the committed leaks-v24.db asset and\nasserted that upgrading it kept its rows: one heap analysis, eight leak\ntraces, three leaks. That contract went when every version below the\ncurrent one started being dropped and recreated, so the six tests\nasserting preserved data failed on CI with \"Expecting \u003c0\u003e to be equal\nto \u003c8\u003e\".\n\nThe database comes back empty on purpose. What is worth checking about\na recreate isn\u0027t there any more, it\u0027s that what\u0027s left works: a half\ndropped schema reads as an empty database and only fails when something\nwrites to it, and nothing here was writing. So the data assertions\nbecome two that say the rows are gone, plus one that inserts an\nanalysis into the recreated database and reads it back.\n\nAlso deletes the database afterwards, which the test never did.\n\nRan on an API 36 emulator alongside the rest of\nleakcanary-android-core, leakcanary-android-instrumentation and\nleakcanary-android: 59 tests, none failing.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "2db4ed7973325334f805d694c19d79d05cae61f4",
      "tree": "f1922ca4c9e71ae33dd27566d3c59d2a05e7926b",
      "parents": [
        "fd1def6a4513f8e221f114bbc50d52a096104561"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:42:23 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:42:23 2026"
      },
      "message": "Say Verdict: Stuck / Expected where a leak trace said Leaking: YES / NO\n\nA leak trace carries a verdict on each object: whether it is meant to\nstill be in memory. It printed that as `Leaking: YES`, `NO` and\n`UNKNOWN`, a dozen times down one trace, and the wording points at the\nwrong thing. A leak is one faulty reference, the one that should have\nbeen cleared, and everything below it is in memory because of that\nsingle mistake — so calling all twenty of those objects \"leaking\" sends\na reader looking at the twenty rather than at the one thing to fix.\n\n`Stuck` says the object should be gone and something is holding it,\nwhich is the question worth asking, without accusing the object of\nbeing the bug. `Expected` says its being in memory is legitimate at\nthis point in the app\u0027s life, which is what an inspector recognizes.\nThe pair is deliberately not antonyms: an object in use can\u0027t be\ncollected either, so the good side has to answer a different question\nthan \"can it leave\".\n\nShark Dive picked these three words and shipped them, and the session\nthat did scoped itself to that window — not as a decision anyone took,\nbut because that was how the task had been asked. It then wrote \"the\nidentifiers didn\u0027t move\" into notes/decisions.md as though it had been.\nTwo vocabularies for one verdict is the thing that section argues\nagainst, and the cost showed up as soon as a reader moved between a\nleak trace and that window. So the words go down into Shark, and the\nsame object gets the same word everywhere.\n\nBreaking, and knowingly so:\n\n- `LeakTraceObject.LeakingStatus` is `Verdict`, `LEAKING` /\n  `NOT_LEAKING` are `STUCK` / `EXPECTED`, `UNKNOWN` keeps its name.\n- `leakingStatus` / `leakingStatusReason` are `verdict` /\n  `verdictReason`.\n- `ObjectReporter.leakingReasons` / `notLeakingReasons` are\n  `stuckReasons` / `expectedReasons`, which every custom\n  `ObjectInspector` adds to.\n- Java serialization writes field names and enum constant names, so a\n  stored analysis would deserialize with a null verdict on a non-null\n  property. `serialVersionUID` is bumped so it fails instead, and\n  LeakCanary\u0027s database goes to version 27, recreating rather than\n  migrating. The incremental migrations below it go with it: they only\n  ever rewrote blobs this version can no longer read.\n- Android Studio\u0027s Profiler parses `Leaking: (YES|NO|UNKNOWN)` out of\n  logcat in its on device mode. That one breaks until Studio updates;\n  its host side mode, which analyzes the dump with its own bundled\n  Shark, is untouched. AndroidStudioProfilerContractTest says why the\n  printed format is deliberately not pinned there.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "c58c8136d37ca8e50f2284ec1d8ea9555de5f226",
      "tree": "e313e5e6213b2d42f713a8c395aceff44093a5ce",
      "parents": [
        "f8704911b79807e3b69adb3fb9f05df50514cfe3"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:32:01 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:32:01 2026"
      },
      "message": "Say why the reasoning text is missing, having pinned the model\n\nThe first version of this blamed `--print` on an experiment that never pinned a\nmodel, which is not sound: `--model opus` resolves to claude-opus-5-5, so an\nunpinned run measures the alias. Pinning claude-opus-5 settles it — a `--print`\nrun records 0 characters, while that same model in a session that is not\n`--print` recorded 47 blocks of 47 with text, median 699 characters, on the same\nclient version and the same entrypoint.\n\nNames the mechanism rather than the symptom: this is the API\u0027s thinking\n`display: \"omitted\"`, empty blocks carrying only the encrypted signature,\nagainst the `\"summarized\"` a non-print session gets. Records that `--effort max`\nand MAX_THINKING_TOKENS change nothing, both measured, so the next reader\ndoesn\u0027t retry them.\n\nAnd that a populated block is a summary written by a different model rather than\nthe reasoning, which the signature carries and nothing here can read — so a\ntranscript is an account of how an agent thought, never the audit log of it.\nUpstream treats the empty blocks as a bug and it has moved with client version\nbefore, so this says to re-measure rather than trust it.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "f8704911b79807e3b69adb3fb9f05df50514cfe3",
      "tree": "e234c16af5b90ed9a407c49580db4fd20d7425fe",
      "parents": [
        "fd1def6a4513f8e221f114bbc50d52a096104561"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:19:47 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sun Oct 04 09:19:47 2026"
      },
      "message": "Follow a harness run\u0027s calls as it makes them\n\nstart-harness.sh printed two things about a run: the agent\u0027s final answer, and\nwhere the Shark Dive log is. Neither shows a run that never arrived. Measured\nhere: a run launched the bundle with `open -a`, made no --cli call at all, and\nfinished by describing the kinds of leak it would have found — an empty agent\nlog beside an answer that reads like a success.\n\nThe client writes every call and every result to a transcript as it goes, so\nwatch-transcript.sh follows that, in the terminal or through claude-code-log in\na browser. Both ways of starting a run print it with the run\u0027s own directory\nfilled in, since harness directories accumulate and the newest is the right\nanswer only until a second run starts.\n\nWhich file it is has to be knowable before the run exists, so the session id is\npinned with --session-id rather than left to the client, and written to\nsession-id.txt beside the prompt. The lookup globs for that id instead of\nbuilding a path: the client keys its projects directory on the physical working\ndirectory, and $TMPDIR here is a symlink into /private/var, so a computed path\nis right on Linux and wrong on this machine. A run staged before this change has\nno id to find, and falls back to the newest transcript in its own configuration\ndirectory, which is sound because that directory is staged per run.\n\nThe reasoning text is not in any of it. A --print run records thinking blocks\nwith their signature and no text, measured three ways — the transcript file,\n--output-format stream-json, and the thinking_delta events under\n--include-partial-messages, which arrive carrying zero characters. Interactive\nsessions do record it, which is what makes the file look like it ought to. The\nheader says so where somebody would otherwise go looking for the option.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "fd1def6a4513f8e221f114bbc50d52a096104561",
      "tree": "94a8f6c29fe55a672d14d5520b3a138c3e70228d",
      "parents": [
        "b521dba3c4cdeb26f120d44ff0d185b073b1bbaa",
        "2120d7c80e705a000335de3453da7639493a9dc0"
      ],
      "author": {
        "name": "py - Pierre Yves Ricau",
        "email": "pyricau@users.noreply.github.com",
        "time": "Sat Oct 03 13:49:51 2026"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Oct 03 13:49:51 2026"
      },
      "message": "Merge pull request #2986 from square/shark-dive-heap-dump-metadata\n\nAnswer what a heap dump says about itself, as LeakCanary\u0027s own map"
    },
    {
      "commit": "2120d7c80e705a000335de3453da7639493a9dc0",
      "tree": "649c01950ba291295f0afb51d4e21d525b7d7526",
      "parents": [
        "512a65218aae85b35c6c7f39b484861c79ea7368"
      ],
      "author": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sat Oct 03 13:40:09 2026"
      },
      "committer": {
        "name": "Pierre-Yves Ricau",
        "email": "py@squareup.com",
        "time": "Sat Oct 03 13:40:09 2026"
      },
      "message": "Scroll the reference index rather than assume it fits\n\nThe index of pages is a scrolling column, and the page this change\nadded made it taller than the window: every row was on screen on\nmacOS and the last was below the fold on Linux, so the assertion\nthat each one is displayed failed on CI and nowhere else. Text\nmetrics decide how much of it fits, and those are the machine\u0027s.\n\n`performScrollTo` is what that assertion should always have been.\nShown both ways before and after: at a 400 dp window the old form\nfails with CI\u0027s message and the new one passes.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    }
  ],
  "next": "b521dba3c4cdeb26f120d44ff0d185b073b1bbaa"
}
