Skip to content

Background tasks marked "lost" without liveness check; resume spawns duplicate concurrent workers #1924

Open

Description

@whyihaveyou

Summary

When a kimi-code process with live background tasks is still running and a second process resumes the same session, markLoadedTasksLost marks the first process's non-terminal tasks as lost — without any liveness probe, even though the task's PID is persisted on disk. The resume_hint returned to the model then explicitly instructs Agent(resume=agent_id) on task.lost, spawning a second concurrent worker on the same task while the first is still alive.

We hit this in production: two subagent instances ran concurrently for ~65 minutes, each editing the same file, each reporting "another session is modifying this file" — they were seeing each other. Both eventually completed; the task record had marked one of them lost while it was demonstrably alive.

Evidence (all paths verified against current master)

  1. packages/agent-core-v2/src/agent/task/taskService.ts:854-869 — markLoadedTasksLost iterates ghost tasks and marks every non-terminal one as lost and writes it back to disk. There is no process.kill(pid, 0)-style probe anywhere in this path.
  2. packages/agent-core-v2/src/agent/task/persist.ts:236,276 — the persisted task record already contains pid (LegacyPersistedTask.pid, mapped in toProcessTaskInfo), so the material for a liveness check is on disk, unused.
  3. packages/agent-core-v2/src/session/subagent/tools/agent.ts:481 — resume_hint lists task.lost as a recovery case and tells the model to call Agent(resume=agent_id), which makes duplicate workers a scripted outcome rather than an accident.
  4. There is no per-session cross-process lock, so two processes resuming the same session (e.g. a supervisor that spawns one CLI process per message with --resume) race: the live one's tasks get marked lost, and the task JSON itself is last-writer-wins between the two processes.

This is the same "killed but not reaped" family as #1882 (torn wire.jsonl after SIGKILL), one layer up in the stack.

Suggested fixes (in order of effort)

  1. Probe before marking lost (small): in markLoadedTasksLost, for kind === 'process' ghosts, run process.kill(pid, 0). If the process is alive, keep the task running and skip the lost notification. A ready-made probe pattern exists in apps/kap-server/src/lock.ts (PID + liveness check for instance locks).
  2. Fix the guidance (trivial): change resume_hint to state that lost does not mean the worker is dead, and that the caller should verify the old instance is gone before resuming.
  3. Per-session cross-process lock (design-level): owner PID + heartbeat, reusing the kap-server/src/lock.ts pattern. reconcile() skips lost-marking while the lock owner is alive. This also covers agent-kind tasks, which have no PID to probe (same-process loops).

We'd be happy to contribute (1) as a PR if the direction sounds right.

Environment

  • kimi-code 0.27.0, agent-core-v2
  • Linux, supervisor spawning one CLI process per message with --resume (cc-connect)

Activity

  1. added a commit that references this issue on Aug 20, 2026
    5409dfa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions