Skip to content

fix(agent-core): probe pid liveness before marking restored tasks lost#2190

Open
Nas01010101 wants to merge 1 commit into
MoonshotAI:mainfrom
Nas01010101:fix/1924-task-liveness-probe
Open

fix(agent-core): probe pid liveness before marking restored tasks lost#2190
Nas01010101 wants to merge 1 commit into
MoonshotAI:mainfrom
Nas01010101:fix/1924-task-liveness-probe

Conversation

@Nas01010101

Copy link
Copy Markdown

Related Issue

Resolve #1924

Problem

See linked issue.

In short: when a session is restored, markLoadedTasksLost reclassifies every non-terminal ghost task as lost without checking whether the task's process is still running. If a second process resumes a session whose tasks are still alive, the first process's tasks are marked lost — and the resume_hint lists task.lost as a recovery case that the model may resume, so a duplicate worker gets started alongside the one that is still running.

The material for the check is already on disk: ProcessTaskInfo carries pid, and persist.ts round-trips it.

What changed

  • Added packages/agent-core-v2/src/agent/task/pidAlive.ts: a process.kill(pid, 0) probe. ESRCH means dead; EPERM means the process exists but belongs to another user, so it counts as alive; any other errno assumes alive so a live task is never clobbered. This mirrors the probe already used by the server-side instance registry (packages/kap-server/src/instanceRegistry.ts).
  • markLoadedTasksLost now skips ghosts of kind process whose pid is still alive. Everything else is unchanged: terminal statuses are still skipped, dead process tasks are still marked lost, and non-process kinds (which have no pid to probe) keep their previous behaviour.

Scope note: this is the small, self-contained half of the problem. It does not add a per-session cross-process lock, and agent-kind tasks still have no pid to probe — those were raised as larger design options in the issue and are deliberately left out of this PR.

Testing

  • New test/agent/task/pidAlive.test.ts covers the probe directly: current pid alive, non-positive/non-integer pids rejected, ESRCH dead, EPERM alive, unknown errno alive, and that it probes with signal 0 so the target is never disturbed.
  • test/agent/task/reconcile.test.ts gains two regression tests: a ghost whose process is alive stays running and emits no task.terminated, and with one live and one dead ghost only the dead one is marked lost.
  • The existing reconcile tests previously depended on whichever pids happened to exist on the machine running them (one fixture used pid: 1, which is alive on Linux). They now mock the probe to a dead pid by default, so their expectations are deterministic and unchanged.
  • Verified the tests actually bite: reverting only the skip line in markLoadedTasksLost fails exactly the two new reconcile tests, with the original nine still passing.
  • pnpm test for packages/agent-core-v2/test/agent/task/: 200 passed. pnpm lint, pnpm typecheck and pnpm sherif are clean. Three plugin test files (app/plugin/archive, manager, manager-consumption, 11 tests) fail identically on unmodified main in my environment, so they are pre-existing and unrelated.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset. — patch changeset added for @moonshot-ai/kimi-code, since this internal-package fix changes user-visible CLI behaviour.
  • Ran gen-docs skill, or this PR needs no doc update. — no documented behaviour changes; lost keeps its meaning, it is just no longer applied to a live task.

markLoadedTasksLost reclassified every non-terminal ghost task as lost
without checking whether its process was still running. When a second
process resumed a session whose tasks were still alive, the first
process tasks were marked lost, and the resume hint tells the model a
lost task may be resumed, which starts a duplicate worker alongside the
live one.

Process tasks already persist their pid, so probe it with
process.kill(pid, 0) and leave a live task untouched.

Closes MoonshotAI#1924
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1ccc3d3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ccc3d39ca

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// kimi-code process that is still driving it. Marking it `lost` tells the
// model it may resume the task, which starts a second worker alongside
// the first one.
if (info.kind === 'process' && pidAlive(info.pid)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile skipped ghosts after their owner exits

When a second process restores the session while the original task is alive, this continue leaves the nonterminal task only in ghosts. Production invokes reconcile() once from restoreAfterReplay; afterward getTask() and wait() return the cached ghost without reloading its persisted record. When the owning process later writes completed or failed, the resumed process therefore reports the task as running forever, TaskOutput remains not_ready, and no terminal notification is emitted. Skipped ghosts need an ownership/status refresh mechanism so they converge after the original task terminates.

Useful? React with 👍 / 👎.

export function pidAlive(pid: number): boolean {
if (!Number.isInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify process identity instead of PID existence

For a stale running record left by a crash, the OS may have reused its PID for an unrelated process before the session is restored. kill(pid, 0) then returns success even though the recorded task is dead, causing reconciliation to suppress the lost state and recovery indefinitely. Persist and compare a process-birth or owner-instance identity in addition to the numeric PID.

Useful? React with 👍 / 👎.

@@ -0,0 +1,24 @@
/**
* Liveness probe for persisted task pids.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Start the helper header with its domain identity

The new helper header starts with a free-form label instead of the required `<domain>` domain (Ln) — <role> identity line, so its layer and external responsibility are not documented consistently. Rewrite the first line in the scoped format and consolidate the later function and branch comments into that header.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L11-L15

Useful? React with 👍 / 👎.

Comment on lines +864 to +867
// A ghost whose OS process is still running belongs to another live
// kimi-code process that is still driving it. Marking it `lost` tells the
// model it may resume the task, which starts a second worker alongside
// the first one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move reconciliation rationale into the file header

This newly added explanatory block is beside method statements, while the scoped convention requires comments to live solely in the top-of-file header and forbids comments beside functions, methods, or statements. Fold the externally relevant rationale into the existing module header and leave this control flow self-explanatory.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L11-L15

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant