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:
parent
90fe3c9107
commit
1f6c9fe34b
2 changed files with 179 additions and 8 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue