-
Notifications
You must be signed in to change notification settings - Fork 754
fix(agent-core): probe pid liveness before marking restored tasks lost #2190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Probe pid liveness before marking restored background tasks as lost. Resuming a session whose tasks were still running in another process no longer reports those tasks as `lost`, which previously invited the model to resume them and start a duplicate worker alongside the live one. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /** | ||
| * Liveness probe for persisted task pids. | ||
| * | ||
| * Task records keep the OS pid of the process they started, so a ghost task | ||
| * restored from disk can be checked against the running system before it is | ||
| * reclassified. Mirrors the probe the server-side instance registry uses | ||
| * (`packages/kap-server/src/instanceRegistry.ts`). | ||
| */ | ||
|
|
||
| /** `process.kill(pid, 0)` probe — true if the pid exists, false on ESRCH. */ | ||
| export function pidAlive(pid: number): boolean { | ||
| if (!Number.isInteger(pid) || pid <= 0) return false; | ||
| try { | ||
| process.kill(pid, 0); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a stale Useful? React with 👍 / 👎. |
||
| return true; | ||
| } catch (error) { | ||
| const code = (error as NodeJS.ErrnoException).code; | ||
| if (code === 'ESRCH') return false; | ||
| // EPERM = process exists but we can't signal it (different user). Treat as alive. | ||
| if (code === 'EPERM') return true; | ||
| // Anything else: be safe, assume alive so we don't clobber a live task. | ||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,7 @@ import { | |
| import { resolveAgentTaskConfig } from './configSection'; | ||
| import { AgentTaskPersistence } from './persist'; | ||
| import { TaskModel, taskStarted, taskTerminated } from './taskOps'; | ||
| import { pidAlive } from '#/agent/task/pidAlive'; | ||
| import { formatTaskList } from '#/agent/task/tools/task-list'; | ||
| import '#/agent/task/tools/task-output'; | ||
| import '#/agent/task/tools/task-stop'; | ||
|
|
@@ -860,6 +861,11 @@ export class AgentTaskService extends Disposable implements IAgentTaskService { | |
| const persistence = this.persistence; | ||
| for (const [taskId, info] of this.ghosts) { | ||
| if (TERMINAL_STATUSES.has(info.status)) continue; | ||
| // 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. | ||
|
Comment on lines
+864
to
+867
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎. |
||
| if (info.kind === 'process' && pidAlive(info.pid)) continue; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a second process restores the session while the original task is alive, this Useful? React with 👍 / 👎. |
||
| const updated: AgentTaskInfo = { | ||
| ...info, | ||
| status: 'lost', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /** | ||
| * Liveness-probe unit tests. | ||
| */ | ||
|
|
||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { pidAlive } from '#/agent/task/pidAlive'; | ||
|
|
||
| function killError(code: string): NodeJS.ErrnoException { | ||
| const err = new Error(code) as NodeJS.ErrnoException; | ||
| err.code = code; | ||
| return err; | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| describe('pidAlive', () => { | ||
| it('reports the current process as alive', () => { | ||
| expect(pidAlive(process.pid)).toBe(true); | ||
| }); | ||
|
|
||
| it('rejects pids that cannot identify a process', () => { | ||
| expect(pidAlive(0)).toBe(false); | ||
| expect(pidAlive(-1)).toBe(false); | ||
| expect(pidAlive(1.5)).toBe(false); | ||
| expect(pidAlive(Number.NaN)).toBe(false); | ||
| }); | ||
|
|
||
| it('treats ESRCH as dead', () => { | ||
| vi.spyOn(process, 'kill').mockImplementation(() => { | ||
| throw killError('ESRCH'); | ||
| }); | ||
|
|
||
| expect(pidAlive(4242)).toBe(false); | ||
| }); | ||
|
|
||
| it('treats EPERM as alive — the process exists but belongs to another user', () => { | ||
| vi.spyOn(process, 'kill').mockImplementation(() => { | ||
| throw killError('EPERM'); | ||
| }); | ||
|
|
||
| expect(pidAlive(4242)).toBe(true); | ||
| }); | ||
|
|
||
| it('assumes alive on an unrecognised errno rather than clobbering a live task', () => { | ||
| vi.spyOn(process, 'kill').mockImplementation(() => { | ||
| throw killError('EINVAL'); | ||
| }); | ||
|
|
||
| expect(pidAlive(4242)).toBe(true); | ||
| }); | ||
|
|
||
| it('probes with signal 0 so it never disturbs the target', () => { | ||
| const kill = vi.spyOn(process, 'kill').mockReturnValue(true); | ||
|
|
||
| expect(pidAlive(4242)).toBe(true); | ||
| expect(kill).toHaveBeenCalledWith(4242, 0); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.