De-interleave log lines: HARD MODE In the face of some regrettable rules that turned the original into a glorified sorting task, I am posting a more challenging variant. Shout out to Luis Mendo for the suggestion of how to improve the original challenge.

You've inherited a server that runs several apps which all output to the same log.
Your task is to de-interleave the lines of the log file by source. Fortunately for you, the person who wrote all the apps was nice enough to leave tags indicating their source.
Logs
Each line will look something like this:
[app_name] Something horrible happened!


App tags are always between square brackets and will contain only alphanumeric characters and underscores.
App tags are nonempty
There may be other square brackets later on any given line, but none will form a valid tag.
There will always be at least one non-space character after a tag.
The log as a whole may be empty.
There is no limit to how many unique app tags will be present in the file.

In some cases, an app tag may be missing. When this is the case, the log line belongs to the most recently logged app.

The first line of the log will always begin with an app tag
A line beginning with [ is not necessarily tagged. If there is an invalid character between the initial square brackets or no ], then the line is not tagged.
No empty lines appear in the log

Expected Output
You should output several fully-separated logs with the app tags removed from each log line where they were present. You do not need to preserve leading whitespace on any log line.
Output logs must be in some sort of key-value mapping or reasonable equivalent. A non-exhaustive list of valid output formats:

A file named after the app tag for each app

You may assume that the output files do not already exist in the output directory in this case.


A dictionary/map/hash/whatever that uses app tags as keys and a newline-separated string of the log lines as values.
A long concatenated string separated by blank lines and preceded by app tags
A list of [key, value] lists
A JSON string with app tags as keys and arrays of log lines as values
A Markdown document with app tags as headers and leading #s of any line escaped with backslashes.
A Javascript function that takes a string as input and outputs the associated log as a newline-separated string.

Basically, if you can't tell which app the log lines came from, the output is invalid.
Example
An entire log might look like this:
[weather] Current temp: 83F
[barkeep] Fish enters bar
Fish orders beer
[stockmarket] PI +3.14
[PI announced merger with E]
[barkeep] Fish leaves bar
[weather] 40% chance of rain detected
[ I have a lovely bunch of coconuts

Which should output three different logs:
weather:
Current temp: 83F
40% chance of rain detected
[ I have a lovely bunch of coconuts

barkeep:
Fish enters bar
Fish orders beer
Fish leaves bar

stockmarket:
PI +3.14
[PI announced merger with E]

You are not given the names of the app tags ahead of time. You must determine them only by analyzing the log file.
Rules and Scoring

This is code-golf, so shortest code wins.
Standard rules and loopholes apply
Use any convenient IO format, provided that each input line is represented as a string, not a pre-parsed tag + message. Parsing is part of this challenge.
Output log lines for each app must appear in the same order that they did in the original log.
You may assume that the input log contains only ASCII characters.
Python 3.8, 95 bytes

import re
lambda x:[((t:=re.match(r'\[(\w*)\]',s)or t)[1],s.split(t[0])[-1].strip())for s in x]

Try it online!
(Expanded TIO example with input)
Explanation:
Python 3.8 is required for the := operator. This takes a list of strings as input, and outputs a list of (tag, body) tuples. First, it uses a Regex match to get the tag:
t:=re.match(r'\[(\w*)\]',s)or t)

This matches any initial sequence of word characters (alphanumeric + underscore) enclosed in square brackets, with the words as a capturing group. If the string does match this regex, t will be a match object with two elements: the full match and the group. For example, if the string is [tag] body, the match will have the elements [tag] and tag.
If the string does not match this regex, then re.match() returns None. The code becomes t = None or t, which is just t = t, so the tag keeps its value from the previous line. If the first line didn't have a match, this would cause an error, but we don't need to worry about that!
The code then constructs the tuple t[1], s.split(t[0])[-1].strip(), where t[1] is the capturing group (the tag without square brackets) and t[0] is the tag with square brackets. Splitting the string on the full tag isolates the body, whether or not the tag actually exists in the string.