vibe-coding-master 0.6.1 → 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 TRANSLATION_MODEL = "translator";
13
13
  const OUTPUT_TRANSLATION_BATCH_DELAY_MS = 10000;
14
14
  const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
15
15
  const TRANSLATION_TASK_FEED_RETENTION_LIMIT = 2000;
16
+ const GATEWAY_TRANSLATION_REUSE_GRACE_MS = 1500;
17
+ const GATEWAY_TRANSLATION_REUSE_POLL_MS = 50;
16
18
  export function createTranslationService(deps) {
17
19
  const now = deps.now ?? (() => new Date().toISOString());
18
20
  const id = deps.id ?? (() => `tr_${Date.now()}_${Math.random().toString(16).slice(2)}`);
@@ -241,11 +243,13 @@ export function createTranslationService(deps) {
241
243
  let displayed = false;
242
244
  if (event.kind === "text") {
243
245
  const shouldTranslate = shouldTranslateTextTranscriptEvent(state, event, config);
246
+ const metadata = getTextTranscriptEntryMetadata(event);
244
247
  displayed = shouldTranslate
245
248
  ? processClaudeOutputText(sessionId, event.text, config, event.id, {
246
- flushImmediately: event.stopReason === "end_turn"
249
+ flushImmediately: event.stopReason === "end_turn",
250
+ metadata
247
251
  })
248
- : pushPreservedProseEntry(sessionId, event.id, event.text, config);
252
+ : pushPreservedProseEntry(sessionId, event.id, event.text, config, metadata);
249
253
  if (displayed && shouldTranslate) {
250
254
  state.lastAssistantText = event.text;
251
255
  }
@@ -267,6 +271,9 @@ export function createTranslationService(deps) {
267
271
  if (config.outputMode === "all") {
268
272
  return true;
269
273
  }
274
+ if (config.outputMode === "round-final") {
275
+ return false;
276
+ }
270
277
  if (event.stopReason !== "end_turn") {
271
278
  return false;
272
279
  }
@@ -279,9 +286,16 @@ export function createTranslationService(deps) {
279
286
  return startClaudeOutputTranslation(sessionId, rawText, config, {
280
287
  entryId,
281
288
  replaceExisting: false,
282
- flushImmediately: options.flushImmediately === true
289
+ flushImmediately: options.flushImmediately === true,
290
+ metadata: options.metadata
283
291
  }) !== undefined;
284
292
  }
293
+ function getTextTranscriptEntryMetadata(event) {
294
+ return {
295
+ ...(event.stopReason !== undefined ? { transcriptStopReason: event.stopReason } : {}),
296
+ transcriptTimestamp: event.timestamp
297
+ };
298
+ }
285
299
  function startClaudeOutputTranslation(sessionId, rawText, config, options) {
286
300
  const session = deps.runtime.getSession(sessionId);
287
301
  const roleSession = deps.sessionRegistry.get(sessionId);
@@ -302,7 +316,8 @@ export function createTranslationService(deps) {
302
316
  config,
303
317
  status: "queued",
304
318
  contextUsed: false,
305
- id: options.entryId
319
+ id: options.entryId,
320
+ ...options.metadata
306
321
  })
307
322
  };
308
323
  if (options.replaceExisting) {
@@ -437,10 +452,10 @@ export function createTranslationService(deps) {
437
452
  function pushPreservedTranscriptEntry(sessionId, entryId, sourceText, config) {
438
453
  return pushPreservedOutputEntry(sessionId, entryId, sourceText, "tool-output", config);
439
454
  }
440
- function pushPreservedProseEntry(sessionId, entryId, sourceText, config) {
441
- return pushPreservedOutputEntry(sessionId, entryId, sourceText, "prose", config);
455
+ function pushPreservedProseEntry(sessionId, entryId, sourceText, config, metadata = {}) {
456
+ return pushPreservedOutputEntry(sessionId, entryId, sourceText, "prose", config, metadata);
442
457
  }
443
- function pushPreservedOutputEntry(sessionId, entryId, sourceText, sourceKind, config) {
458
+ function pushPreservedOutputEntry(sessionId, entryId, sourceText, sourceKind, config, metadata = {}) {
444
459
  if (!sourceText.trim()) {
445
460
  return false;
446
461
  }
@@ -460,7 +475,8 @@ export function createTranslationService(deps) {
460
475
  contextUsed: false,
461
476
  id: entryId,
462
477
  translatedText: sourceText,
463
- completedAt: now()
478
+ completedAt: now(),
479
+ ...metadata
464
480
  });
465
481
  pushEntry(sessionId, entry);
466
482
  return true;
@@ -550,7 +566,11 @@ export function createTranslationService(deps) {
550
566
  markFailureRetrying(sessionId, existingFailure);
551
567
  const retrying = startClaudeOutputTranslation(sessionId, original.sourceText, config, {
552
568
  entryId: original.id,
553
- replaceExisting: true
569
+ replaceExisting: true,
570
+ metadata: {
571
+ transcriptStopReason: original.transcriptStopReason,
572
+ transcriptTimestamp: original.transcriptTimestamp
573
+ }
554
574
  });
555
575
  if (!retrying) {
556
576
  throw new VcmError({
@@ -574,6 +594,8 @@ export function createTranslationService(deps) {
574
594
  translatedText: input.translatedText ?? "",
575
595
  status: input.status,
576
596
  contextUsed: input.contextUsed,
597
+ transcriptStopReason: input.transcriptStopReason,
598
+ transcriptTimestamp: input.transcriptTimestamp,
577
599
  boundaryKind: input.boundaryKind,
578
600
  conversationTurn: input.conversationTurn,
579
601
  occurredAt: input.occurredAt,
@@ -674,6 +696,7 @@ export function createTranslationService(deps) {
674
696
  };
675
697
  },
676
698
  async pollTaskFeed(input) {
699
+ const config = await loadConfig();
677
700
  const cursor = Number.isFinite(input.after) ? Math.max(1, Math.floor(input.after)) : 1;
678
701
  const maxEvents = Math.min(Math.max(1, Math.floor(input.limit ?? 500)), 1000);
679
702
  const roleSessions = await deps.sessionService.listRoleSessions(input.repoRoot, input.taskSlug);
@@ -698,6 +721,9 @@ export function createTranslationService(deps) {
698
721
  status: state.status
699
722
  });
700
723
  }
724
+ if (config.outputMode === "round-final") {
725
+ await translateRoundFinalReplyIfReady(input, config);
726
+ }
701
727
  const feed = getTaskFeed(input.taskRepoRoot, input.taskSlug);
702
728
  const events = feed.events
703
729
  .filter((event) => event.seq >= cursor)
@@ -1004,6 +1030,10 @@ export function createTranslationService(deps) {
1004
1030
  },
1005
1031
  async translateGatewayOutput(input) {
1006
1032
  const config = await loadConfig();
1033
+ const reusable = await findReusableGatewayOutputTranslation(input, config);
1034
+ if (reusable) {
1035
+ return reusable.trim();
1036
+ }
1007
1037
  const translation = await translateText({
1008
1038
  repoRoot: input.repoRoot,
1009
1039
  taskSlug: input.taskSlug,
@@ -1033,6 +1063,228 @@ export function createTranslationService(deps) {
1033
1063
  };
1034
1064
  }
1035
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
+ }
1146
+ async function findReusableGatewayOutputTranslation(input, config) {
1147
+ const graceDeadline = Date.now() + (input.sourceEntryIds?.length ? GATEWAY_TRANSLATION_REUSE_GRACE_MS : 0);
1148
+ while (true) {
1149
+ const lookup = await lookupGatewayOutputTranslation(input);
1150
+ if (lookup.kind === "translated") {
1151
+ return lookup.text;
1152
+ }
1153
+ if (lookup.kind === "active") {
1154
+ return waitForReusableGatewayOutputTranslation(input, config);
1155
+ }
1156
+ if (!input.sourceEntryIds?.length || Date.now() >= graceDeadline) {
1157
+ return undefined;
1158
+ }
1159
+ await delay(GATEWAY_TRANSLATION_REUSE_POLL_MS);
1160
+ }
1161
+ }
1162
+ async function waitForReusableGatewayOutputTranslation(input, config) {
1163
+ const deadline = Date.now() + config.requestTimeoutMs;
1164
+ while (Date.now() <= deadline) {
1165
+ const lookup = await lookupGatewayOutputTranslation(input);
1166
+ if (lookup.kind === "translated") {
1167
+ return lookup.text;
1168
+ }
1169
+ if (lookup.kind !== "active") {
1170
+ return undefined;
1171
+ }
1172
+ await delay(GATEWAY_TRANSLATION_REUSE_POLL_MS);
1173
+ }
1174
+ throw new VcmError({
1175
+ code: "TRANSLATION_TIMEOUT",
1176
+ message: "Gateway output translation timed out while waiting for the existing PM reply translation.",
1177
+ statusCode: 504
1178
+ });
1179
+ }
1180
+ async function lookupGatewayOutputTranslation(input) {
1181
+ const states = await getGatewayOutputCandidateStates(input);
1182
+ return selectGatewayOutputTranslation(states, input);
1183
+ }
1184
+ async function getGatewayOutputCandidateStates(input) {
1185
+ const states = [];
1186
+ const seen = new Set();
1187
+ const add = (state) => {
1188
+ if (seen.has(state) || !isGatewayOutputCandidateState(state, input)) {
1189
+ return;
1190
+ }
1191
+ seen.add(state);
1192
+ states.push(state);
1193
+ };
1194
+ const roleSession = await getGatewayRoleSession(input);
1195
+ if (roleSession) {
1196
+ const state = await prepareCache({
1197
+ repoRoot: roleSession.cwd,
1198
+ baseRepoRoot: input.repoRoot,
1199
+ taskSlug: input.taskSlug,
1200
+ role: input.role,
1201
+ sessionId: roleSession.id
1202
+ });
1203
+ if (roleSession.status === "running") {
1204
+ startTranscriptTail(roleSession);
1205
+ }
1206
+ add(state);
1207
+ }
1208
+ for (const state of sessionStates.values()) {
1209
+ add(state);
1210
+ }
1211
+ return states;
1212
+ }
1213
+ async function getGatewayRoleSession(input) {
1214
+ try {
1215
+ return await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role);
1216
+ }
1217
+ catch {
1218
+ return undefined;
1219
+ }
1220
+ }
1221
+ function isGatewayOutputCandidateState(state, input) {
1222
+ if (state.taskSlug !== input.taskSlug || state.role !== input.role) {
1223
+ return false;
1224
+ }
1225
+ return state.baseRepoRoot === input.repoRoot
1226
+ || state.repoRoot === input.repoRoot
1227
+ || Boolean(state.repoRoot?.startsWith(`${input.repoRoot}${path.sep}`));
1228
+ }
1229
+ function selectGatewayOutputTranslation(states, input) {
1230
+ const sourceEntryIds = normalizeGatewaySourceEntryIds(input.sourceEntryIds);
1231
+ if (sourceEntryIds.length > 0) {
1232
+ const entries = sourceEntryIds
1233
+ .map((entryId) => findGatewayOutputEntryById(states, input, entryId));
1234
+ const foundEntries = entries.filter((entry) => Boolean(entry));
1235
+ if (foundEntries.length === sourceEntryIds.length) {
1236
+ if (foundEntries.some(isActiveTranslationEntry)) {
1237
+ return { kind: "active" };
1238
+ }
1239
+ if (foundEntries.every(isReusableGatewayOutputEntry)) {
1240
+ return {
1241
+ kind: "translated",
1242
+ text: foundEntries.map((entry) => entry.translatedText).join("\n\n")
1243
+ };
1244
+ }
1245
+ }
1246
+ else if (foundEntries.some(isActiveTranslationEntry)) {
1247
+ return { kind: "active" };
1248
+ }
1249
+ }
1250
+ const normalizedSourceText = normalizeGatewaySourceText(input.text);
1251
+ const sourceMatches = states
1252
+ .flatMap((state) => state.entries)
1253
+ .filter((entry) => isGatewayOutputEntry(entry, input)
1254
+ && normalizeGatewaySourceText(entry.sourceText) === normalizedSourceText);
1255
+ const translated = [...sourceMatches].reverse().find(isReusableGatewayOutputEntry);
1256
+ if (translated) {
1257
+ return { kind: "translated", text: translated.translatedText };
1258
+ }
1259
+ if (sourceMatches.some(isActiveTranslationEntry)) {
1260
+ return { kind: "active" };
1261
+ }
1262
+ return { kind: "missing" };
1263
+ }
1264
+ function findGatewayOutputEntryById(states, input, entryId) {
1265
+ for (const state of states) {
1266
+ const entry = state.entries.find((candidate) => candidate.id === entryId && isGatewayOutputEntry(candidate, input));
1267
+ if (entry) {
1268
+ return entry;
1269
+ }
1270
+ }
1271
+ return undefined;
1272
+ }
1273
+ function isGatewayOutputEntry(entry, input) {
1274
+ return entry.taskSlug === input.taskSlug
1275
+ && entry.role === input.role
1276
+ && entry.direction === "cc-output-to-user"
1277
+ && entry.sourceKind === "prose";
1278
+ }
1279
+ function isReusableGatewayOutputEntry(entry) {
1280
+ return entry.status === "translated" && Boolean(entry.translatedText.trim());
1281
+ }
1282
+ function normalizeGatewaySourceEntryIds(sourceEntryIds) {
1283
+ return Array.from(new Set((sourceEntryIds ?? []).map((entryId) => entryId.trim()).filter(Boolean)));
1284
+ }
1285
+ function normalizeGatewaySourceText(text) {
1286
+ return text.trim();
1287
+ }
1036
1288
  async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
1037
1289
  const record = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
1038
1290
  if (!record || record.status !== "running") {
@@ -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" }