chenhg5/cc-connect · error

codex app-server resume returned empty thread id

Error message

codex app-server resume returned empty thread id

What it means

This error is returned when resuming an existing codex thread: the "thread/resume" JSON-RPC request succeeded but its response contained an empty thread id. The library treats a resume without an id as invalid because every subsequent turn must reference the thread id. It guards against protocol drift where the response shape changed (e.g. id moved to a different field).

Source

Thrown at agent/codex/appserver_session.go:335

	}
	if err := s.notify("initialized", nil); err != nil {
		return fmt.Errorf("codex app-server initialized notify: %w", err)
	}
	return nil
}

func (s *appServerSession) ensureThread(resumeID string) error {
	if resumeID != "" && resumeID != core.ContinueSession {
		params := s.threadRequestParams()
		params["threadId"] = resumeID
		params["persistExtendedHistory"] = true

		var resp threadResumeResponse
		if err := s.request("thread/resume", params, &resp); err != nil {
			return err
		}
		if resp.Thread.ID == "" {
			return fmt.Errorf("codex app-server resume returned empty thread id")
		}
		s.applyThreadRuntimeState(resp.Cwd, resp.Model, resp.ReasoningEffort)
		s.threadID.Store(resp.Thread.ID)
		slog.Info("codex app-server thread resumed", "thread_id", resp.Thread.ID)
		return nil
	}

	var resp threadStartResponse
	if err := s.request("thread/start", s.threadRequestParams(), &resp); err != nil {
		return err
	}
	if resp.Thread.ID == "" {
		return fmt.Errorf("codex app-server start returned empty thread id")
	}
	s.applyThreadRuntimeState(resp.Cwd, resp.Model, resp.ReasoningEffort)
	s.threadID.Store(resp.Thread.ID)
	slog.Info("codex app-server thread started", "thread_id", resp.Thread.ID)
	return nil

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Compare the actual thread/resume JSON against the threadResumeResponse struct; update the struct to the current codex protocol.
  2. Log the raw response body on this error path to see the real shape.
  3. Start a new thread (drop the stale resume id) if the server no longer knows it.
  4. Pin/downgrade the codex CLI to a version matching the expected response schema.
  5. Verify the resumeID being passed is a valid id previously returned by thread/start, not a user-supplied session label.

Example fix

// before
type threadResumeResponse struct {
    Thread struct{ ID string `json:"id"` } `json:"thread"`
}
// after — after inspecting the raw server response for schema drift:
type threadResumeResponse struct {
    Thread struct {
        ID string `json:"id"`
    } `json:"thread"`
    // fallback for servers that return the id at top level
    ThreadID string `json:"threadId"`
}
func (r threadResumeResponse) id() string {
    if r.Thread.ID != "" { return r.Thread.ID }
    return r.ThreadID
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Before resuming, verify the id is a thread id previously returned by the app-server
if resumeID == "" || !isValidThreadID(resumeID) {
    return errors.New("invalid or missing thread id for resume; start a new thread")
}

Type guard

func hasValidThread(resp threadResumeResponse) bool {
    return resp.Thread.ID != ""
}

if !hasValidThread(resp) {
    slog.Warn("thread/resume returned no id; starting fresh thread")
    return s.ensureThread("")
}

Try / catch

if err := session.Send(ctx, prompt); err != nil {
    if strings.Contains(err.Error(), "resume returned empty thread id") {
        // schema drift or stale id: fall back to a new thread
        session, err = agent.StartSession(ctx, core.StartOptions{})
    }
    return err
}

Prevention

When it happens

Trigger: Calling Send/SendWithResume on a codex session with a non-empty resumeID; the app-server answers thread/resume but resp.Thread.ID is "" — either the server returned a malformed/degraded response or the client's threadResumeResponse struct no longer matches the server's JSON shape.

Common situations: Codex CLI upgrade changed the resume response schema (thread id relocated/renamed); resuming a thread id that the server silently cannot resolve and returns an empty object for; corrupted session store on the app-server side.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/8341290ab6c997fb. Report an issue: GitHub.