vibe-coding-master 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,8 @@ const POLL_LONG_BACKOFF_MS = 30_000;
13
13
  const MAX_FAILURES_BEFORE_LONG_BACKOFF = 3;
14
14
  const DEFAULT_POLL_TIMEOUT_MS = 35_000;
15
15
  const LARK_REGISTRATION_CONFIRM_TIMEOUT_MS = 15_000;
16
+ const GATEWAY_ROUND_FINAL_WAIT_MS = 12_000;
17
+ const GATEWAY_ROUND_FINAL_POLL_MS = 250;
16
18
  const GATEWAY_TRANSLATION_FAILURE_TEXT = "PM 回复已收到,但翻译失败。\n发送 /retry 重新翻译。";
17
19
  const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
18
20
  "help",
@@ -663,12 +665,15 @@ export function createGatewayService(deps) {
663
665
  }
664
666
  async function enableGatewayTranslationRuntime() {
665
667
  const preferences = await deps.appSettings.getPreferences();
666
- if (preferences.translationEnabled && preferences.translationAutoSendEnabled) {
668
+ if (preferences.translationEnabled &&
669
+ preferences.translationAutoSendEnabled &&
670
+ preferences.translationOutputMode === "round-final") {
667
671
  return;
668
672
  }
669
673
  await deps.appSettings.updatePreferences({
670
674
  translationEnabled: true,
671
- translationAutoSendEnabled: true
675
+ translationAutoSendEnabled: true,
676
+ translationOutputMode: "round-final"
672
677
  });
673
678
  }
674
679
  async function startGateway() {
@@ -1111,19 +1116,12 @@ export function createGatewayService(deps) {
1111
1116
  return;
1112
1117
  }
1113
1118
  const events = await readTranscriptTextEvents(transcriptPath);
1114
- const latestReply = selectLatestTurnReply(events, input.session);
1115
- if (latestReply) {
1116
- await saveLatestPmReply(input, latestReply);
1117
- }
1118
- const settings = await deps.settings.loadSettings();
1119
- const account = toAccount(settings);
1120
- const boundUserId = settings.binding.boundUserId;
1121
- // A disarmed gateway never touches the channel: skip the outbound push.
1122
- // The latest reply was already cached above and replays on the next /start.
1123
- if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
1119
+ const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug);
1120
+ if (!round) {
1124
1121
  return;
1125
1122
  }
1126
1123
  const cursorKey = `${input.taskSlug}:project-manager:${input.session.claudeSessionId}`;
1124
+ const settings = await deps.settings.loadSettings();
1127
1125
  const cursor = settings.pushCursors[cursorKey];
1128
1126
  const nextEvents = selectEventsAfterCursor(events, cursor?.lastTranscriptEventId)
1129
1127
  .filter(isFinalTurnTextEvent);
@@ -1134,7 +1132,18 @@ export function createGatewayService(deps) {
1134
1132
  if (!text) {
1135
1133
  return;
1136
1134
  }
1137
- const roundNotice = await getGatewayRoundNotice(input.repoRoot, input.taskSlug);
1135
+ const latestReply = selectLatestTurnReply(nextEvents, input.session);
1136
+ if (latestReply) {
1137
+ await saveLatestPmReply(input, latestReply);
1138
+ }
1139
+ const account = toAccount(settings);
1140
+ const boundUserId = settings.binding.boundUserId;
1141
+ // A disarmed gateway never touches the channel: skip the outbound push.
1142
+ // The round-final PM reply was already cached above and replays on the next /start.
1143
+ if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
1144
+ return;
1145
+ }
1146
+ const roundNotice = formatGatewayRoundNotice(round);
1138
1147
  const originalMessage = formatGatewayPmOriginalReply(text, roundNotice);
1139
1148
  await sendGatewayText(settings, boundUserId, originalMessage);
1140
1149
  let output;
@@ -1182,6 +1191,40 @@ export function createGatewayService(deps) {
1182
1191
  error: output?.translationError
1183
1192
  });
1184
1193
  },
1194
+ async handleRoleStopFailure(input) {
1195
+ const round = await getGatewayRoundState(input.repoRoot, input.taskSlug);
1196
+ if (round?.stopReason === "manual-interrupt") {
1197
+ return;
1198
+ }
1199
+ const settings = await deps.settings.loadSettings();
1200
+ const account = toAccount(settings);
1201
+ const boundUserId = settings.binding.boundUserId;
1202
+ if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
1203
+ return;
1204
+ }
1205
+ const message = formatGatewayRoleStopFailure(input, round);
1206
+ await sendGatewayText(settings, boundUserId, message);
1207
+ const current = await deps.settings.loadSettings();
1208
+ await deps.settings.saveSettings({
1209
+ ...current,
1210
+ lastMessageStatus: {
1211
+ checkedAt: now(),
1212
+ direction: "outbound",
1213
+ result: "error",
1214
+ command: "role-stop-failure",
1215
+ preview: message.slice(0, 160),
1216
+ error: input.error
1217
+ },
1218
+ updatedAt: now()
1219
+ });
1220
+ await deps.audit.record({
1221
+ type: "gateway.role_stop_failure",
1222
+ result: "error",
1223
+ command: "role-stop-failure",
1224
+ preview: message,
1225
+ error: input.error
1226
+ });
1227
+ },
1185
1228
  getDiagnostics() {
1186
1229
  return {
1187
1230
  polling: isRunning()
@@ -1194,7 +1237,21 @@ export function createGatewayService(deps) {
1194
1237
  }
1195
1238
  return settings.latestPmReplies[latestPmReplyKey(settings.currentProjectId, settings.currentTaskSlug)];
1196
1239
  }
1197
- async function getGatewayRoundNotice(repoRoot, taskSlug) {
1240
+ async function waitForGatewayRoundFinal(repoRoot, taskSlug) {
1241
+ const startedAt = Date.now();
1242
+ while (Date.now() - startedAt <= GATEWAY_ROUND_FINAL_WAIT_MS) {
1243
+ const round = await getGatewayRoundState(repoRoot, taskSlug);
1244
+ if (round && isGatewayRoundFinal(round)) {
1245
+ return round;
1246
+ }
1247
+ if (round && isGatewayNonFinalTerminalRound(round)) {
1248
+ return undefined;
1249
+ }
1250
+ await delay(GATEWAY_ROUND_FINAL_POLL_MS);
1251
+ }
1252
+ return undefined;
1253
+ }
1254
+ async function getGatewayRoundState(repoRoot, taskSlug) {
1198
1255
  try {
1199
1256
  const [projectConfig, task] = await Promise.all([
1200
1257
  deps.projectService.loadConfig(repoRoot),
@@ -1206,12 +1263,24 @@ export function createGatewayService(deps) {
1206
1263
  stateRoot: projectConfig.stateRoot,
1207
1264
  taskSlug
1208
1265
  });
1209
- return formatGatewayRoundNotice(round);
1266
+ return round;
1210
1267
  }
1211
1268
  catch {
1212
1269
  return undefined;
1213
1270
  }
1214
1271
  }
1272
+ function isGatewayRoundFinal(round) {
1273
+ return round.status === "stopped"
1274
+ && Boolean(round.roundId)
1275
+ && !round.stopReason
1276
+ && round.roleRecovery?.status !== "failed"
1277
+ && round.flowPause?.reason !== "role-recovery-failed";
1278
+ }
1279
+ function isGatewayNonFinalTerminalRound(round) {
1280
+ return round.status === "stopped" && (Boolean(round.stopReason) ||
1281
+ round.roleRecovery?.status === "failed" ||
1282
+ round.flowPause?.reason === "role-recovery-failed");
1283
+ }
1215
1284
  function formatGatewayRoundNotice(round) {
1216
1285
  const roundLabel = round.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
1217
1286
  if (round.status === "stopped") {
@@ -1220,6 +1289,27 @@ export function createGatewayService(deps) {
1220
1289
  const activeRole = round.activeRole ? `,当前角色:${round.activeRole}` : "";
1221
1290
  return `${roundLabel}运行中${activeRole}。`;
1222
1291
  }
1292
+ function formatGatewayRoleStopFailure(input, round) {
1293
+ const roundLabel = round?.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
1294
+ const retryLine = input.maxAttempts !== undefined
1295
+ ? `Retry: ${input.attempt ?? 0}/${input.maxAttempts}`
1296
+ : undefined;
1297
+ return [
1298
+ "VCM 角色异常中断,流程已暂停。",
1299
+ `Task: ${input.taskSlug}`,
1300
+ `Role: ${input.role}`,
1301
+ `Round: ${roundLabel}`,
1302
+ input.error ? `Reason: ${input.error}` : undefined,
1303
+ retryLine,
1304
+ input.errorDetails ? `Details: ${truncateGatewayFailureDetails(input.errorDetails)}` : undefined,
1305
+ "",
1306
+ "请在 VCM 中检查状态,或修复问题后继续给出下一步指令。"
1307
+ ].filter((line) => line !== undefined).join("\n");
1308
+ }
1309
+ function truncateGatewayFailureDetails(value) {
1310
+ const trimmed = value.trim();
1311
+ return trimmed.length > 600 ? `${trimmed.slice(0, 600)}...` : trimmed;
1312
+ }
1223
1313
  function formatGatewayPmOriginalReply(text, roundNotice) {
1224
1314
  return [
1225
1315
  "PM final reply 原文:",
@@ -1442,6 +1532,9 @@ function sleep(ms, signal) {
1442
1532
  }, { once: true });
1443
1533
  });
1444
1534
  }
1535
+ function delay(ms) {
1536
+ return new Promise((resolve) => setTimeout(resolve, ms));
1537
+ }
1445
1538
  function normalizeBaseUrl(input, fallback) {
1446
1539
  const trimmed = input.trim();
1447
1540
  if (!trimmed) {
@@ -277,6 +277,7 @@ export function createDefaultServerDeps(options = {}) {
277
277
  translationWorkerService,
278
278
  fs,
279
279
  projectService,
280
+ roundService,
280
281
  appSettings
281
282
  });
282
283
  const gatewayChannels = createGatewayChannelRegistry([
@@ -241,15 +241,27 @@ export function createClaudeHookService(deps) {
241
241
  }
242
242
  const failure = parseStopFailureDiagnostic(input.event);
243
243
  if (!failure.retryable) {
244
+ if (await isManualInterruptedStopFailure(context, input.role)) {
245
+ return recordTurnEnd(input, context, eventName, {
246
+ dispatchRouteFiles: false,
247
+ notifyGateway: false,
248
+ settleGuard: false
249
+ });
250
+ }
244
251
  await markStopFailureRecoveryFailed(input, context, failure, 0);
245
252
  return recordTurnEnd(input, context, eventName, {
246
253
  dispatchRouteFiles: false,
247
254
  notifyGateway: false,
248
- settleGuard: false
255
+ settleGuard: false,
256
+ gatewayStopFailure: {
257
+ ...failure,
258
+ attempt: 0,
259
+ maxAttempts: MAX_ROLE_RETRY_ATTEMPTS
260
+ }
249
261
  });
250
262
  }
251
- const retryScheduled = await scheduleStopFailureRetry(input, context, failure);
252
- if (retryScheduled) {
263
+ const retryResult = await scheduleStopFailureRetry(input, context, failure);
264
+ if (retryResult === "scheduled") {
253
265
  return {
254
266
  ok: true,
255
267
  eventName,
@@ -262,7 +274,8 @@ export function createClaudeHookService(deps) {
262
274
  return recordTurnEnd(input, context, eventName, {
263
275
  dispatchRouteFiles: false,
264
276
  notifyGateway: false,
265
- settleGuard: false
277
+ settleGuard: false,
278
+ ...(retryResult === "manual-interrupt" ? {} : { gatewayStopFailure: failure })
266
279
  });
267
280
  }
268
281
  async function processPostCompactHook(input) {
@@ -344,6 +357,17 @@ export function createClaudeHookService(deps) {
344
357
  session
345
358
  }).catch(() => undefined);
346
359
  }
360
+ if (boundToTask && options.gatewayStopFailure) {
361
+ void deps.gatewayService?.handleRoleStopFailure({
362
+ repoRoot: context.project.repoRoot,
363
+ taskSlug: context.taskSlug,
364
+ role: input.role,
365
+ error: options.gatewayStopFailure.error,
366
+ errorDetails: options.gatewayStopFailure.errorDetails,
367
+ attempt: options.gatewayStopFailure.attempt,
368
+ maxAttempts: options.gatewayStopFailure.maxAttempts
369
+ }).catch(() => undefined);
370
+ }
347
371
  const dispatched = options.dispatchRouteFiles
348
372
  ? await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput)
349
373
  : [];
@@ -370,14 +394,14 @@ export function createClaudeHookService(deps) {
370
394
  async function scheduleStopFailureRetry(input, context, failure) {
371
395
  const preferences = await deps.appSettings.getPreferences();
372
396
  if (!preferences.roleRetryEnabled || !deps.runtime) {
373
- return false;
397
+ return "not-scheduled";
374
398
  }
375
399
  const stateInput = createRoundStateInput(context);
376
400
  const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
377
401
  if (currentRoundState.stopReason === "manual-interrupt"
378
402
  && currentRoundState.status === "stopped"
379
403
  && currentRoundState.activeRole === input.role) {
380
- return false;
404
+ return "manual-interrupt";
381
405
  }
382
406
  const previousAttempt = currentRoundState.roleRecovery?.role === input.role &&
383
407
  currentRoundState.roleRecovery.status !== "failed"
@@ -388,7 +412,7 @@ export function createClaudeHookService(deps) {
388
412
  if (attempt > MAX_ROLE_RETRY_ATTEMPTS) {
389
413
  clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
390
414
  await markStopFailureRecoveryFailed(input, context, failure, MAX_ROLE_RETRY_ATTEMPTS, timestamp);
391
- return false;
415
+ return "not-scheduled";
392
416
  }
393
417
  const nextRetryAt = new Date(Date.parse(timestamp) + attempt * ROLE_RETRY_BASE_DELAY_MS).toISOString();
394
418
  await deps.roundService.setRoleRecovery({
@@ -408,7 +432,18 @@ export function createClaudeHookService(deps) {
408
432
  });
409
433
  await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
410
434
  scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
411
- return true;
435
+ return "scheduled";
436
+ }
437
+ async function isManualInterruptedStopFailure(context, role) {
438
+ try {
439
+ const state = await deps.roundService.getSessionRoundState(createRoundStateInput(context));
440
+ return state.status === "stopped" &&
441
+ state.stopReason === "manual-interrupt" &&
442
+ state.activeRole === role;
443
+ }
444
+ catch {
445
+ return false;
446
+ }
412
447
  }
413
448
  function scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt) {
414
449
  const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
@@ -441,7 +476,15 @@ export function createClaudeHookService(deps) {
441
476
  await recordTurnEnd(input, context, "StopFailure", {
442
477
  dispatchRouteFiles: false,
443
478
  notifyGateway: false,
444
- settleGuard: false
479
+ settleGuard: false,
480
+ gatewayStopFailure: {
481
+ error: recovery.error ?? "stop_failure",
482
+ errorDetails: recovery.errorDetails,
483
+ lastAssistantMessage: recovery.lastAssistantMessage,
484
+ retryable: recovery.retryable ?? true,
485
+ attempt: recovery.attempt,
486
+ maxAttempts: recovery.maxAttempts
487
+ }
445
488
  });
446
489
  return;
447
490
  }
@@ -243,11 +243,13 @@ export function createTranslationService(deps) {
243
243
  let displayed = false;
244
244
  if (event.kind === "text") {
245
245
  const shouldTranslate = shouldTranslateTextTranscriptEvent(state, event, config);
246
+ const metadata = getTextTranscriptEntryMetadata(event);
246
247
  displayed = shouldTranslate
247
248
  ? processClaudeOutputText(sessionId, event.text, config, event.id, {
248
- flushImmediately: event.stopReason === "end_turn"
249
+ flushImmediately: event.stopReason === "end_turn",
250
+ metadata
249
251
  })
250
- : pushPreservedProseEntry(sessionId, event.id, event.text, config);
252
+ : pushPreservedProseEntry(sessionId, event.id, event.text, config, metadata);
251
253
  if (displayed && shouldTranslate) {
252
254
  state.lastAssistantText = event.text;
253
255
  }
@@ -269,6 +271,9 @@ export function createTranslationService(deps) {
269
271
  if (config.outputMode === "all") {
270
272
  return true;
271
273
  }
274
+ if (config.outputMode === "round-final") {
275
+ return false;
276
+ }
272
277
  if (event.stopReason !== "end_turn") {
273
278
  return false;
274
279
  }
@@ -281,9 +286,16 @@ export function createTranslationService(deps) {
281
286
  return startClaudeOutputTranslation(sessionId, rawText, config, {
282
287
  entryId,
283
288
  replaceExisting: false,
284
- flushImmediately: options.flushImmediately === true
289
+ flushImmediately: options.flushImmediately === true,
290
+ metadata: options.metadata
285
291
  }) !== undefined;
286
292
  }
293
+ function getTextTranscriptEntryMetadata(event) {
294
+ return {
295
+ ...(event.stopReason !== undefined ? { transcriptStopReason: event.stopReason } : {}),
296
+ transcriptTimestamp: event.timestamp
297
+ };
298
+ }
287
299
  function startClaudeOutputTranslation(sessionId, rawText, config, options) {
288
300
  const session = deps.runtime.getSession(sessionId);
289
301
  const roleSession = deps.sessionRegistry.get(sessionId);
@@ -304,7 +316,8 @@ export function createTranslationService(deps) {
304
316
  config,
305
317
  status: "queued",
306
318
  contextUsed: false,
307
- id: options.entryId
319
+ id: options.entryId,
320
+ ...options.metadata
308
321
  })
309
322
  };
310
323
  if (options.replaceExisting) {
@@ -439,10 +452,10 @@ export function createTranslationService(deps) {
439
452
  function pushPreservedTranscriptEntry(sessionId, entryId, sourceText, config) {
440
453
  return pushPreservedOutputEntry(sessionId, entryId, sourceText, "tool-output", config);
441
454
  }
442
- function pushPreservedProseEntry(sessionId, entryId, sourceText, config) {
443
- return pushPreservedOutputEntry(sessionId, entryId, sourceText, "prose", config);
455
+ function pushPreservedProseEntry(sessionId, entryId, sourceText, config, metadata = {}) {
456
+ return pushPreservedOutputEntry(sessionId, entryId, sourceText, "prose", config, metadata);
444
457
  }
445
- function pushPreservedOutputEntry(sessionId, entryId, sourceText, sourceKind, config) {
458
+ function pushPreservedOutputEntry(sessionId, entryId, sourceText, sourceKind, config, metadata = {}) {
446
459
  if (!sourceText.trim()) {
447
460
  return false;
448
461
  }
@@ -462,7 +475,8 @@ export function createTranslationService(deps) {
462
475
  contextUsed: false,
463
476
  id: entryId,
464
477
  translatedText: sourceText,
465
- completedAt: now()
478
+ completedAt: now(),
479
+ ...metadata
466
480
  });
467
481
  pushEntry(sessionId, entry);
468
482
  return true;
@@ -552,7 +566,11 @@ export function createTranslationService(deps) {
552
566
  markFailureRetrying(sessionId, existingFailure);
553
567
  const retrying = startClaudeOutputTranslation(sessionId, original.sourceText, config, {
554
568
  entryId: original.id,
555
- replaceExisting: true
569
+ replaceExisting: true,
570
+ metadata: {
571
+ transcriptStopReason: original.transcriptStopReason,
572
+ transcriptTimestamp: original.transcriptTimestamp
573
+ }
556
574
  });
557
575
  if (!retrying) {
558
576
  throw new VcmError({
@@ -576,6 +594,8 @@ export function createTranslationService(deps) {
576
594
  translatedText: input.translatedText ?? "",
577
595
  status: input.status,
578
596
  contextUsed: input.contextUsed,
597
+ transcriptStopReason: input.transcriptStopReason,
598
+ transcriptTimestamp: input.transcriptTimestamp,
579
599
  boundaryKind: input.boundaryKind,
580
600
  conversationTurn: input.conversationTurn,
581
601
  occurredAt: input.occurredAt,
@@ -676,6 +696,7 @@ export function createTranslationService(deps) {
676
696
  };
677
697
  },
678
698
  async pollTaskFeed(input) {
699
+ const config = await loadConfig();
679
700
  const cursor = Number.isFinite(input.after) ? Math.max(1, Math.floor(input.after)) : 1;
680
701
  const maxEvents = Math.min(Math.max(1, Math.floor(input.limit ?? 500)), 1000);
681
702
  const roleSessions = await deps.sessionService.listRoleSessions(input.repoRoot, input.taskSlug);
@@ -700,6 +721,9 @@ export function createTranslationService(deps) {
700
721
  status: state.status
701
722
  });
702
723
  }
724
+ if (config.outputMode === "round-final") {
725
+ await translateRoundFinalReplyIfReady(input, config);
726
+ }
703
727
  const feed = getTaskFeed(input.taskRepoRoot, input.taskSlug);
704
728
  const events = feed.events
705
729
  .filter((event) => event.seq >= cursor)
@@ -1039,6 +1063,86 @@ export function createTranslationService(deps) {
1039
1063
  };
1040
1064
  }
1041
1065
  };
1066
+ async function translateRoundFinalReplyIfReady(input, config) {
1067
+ if (!deps.roundService || !deps.projectService) {
1068
+ return;
1069
+ }
1070
+ const projectConfig = await deps.projectService.loadConfig(input.repoRoot);
1071
+ const round = await deps.roundService.getSessionRoundState({
1072
+ repoRoot: input.repoRoot,
1073
+ stateRepoRoot: input.taskRepoRoot,
1074
+ stateRoot: projectConfig.stateRoot,
1075
+ taskSlug: input.taskSlug
1076
+ });
1077
+ if (!isNormalStoppedRound(round)) {
1078
+ return;
1079
+ }
1080
+ const candidate = findRoundFinalReplyCandidate(input, round);
1081
+ if (!candidate || candidate.entry.status !== "preserved") {
1082
+ return;
1083
+ }
1084
+ startClaudeOutputTranslation(candidate.sessionId, candidate.entry.sourceText, config, {
1085
+ entryId: candidate.entry.id,
1086
+ replaceExisting: true,
1087
+ flushImmediately: true,
1088
+ metadata: {
1089
+ transcriptStopReason: candidate.entry.transcriptStopReason,
1090
+ transcriptTimestamp: candidate.entry.transcriptTimestamp
1091
+ }
1092
+ });
1093
+ }
1094
+ function isNormalStoppedRound(round) {
1095
+ return round.status === "stopped"
1096
+ && Boolean(round.roundId)
1097
+ && !round.stopReason
1098
+ && round.roleRecovery?.status !== "failed"
1099
+ && round.flowPause?.reason !== "role-recovery-failed";
1100
+ }
1101
+ function findRoundFinalReplyCandidate(input, round) {
1102
+ const candidates = [];
1103
+ for (const [sessionId, state] of sessionStates) {
1104
+ if (!isRoundFinalCandidateState(state, input, round.activeRole)) {
1105
+ continue;
1106
+ }
1107
+ for (const entry of state.entries) {
1108
+ if (isRoundFinalCandidateEntry(entry, input, round.stoppedAt)) {
1109
+ candidates.push({ sessionId, entry });
1110
+ }
1111
+ }
1112
+ }
1113
+ return candidates
1114
+ .sort((left, right) => compareRoundFinalCandidates(left.entry, right.entry))
1115
+ .at(-1);
1116
+ }
1117
+ function isRoundFinalCandidateState(state, input, activeRole) {
1118
+ if (state.repoRoot !== input.taskRepoRoot || state.taskSlug !== input.taskSlug || !state.role) {
1119
+ return false;
1120
+ }
1121
+ return !activeRole || state.role === activeRole;
1122
+ }
1123
+ function isRoundFinalCandidateEntry(entry, input, stoppedAt) {
1124
+ if (entry.taskSlug !== input.taskSlug ||
1125
+ entry.direction !== "cc-output-to-user" ||
1126
+ entry.sourceKind !== "prose" ||
1127
+ entry.transcriptStopReason !== "end_turn") {
1128
+ return false;
1129
+ }
1130
+ if (entry.status !== "preserved" && entry.status !== "queued" && entry.status !== "translating" && entry.status !== "translated") {
1131
+ return false;
1132
+ }
1133
+ if (!stoppedAt || !entry.transcriptTimestamp) {
1134
+ return true;
1135
+ }
1136
+ return entry.transcriptTimestamp <= stoppedAt;
1137
+ }
1138
+ function compareRoundFinalCandidates(left, right) {
1139
+ const leftTime = left.transcriptTimestamp ?? left.createdAt;
1140
+ const rightTime = right.transcriptTimestamp ?? right.createdAt;
1141
+ if (leftTime === rightTime) {
1142
+ return left.id.localeCompare(right.id);
1143
+ }
1144
+ return leftTime.localeCompare(rightTime);
1145
+ }
1042
1146
  async function findReusableGatewayOutputTranslation(input, config) {
1043
1147
  const graceDeadline = Date.now() + (input.sourceEntryIds?.length ? GATEWAY_TRANSLATION_REUSE_GRACE_MS : 0);
1044
1148
  while (true) {
@@ -12,6 +12,7 @@ export const TRANSLATION_TARGET_LANGUAGE_OPTIONS = [
12
12
  { value: "es", label: "Spanish" }
13
13
  ];
14
14
  export const TRANSLATION_OUTPUT_MODE_OPTIONS = [
15
+ { value: "round-final", label: "Round final reply" },
15
16
  { value: "pm-final-only", label: "PM final reply" },
16
17
  { value: "final-only", label: "Each role final reply" },
17
18
  { value: "all", label: "All replies" }