vibe-coding-master 0.6.0 → 0.6.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.
@@ -5,7 +5,7 @@ export function registerProjectRoutes(app, deps) {
5
5
  });
6
6
  app.post("/api/projects/connect", async (request) => {
7
7
  const project = await deps.projectService.connectProject(request.body);
8
- await deps.translationWorkerService?.cleanupStartupRuntime(project.repoRoot);
8
+ await deps.runtimeRecoveryService?.recoverProject(project.repoRoot);
9
9
  return project;
10
10
  });
11
11
  app.get("/api/projects/current", async () => {
@@ -21,6 +21,8 @@ const COMMANDS_ALLOWED_WHEN_DISABLED = new Set([
21
21
  "projects",
22
22
  "tasks"
23
23
  ]);
24
+ class HandledGatewayInboundError extends Error {
25
+ }
24
26
  export function createGatewayService(deps) {
25
27
  const now = deps.now ?? (() => new Date().toISOString());
26
28
  const larkRegistration = deps.larkRegistration ?? createLarkRegistrationClient();
@@ -219,6 +221,10 @@ export function createGatewayService(deps) {
219
221
  return;
220
222
  }
221
223
  const command = parseGatewayCommand(update.text);
224
+ if (command.kind === "plain" && settings.enabled) {
225
+ await handlePlainInbound(update, command.text);
226
+ return;
227
+ }
222
228
  try {
223
229
  const output = await executeCommand(command, settings);
224
230
  await reply(await deps.settings.loadSettings(), update.fromUserId, output);
@@ -247,6 +253,69 @@ export function createGatewayService(deps) {
247
253
  });
248
254
  }
249
255
  }
256
+ async function handlePlainInbound(update, text) {
257
+ try {
258
+ const target = await resolvePlainTextPmTarget(text);
259
+ if (typeof target === "string") {
260
+ await reply(await deps.settings.loadSettings(), update.fromUserId, target);
261
+ await recordInbound(update, "ok", "plain");
262
+ return;
263
+ }
264
+ if (target.settings.translationEnabled) {
265
+ await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在翻译...");
266
+ }
267
+ else {
268
+ await reply(await deps.settings.loadSettings(), update.fromUserId, "已收到,正在发送给 PM...");
269
+ }
270
+ const englishText = target.settings.translationEnabled
271
+ ? await translatePlainTextForPm(target, text, update)
272
+ : text;
273
+ await submitTerminalInput(deps.runtime, target.session.id, englishText);
274
+ await reply(await deps.settings.loadSettings(), update.fromUserId, target.settings.translationEnabled
275
+ ? formatGatewayInputTranslationSuccess(englishText)
276
+ : "已发送给 PM。");
277
+ await recordInbound(update, "ok", "plain");
278
+ }
279
+ catch (error) {
280
+ if (error instanceof HandledGatewayInboundError) {
281
+ return;
282
+ }
283
+ const message = errorMessage(error);
284
+ await reply(await deps.settings.loadSettings(), update.fromUserId, `Error: ${message}`);
285
+ await recordInbound(update, "error", "plain", message);
286
+ }
287
+ }
288
+ async function translatePlainTextForPm(target, text, update) {
289
+ try {
290
+ return (await deps.translationService.translateUserInput({
291
+ repoRoot: target.project.repoRoot,
292
+ taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
293
+ taskSlug: target.task.taskSlug,
294
+ role: "project-manager",
295
+ text,
296
+ useContext: false,
297
+ send: false
298
+ })).englishPreview;
299
+ }
300
+ catch (error) {
301
+ const message = errorMessage(error);
302
+ await reply(await deps.settings.loadSettings(), update.fromUserId, formatGatewayInputTranslationFailure(message));
303
+ await recordInbound(update, "error", "plain", message);
304
+ throw new HandledGatewayInboundError();
305
+ }
306
+ }
307
+ async function recordInbound(update, result, command, error) {
308
+ await recordMessageStatus("inbound", result, update.text, error, command);
309
+ await deps.audit.record({
310
+ type: "gateway.command",
311
+ result,
312
+ messageId: update.messageId,
313
+ userId: update.fromUserId,
314
+ command,
315
+ preview: update.text,
316
+ error
317
+ });
318
+ }
250
319
  async function saveInboundMetadata(settings, update, options = {}) {
251
320
  const bindMode = options.bind ?? "never";
252
321
  return deps.settings.saveSettings({
@@ -633,6 +702,25 @@ export function createGatewayService(deps) {
633
702
  return lines.join("\n");
634
703
  }
635
704
  async function sendPlainTextToPm(text) {
705
+ const target = await resolvePlainTextPmTarget(text);
706
+ if (typeof target === "string") {
707
+ return target;
708
+ }
709
+ const englishText = target.settings.translationEnabled
710
+ ? (await deps.translationService.translateUserInput({
711
+ repoRoot: target.project.repoRoot,
712
+ taskRepoRoot: getTaskRuntimeRepoRoot(target.task),
713
+ taskSlug: target.task.taskSlug,
714
+ role: "project-manager",
715
+ text,
716
+ useContext: false,
717
+ send: false
718
+ })).englishPreview
719
+ : text;
720
+ await submitTerminalInput(deps.runtime, target.session.id, englishText);
721
+ return "Sent to PM.";
722
+ }
723
+ async function resolvePlainTextPmTarget(text) {
636
724
  if (!text.trim()) {
637
725
  return "Empty message ignored.";
638
726
  }
@@ -657,21 +745,13 @@ export function createGatewayService(deps) {
657
745
  if (session.activityStatus === "running") {
658
746
  return "PM is still working on the current turn. Please wait and send again later.";
659
747
  }
660
- const englishText = settings.translationEnabled
661
- ? (await deps.translationService.translateUserInput({
662
- repoRoot: project.repoRoot,
663
- taskRepoRoot: getTaskRuntimeRepoRoot(task),
664
- taskSlug: task.taskSlug,
665
- role: "project-manager",
666
- text,
667
- useContext: false,
668
- send: false
669
- })).englishPreview
670
- : text;
671
- await submitTerminalInput(deps.runtime, session.id, englishText);
672
- return "Sent to PM.";
748
+ return { project, task, session, settings };
673
749
  }
674
750
  async function reply(settings, userId, text) {
751
+ await sendGatewayText(settings, userId, text);
752
+ await recordMessageStatus("outbound", "ok", text);
753
+ }
754
+ async function sendGatewayText(settings, userId, text) {
675
755
  const account = toAccount(settings);
676
756
  if (!account) {
677
757
  return;
@@ -685,7 +765,6 @@ export function createGatewayService(deps) {
685
765
  contextToken,
686
766
  text
687
767
  });
688
- await recordMessageStatus("outbound", "ok", text);
689
768
  }
690
769
  async function recordMessageStatus(direction, result, preview, error, command) {
691
770
  const settings = await deps.settings.loadSettings();
@@ -1055,19 +1134,25 @@ export function createGatewayService(deps) {
1055
1134
  if (!text) {
1056
1135
  return;
1057
1136
  }
1058
- const output = await renderGatewayPmOutput({
1059
- settings,
1060
- repoRoot: input.repoRoot,
1061
- taskSlug: input.taskSlug,
1062
- sourceText: text
1063
- });
1064
- await resolveChannel(settings).sendText({
1065
- account,
1066
- toUserId: boundUserId,
1067
- chatId: settings.binding.chatIds[boundUserId] ?? settings.binding.homeChatId ?? undefined,
1068
- contextToken: settings.binding.contextTokens[boundUserId],
1069
- text: output.text
1070
- });
1137
+ const roundNotice = await getGatewayRoundNotice(input.repoRoot, input.taskSlug);
1138
+ const originalMessage = formatGatewayPmOriginalReply(text, roundNotice);
1139
+ await sendGatewayText(settings, boundUserId, originalMessage);
1140
+ let output;
1141
+ if (settings.translationEnabled) {
1142
+ output = await renderGatewayPmOutput({
1143
+ settings,
1144
+ repoRoot: input.repoRoot,
1145
+ taskSlug: input.taskSlug,
1146
+ sourceText: text,
1147
+ sourceEntryIds: nextEvents.map((event) => event.id)
1148
+ });
1149
+ await sendGatewayText(settings, boundUserId, output.translationFailed
1150
+ ? formatGatewayPmTranslationFailure(output.translationError)
1151
+ : formatGatewayPmTranslatedReply(output.text));
1152
+ }
1153
+ else {
1154
+ clearFailedTranslation(input.repoRoot, input.taskSlug);
1155
+ }
1071
1156
  const lastEvent = nextEvents.at(-1);
1072
1157
  const current = await deps.settings.loadSettings();
1073
1158
  await deps.settings.saveSettings({
@@ -1082,19 +1167,19 @@ export function createGatewayService(deps) {
1082
1167
  lastMessageStatus: {
1083
1168
  checkedAt: now(),
1084
1169
  direction: "outbound",
1085
- result: output.translationFailed ? "error" : "ok",
1170
+ result: output?.translationFailed ? "error" : "ok",
1086
1171
  command: "pm-stop",
1087
- preview: output.text.slice(0, 160),
1088
- error: output.translationError
1172
+ preview: (output?.text ?? originalMessage).slice(0, 160),
1173
+ error: output?.translationError
1089
1174
  },
1090
1175
  updatedAt: now()
1091
1176
  });
1092
1177
  await deps.audit.record({
1093
1178
  type: "gateway.pm_push",
1094
- result: output.translationFailed ? "error" : "ok",
1179
+ result: output?.translationFailed ? "error" : "ok",
1095
1180
  command: "pm-stop",
1096
- preview: output.text,
1097
- error: output.translationError
1181
+ preview: output ? `${originalMessage}\n\n${output.text}` : originalMessage,
1182
+ error: output?.translationError
1098
1183
  });
1099
1184
  },
1100
1185
  getDiagnostics() {
@@ -1109,12 +1194,75 @@ export function createGatewayService(deps) {
1109
1194
  }
1110
1195
  return settings.latestPmReplies[latestPmReplyKey(settings.currentProjectId, settings.currentTaskSlug)];
1111
1196
  }
1197
+ async function getGatewayRoundNotice(repoRoot, taskSlug) {
1198
+ try {
1199
+ const [projectConfig, task] = await Promise.all([
1200
+ deps.projectService.loadConfig(repoRoot),
1201
+ deps.taskService.loadTask(repoRoot, taskSlug)
1202
+ ]);
1203
+ const round = await deps.roundService.getSessionRoundState({
1204
+ repoRoot,
1205
+ stateRepoRoot: getTaskRuntimeRepoRoot(task),
1206
+ stateRoot: projectConfig.stateRoot,
1207
+ taskSlug
1208
+ });
1209
+ return formatGatewayRoundNotice(round);
1210
+ }
1211
+ catch {
1212
+ return undefined;
1213
+ }
1214
+ }
1215
+ function formatGatewayRoundNotice(round) {
1216
+ const roundLabel = round.roundSequence ? `第 ${round.roundSequence} 轮` : "当前 round";
1217
+ if (round.status === "stopped") {
1218
+ return `${roundLabel}已结束。现在需要你给出下一步指令。`;
1219
+ }
1220
+ const activeRole = round.activeRole ? `,当前角色:${round.activeRole}` : "";
1221
+ return `${roundLabel}运行中${activeRole}。`;
1222
+ }
1223
+ function formatGatewayPmOriginalReply(text, roundNotice) {
1224
+ return [
1225
+ "PM final reply 原文:",
1226
+ "",
1227
+ text.trim(),
1228
+ roundNotice ? "" : undefined,
1229
+ roundNotice ? `Round: ${roundNotice}` : undefined
1230
+ ].filter((line) => line !== undefined).join("\n");
1231
+ }
1232
+ function formatGatewayPmTranslatedReply(text) {
1233
+ return [
1234
+ "PM final reply 翻译:",
1235
+ "",
1236
+ text.trim()
1237
+ ].join("\n");
1238
+ }
1239
+ function formatGatewayPmTranslationFailure(error) {
1240
+ return [
1241
+ GATEWAY_TRANSLATION_FAILURE_TEXT,
1242
+ error ? `原因:${error}` : undefined
1243
+ ].filter((line) => line !== undefined).join("\n");
1244
+ }
1245
+ function formatGatewayInputTranslationSuccess(englishText) {
1246
+ return [
1247
+ "翻译完成,已发送给 PM:",
1248
+ "",
1249
+ englishText.trim()
1250
+ ].join("\n");
1251
+ }
1252
+ function formatGatewayInputTranslationFailure(error) {
1253
+ return [
1254
+ "翻译失败,消息未发送给 PM。",
1255
+ `原因:${error}`,
1256
+ "请重新输入后再试。"
1257
+ ].join("\n");
1258
+ }
1112
1259
  async function renderLatestPmReply(settings, reply) {
1113
1260
  const rendered = await renderGatewayPmOutput({
1114
1261
  settings,
1115
1262
  repoRoot: reply.repoRoot,
1116
1263
  taskSlug: reply.taskSlug,
1117
- sourceText: reply.text
1264
+ sourceText: reply.text,
1265
+ sourceEntryIds: reply.transcriptEventId ? [reply.transcriptEventId] : undefined
1118
1266
  });
1119
1267
  return {
1120
1268
  ...rendered,
@@ -1134,7 +1282,8 @@ export function createGatewayService(deps) {
1134
1282
  repoRoot: input.repoRoot,
1135
1283
  taskSlug: input.taskSlug,
1136
1284
  role: "project-manager",
1137
- text: input.sourceText
1285
+ text: input.sourceText,
1286
+ sourceEntryIds: input.sourceEntryIds
1138
1287
  });
1139
1288
  clearFailedTranslation(input.repoRoot, input.taskSlug);
1140
1289
  return {
@@ -28,6 +28,7 @@ export function createNodePtyTerminalRuntime(deps) {
28
28
  });
29
29
  const session = {
30
30
  id: sessionId,
31
+ repoRoot: input.repoRoot,
31
32
  taskSlug: input.taskSlug,
32
33
  role: input.role,
33
34
  status: "running",
@@ -31,9 +31,11 @@ import { createSessionService } from "./services/session-service.js";
31
31
  import { createMessageService } from "./services/message-service.js";
32
32
  import { createRoundService } from "./services/round-service.js";
33
33
  import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
34
+ import { createRuntimeRecoveryService } from "./services/runtime-recovery-service.js";
34
35
  import { createStatusService } from "./services/status-service.js";
35
36
  import { createTaskService } from "./services/task-service.js";
36
37
  import { createTaskLaunchService } from "./services/task-launch-service.js";
38
+ import { createTerminalInterruptService } from "./services/terminal-interrupt-service.js";
37
39
  import { createTranslationService } from "./services/translation-service.js";
38
40
  import { createDiagnosticsService } from "./services/diagnostics-service.js";
39
41
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
@@ -86,7 +88,7 @@ export async function createServer(deps, options = {}) {
86
88
  });
87
89
  registerProjectRoutes(app, {
88
90
  projectService: deps.projectService,
89
- translationWorkerService: deps.translationWorkerService
91
+ runtimeRecoveryService: deps.runtimeRecoveryService
90
92
  });
91
93
  registerHarnessRoutes(app, {
92
94
  projectService: deps.projectService,
@@ -143,7 +145,10 @@ export async function createServer(deps, options = {}) {
143
145
  translationService: deps.translationService
144
146
  });
145
147
  registerGatewayRoutes(app, { gatewayService: deps.gatewayService });
146
- registerTerminalWs(app, { runtime: deps.runtime });
148
+ registerTerminalWs(app, {
149
+ runtime: deps.runtime,
150
+ onManualInterrupt: (sessionId) => deps.terminalInterruptService.handleManualInterrupt(sessionId)
151
+ });
147
152
  app.addHook("onReady", async () => {
148
153
  await cleanupRecentTranslationRuntime(deps);
149
154
  await deps.gatewayService.start();
@@ -314,6 +319,13 @@ export function createDefaultServerDeps(options = {}) {
314
319
  return (await projectService.loadConfig(repoRoot)).stateRoot;
315
320
  }
316
321
  });
322
+ const runtimeRecoveryService = createRuntimeRecoveryService({
323
+ fs,
324
+ runtime,
325
+ projectService,
326
+ taskService,
327
+ translationWorkerService
328
+ });
317
329
  const claudeHookService = createClaudeHookService({
318
330
  projectService,
319
331
  taskService,
@@ -329,6 +341,13 @@ export function createDefaultServerDeps(options = {}) {
329
341
  jobGuard: createJobGuardService(),
330
342
  translationWorkerService
331
343
  });
344
+ const terminalInterruptService = createTerminalInterruptService({
345
+ runtime,
346
+ projectService,
347
+ taskService,
348
+ sessionService,
349
+ roundService
350
+ });
332
351
  const diagnosticsService = createDiagnosticsService({
333
352
  appRoot,
334
353
  runtime,
@@ -354,6 +373,8 @@ export function createDefaultServerDeps(options = {}) {
354
373
  translationService,
355
374
  gatewayService,
356
375
  runtimeCoordinator,
376
+ runtimeRecoveryService,
377
+ terminalInterruptService,
357
378
  runtime,
358
379
  diagnosticsService
359
380
  };
@@ -374,6 +374,11 @@ export function createClaudeHookService(deps) {
374
374
  }
375
375
  const stateInput = createRoundStateInput(context);
376
376
  const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
377
+ if (currentRoundState.stopReason === "manual-interrupt"
378
+ && currentRoundState.status === "stopped"
379
+ && currentRoundState.activeRole === input.role) {
380
+ return false;
381
+ }
377
382
  const previousAttempt = currentRoundState.roleRecovery?.role === input.role &&
378
383
  currentRoundState.roleRecovery.status !== "failed"
379
384
  ? currentRoundState.roleRecovery.attempt
@@ -246,6 +246,25 @@ export function createRoundService(deps) {
246
246
  recordClaudeHookEvent(input) {
247
247
  return recordRoleTurnEvent(input);
248
248
  },
249
+ async recordManualInterrupt(input) {
250
+ return withTaskLock(input, async () => {
251
+ const timestamp = now();
252
+ const state = await load(input);
253
+ const next = applyManualInterrupt({
254
+ state,
255
+ taskSlug: input.taskSlug,
256
+ role: input.role,
257
+ timestamp
258
+ });
259
+ if (!manualInterruptWasApplied(state, next, input.role)) {
260
+ return toSessionRoundState(state, timestamp);
261
+ }
262
+ await save(input, next);
263
+ clearSettleTimer(input);
264
+ await updateSessionStatus(input, next.currentRound?.status === "running" ? "running" : "stopped");
265
+ return toSessionRoundState(next, timestamp);
266
+ });
267
+ },
249
268
  async setRoleRecovery(input) {
250
269
  return withTaskLock(input, async () => {
251
270
  const timestamp = now();
@@ -327,6 +346,7 @@ function applyPromptSubmitted(input) {
327
346
  lastTurnStartedAt: input.timestamp,
328
347
  settleDeadlineAt: undefined,
329
348
  stoppedAt: undefined,
349
+ stopReason: undefined,
330
350
  activeTurnStartedAt: current.activeTurnStartedAt ?? input.timestamp,
331
351
  turnCount: current.turnCount + 1,
332
352
  roles: appendUniqueRole(current.roles, input.role)
@@ -360,6 +380,7 @@ function applyStop(input) {
360
380
  activeRole: input.role,
361
381
  lastTurnEndedAt: input.timestamp,
362
382
  settleDeadlineAt: addMilliseconds(input.timestamp, input.settleMs),
383
+ stopReason: undefined,
363
384
  activeTurnStartedAt: undefined,
364
385
  ccActiveMs: current.ccActiveMs + activeDurationMs,
365
386
  completedTurnCount: current.completedTurnCount + 1,
@@ -374,6 +395,47 @@ function applyStop(input) {
374
395
  updatedAt: input.timestamp
375
396
  };
376
397
  }
398
+ function applyManualInterrupt(input) {
399
+ const current = input.state.currentRound;
400
+ if (!current || current.status === "stopped" || !current.activeTurnStartedAt || current.activeRole !== input.role) {
401
+ return {
402
+ ...input.state,
403
+ taskSlug: input.taskSlug,
404
+ updatedAt: input.timestamp
405
+ };
406
+ }
407
+ const activeDurationMs = getDurationMs(current.activeTurnStartedAt, input.timestamp);
408
+ const stopped = {
409
+ ...current,
410
+ status: "stopped",
411
+ activeRole: input.role,
412
+ lastTurnEndedAt: input.timestamp,
413
+ stoppedAt: input.timestamp,
414
+ stopReason: "manual-interrupt",
415
+ settleDeadlineAt: undefined,
416
+ activeTurnStartedAt: undefined,
417
+ ccActiveMs: current.ccActiveMs + activeDurationMs,
418
+ completedTurnCount: current.completedTurnCount + 1,
419
+ roles: appendUniqueRole(current.roles, input.role)
420
+ };
421
+ return {
422
+ ...input.state,
423
+ taskSlug: input.taskSlug,
424
+ currentRound: stopped,
425
+ lastStoppedRound: stopped,
426
+ totalCompletedTurnCount: input.state.totalCompletedTurnCount + 1,
427
+ totalCcActiveMs: input.state.totalCcActiveMs + activeDurationMs,
428
+ pendingUserReply: undefined,
429
+ updatedAt: input.timestamp
430
+ };
431
+ }
432
+ function manualInterruptWasApplied(previous, next, role) {
433
+ return Boolean(previous.currentRound?.status === "running"
434
+ && previous.currentRound.activeTurnStartedAt
435
+ && previous.currentRound.activeRole === role
436
+ && next.currentRound?.stopReason === "manual-interrupt"
437
+ && next.currentRound.status === "stopped");
438
+ }
377
439
  function toSessionRoundState(state, updatedAt) {
378
440
  const current = state.currentRound;
379
441
  if (!current) {
@@ -407,6 +469,7 @@ function toSessionRoundState(state, updatedAt) {
407
469
  lastTurnEndedAt: current.lastTurnEndedAt,
408
470
  settleDeadlineAt: current.settleDeadlineAt,
409
471
  stoppedAt: current.stoppedAt,
472
+ stopReason: current.stopReason,
410
473
  activeTurnStartedAt: current.activeTurnStartedAt,
411
474
  roundSequence: current.sequence,
412
475
  turnCount: current.turnCount,
@@ -439,8 +502,10 @@ function computeFlowPause(current, roleRecovery, awaitingUser) {
439
502
  return undefined;
440
503
  }
441
504
  const roundStopped = Boolean(current) && current.status === "stopped" && Boolean(current.id);
505
+ const nonAlertingStop = roundStopped && (current.stopReason === "manual-interrupt" ||
506
+ current.stopReason === "runtime-recovery");
442
507
  if (roleRecovery?.status === "failed") {
443
- return roundStopped
508
+ return roundStopped && !nonAlertingStop
444
509
  ? {
445
510
  paused: true,
446
511
  reason: "role-recovery-failed",
@@ -459,7 +524,7 @@ function computeFlowPause(current, roleRecovery, awaitingUser) {
459
524
  messageTruncated: awaitingUser.messageTruncated
460
525
  };
461
526
  }
462
- if (roundStopped) {
527
+ if (roundStopped && !nonAlertingStop) {
463
528
  return {
464
529
  paused: true,
465
530
  reason: "stopped-no-next-turn",
@@ -633,6 +698,9 @@ function normalizeRound(input) {
633
698
  : typeof legacy.pausedAt === "string"
634
699
  ? legacy.pausedAt
635
700
  : undefined,
701
+ stopReason: input.stopReason === "manual-interrupt" || input.stopReason === "runtime-recovery"
702
+ ? input.stopReason
703
+ : undefined,
636
704
  activeTurnStartedAt: typeof input.activeTurnStartedAt === "string"
637
705
  ? input.activeTurnStartedAt
638
706
  : typeof legacy.runningSince === "string"