vibe-coding-master 0.4.41 → 0.5.0

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,9 +13,22 @@ const FILE_RUNTIME_JOBS_DIR = `${TRANSLATIONS_RUNTIME_DIR}/files/jobs`;
13
13
  const BOOTSTRAP_INDEX_PATH = `${TRANSLATIONS_ROOT}/bootstrap/index.json`;
14
14
  const BOOTSTRAP_RUNTIME_RUNS_DIR = `${TRANSLATIONS_RUNTIME_DIR}/bootstrap/runs`;
15
15
  const CONVERSATION_RUNTIME_DIR = `${TRANSLATIONS_RUNTIME_DIR}/conversations`;
16
+ // Single shared, self-describing conversation output file: one folder + one file
17
+ // for all conversation translation. It carries the batch identity plus per-index
18
+ // results so crash recovery can re-associate the file with the active queue item
19
+ // without a unique per-task/per-batch path. See ConversationBatchResultFile.
20
+ const CONVERSATION_RESULT_PATH = `${CONVERSATION_RUNTIME_DIR}/result.json`;
16
21
  const MEMORY_UPDATE_RUNTIME_DIR = `${TRANSLATIONS_RUNTIME_DIR}/memory-updates`;
17
22
  const DEFAULT_PROFILE = "default";
18
23
  const DEFAULT_CHUNK_SOURCE_TOKEN_TARGET = 80000;
24
+ // In-flight conversation queue items normally finalize when the Translator
25
+ // session's Stop/StopFailure hook reaches the backend. If that hook is lost
26
+ // (session crash, backend restart/reconnect) a conversation item with no result
27
+ // on disk would block the queue head forever. Treat such an item as stuck once it
28
+ // has been in-flight past this bound and release it so later items can dispatch.
29
+ // Kept comfortably above a normal short composer translation, so a genuinely
30
+ // running conversation turn is never released mid-flight.
31
+ const STALE_CONVERSATION_ITEM_MS = 90000;
19
32
  const BOOTSTRAP_DEFAULT_LIMIT = 12;
20
33
  const MEMORY_TOTAL_LIMIT_BYTES = 80 * 1024;
21
34
  const MEMORY_INITIALIZED_MIN_FILES = 2;
@@ -183,12 +196,15 @@ export function createTranslationWorkerService(deps) {
183
196
  if (!deps.runtime || !deps.sessionService) {
184
197
  return;
185
198
  }
186
- const queue = await loadQueue(repoRoot);
199
+ let queue = await loadQueue(repoRoot);
187
200
  const active = queue.activeItemId
188
201
  ? queue.items.find((item) => item.id === queue.activeItemId)
189
202
  : undefined;
190
203
  if (active && ["dispatching", "running", "validating"].includes(active.status)) {
191
- return;
204
+ if (!(await reconcileStuckActiveItem(repoRoot, active))) {
205
+ return;
206
+ }
207
+ queue = await loadQueue(repoRoot);
192
208
  }
193
209
  const next = queue.items.find((item) => item.status === "queued");
194
210
  if (!next) {
@@ -236,11 +252,14 @@ export function createTranslationWorkerService(deps) {
236
252
  }
237
253
  }
238
254
  async function prepareConversationBatch(repoRoot, queue, leader) {
239
- const candidates = await collectConversationBatchItems(repoRoot, queue, leader);
255
+ const candidates = collectConversationBatchItems(queue, leader);
240
256
  const batchId = `batch-${Date.now()}-${createId().slice(0, 8)}`;
241
- const batchResultPath = `${CONVERSATION_RUNTIME_DIR}/batches/${batchId}/result.txt`;
257
+ // Every batched item points at the single shared, self-describing result.json
258
+ // (carrying batchId + per-index results); each item keeps its unique
259
+ // expectedResultPath as a logical lookup key.
260
+ const batchResultPath = CONVERSATION_RESULT_PATH;
242
261
  await deps.fs.ensureDir(path.dirname(resolveRepoPath(repoRoot, batchResultPath)));
243
- const prompt = await buildConversationBatchPrompt(repoRoot, candidates, batchResultPath);
262
+ const prompt = buildConversationBatchPrompt(repoRoot, candidates, batchResultPath, batchId);
244
263
  const timestamp = now();
245
264
  candidates.forEach((item, index) => {
246
265
  item.status = "dispatching";
@@ -301,9 +320,11 @@ export function createTranslationWorkerService(deps) {
301
320
  "Complete the request described in request.json, then stop."
302
321
  ].filter(Boolean).join("\n");
303
322
  }
304
- async function loadConversationRequest(repoRoot, item) {
305
- const request = await deps.fs.readJson(resolveRepoPath(repoRoot, item.requestPath));
306
- const sourceText = typeof request.sourceText === "string" ? request.sourceText : "";
323
+ function loadConversationRequest(item) {
324
+ // Conversation source is carried inline on the queue item (queue.json); there
325
+ // is no per-job request.json to read.
326
+ const source = item.conversation;
327
+ const sourceText = typeof source?.sourceText === "string" ? source.sourceText : "";
307
328
  if (!sourceText.trim()) {
308
329
  throw new VcmError({
309
330
  code: "TRANSLATION_INPUT_EMPTY",
@@ -311,9 +332,7 @@ export function createTranslationWorkerService(deps) {
311
332
  statusCode: 400
312
333
  });
313
334
  }
314
- const job = isPartialConversationJob(request.job) ? request.job : undefined;
315
- const resultRelativePath = item.expectedResultPath ?? job?.resultPath;
316
- if (!resultRelativePath) {
335
+ if (!item.expectedResultPath) {
317
336
  throw new VcmError({
318
337
  code: "TRANSLATION_RESULT_PATH_MISSING",
319
338
  message: "Conversation translation result path is missing.",
@@ -321,17 +340,16 @@ export function createTranslationWorkerService(deps) {
321
340
  });
322
341
  }
323
342
  return {
324
- ...request,
325
- job,
326
- direction: typeof request.direction === "string" ? request.direction : job?.direction ?? "cc-output-to-user",
327
- sourceHash: typeof request.sourceHash === "string" ? request.sourceHash : job?.sourceHash,
328
- sourceLanguage: typeof request.sourceLanguage === "string" ? request.sourceLanguage : job?.sourceLanguage ?? "auto",
329
- targetLanguage: typeof request.targetLanguage === "string" ? request.targetLanguage : job?.targetLanguage ?? item.targetLanguage,
343
+ direction: source?.direction ?? "cc-output-to-user",
344
+ sourceHash: source?.sourceHash,
345
+ sourceLanguage: source?.sourceLanguage ?? "auto",
346
+ targetLanguage: item.targetLanguage,
347
+ contextText: source?.contextText,
330
348
  sourceText
331
349
  };
332
350
  }
333
- async function collectConversationBatchItems(repoRoot, queue, leader) {
334
- const leaderRequest = await loadConversationRequest(repoRoot, leader);
351
+ function collectConversationBatchItems(queue, leader) {
352
+ const leaderRequest = loadConversationRequest(leader);
335
353
  const leaderIndex = queue.items.findIndex((item) => item.id === leader.id);
336
354
  if (leaderIndex < 0) {
337
355
  return [leader];
@@ -343,7 +361,7 @@ export function createTranslationWorkerService(deps) {
343
361
  }
344
362
  const request = candidate.id === leader.id
345
363
  ? leaderRequest
346
- : await loadConversationRequest(repoRoot, candidate);
364
+ : loadConversationRequest(candidate);
347
365
  if (request.sourceLanguage !== leaderRequest.sourceLanguage ||
348
366
  request.targetLanguage !== leaderRequest.targetLanguage ||
349
367
  request.direction !== leaderRequest.direction) {
@@ -353,18 +371,27 @@ export function createTranslationWorkerService(deps) {
353
371
  }
354
372
  return items;
355
373
  }
356
- async function buildConversationBatchPrompt(repoRoot, items, batchResultPath) {
357
- const requests = await Promise.all(items.map((item) => loadConversationRequest(repoRoot, item)));
374
+ function buildConversationBatchPrompt(repoRoot, items, batchResultPath, batchId) {
375
+ // Instruct the Translator to write one self-describing result.json (shape
376
+ // ConversationBatchResultFile) to the shared result path, echoing the exact
377
+ // batchId so crash recovery can verify identity and map each result entry to
378
+ // a queue item by index.
379
+ const requests = items.map((item) => loadConversationRequest(item));
358
380
  const first = requests[0];
359
381
  const absoluteResultPath = resolveRepoPath(repoRoot, batchResultPath);
382
+ const resultTemplate = {
383
+ version: 1,
384
+ batchId,
385
+ results: requests.map((_, index) => ({
386
+ index: index + 1,
387
+ translatedText: "<translated text>"
388
+ }))
389
+ };
360
390
  return [
361
391
  `Translate each <VCM_TEXT> item from ${first.sourceLanguage} to ${first.targetLanguage}. Write all results to Result Path: ${absoluteResultPath}`,
362
392
  "",
363
- "Use this exact delimiter format between translated results:",
364
- "<VCM_RESULT1>",
365
- "translated text",
366
- "<VCM_RESULT2>",
367
- "translated text",
393
+ "Write a single JSON file in exactly this shape. Echo the batchId verbatim and provide one results entry per <VCM_TEXT> index:",
394
+ JSON.stringify(resultTemplate, null, 2),
368
395
  "",
369
396
  ...requests.flatMap((request, index) => [
370
397
  `<VCM_TEXT${index + 1}>`,
@@ -404,6 +431,106 @@ export function createTranslationWorkerService(deps) {
404
431
  "When finished, ensure the four memory files together are within budget, then stop."
405
432
  ].join("\n");
406
433
  }
434
+ // Recover an active queue item whose finalizing Stop/StopFailure hook never
435
+ // arrived. Returns true when the item was finalized (and `activeItemId`
436
+ // cleared) so the caller can dispatch the next queued item; returns false when
437
+ // the item is still legitimately in flight and the queue head must be held.
438
+ async function reconcileStuckActiveItem(repoRoot, active) {
439
+ if (await activeItemResultAvailable(repoRoot, active)) {
440
+ await validateActiveQueueItem(repoRoot);
441
+ return true;
442
+ }
443
+ if (active.type === "conversation" && isStaleActiveItem(active)) {
444
+ await validateActiveQueueItem(repoRoot);
445
+ return true;
446
+ }
447
+ return false;
448
+ }
449
+ async function activeItemResultAvailable(repoRoot, item) {
450
+ // For conversation items, defer to the all-or-nothing result.json predicate
451
+ // so a torn write or a leftover previous-batch file is treated as absent
452
+ // rather than mis-attributed. Non-conversation items keep the simple
453
+ // expected-output path-exists check.
454
+ if (item.type === "conversation") {
455
+ return conversationResultAvailable(repoRoot, item);
456
+ }
457
+ const resultPath = item.expectedResultPath;
458
+ if (!resultPath) {
459
+ return false;
460
+ }
461
+ return deps.fs.pathExists(resolveRepoPath(repoRoot, resultPath));
462
+ }
463
+ /**
464
+ * Reader-side all-or-nothing availability predicate for the shared
465
+ * conversation result.json. Returns true ONLY when the file exists, parses,
466
+ * its `batchId` equals the active item's `batchId`, AND every expected
467
+ * `batchIndex` of the active batch is present. Any failed clause ⇒ false
468
+ * (treated as "no result this cycle"), which guarantees a torn write or a
469
+ * stale previous-batch file is never mis-assigned to the active item. This is
470
+ * the safety core that preserves the issue #13 / KI-010 crash recovery under a
471
+ * single shared output file.
472
+ */
473
+ async function conversationResultAvailable(repoRoot, active) {
474
+ const parsed = await readMatchingConversationResult(repoRoot, active);
475
+ if (!parsed) {
476
+ return false;
477
+ }
478
+ const queue = await loadQueue(repoRoot);
479
+ const expectedIndexes = conversationBatchIndexes(queue, active.batchId);
480
+ if (expectedIndexes.length === 0) {
481
+ return false;
482
+ }
483
+ const presentIndexes = new Set(parsed.results.map((entry) => entry.index));
484
+ return expectedIndexes.every((index) => presentIndexes.has(index));
485
+ }
486
+ /**
487
+ * Read and parse the shared conversation result.json for the active item and
488
+ * confirm its self-describing `batchId` matches. Returns undefined when the
489
+ * file is absent, unparseable/torn, or identity-mismatched (foreign batch) so
490
+ * every caller treats those cases uniformly as "no result available".
491
+ */
492
+ async function readMatchingConversationResult(repoRoot, active) {
493
+ if (active.type !== "conversation" || !active.batchId || !active.batchResultPath) {
494
+ return undefined;
495
+ }
496
+ const resultPath = resolveRepoPath(repoRoot, active.batchResultPath);
497
+ if (!(await deps.fs.pathExists(resultPath))) {
498
+ return undefined;
499
+ }
500
+ const parsed = parseConversationResultFile(await deps.fs.readText(resultPath));
501
+ if (!parsed || parsed.batchId !== active.batchId) {
502
+ return undefined;
503
+ }
504
+ return parsed;
505
+ }
506
+ function conversationBatchIndexes(queue, batchId) {
507
+ if (!batchId) {
508
+ return [];
509
+ }
510
+ return queue.items
511
+ .filter((item) => item.type === "conversation" &&
512
+ item.batchId === batchId &&
513
+ ["dispatching", "running", "validating"].includes(item.status))
514
+ .map((item) => item.batchIndex ?? 0);
515
+ }
516
+ // Fallback read for a batched conversation item whose translatedText was not
517
+ // captured at finalize: parse the identity-matched shared result.json and pick
518
+ // the entry for this item's batchIndex. Returns "" when no matching result.
519
+ async function readBatchedConversationResult(repoRoot, item) {
520
+ const parsed = await readMatchingConversationResult(repoRoot, item);
521
+ if (!parsed) {
522
+ return "";
523
+ }
524
+ const entry = parsed.results.find((candidate) => candidate.index === item.batchIndex);
525
+ return entry?.translatedText ?? "";
526
+ }
527
+ function isStaleActiveItem(item) {
528
+ const updatedAtMs = Date.parse(item.updatedAt ?? "");
529
+ if (!Number.isFinite(updatedAtMs)) {
530
+ return true;
531
+ }
532
+ return Date.now() - updatedAtMs >= STALE_CONVERSATION_ITEM_MS;
533
+ }
407
534
  async function validateActiveQueueItem(repoRoot) {
408
535
  const queue = await loadQueue(repoRoot);
409
536
  const active = queue.activeItemId
@@ -455,14 +582,22 @@ export function createTranslationWorkerService(deps) {
455
582
  }
456
583
  queue.updatedAt = validatingAt;
457
584
  await saveQueue(repoRoot, queue);
458
- const batchResultPath = active.batchResultPath;
459
- const parsed = batchResultPath && await deps.fs.pathExists(resolveRepoPath(repoRoot, batchResultPath))
460
- ? parseConversationBatchResults(await deps.fs.readText(resolveRepoPath(repoRoot, batchResultPath)))
461
- : new Map();
585
+ // Read+parse the shared result.json and only apply results whose self-
586
+ // describing batchId matches the active batch; foreign or torn content yields
587
+ // an empty map so no item is completed from it. A missing expected index here
588
+ // is a genuine per-item failure: this finalize runs either on a Translator
589
+ // Stop/StopFailure hook (definitively done) or after the all-or-nothing
590
+ // availability predicate already confirmed every index is present (reconcile),
591
+ // so an absent index is never a still-writing batch.
592
+ const parsed = await readMatchingConversationResult(repoRoot, active);
593
+ const resultsByIndex = new Map();
594
+ for (const entry of parsed?.results ?? []) {
595
+ resultsByIndex.set(entry.index, entry.translatedText);
596
+ }
462
597
  const completedAt = now();
463
598
  for (const item of batchItems) {
464
599
  const index = item.batchIndex ?? 0;
465
- const translatedText = parsed.get(index)?.trim();
600
+ const translatedText = resultsByIndex.get(index)?.trim();
466
601
  if (translatedText) {
467
602
  item.status = "completed";
468
603
  item.translatedText = translatedText;
@@ -470,7 +605,7 @@ export function createTranslationWorkerService(deps) {
470
605
  }
471
606
  else {
472
607
  item.status = "failed";
473
- item.error = `Missing translated result for VCM_RESULT${index || "?"}.`;
608
+ item.error = `Missing translated result for batch index ${index || "?"}.`;
474
609
  }
475
610
  item.updatedAt = completedAt;
476
611
  }
@@ -1163,6 +1298,12 @@ export function createTranslationWorkerService(deps) {
1163
1298
  createdAt: timestamp,
1164
1299
  updatedAt: timestamp
1165
1300
  };
1301
+ // Carry the conversation source inline on the queue item instead of writing
1302
+ // a per-job request.json. `expectedResultPath` stays a UNIQUE per-job
1303
+ // logical lookup key (no file is written there); only `batchResultPath`
1304
+ // points at the shared physical CONVERSATION_RESULT_PATH, set in
1305
+ // prepareConversationBatch.
1306
+ const contextText = input.contextText?.trim() ? input.contextText : undefined;
1166
1307
  const queueItem = await enqueue(repoRoot, {
1167
1308
  id: `queue-${jobId}`,
1168
1309
  type: "conversation",
@@ -1171,41 +1312,37 @@ export function createTranslationWorkerService(deps) {
1171
1312
  taskSlug,
1172
1313
  jobId,
1173
1314
  requestPath: job.requestPath,
1174
- expectedResultPath: job.resultPath
1175
- });
1176
- job.queueItemId = queueItem.id;
1177
- await deps.fs.writeJsonAtomic(resolveRepoPath(repoRoot, job.requestPath), {
1178
- version: 1,
1179
- baseRepoRoot: repoRoot,
1180
- pathBase: "baseRepoRoot",
1181
- direction: input.direction,
1182
- sourceLanguage: job.sourceLanguage,
1183
- targetLanguage,
1184
- translationProfile: input.translationProfile?.trim() || DEFAULT_PROFILE,
1185
- sourceContentBoundary: "VCM_TEXT",
1186
- sourceText,
1187
- absolutePaths: {
1188
- requestPath: resolveRepoPath(repoRoot, job.requestPath),
1189
- resultPath: resolveRepoPath(repoRoot, job.resultPath)
1190
- },
1191
- outputContract: {
1192
- resultPath: job.resultPath,
1193
- absoluteResultPath: resolveRepoPath(repoRoot, job.resultPath),
1194
- format: "plain-text"
1315
+ expectedResultPath: job.resultPath,
1316
+ conversation: {
1317
+ direction: input.direction,
1318
+ sourceLanguage: job.sourceLanguage,
1319
+ sourceHash,
1320
+ sourceText,
1321
+ contextText
1195
1322
  }
1196
1323
  });
1324
+ job.queueItemId = queueItem.id;
1197
1325
  if (!input.deferDispatch) {
1198
1326
  void dispatchNext(repoRoot);
1199
1327
  }
1200
1328
  return job;
1201
1329
  },
1202
1330
  async validateConversationResult(repoRoot, input) {
1331
+ // The item lookup stays keyed on the unique `expectedResultPath` (unchanged
1332
+ // interface contract). When a finalized item already carries translatedText
1333
+ // it is authoritative; otherwise the fallback parses the shared result.json
1334
+ // (item.batchResultPath) and selects the entry matching this item's
1335
+ // batchId + batchIndex. With no matching queue item we read the given path
1336
+ // directly as a standalone result.
1203
1337
  const queue = await loadQueue(repoRoot);
1204
1338
  const item = queue.items.find((candidate) => candidate.type === "conversation" &&
1205
1339
  candidate.expectedResultPath === input.resultPath);
1206
1340
  const resultPath = resolveRepoPath(repoRoot, item?.batchResultPath ?? input.resultPath);
1207
1341
  assertInsideRepo(repoRoot, resultPath);
1208
- const translatedText = item?.translatedText ?? (await readStandaloneConversationResult(repoRoot, input.resultPath, deps.fs));
1342
+ const translatedText = item?.translatedText
1343
+ ?? (item
1344
+ ? await readBatchedConversationResult(repoRoot, item)
1345
+ : await readStandaloneConversationResult(repoRoot, input.resultPath, deps.fs));
1209
1346
  if (!translatedText.trim()) {
1210
1347
  throw invalidResult("Conversation translation result is empty.");
1211
1348
  }
@@ -1538,9 +1675,6 @@ function isBootstrapRun(value) {
1538
1675
  typeof candidate.targetLanguage === "string" &&
1539
1676
  Array.isArray(candidate.candidatePaths);
1540
1677
  }
1541
- function isPartialConversationJob(value) {
1542
- return typeof value === "object" && value !== null;
1543
- }
1544
1678
  function isFileTranslationChunk(value) {
1545
1679
  const candidate = value;
1546
1680
  return typeof candidate?.index === "number" &&
@@ -1808,21 +1942,43 @@ async function readStandaloneConversationResult(repoRoot, resultRelativePath, fs
1808
1942
  }
1809
1943
  return fs.readText(resultPath);
1810
1944
  }
1811
- function parseConversationBatchResults(text) {
1812
- const results = new Map();
1813
- const marker = /<VCM_RESULT(\d+)>/g;
1814
- const matches = [...text.matchAll(marker)];
1815
- for (let index = 0; index < matches.length; index += 1) {
1816
- const match = matches[index];
1817
- const itemIndex = Number(match[1]);
1818
- if (!Number.isInteger(itemIndex) || itemIndex < 1) {
1819
- continue;
1820
- }
1821
- const start = (match.index ?? 0) + match[0].length;
1822
- const end = matches[index + 1]?.index ?? text.length;
1823
- results.set(itemIndex, text.slice(start, end).trim());
1945
+ /**
1946
+ * Parse the shared conversation result.json into its structured form. Returns
1947
+ * undefined when the text is empty, not valid JSON, or does not match the
1948
+ * ConversationBatchResultFile shape (e.g. a truncated / torn write). Callers
1949
+ * MUST treat undefined as "no result available" never partially apply a
1950
+ * malformed file — so recovery degrades safely to stale-release.
1951
+ */
1952
+ function parseConversationResultFile(text) {
1953
+ if (!text.trim()) {
1954
+ return undefined;
1955
+ }
1956
+ let parsed;
1957
+ try {
1958
+ parsed = JSON.parse(text);
1959
+ }
1960
+ catch {
1961
+ return undefined;
1824
1962
  }
1825
- return results;
1963
+ return isConversationBatchResultFile(parsed) ? parsed : undefined;
1964
+ }
1965
+ function isConversationBatchResultFile(value) {
1966
+ if (typeof value !== "object" || value === null) {
1967
+ return false;
1968
+ }
1969
+ const candidate = value;
1970
+ return candidate.version === 1 &&
1971
+ typeof candidate.batchId === "string" &&
1972
+ candidate.batchId.length > 0 &&
1973
+ Array.isArray(candidate.results) &&
1974
+ candidate.results.every(isConversationBatchResultEntry);
1975
+ }
1976
+ function isConversationBatchResultEntry(value) {
1977
+ const candidate = value;
1978
+ return typeof candidate?.index === "number" &&
1979
+ Number.isInteger(candidate.index) &&
1980
+ candidate.index >= 1 &&
1981
+ typeof candidate.translatedText === "string";
1826
1982
  }
1827
1983
  function assertInsideRepo(repoRoot, absolutePath) {
1828
1984
  const relative = toRepoRelativePath(repoRoot, absolutePath);