I will parse the encoded expression with a stack of frames. Ordinary lowercase letters append literal text. A positive decimal count immediately followed by an opening bracket starts a repeated group, and its matching closing bracket completes that group. Nested groups are allowed. The function will reject malformed input rather than return a partial expansion. Examples such as 3[ab]2[c] and 2[a3[b]] illustrate sequential and nested groups respectively.

The main state is the current list of fragments plus an optional count being read. When an opening bracket arrives, the parser saves the current fragments and the count on the stack, then begins an empty inner frame. When a closing bracket arrives, it joins the inner fragments, repeats that string by the saved count, and appends the result to the saved outer fragments. This is exactly the nesting structure of the expression, so the stack naturally pairs each close bracket with the latest unmatched open bracket.

Using fragments avoids repeatedly concatenating a growing string for every literal letter. The join at the end of a frame creates that frame's content once. Expansion itself necessarily allocates the output, so there is no promise that a very short encoding produces a small result. For this exercise the decoded result is assumed to fit in memory. Adding an output-size budget would be a separate interface decision; I will keep the requested return type as one complete string.

Counts need a little care. A count of zero is invalid under the positive-count grammar. A sequence of digits at the end is incomplete because it has no group. Digits followed by a letter are likewise invalid, since the grammar does not allow numeric literals as decoded text. Leading zeroes are accepted if the resulting count is positive: 03[a] means the same as 3[a]. That choice follows a decimal-count grammar without an additional canonical-format restriction.

An empty repeated body is valid. For example, 2[] expands to an empty string. Rejecting it would not make the parser safer or simpler; it would merely introduce another grammar condition. Empty overall input also returns an empty string. A bare bracket cannot appear as literal data because the grammar reserves brackets for structure. Characters outside lowercase ASCII letters and decimal ASCII digits are rejected, which keeps the accepted language aligned with the stated input format.

I should avoid str.isdigit here. It accepts more Unicode digit characters than the simple decimal syntax promises, and some of those characters do not convert through int in the way a caller might expect. Testing membership between "0" and "9" is both explicit and consistent with accumulating the count one character at a time. The same argument applies to literal letters: the grammar says lowercase a through z, so other alphabetic characters do not become accidental extensions.

The final scan must check both unfinished counts and unclosed frames. A nonempty stack means some opening bracket never received its close. A remaining count means trailing digits were read but never used. Checking only the stack would accept an encoding such as ab12 incorrectly. The function below makes those end conditions separate so that its failure modes follow directly from the parser state.

The useful invariant is that pieces represents the decoded content already completed at the current nesting depth. Each stored frame represents a suspended outer depth and its repetition count. Reading a literal extends only the current content. Reading digits changes the pending count but produces no decoded text. Opening a group suspends a valid outer prefix. Closing a group converts a complete inner prefix into one complete outer fragment, preserving the meaning of both depths. This gives an inductive argument over the scan.

For 2[a3[b]], the first opening bracket saves the empty outer list with count two. The letter a becomes the first fragment inside that frame. The next opening bracket saves the list containing a with count three. Closing the inner frame appends bbb to the list containing a; closing the outer frame repeats abbb twice. The result is abbbabbb. At no point is the outer repetition applied prematurely to the inner count or to text preceding the group.

The error checks also preserve the invariant rather than trying to repair malformed input. If a closing bracket arrives while a count is pending, the count has no associated opening bracket. If it arrives with an empty stack, there is no suspended outer frame to resume. If a letter follows digits, the digits cannot be interpreted as either a literal or a complete group under this grammar. Rejecting at those positions avoids returning a plausible but unsupported interpretation.

For complexity, the input scan is linear apart from string construction. Work depends on the decoded material built at each nesting level; a conservative bound is O(n + dL), where n is encoded length, d is maximum nesting depth, and L is final output length. The stack itself uses O(d) frames, while fragments and constructed strings require storage related to the output. Claiming unconditional O(n) time would be wrong because the output may be exponentially larger than the encoding. With the stated memory premise, the function above completes and returns the exact expansion.

```python
def decode_groups(encoded):
    """Expand positive-count bracket groups over lowercase ASCII letters."""
    frames = []
    pieces = []
    count = None
    for character in encoded:
        if "0" <= character <= "9":
            # None distinguishes an absent count from a count whose value
            # is currently zero. The latter can still become positive as
            # more digits arrive, as in 004[a]. Accumulation preserves the
            # usual left-to-right decimal interpretation without slicing.
            count = (0 if count is None else count) * 10 + int(character)
        elif character == "[":
            if count is None or count <= 0:
                raise ValueError("an opening bracket needs a positive count")
            # Saving the outer fragment list preserves everything before
            # this group without copying it. A fresh list isolates the
            # group's contents, so nested completions cannot append into
            # an outer frame before the matching close bracket appears.
            frames.append((pieces, count))
            pieces = []
            count = None
        elif character == "]":
            if count is not None or not frames:
                raise ValueError("unexpected closing bracket")
            inner = "".join(pieces)
            pieces, repetitions = frames.pop()
            # Only this frame's completed content is repeated. Earlier
            # outer text remains outside the multiplication, which is the
            # key distinction between a2[b] and 2[ab]. An empty inner string
            # contributes an empty fragment and remains grammatically valid.
            pieces.append(inner * repetitions)
        elif "a" <= character <= "z":
            if count is not None:
                raise ValueError("a count must be followed by an opening bracket")
            pieces.append(character)
        else:
            raise ValueError("unsupported character")
    if frames:
        raise ValueError("unclosed group")
    if count is not None:
        raise ValueError("unused trailing count")
    return "".join(pieces)


if __name__ == "__main__":
    for expression in ("3[ab]2[c]", "2[a3[b]]", "plain", "2[]", ""):
        print(repr(expression), repr(decode_groups(expression)))
    for malformed in ("[ab]", "0[a]", "2[ab", "ab12"):
        try:
            decode_groups(malformed)
        except ValueError:
            print(repr(malformed), "rejected")
```
