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;
|
pane?: TmuxPaneRuntimeInfo;
|
||||||
processRows: readonly RuntimeProcessTableRow[];
|
processRows: readonly RuntimeProcessTableRow[];
|
||||||
processTableAvailable: boolean;
|
processTableAvailable: boolean;
|
||||||
|
targetedProcess?: {
|
||||||
|
pid: number;
|
||||||
|
command: string;
|
||||||
|
};
|
||||||
nowIso: string;
|
nowIso: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,12 +169,24 @@ function collectDescendants(
|
||||||
function isVerifiedRuntimeProcess(params: {
|
function isVerifiedRuntimeProcess(params: {
|
||||||
row: RuntimeProcessTableRow;
|
row: RuntimeProcessTableRow;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
memberName?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
allowAgentNameFallback?: boolean;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return (
|
if (!commandArgEquals(params.row.command, '--team-name', params.teamName)) {
|
||||||
commandArgEquals(params.row.command, '--team-name', params.teamName) &&
|
return false;
|
||||||
commandArgEquals(params.row.command, '--agent-id', params.agentId)
|
}
|
||||||
);
|
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 {
|
function isOpenCodeRuntimeProcess(command: string | undefined): boolean {
|
||||||
|
|
@ -253,7 +269,13 @@ export function resolveTeamMemberRuntimeLiveness(
|
||||||
|
|
||||||
const verifiedProcess = input.processRows
|
const verifiedProcess = input.processRows
|
||||||
.filter((row) =>
|
.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];
|
.sort((left, right) => right.pid - left.pid)[0];
|
||||||
if (verifiedProcess) {
|
if (verifiedProcess) {
|
||||||
|
|
@ -274,6 +296,35 @@ export function resolveTeamMemberRuntimeLiveness(
|
||||||
typeof runtimePid === 'number' && runtimePid > 0
|
typeof runtimePid === 'number' && runtimePid > 0
|
||||||
? input.processRows.find((row) => row.pid === runtimePid)
|
? input.processRows.find((row) => row.pid === runtimePid)
|
||||||
: undefined;
|
: 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') {
|
if (runtimePidRow && input.providerId === 'opencode') {
|
||||||
const processCommand = sanitizeProcessCommandForDiagnostics(runtimePidRow.command);
|
const processCommand = sanitizeProcessCommandForDiagnostics(runtimePidRow.command);
|
||||||
if (isOpenCodeRuntimeProcess(runtimePidRow.command)) {
|
if (isOpenCodeRuntimeProcess(runtimePidRow.command)) {
|
||||||
|
|
@ -351,7 +402,14 @@ export function resolveTeamMemberRuntimeLiveness(
|
||||||
const descendants = collectDescendants(input.processRows, pane.panePid);
|
const descendants = collectDescendants(input.processRows, pane.panePid);
|
||||||
const verifiedDescendant = descendants
|
const verifiedDescendant = descendants
|
||||||
.filter((row) =>
|
.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];
|
.sort((left, right) => right.pid - left.pid)[0];
|
||||||
if (verifiedDescendant) {
|
if (verifiedDescendant) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resolveTeamMemberRuntimeLiveness,
|
resolveTeamMemberRuntimeLiveness,
|
||||||
sanitizeProcessCommandForDiagnostics,
|
sanitizeProcessCommandForDiagnostics,
|
||||||
} from '@main/services/team/TeamRuntimeLivenessResolver';
|
} from '@main/services/team/TeamRuntimeLivenessResolver';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const NOW = '2026-04-24T12:00:00.000Z';
|
const NOW = '2026-04-24T12:00:00.000Z';
|
||||||
|
|
||||||
|
|
@ -50,6 +49,120 @@ describe('resolveTeamMemberRuntimeLiveness', () => {
|
||||||
expect(result.pid).toBe(222);
|
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', () => {
|
it('keeps a verified process pid visible after bootstrap is confirmed', () => {
|
||||||
const result = resolveTeamMemberRuntimeLiveness({
|
const result = resolveTeamMemberRuntimeLiveness({
|
||||||
teamName: 'demo',
|
teamName: 'demo',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue