feat(team): add targeted runtime pid liveness check

resolveTeamMemberRuntimeLiveness принимает опциональный targetedProcess
с pid + command — если строка процесса проходит team/agent verification,
liveness отмечается как 'runtime_process' даже когда полная process table
не нашла его (например при гонке snapshot vs spawn). Дополнительно для
direct-process backend разрешён fallback по --agent-name, когда команда
запущена без --agent-id.
This commit is contained in:
777genius 2026-05-28 23:55:51 +03:00
parent 90fe3c9107
commit 1f6c9fe34b
2 changed files with 179 additions and 8 deletions

View file

@ -23,6 +23,10 @@ export interface ResolveTeamMemberRuntimeLivenessInput {
pane?: TmuxPaneRuntimeInfo;
processRows: readonly RuntimeProcessTableRow[];
processTableAvailable: boolean;
targetedProcess?: {
pid: number;
command: string;
};
nowIso: string;
}
@ -165,12 +169,24 @@ function collectDescendants(
function isVerifiedRuntimeProcess(params: {
row: RuntimeProcessTableRow;
teamName: string;
memberName?: string;
agentId?: string;
allowAgentNameFallback?: boolean;
}): boolean {
return (
commandArgEquals(params.row.command, '--team-name', params.teamName) &&
commandArgEquals(params.row.command, '--agent-id', params.agentId)
);
if (!commandArgEquals(params.row.command, '--team-name', params.teamName)) {
return false;
}
if (commandArgEquals(params.row.command, '--agent-id', params.agentId)) {
return true;
}
if (!params.allowAgentNameFallback) {
return false;
}
const expectedAgentId = params.agentId?.trim();
if (expectedAgentId && extractCliArgValues(params.row.command, '--agent-id').length > 0) {
return false;
}
return commandArgEquals(params.row.command, '--agent-name', params.memberName);
}
function isOpenCodeRuntimeProcess(command: string | undefined): boolean {
@ -253,7 +269,13 @@ export function resolveTeamMemberRuntimeLiveness(
const verifiedProcess = input.processRows
.filter((row) =>
isVerifiedRuntimeProcess({ row, teamName: input.teamName, agentId: input.agentId })
isVerifiedRuntimeProcess({
row,
teamName: input.teamName,
memberName: input.memberName,
agentId: input.agentId,
allowAgentNameFallback: input.providerId !== 'opencode' && input.backendType === 'process',
})
)
.sort((left, right) => right.pid - left.pid)[0];
if (verifiedProcess) {
@ -274,6 +296,35 @@ export function resolveTeamMemberRuntimeLiveness(
typeof runtimePid === 'number' && runtimePid > 0
? input.processRows.find((row) => row.pid === runtimePid)
: undefined;
const targetedProcess =
typeof runtimePid === 'number' &&
runtimePid > 0 &&
input.targetedProcess?.pid === runtimePid &&
isVerifiedRuntimeProcess({
row: {
pid: input.targetedProcess.pid,
ppid: 0,
command: input.targetedProcess.command,
},
teamName: input.teamName,
memberName: input.memberName,
agentId: input.agentId,
allowAgentNameFallback: input.providerId !== 'opencode' && input.backendType === 'process',
})
? input.targetedProcess
: undefined;
if (targetedProcess) {
return result({
alive: true,
livenessKind: 'runtime_process',
pidSource: 'agent_process_table',
pid: targetedProcess.pid,
runtimeSessionId,
processCommand: sanitizeProcessCommandForDiagnostics(targetedProcess.command),
runtimeDiagnostic: 'verified runtime process detected by targeted pid check',
diagnostics: [...diagnostics, 'matched targeted process by pid and team/member identity'],
});
}
if (runtimePidRow && input.providerId === 'opencode') {
const processCommand = sanitizeProcessCommandForDiagnostics(runtimePidRow.command);
if (isOpenCodeRuntimeProcess(runtimePidRow.command)) {
@ -351,7 +402,14 @@ export function resolveTeamMemberRuntimeLiveness(
const descendants = collectDescendants(input.processRows, pane.panePid);
const verifiedDescendant = descendants
.filter((row) =>
isVerifiedRuntimeProcess({ row, teamName: input.teamName, agentId: input.agentId })
isVerifiedRuntimeProcess({
row,
teamName: input.teamName,
memberName: input.memberName,
agentId: input.agentId,
allowAgentNameFallback:
input.providerId !== 'opencode' && input.backendType === 'process',
})
)
.sort((left, right) => right.pid - left.pid)[0];
if (verifiedDescendant) {

View file

@ -1,9 +1,8 @@
import { describe, expect, it } from 'vitest';
import {
resolveTeamMemberRuntimeLiveness,
sanitizeProcessCommandForDiagnostics,
} from '@main/services/team/TeamRuntimeLivenessResolver';
import { describe, expect, it } from 'vitest';
const NOW = '2026-04-24T12:00:00.000Z';
@ -50,6 +49,120 @@ describe('resolveTeamMemberRuntimeLiveness', () => {
expect(result.pid).toBe(222);
});
it('promotes a verified team and agent-name process when agent id metadata is missing', () => {
const result = resolveTeamMemberRuntimeLiveness({
teamName: 'demo',
memberName: 'alice',
backendType: 'process',
persistedRuntimePid: 222,
processRows: [
{
pid: 222,
ppid: 1,
command: 'node runtime --team-name demo --agent-name alice',
},
],
processTableAvailable: true,
nowIso: NOW,
});
expect(result.alive).toBe(true);
expect(result.livenessKind).toBe('runtime_process');
expect(result.pidSource).toBe('agent_process_table');
expect(result.pid).toBe(222);
});
it('does not let matching agent name override a mismatched command agent id', () => {
const result = resolveTeamMemberRuntimeLiveness({
teamName: 'demo',
memberName: 'alice',
agentId: 'alice@demo',
backendType: 'process',
persistedRuntimePid: 222,
processRows: [
{
pid: 222,
ppid: 1,
command: 'node runtime --team-name demo --agent-id other@demo --agent-name alice',
},
],
processTableAvailable: true,
nowIso: NOW,
});
expect(result.alive).toBe(false);
expect(result.livenessKind).toBe('registered_only');
});
it('does not use agent-name fallback for OpenCode runtime rows', () => {
const result = resolveTeamMemberRuntimeLiveness({
teamName: 'demo',
memberName: 'alice',
providerId: 'opencode',
backendType: 'process',
persistedRuntimePid: 222,
processRows: [
{
pid: 222,
ppid: 1,
command: 'opencode runtime --team-name demo --agent-name alice',
},
],
processTableAvailable: true,
nowIso: NOW,
});
expect(result.alive).toBe(false);
expect(result.livenessKind).toBe('runtime_process_candidate');
expect(result.pidSource).toBe('opencode_bridge');
});
it('uses targeted pid verification when the full process table missed a live direct process', () => {
const result = resolveTeamMemberRuntimeLiveness({
teamName: 'demo',
memberName: 'alice',
agentId: 'alice@demo',
backendType: 'process',
persistedRuntimePid: 222,
processRows: [],
processTableAvailable: true,
targetedProcess: {
pid: 222,
command: 'node runtime --agent-id alice@demo --agent-name alice --team-name demo',
},
nowIso: NOW,
});
expect(result.alive).toBe(true);
expect(result.livenessKind).toBe('runtime_process');
expect(result.pidSource).toBe('agent_process_table');
expect(result.pid).toBe(222);
expect(result.runtimeDiagnostic).toBe(
'verified runtime process detected by targeted pid check'
);
});
it('does not trust targeted pid verification with mismatched team identity', () => {
const result = resolveTeamMemberRuntimeLiveness({
teamName: 'demo',
memberName: 'alice',
agentId: 'alice@demo',
backendType: 'process',
persistedRuntimePid: 222,
processRows: [],
processTableAvailable: true,
targetedProcess: {
pid: 222,
command: 'node runtime --agent-id alice@other --agent-name alice --team-name other',
},
nowIso: NOW,
});
expect(result.alive).toBe(false);
expect(result.livenessKind).toBe('stale_metadata');
expect(result.pidSource).toBe('persisted_metadata');
});
it('keeps a verified process pid visible after bootstrap is confirmed', () => {
const result = resolveTeamMemberRuntimeLiveness({
teamName: 'demo',