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.
- package/dist/backend/gateway/gateway-service.js +108 -15
- package/dist/backend/server.js +1 -0
- package/dist/backend/services/claude-hook-service.js +52 -9
- package/dist/backend/services/translation-service.js +113 -9
- package/dist/shared/types/app-settings.js +1 -0
- package/dist-frontend/assets/{index-D5LBogZG.js → index-UT2aoyEt.js} +1 -1
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -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 &&
|
|
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
|
|
1115
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/dist/backend/server.js
CHANGED
|
@@ -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
|
|
252
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" }
|