iOfficeAI/OfficeCLI · error · OfficeCliError

-1

-1

Error message

resident is running but the command could not be delivered (pipe busy or unresponsive); retry, or close and reopen [{e}]

What it means

Raised by the Python SDK (code -1) when sending a command to the resident pipe fails with OSError on every attempt up to max_retries+1. The resident is running (the connect succeeded) but the pipe is busy or unresponsive during delivery. The SDK retries with 50*(n+1)ms backoff before giving up; liveness probes (_serves) pass max_retries=0 so a stale pipe fails fast instead of sleeping.

Source

Thrown at sdk/python/officecli.py:224

    """Forward one request, mirroring officecli's TrySend: bounded connect + a few
    retries with backoff, then a blocking read. A retry only re-attempts the
    connect (before the command runs), so it never double-applies a mutation. If
    the command still can't be delivered, raise a busy/unresponsive error — never
    fall back to touching the file directly (that would race the resident).

    `max_retries` overrides the busy-retry count. Liveness probes (_serves) pass 0
    so a missing/stale pipe fails FAST instead of sleeping through ~0.3s of backoff
    — retrying a probe the resident isn't answering can't make it answer; the
    busy-retry policy is for delivering a real command to a slow-but-live pipe."""
    line = (json.dumps(req, ensure_ascii=False) + "\n").encode("utf-8")
    send = _send_win if _IS_WIN else _send_unix
    for attempt in range(max_retries + 1):
        try:
            raw = send(sock_path, line, connect_timeout)
            break
        except OSError as e:
            if attempt >= max_retries:
                raise OfficeCliError(-1,
                    f"resident is running but the command could not be delivered "
                    f"(pipe busy or unresponsive); retry, or close and reopen [{e}]")
            time.sleep(0.05 * (attempt + 1))    # = TrySend's 50*(n+1)ms backoff
    # utf-8-sig: the resident's StreamWriter (Encoding.UTF8) prepends a BOM the
    # C# StreamReader strips; we must too, or json.loads chokes on the leading .
    text = raw.decode("utf-8-sig")
    if not text.strip():
        # Empty/closed reply: the resident accepted the connection but closed
        # without a complete response (e.g. crashed mid-serve). We refuse to
        # re-send — the command may already have been APPLIED before the resident
        # died, so re-sending would double-apply a non-idempotent op — and raise
        # instead. officecli's TrySend now matches: its retry covers only the
        # connect phase (before the command is written); on an empty reply after a
        # successful write it returns null without re-sending, the C# equivalent of
        # this raise. _cmd's recovery then restarts a dead resident and retries once
        # (a fresh connect, before re-send), and _serves()/alive() (which swallow
        # OfficeCliError) read an empty reply as "not alive".
        raise OfficeCliError(-1,

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Retry the operation at the caller level with backoff — the failure is often transient.
  2. Reduce concurrency against the single resident, or batch writes to lower round-trips.
  3. Close and reopen the Document (restarts the resident) if it stays unresponsive.

Example fix

# before
doc.set('/Sheet1/A1', {'text': 'x'})
# after — caller retry with backoff
import time
for attempt in range(5):
    try:
        doc.set('/Sheet1/A1', {'text': 'x'}); break
    except officecli.OfficeCliError as e:
        if attempt == 4: raise
        time.sleep(0.1 * (attempt + 1))
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

import time
import officecli

def send_resilient(doc, fn, *args, retries=4, **kw):
    last = None
    for attempt in range(retries + 1):
        try:
            return fn(*args, **kw)
        except officecli.OfficeCliError as e:
            last = e
            if 'could not be delivered' not in str(e) or attempt == retries:
                raise
            time.sleep(0.1 * (attempt + 1))
    raise last

Prevention

When it happens

Trigger: High pipe contention from many concurrent SDK clients; a slow/hung resident that accepts connections but cannot drain commands; transient OS pipe errors under load. Surfaces after all configured retries are exhausted.

Common situations: Many threads/processes sharing one resident; a large batch blocking the resident so single commands time out on delivery; antivirus or sandbox interfering with the named pipe/socket.

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/f18b08e50bed1584. Report an issue: GitHub.