Skip to content

[Bug]: Pi provider's resolveMissingConversationSession destroys leafEntryId/sessionFile on missing-session false alarm (same class as #980, not covered by #981) #1043

Description

@licongy

Before submitting

  • I searched existing issues and did not find the same problem.
  • I have read the relevant documentation (e.g., the README) but could not resolve this issue.

Problem

The Pi provider's resolveMissingConversationSession has the same reference-integrity
defect that issue #980 / PR #981 fixed for the Claude provider — but #981 did not
cover the Pi path. When Claudian's session-availability scan fires a
provider-session-missing false alarm for a Pi-backed conversation, the reconciler
nulls the top-level sessionId and, because Pi conversations historically persist
providerState without a redundant providerState.sessionId, the cleanup then drops
leafEntryId as well. The conversation's link to its Pi transcript jsonl is destroyed
and it becomes unresumable (vanishes from the history list / opens blank).

This is not a new regression: a backup of .claudian/sessions/ taken before any
manual repair showed all 16 conversations across multiple days (Jul 28 – Aug 4)
with sessionId: null and no providerState binding — the destructive write had
already run and persisted, silently, at some earlier point.

The Pi transcript jsonl files themselves are never deleted, so the data is recoverable
in principle, but the pointer Claudian uses to open them is gone.

Steps to reproduce

This is a silent, opportunistic bug triggered by the Pi provider's
missing-session reconciliation, so an exact deterministic trigger is harder than
#980's (which was an env-var change). What is confirmed:

  1. Use Claudian with the Pi provider (pi:... models) for several sessions
    over multiple days. Let at least one session sit unopened for a while.
  2. At some point Claudian's session-availability scan reports a Pi session as
    "missing" (observed after a plugin upgrade / restart; the scan path calls
    handleMissingProviderSessionresolveMissingConversationSession).
  3. Inspect .claudian/sessions/conv-*.meta.json — the affected conversations now
    have "sessionId": null and their providerState (the object holding
    sessionFile / leafEntryId) is missing entirely, not merely reset.
  4. Open the affected conversation from the history list → it opens blank /
    cannot resume. Restarting does not self-heal.

Observed state across 16 conversations (all Pi provider) after the bug fired:

top_sid=n ps_sid=n sf=n leaf=n   ← every non-fork conversation
top_sid=n ps_sid=n sf=n leaf=n fork=Y   ← one fork kept only forkSource

Expected behavior

resolveMissingConversationSession for the Pi provider should preserve the
transcript link (leafEntryId / sessionFile) the way #981 made the Claude path
non-destructive — or at minimum should not destroy leafEntryId when it cannot be
re-derived. A "missing" session should be marked for re-init-on-resume, not have
its pointer hard-deleted.

If the top-level sessionId were preserved (or re-derived from the transcript),
the existing conditional !a.sessionFile && !a.sessionId && !e.sessionId would
stay false and leafEntryId would survive. Today, for Pi conversations, neither
field is reliably present, so the guard fails and the cleanup cascades.

Environment

  • Claudian version: 2.0.44
  • Obsidian version: 1.7.2 (Desktop)
  • Operating system: macOS (darwin, arm64)
  • Provider: pi (pi:openrouter/... models)
  • Provider CLI version: @earendil-works/pi-coding-agent 0.83.0
  • Provider CLI installation method: npm global (pi)

Logs and other evidence

Root cause — Pi provider's resolveMissingConversationSession (main.js, v2.0.44):

async resolveMissingConversationSession(e,n,i){
  var l,c,d,u;
  let r=mn(e.providerState),
      o=(d=(c=(l=r.sessionFile)!=null?l:r.sessionId)!=null?c:e.sessionId)!=null?d:null;
  if(!i||!o||i!==o) return "preserve";          // only acts when the missing id matches
  let s={...(u=e.providerState)!=null?u:{}};
  r.sessionFile===o && delete s.sessionFile;
  r.sessionId===o   && delete s.sessionId;
  e.sessionId===o   && (e.sessionId=null);       // nulls the top-level pointer
  let a=mn(s);
  return !a.sessionFile && !a.sessionId && !e.sessionId
    && (delete s.leafEntryId, delete s.parentSession),   // ← drops the transcript position
    e.providerState = Object.keys(s).length>0 ? s : void 0,   // ← providerState removed if now empty
    this.hydratedKeys.delete(e.id),
    "reset";
}

The final guard !a.sessionFile && !a.sessionId && !e.sessionId is the cascade:
once the top-level sessionId is null and providerState.sessionId is absent (the
norm for Pi conversations, which store sessionFile + leafEntryId), the cleanup
also deletes leafEntryId, then sets providerState = undefined. The conversation
can no longer resolve its transcript via resolveSessionIdForConversation, which
reads providerState.sessionFile ?? providerState.sessionId ?? e.sessionId ?? forkSource.sessionId.

This is distinct from #980's fde. The Claude-path function
(t.sessionId=null, delete t.resumeAtMessageId, delete e.providerSessionId, ...)
no longer exists in v2.0.44's bundle — #981 removed/restructured it. The Pi path
above is a separate resolveMissingConversationSession implementation (there are
6 per-provider implementations) and was not touched by #981.

Caller chain: handleMissingProviderSession(convId, missingSessionId)
ConversationHistoryService.resolveMissingConversationSession(conv, vault, i)
→ the Pi implementation above. i is the provider-reported missing session id;
when it matches the conversation's resolved id o, the destructive branch runs.

Evidence of damage (pre-repair backup, all 16 Pi conversations):

// conv-1785854869867-5gvhuij4k.meta.json  (after the bug, before repair)
{
  "id": "conv-1785854869867-5gvhuij4k",
  "providerId": "pi",
  "title": "分析重构设想中新人物设定",
  "sessionId": null,                 // ← nulled
  // providerState absent entirely   // ← leafEntryId/sessionFile/sessionId all gone
}

The matching Pi transcript is intact at
~/.pi/agent/sessions/<vault-key>/<timestamp>_<uuid>.jsonl (header {type:"session",id:<uuid>,cwd:<vault>});
only Claudian's pointer to it was destroyed.

Additional context

Happy to help test a fix against the affected vault.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions