feat: add thread status documents and safe thread reply handling
This commit is contained in:
@@ -2882,6 +2882,8 @@ function appendThreadProgressEventInState(
|
||||
return event;
|
||||
}
|
||||
|
||||
const THREAD_STATUS_FULL_SYNC_INTERVAL_MS = 15 * 60_000;
|
||||
|
||||
function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
const base = cloneInitialState();
|
||||
if (!raw) return syncDerivedState(base);
|
||||
@@ -5962,6 +5964,7 @@ function appendDispatchExecutionResultInState(
|
||||
targetThreadDisplayName?: string;
|
||||
rawThreadReply?: string;
|
||||
masterSummary?: string;
|
||||
failureReason?: string;
|
||||
},
|
||||
) {
|
||||
const execution = state.dispatchExecutions.find(
|
||||
@@ -6047,7 +6050,9 @@ function appendDispatchExecutionResultInState(
|
||||
masterSummary = pushProjectLedgerMessage(state, payload.groupProjectId, {
|
||||
sender: "ops",
|
||||
senderLabel: "主 Agent Relay",
|
||||
body: `${threadTitle} 执行失败,请稍后重试。`,
|
||||
body: payload.failureReason?.trim()
|
||||
? `${threadTitle} 执行失败:${payload.failureReason.trim()}`
|
||||
: `${threadTitle} 执行失败,请稍后重试。`,
|
||||
kind: "text",
|
||||
});
|
||||
}
|
||||
@@ -6321,6 +6326,7 @@ export async function completeMasterAgentTask(payload: {
|
||||
targetThreadDisplayName: task.targetThreadDisplayName,
|
||||
rawThreadReply: payload.rawThreadReply?.trim() || task.replyBody,
|
||||
masterSummary: payload.replyBody?.trim(),
|
||||
failureReason: payload.errorMessage?.trim(),
|
||||
});
|
||||
} else if (!attachmentProjectId && payload.status === "completed" && task.replyBody) {
|
||||
const isDeviceImportUnderstanding =
|
||||
@@ -7133,20 +7139,28 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
if (!matchingProject) {
|
||||
continue;
|
||||
}
|
||||
const previousObservedAt = matchingProject.threadMeta.lastObservedCodexActivityAt;
|
||||
matchingProject.threadMeta.lastObservedCodexActivityAt = latestIsoTimestamp(
|
||||
matchingProject.threadMeta.lastObservedCodexActivityAt,
|
||||
previousObservedAt,
|
||||
candidate.lastActiveAt,
|
||||
) ?? candidate.lastActiveAt;
|
||||
appendThreadProgressEventInState(state, {
|
||||
projectId: matchingProject.id,
|
||||
threadId: matchingProject.threadMeta.threadId,
|
||||
threadDisplayName: matchingProject.threadMeta.threadDisplayName,
|
||||
deviceId: matchingProject.deviceIds[0] ?? payload.deviceId,
|
||||
eventType: "progress_updated",
|
||||
summary: buildHeartbeatProgressSummary(candidate.threadDisplayName),
|
||||
createdAt: candidate.lastActiveAt,
|
||||
sourceTaskId: `heartbeat-${candidate.candidateId}`,
|
||||
});
|
||||
const previousObservedTs = Date.parse(previousObservedAt ?? "1970-01-01T00:00:00.000Z");
|
||||
const nextObservedTs = Date.parse(candidate.lastActiveAt);
|
||||
const hasNewObservedActivity =
|
||||
Number.isFinite(nextObservedTs) &&
|
||||
(!Number.isFinite(previousObservedTs) || nextObservedTs > previousObservedTs);
|
||||
if (hasNewObservedActivity) {
|
||||
appendThreadProgressEventInState(state, {
|
||||
projectId: matchingProject.id,
|
||||
threadId: matchingProject.threadMeta.threadId,
|
||||
threadDisplayName: matchingProject.threadMeta.threadDisplayName,
|
||||
deviceId: matchingProject.deviceIds[0] ?? payload.deviceId,
|
||||
eventType: "progress_updated",
|
||||
summary: buildHeartbeatProgressSummary(candidate.threadDisplayName),
|
||||
createdAt: candidate.lastActiveAt,
|
||||
sourceTaskId: `heartbeat-${candidate.candidateId}`,
|
||||
});
|
||||
}
|
||||
if (shouldQueueProjectUnderstandingSync(matchingProject, candidate.lastActiveAt, state)) {
|
||||
projectUnderstandingSyncRequests.push({
|
||||
projectId: matchingProject.id,
|
||||
@@ -7608,7 +7622,21 @@ function shouldQueueProjectUnderstandingSync(project: Project, observedActivityA
|
||||
(item) => item.projectId === project.id && item.threadId === project.threadMeta.threadId,
|
||||
);
|
||||
if (project.projectUnderstanding && hasThreadStatusDocument) {
|
||||
return false;
|
||||
const lastSyncedTs = Date.parse(
|
||||
project.threadMeta.lastProjectUnderstandingSyncedAt ??
|
||||
project.projectUnderstanding.updatedAt ??
|
||||
"1970-01-01T00:00:00.000Z",
|
||||
);
|
||||
const understandingLooksThin =
|
||||
!project.projectUnderstanding.currentProgress?.trim() ||
|
||||
!project.projectUnderstanding.recommendedNextStep?.trim();
|
||||
if (
|
||||
!understandingLooksThin &&
|
||||
Number.isFinite(lastSyncedTs) &&
|
||||
observedTs - lastSyncedTs < THREAD_STATUS_FULL_SYNC_INTERVAL_MS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !state.masterAgentTasks.some(
|
||||
(task) =>
|
||||
@@ -8571,7 +8599,8 @@ export async function appendProjectMessage(payload: {
|
||||
project.preview = message.body;
|
||||
|
||||
const shouldTrackThreadProgress =
|
||||
payload.sender !== "user" &&
|
||||
payload.sender === "device" &&
|
||||
(payload.kind ?? "text") === "text" &&
|
||||
isDispatchableThreadProject(project) &&
|
||||
Boolean(project.threadMeta.codexThreadRef?.trim());
|
||||
if (shouldTrackThreadProgress) {
|
||||
|
||||
@@ -25,17 +25,66 @@ function trimToDefined(value: string | undefined) {
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function looksLikeThreadEnvironmentDiagnostic(value: string | undefined) {
|
||||
const text = trimToDefined(value);
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const primarySignals = [
|
||||
"当前会话环境从只读改回可写",
|
||||
"当前会话环境只读",
|
||||
"不能直接把当前会话环境",
|
||||
"cwd 我可以在命令里指向",
|
||||
"文件系统:read-only",
|
||||
];
|
||||
const secondarySignals = [
|
||||
"只读权限",
|
||||
"切回可写",
|
||||
"不能继续开发",
|
||||
"不能写文件",
|
||||
"不能提交",
|
||||
];
|
||||
|
||||
const primaryMatchCount = primarySignals.filter((fragment) => text.includes(fragment)).length;
|
||||
const secondaryMatchCount = secondarySignals.filter((fragment) => text.includes(fragment)).length;
|
||||
|
||||
return primaryMatchCount >= 2 || (primaryMatchCount >= 1 && secondaryMatchCount >= 1);
|
||||
}
|
||||
|
||||
function buildThreadEnvironmentErrorMessage() {
|
||||
return "THREAD_ENVIRONMENT_INVALID: 线程返回了内部环境提示,已拦截,请检查线程绑定或工作目录。";
|
||||
}
|
||||
|
||||
export function normalizeRemoteExecutionResult(
|
||||
input: RemoteExecutionResultInput,
|
||||
): NormalizedRemoteExecutionResult {
|
||||
const rawThreadReply = trimToDefined(input.rawThreadReply);
|
||||
const replyBody = trimToDefined(input.replyBody);
|
||||
const errorMessage = trimToDefined(input.errorMessage);
|
||||
const hasEnvironmentDiagnostic =
|
||||
looksLikeThreadEnvironmentDiagnostic(rawThreadReply) ||
|
||||
looksLikeThreadEnvironmentDiagnostic(replyBody);
|
||||
|
||||
if (hasEnvironmentDiagnostic) {
|
||||
return {
|
||||
status: "failed",
|
||||
dispatchExecutionId: trimToDefined(input.dispatchExecutionId),
|
||||
targetProjectId: trimToDefined(input.targetProjectId),
|
||||
targetThreadId: trimToDefined(input.targetThreadId),
|
||||
errorMessage: errorMessage || buildThreadEnvironmentErrorMessage(),
|
||||
requestId: trimToDefined(input.requestId),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: input.status === "failed" ? "failed" : "completed",
|
||||
dispatchExecutionId: trimToDefined(input.dispatchExecutionId),
|
||||
targetProjectId: trimToDefined(input.targetProjectId),
|
||||
targetThreadId: trimToDefined(input.targetThreadId),
|
||||
rawThreadReply: trimToDefined(input.rawThreadReply),
|
||||
replyBody: trimToDefined(input.replyBody),
|
||||
errorMessage: trimToDefined(input.errorMessage),
|
||||
rawThreadReply,
|
||||
replyBody,
|
||||
errorMessage,
|
||||
requestId: trimToDefined(input.requestId),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user