vibe-coding-master 0.2.1 → 0.2.2

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.
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { TRANSLATION_PROMPT_KEYS } from "../../shared/types/translation.js";
2
+ import { TRANSLATION_ENTRY_RETENTION_LIMIT, TRANSLATION_PROMPT_KEYS } from "../../shared/types/translation.js";
3
3
  import { TranslationProviderError } from "../adapters/translation-provider.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -17,11 +17,11 @@ const DEFAULT_SETTINGS = {
17
17
  inputMode: "review-before-send",
18
18
  translateOutput: true,
19
19
  translateUserInput: true,
20
- contextEnabled: true,
20
+ contextEnabled: false,
21
21
  preserveTechnicalTokens: true,
22
22
  skipCjkText: true,
23
23
  redactSecrets: true,
24
- requestTimeoutMs: 15000,
24
+ requestTimeoutMs: 120000,
25
25
  temperature: 0.1
26
26
  };
27
27
  const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
@@ -62,6 +62,7 @@ export function createTranslationService(deps) {
62
62
  listeners: new Set(),
63
63
  seenTranscriptIds: new Set(),
64
64
  entries: [],
65
+ failures: new Map(),
65
66
  status: "ready",
66
67
  events: [],
67
68
  nextSeq: 1
@@ -92,6 +93,11 @@ export function createTranslationService(deps) {
92
93
  appendEvent(sessionId, { type: "error", id, message });
93
94
  emit(sessionId, { type: "translation-error", id, message });
94
95
  }
96
+ function publishFailures(sessionId) {
97
+ const failures = getFailureItems(getState(sessionId));
98
+ appendEvent(sessionId, { type: "failures", failures });
99
+ emit(sessionId, { type: "translation-failures", failures });
100
+ }
95
101
  function appendEvent(sessionId, input) {
96
102
  const state = getState(sessionId);
97
103
  const event = {
@@ -117,6 +123,7 @@ export function createTranslationService(deps) {
117
123
  if (!state.cacheLoaded) {
118
124
  await loadCachedEvents(state);
119
125
  state.cacheLoaded = true;
126
+ pruneTranslationEntries(input.sessionId);
120
127
  }
121
128
  await deps.fs.ensureDir(path.dirname(cachePath));
122
129
  return state;
@@ -150,6 +157,9 @@ export function createTranslationService(deps) {
150
157
  else if (event.type === "error") {
151
158
  state.status = "failed";
152
159
  }
160
+ else if (event.type === "failures") {
161
+ state.failures = new Map(event.failures.map((failure) => [failure.translationId, failure]));
162
+ }
153
163
  }
154
164
  }
155
165
  async function persistEvents(state) {
@@ -216,14 +226,20 @@ export function createTranslationService(deps) {
216
226
  }
217
227
  }
218
228
  function processClaudeOutputText(sessionId, rawText, config, entryId) {
229
+ return startClaudeOutputTranslation(sessionId, rawText, config, {
230
+ entryId,
231
+ replaceExisting: false
232
+ }) !== undefined;
233
+ }
234
+ function startClaudeOutputTranslation(sessionId, rawText, config, options) {
219
235
  const session = deps.runtime.getSession(sessionId);
220
236
  const roleSession = deps.sessionRegistry.get(sessionId);
221
237
  if (!session && !roleSession) {
222
- return false;
238
+ return undefined;
223
239
  }
224
240
  const { settings, secrets } = config;
225
241
  if (!rawText.trim()) {
226
- return false;
242
+ return undefined;
227
243
  }
228
244
  const text = rawText;
229
245
  const baseEntry = {
@@ -236,11 +252,16 @@ export function createTranslationService(deps) {
236
252
  settings,
237
253
  status: "translating",
238
254
  contextUsed: false,
239
- id: entryId
255
+ id: options.entryId
240
256
  }),
241
257
  translationStartedAt: now()
242
258
  };
243
- pushEntry(sessionId, baseEntry);
259
+ if (options.replaceExisting) {
260
+ replaceEntry(sessionId, baseEntry);
261
+ }
262
+ else {
263
+ pushEntry(sessionId, baseEntry);
264
+ }
244
265
  const queue = queues.getQueue(sessionId);
245
266
  void queue.enqueue(async () => {
246
267
  publishStatus(sessionId, "translating");
@@ -265,6 +286,7 @@ export function createTranslationService(deps) {
265
286
  tokenUsage: result.tokenUsage
266
287
  };
267
288
  replaceEntry(sessionId, completed);
289
+ clearFailure(sessionId, completed.id);
268
290
  getState(sessionId).lastAssistantText = text;
269
291
  publishStatus(sessionId, "ready");
270
292
  }
@@ -276,15 +298,23 @@ export function createTranslationService(deps) {
276
298
  completedAt: now()
277
299
  };
278
300
  replaceEntry(sessionId, failed);
301
+ recordFailure(sessionId, failed);
279
302
  publishStatus(sessionId, "failed");
280
303
  }
281
304
  }).catch((error) => {
282
305
  publishError(sessionId, error instanceof Error ? error.message : "Translation failed.");
283
306
  });
284
- return true;
307
+ return baseEntry;
285
308
  }
286
309
  function pushEntry(sessionId, entry) {
287
310
  getState(sessionId).entries.push(entry);
311
+ pruneTranslationEntries(sessionId, new Set([entry.id]));
312
+ publishEntry(sessionId, entry);
313
+ }
314
+ function upsertAndPublishEntry(sessionId, entry) {
315
+ const state = getState(sessionId);
316
+ state.entries = upsertEntry(state.entries, entry);
317
+ pruneTranslationEntries(sessionId, new Set([entry.id]));
288
318
  publishEntry(sessionId, entry);
289
319
  }
290
320
  function pushPreservedTranscriptEntry(sessionId, entryId, sourceText, settings) {
@@ -311,9 +341,100 @@ export function createTranslationService(deps) {
311
341
  }
312
342
  function replaceEntry(sessionId, entry) {
313
343
  const state = getState(sessionId);
314
- state.entries = state.entries.map((current) => current.id === entry.id ? entry : current);
344
+ state.entries = upsertEntry(state.entries, entry);
345
+ pruneTranslationEntries(sessionId, new Set([entry.id]));
315
346
  publishEntry(sessionId, entry);
316
347
  }
348
+ function recordFailure(sessionId, entry) {
349
+ if (!isRetryableFailedEntry(entry)) {
350
+ return;
351
+ }
352
+ const state = getState(sessionId);
353
+ const existing = state.failures.get(entry.id);
354
+ state.failures.set(entry.id, {
355
+ translationId: entry.id,
356
+ sessionId,
357
+ taskSlug: entry.taskSlug,
358
+ role: entry.role,
359
+ sourceText: entry.sourceText,
360
+ error: entry.error ?? "Translation failed.",
361
+ failedAt: entry.completedAt ?? now(),
362
+ retryCount: existing?.retryCount ?? 0,
363
+ lastRetryAt: existing?.lastRetryAt
364
+ });
365
+ publishFailures(sessionId);
366
+ }
367
+ function clearFailure(sessionId, translationId) {
368
+ const state = getState(sessionId);
369
+ if (state.failures.delete(translationId)) {
370
+ publishFailures(sessionId);
371
+ }
372
+ }
373
+ function pruneTranslationEntries(sessionId, protectedIds = new Set()) {
374
+ const state = getState(sessionId);
375
+ const overflow = state.entries.length - TRANSLATION_ENTRY_RETENTION_LIMIT;
376
+ if (overflow <= 0) {
377
+ return;
378
+ }
379
+ const removedIds = new Set();
380
+ for (const entry of state.entries) {
381
+ if (removedIds.size >= overflow) {
382
+ break;
383
+ }
384
+ if (protectedIds.has(entry.id) || isActiveTranslationEntry(entry)) {
385
+ continue;
386
+ }
387
+ removedIds.add(entry.id);
388
+ }
389
+ if (removedIds.size === 0) {
390
+ return;
391
+ }
392
+ state.entries = state.entries.filter((entry) => !removedIds.has(entry.id));
393
+ state.events = pruneTranslationEntryEvents(state.events, removedIds);
394
+ void persistEvents(state);
395
+ let failuresChanged = false;
396
+ for (const entryId of removedIds) {
397
+ failuresChanged = state.failures.delete(entryId) || failuresChanged;
398
+ }
399
+ if (failuresChanged) {
400
+ publishFailures(sessionId);
401
+ }
402
+ }
403
+ function markFailureRetrying(sessionId, failure) {
404
+ const retrying = {
405
+ ...failure,
406
+ retryCount: failure.retryCount + 1,
407
+ lastRetryAt: now()
408
+ };
409
+ getState(sessionId).failures.set(failure.translationId, retrying);
410
+ return retrying;
411
+ }
412
+ function retryOneTranslation(sessionId, original, config) {
413
+ const state = getState(sessionId);
414
+ const existingFailure = state.failures.get(original.id) ?? {
415
+ translationId: original.id,
416
+ sessionId,
417
+ taskSlug: original.taskSlug,
418
+ role: original.role,
419
+ sourceText: original.sourceText,
420
+ error: original.error ?? "Translation failed.",
421
+ failedAt: original.completedAt ?? now(),
422
+ retryCount: 0
423
+ };
424
+ markFailureRetrying(sessionId, existingFailure);
425
+ const retrying = startClaudeOutputTranslation(sessionId, original.sourceText, config, {
426
+ entryId: original.id,
427
+ replaceExisting: true
428
+ });
429
+ if (!retrying) {
430
+ throw new VcmError({
431
+ code: "TRANSLATION_RETRY_UNSUPPORTED",
432
+ message: "Translation entry cannot be retried.",
433
+ statusCode: 400
434
+ });
435
+ }
436
+ return retrying;
437
+ }
317
438
  function createEntry(input) {
318
439
  return {
319
440
  id: input.id ?? id(),
@@ -327,6 +448,9 @@ export function createTranslationService(deps) {
327
448
  translatedText: input.translatedText ?? "",
328
449
  status: input.status,
329
450
  contextUsed: input.contextUsed,
451
+ boundaryKind: input.boundaryKind,
452
+ conversationTurn: input.conversationTurn,
453
+ occurredAt: input.occurredAt,
330
454
  createdAt: now(),
331
455
  completedAt: input.completedAt,
332
456
  provider: input.settings.providerType,
@@ -413,6 +537,44 @@ export function createTranslationService(deps) {
413
537
  events
414
538
  };
415
539
  },
540
+ async recordConversationBoundary(input) {
541
+ const { settings } = await loadConfig();
542
+ const state = await prepareCache({
543
+ repoRoot: input.taskRepoRoot ?? input.repoRoot,
544
+ taskSlug: input.taskSlug,
545
+ role: input.role,
546
+ sessionId: input.sessionId
547
+ });
548
+ const conversationTurn = resolveConversationBoundaryTurn(state, input.boundaryKind);
549
+ if (conversationTurn === undefined) {
550
+ return undefined;
551
+ }
552
+ const entryId = `boundary:${input.sessionId}:${conversationTurn}:${input.boundaryKind}`;
553
+ const existing = state.entries.find((entry) => entry.id === entryId);
554
+ if (existing) {
555
+ return existing;
556
+ }
557
+ const occurredAt = input.occurredAt ?? now();
558
+ const sourceText = formatConversationBoundaryText(input.boundaryKind, conversationTurn, occurredAt);
559
+ const entry = createEntry({
560
+ taskSlug: input.taskSlug,
561
+ role: input.role,
562
+ direction: "cc-output-to-user",
563
+ sourceKind: "conversation-boundary",
564
+ sourceText,
565
+ settings,
566
+ status: "preserved",
567
+ contextUsed: false,
568
+ id: entryId,
569
+ translatedText: sourceText,
570
+ completedAt: occurredAt,
571
+ boundaryKind: input.boundaryKind,
572
+ conversationTurn,
573
+ occurredAt
574
+ });
575
+ upsertAndPublishEntry(input.sessionId, entry);
576
+ return entry;
577
+ },
416
578
  async translateUserInput(input) {
417
579
  const { settings, secrets } = await loadConfig();
418
580
  if (!input.text.trim()) {
@@ -524,6 +686,7 @@ export function createTranslationService(deps) {
524
686
  for (const entry of state.entries) {
525
687
  listener({ type: "translation-entry", entry });
526
688
  }
689
+ listener({ type: "translation-failures", failures: getFailureItems(state) });
527
690
  if (!state.unsubscribeTranscript) {
528
691
  if (!roleSession) {
529
692
  listener({
@@ -545,6 +708,7 @@ export function createTranslationService(deps) {
545
708
  async clearSession(sessionId) {
546
709
  const state = getState(sessionId);
547
710
  state.entries = [];
711
+ state.failures.clear();
548
712
  state.events = [];
549
713
  state.nextSeq = 1;
550
714
  queues.clearQueue(sessionId);
@@ -577,16 +741,45 @@ export function createTranslationService(deps) {
577
741
  statusCode: 404
578
742
  });
579
743
  }
580
- if (original.direction !== "cc-output-to-user") {
744
+ if (!isRetryableFailedEntry(original)) {
581
745
  throw new VcmError({
582
746
  code: "TRANSLATION_RETRY_UNSUPPORTED",
583
- message: "Only Claude Code output translation entries can be retried.",
747
+ message: "Only failed Claude Code output prose translation entries can be retried.",
584
748
  statusCode: 400
585
749
  });
586
750
  }
587
751
  const config = await loadConfig();
588
- processClaudeOutputText(sessionId, original.sourceText, config);
589
- return state.entries[state.entries.length - 1] ?? original;
752
+ const retrying = retryOneTranslation(sessionId, original, config);
753
+ publishFailures(sessionId);
754
+ return retrying;
755
+ },
756
+ async retryFailedTranslations(sessionId) {
757
+ const state = getState(sessionId);
758
+ const failures = getFailureItems(state);
759
+ const config = await loadConfig();
760
+ let changed = false;
761
+ for (const failure of failures) {
762
+ const original = state.entries.find((entry) => entry.id === failure.translationId);
763
+ if (!original || !isRetryableFailedEntry(original)) {
764
+ state.failures.delete(failure.translationId);
765
+ changed = true;
766
+ continue;
767
+ }
768
+ retryOneTranslation(sessionId, original, config);
769
+ changed = true;
770
+ }
771
+ if (changed) {
772
+ publishFailures(sessionId);
773
+ }
774
+ return { failures: getFailureItems(state) };
775
+ },
776
+ async ignoreTranslationFailures(sessionId) {
777
+ const state = getState(sessionId);
778
+ if (state.failures.size > 0) {
779
+ state.failures.clear();
780
+ publishFailures(sessionId);
781
+ }
782
+ return { failures: [] };
590
783
  }
591
784
  };
592
785
  async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
@@ -658,6 +851,80 @@ function formatUnknown(value) {
658
851
  return String(value);
659
852
  }
660
853
  }
854
+ function resolveConversationBoundaryTurn(state, boundaryKind) {
855
+ const openTurn = getOpenConversationTurn(state);
856
+ if (boundaryKind === "start") {
857
+ return openTurn ?? getMaxConversationTurn(state) + 1;
858
+ }
859
+ return openTurn;
860
+ }
861
+ function getOpenConversationTurn(state) {
862
+ const startTurns = getBoundaryEntries(state)
863
+ .filter((entry) => entry.boundaryKind === "start")
864
+ .map((entry) => entry.conversationTurn)
865
+ .filter((turn) => typeof turn === "number" && Number.isFinite(turn))
866
+ .sort((left, right) => right - left);
867
+ return startTurns.find((turn) => !hasBoundaryEntry(state, turn, "end"));
868
+ }
869
+ function getMaxConversationTurn(state) {
870
+ return Math.max(0, ...getBoundaryEntries(state)
871
+ .map((entry) => entry.conversationTurn)
872
+ .filter((turn) => typeof turn === "number" && Number.isFinite(turn)));
873
+ }
874
+ function hasBoundaryEntry(state, conversationTurn, boundaryKind) {
875
+ return getBoundaryEntries(state).some((entry) => entry.boundaryKind === boundaryKind &&
876
+ entry.conversationTurn === conversationTurn);
877
+ }
878
+ function getBoundaryEntries(state) {
879
+ return state.entries.filter((entry) => entry.sourceKind === "conversation-boundary");
880
+ }
881
+ function getFailureItems(state) {
882
+ return Array.from(state.failures.values());
883
+ }
884
+ function isActiveTranslationEntry(entry) {
885
+ return entry.status === "queued" || entry.status === "translating";
886
+ }
887
+ function isRetryableFailedEntry(entry) {
888
+ return entry.status === "failed"
889
+ && entry.direction === "cc-output-to-user"
890
+ && entry.sourceKind === "prose";
891
+ }
892
+ function pruneTranslationEntryEvents(events, removedIds) {
893
+ const pruned = [];
894
+ for (const event of events) {
895
+ if (event.type === "entry") {
896
+ if (!removedIds.has(event.entry.id)) {
897
+ pruned.push(event);
898
+ }
899
+ continue;
900
+ }
901
+ if (event.type === "failures") {
902
+ pruned.push({
903
+ ...event,
904
+ failures: event.failures.filter((failure) => !removedIds.has(failure.translationId))
905
+ });
906
+ continue;
907
+ }
908
+ pruned.push(event);
909
+ }
910
+ return pruned;
911
+ }
912
+ function formatConversationBoundaryText(boundaryKind, conversationTurn, occurredAt) {
913
+ const label = boundaryKind === "start" ? "开始" : "结束";
914
+ return `-------${label}---第 ${conversationTurn} 轮----${formatBoundaryTime(occurredAt)}---------------`;
915
+ }
916
+ function formatBoundaryTime(value) {
917
+ const date = new Date(value);
918
+ if (Number.isNaN(date.getTime())) {
919
+ return value;
920
+ }
921
+ return new Intl.DateTimeFormat(undefined, {
922
+ hour: "2-digit",
923
+ minute: "2-digit",
924
+ second: "2-digit",
925
+ hour12: false
926
+ }).format(date);
927
+ }
661
928
  function upsertEntry(entries, entry) {
662
929
  const index = entries.findIndex((current) => current.id === entry.id);
663
930
  if (index === -1) {
@@ -23,6 +23,8 @@ export function renderReviewerHarnessRules() {
23
23
 
24
24
  - Independently design the test coverage needed to prove the implemented behavior, including missing cases beyond coder's baseline tests.
25
25
  - Decide whether stronger L1/L2/L3 validation is needed for final confidence.
26
+ - Before final validation, perform a full cache cleanup, then rerun validation from a clean state.
27
+ - Do not use validation results produced before full cache cleanup as final acceptance evidence.
26
28
  - Add or modify tests needed for test adequacy.
27
29
  - Do not skip smoke, integration, or E2E checks only because coder did not run them.
28
30
 
@@ -3,3 +3,4 @@ export const TRANSLATION_PROMPT_KEYS = [
3
3
  "zh-to-en-with-context",
4
4
  "en-to-zh"
5
5
  ];
6
+ export const TRANSLATION_ENTRY_RETENTION_LIMIT = 500;