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.
- package/dist/backend/api/project-routes.js +1 -1
- package/dist/backend/gateway/gateway-service.js +184 -35
- package/dist/backend/runtime/node-pty-runtime.js +1 -0
- package/dist/backend/server.js +23 -2
- package/dist/backend/services/claude-hook-service.js +5 -0
- package/dist/backend/services/round-service.js +70 -2
- package/dist/backend/services/runtime-recovery-service.js +322 -0
- package/dist/backend/services/session-service.js +88 -15
- package/dist/backend/services/terminal-interrupt-service.js +29 -0
- package/dist/backend/services/translation-service.js +148 -0
- package/dist/backend/services/translation-worker-service.js +54 -142
- package/dist/backend/templates/harness/claude-root.js +5 -5
- package/dist/backend/templates/harness/gate-review.js +2 -2
- package/dist/backend/templates/harness/project-manager-agent.js +2 -1
- package/dist/backend/ws/terminal-ws.js +11 -5
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
|
1170
|
+
result: output?.translationFailed ? "error" : "ok",
|
|
1086
1171
|
command: "pm-stop",
|
|
1087
|
-
preview: output
|
|
1088
|
-
error: output
|
|
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
|
|
1179
|
+
result: output?.translationFailed ? "error" : "ok",
|
|
1095
1180
|
command: "pm-stop",
|
|
1096
|
-
preview: output.text,
|
|
1097
|
-
error: output
|
|
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 {
|
package/dist/backend/server.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
|
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"
|