vibe-coding-master 0.7.24 → 0.7.26
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/runtime/node-pty-runtime.js +18 -3
- package/dist/backend/server.js +11 -7
- package/dist/backend/services/claude-hook-service.js +0 -12
- package/dist/backend/services/gate-review-service.js +4 -19
- package/dist/backend/services/round-service.js +40 -2
- package/dist/backend/services/runtime-coordinator-service.js +0 -2
- package/dist/backend/services/session-service.js +59 -0
- package/dist/backend/services/terminal-process-exit-service.js +43 -0
- package/package.json +1 -1
- package/dist/backend/services/turn-reconciler-service.js +0 -59
|
@@ -3,6 +3,7 @@ import { VcmError } from "../errors.js";
|
|
|
3
3
|
export const TERMINAL_REPLAY_TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
4
4
|
export function createNodePtyTerminalRuntime(deps) {
|
|
5
5
|
const entries = new Map();
|
|
6
|
+
const processExitListeners = new Set();
|
|
6
7
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
7
8
|
const id = deps.id ?? (() => `session_${Date.now()}_${Math.random().toString(16).slice(2)}`);
|
|
8
9
|
const emit = (entry, event) => {
|
|
@@ -68,6 +69,12 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
68
69
|
if (!disposeEntry(entries, entry, exitCode === 0 ? "exited" : "crashed", exitCode)) {
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
72
|
+
for (const listener of processExitListeners) {
|
|
73
|
+
listener({
|
|
74
|
+
session: { ...entry.session },
|
|
75
|
+
exitCode
|
|
76
|
+
});
|
|
77
|
+
}
|
|
71
78
|
emit(entry, {
|
|
72
79
|
sessionId: session.id,
|
|
73
80
|
taskSlug: input.taskSlug,
|
|
@@ -117,14 +124,16 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
117
124
|
},
|
|
118
125
|
async stop(sessionId) {
|
|
119
126
|
const entry = getEntry(entries, sessionId);
|
|
127
|
+
const exitCode = entry.session.exitCode ?? null;
|
|
128
|
+
const disposed = disposeEntry(entries, entry, "exited", exitCode);
|
|
120
129
|
entry.process.kill();
|
|
121
|
-
if (
|
|
130
|
+
if (disposed) {
|
|
122
131
|
emit(entry, {
|
|
123
132
|
sessionId,
|
|
124
133
|
taskSlug: entry.session.taskSlug,
|
|
125
134
|
role: entry.session.role,
|
|
126
135
|
type: "exit",
|
|
127
|
-
exitCode
|
|
136
|
+
exitCode
|
|
128
137
|
});
|
|
129
138
|
entry.listeners.clear();
|
|
130
139
|
}
|
|
@@ -132,8 +141,8 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
132
141
|
},
|
|
133
142
|
async restart(sessionId) {
|
|
134
143
|
const entry = getEntry(entries, sessionId);
|
|
135
|
-
entry.process.kill();
|
|
136
144
|
disposeEntry(entries, entry, "exited", entry.session.exitCode ?? null);
|
|
145
|
+
entry.process.kill();
|
|
137
146
|
entry.listeners.clear();
|
|
138
147
|
await entry.logWriter.close();
|
|
139
148
|
return create(entry.input, sessionId);
|
|
@@ -175,6 +184,12 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
175
184
|
return () => {
|
|
176
185
|
entry.listeners.delete(listener);
|
|
177
186
|
};
|
|
187
|
+
},
|
|
188
|
+
subscribeProcessExits(listener) {
|
|
189
|
+
processExitListeners.add(listener);
|
|
190
|
+
return () => {
|
|
191
|
+
processExitListeners.delete(listener);
|
|
192
|
+
};
|
|
178
193
|
}
|
|
179
194
|
};
|
|
180
195
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -42,9 +42,9 @@ import { createTaskCloseService } from "./services/task-close-service.js";
|
|
|
42
42
|
import { createTaskWorkflowService } from "./services/task-workflow-service.js";
|
|
43
43
|
import { createTaskLaunchService } from "./services/task-launch-service.js";
|
|
44
44
|
import { createTerminalInterruptService } from "./services/terminal-interrupt-service.js";
|
|
45
|
+
import { createTerminalProcessExitService } from "./services/terminal-process-exit-service.js";
|
|
45
46
|
import { createTranslationService } from "./services/translation-service.js";
|
|
46
47
|
import { createUsageAnalyticsService } from "./services/usage-analytics-service.js";
|
|
47
|
-
import { createTurnReconcilerService } from "./services/turn-reconciler-service.js";
|
|
48
48
|
import { createDiagnosticsService } from "./services/diagnostics-service.js";
|
|
49
49
|
import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
|
|
50
50
|
import { registerArtifactRoutes } from "./api/artifact-routes.js";
|
|
@@ -174,10 +174,12 @@ export async function createServer(deps, options = {}) {
|
|
|
174
174
|
app.addHook("onReady", async () => {
|
|
175
175
|
await deps.ccrIntegration.initialize();
|
|
176
176
|
await cleanupRecentTranslationRuntime(deps);
|
|
177
|
+
deps.terminalProcessExitService.start();
|
|
177
178
|
deps.runtimeCoordinator.start();
|
|
178
179
|
await deps.gatewayService.start();
|
|
179
180
|
});
|
|
180
181
|
app.addHook("onClose", async () => {
|
|
182
|
+
deps.terminalProcessExitService.stop();
|
|
181
183
|
deps.runtimeCoordinator.stop();
|
|
182
184
|
await deps.gatewayService.stop();
|
|
183
185
|
});
|
|
@@ -390,11 +392,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
390
392
|
translationWorkerService,
|
|
391
393
|
architectRestartService
|
|
392
394
|
});
|
|
393
|
-
const turnReconciler = createTurnReconcilerService({
|
|
394
|
-
sessionService,
|
|
395
|
-
roundService,
|
|
396
|
-
claudeHookService
|
|
397
|
-
});
|
|
398
395
|
const runtimeCoordinator = createRuntimeCoordinatorService({
|
|
399
396
|
appSettings,
|
|
400
397
|
projectService,
|
|
@@ -406,7 +403,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
406
403
|
autoMemoryService,
|
|
407
404
|
roundService,
|
|
408
405
|
gatewayService,
|
|
409
|
-
turnReconciler,
|
|
410
406
|
async getStateRoot(repoRoot) {
|
|
411
407
|
return (await projectService.loadConfig(repoRoot)).stateRoot;
|
|
412
408
|
}
|
|
@@ -418,6 +414,13 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
418
414
|
sessionService,
|
|
419
415
|
roundService
|
|
420
416
|
});
|
|
417
|
+
const terminalProcessExitService = createTerminalProcessExitService({
|
|
418
|
+
runtime,
|
|
419
|
+
projectService,
|
|
420
|
+
taskService,
|
|
421
|
+
sessionService,
|
|
422
|
+
roundService
|
|
423
|
+
});
|
|
421
424
|
const diagnosticsService = createDiagnosticsService({
|
|
422
425
|
appRoot,
|
|
423
426
|
runtime,
|
|
@@ -450,6 +453,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
450
453
|
runtimeCoordinator,
|
|
451
454
|
runtimeRecoveryService,
|
|
452
455
|
terminalInterruptService,
|
|
456
|
+
terminalProcessExitService,
|
|
453
457
|
runtime,
|
|
454
458
|
diagnosticsService,
|
|
455
459
|
usageAnalyticsService
|
|
@@ -837,18 +837,6 @@ export function createClaudeHookService(deps) {
|
|
|
837
837
|
return processStopHook(input, { allowBlock: true });
|
|
838
838
|
});
|
|
839
839
|
},
|
|
840
|
-
handleReconciledTurnEnd(input) {
|
|
841
|
-
return withRoleHookLock(input, async () => {
|
|
842
|
-
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
843
|
-
if (eventName === "Stop") {
|
|
844
|
-
return processStopHook(input, { allowBlock: false });
|
|
845
|
-
}
|
|
846
|
-
if (eventName === "StopFailure") {
|
|
847
|
-
return processStopFailureHook(input);
|
|
848
|
-
}
|
|
849
|
-
throwUnsupportedEvent(eventName);
|
|
850
|
-
});
|
|
851
|
-
},
|
|
852
840
|
handlePermissionRequestHook
|
|
853
841
|
};
|
|
854
842
|
}
|
|
@@ -12,7 +12,6 @@ const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
|
|
|
12
12
|
const GATE_REVIEW_VERSION = 1;
|
|
13
13
|
const REVIEWER_ROLE = "reviewer";
|
|
14
14
|
const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
|
|
15
|
-
const DEFAULT_REPORT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
16
15
|
const activeRuns = new Set();
|
|
17
16
|
const ARCHITECTURE_ANALYSIS_FIELDS = [
|
|
18
17
|
"Evidence Read",
|
|
@@ -92,7 +91,6 @@ const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
|
92
91
|
export function createGateReviewService(deps) {
|
|
93
92
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
94
93
|
const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
|
|
95
|
-
const reportTimeoutMs = deps.reportTimeoutMs ?? DEFAULT_REPORT_TIMEOUT_MS;
|
|
96
94
|
async function getContext(repoRoot, taskSlug) {
|
|
97
95
|
const projectConfig = await deps.projectService.loadConfig(repoRoot);
|
|
98
96
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -457,10 +455,7 @@ export function createGateReviewService(deps) {
|
|
|
457
455
|
role: REVIEWER_ROLE,
|
|
458
456
|
eventName: "UserPromptSubmit"
|
|
459
457
|
});
|
|
460
|
-
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(),
|
|
461
|
-
intervalMs: reportPollIntervalMs,
|
|
462
|
-
timeoutMs: reportTimeoutMs
|
|
463
|
-
});
|
|
458
|
+
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), reportPollIntervalMs);
|
|
464
459
|
const completedAt = now();
|
|
465
460
|
await updateRequestStatus(deps.fs, context, requestId, "completed", {
|
|
466
461
|
completedAt,
|
|
@@ -1093,28 +1088,18 @@ Decision: approve|request_changes
|
|
|
1093
1088
|
Summary: <one or two sentences>
|
|
1094
1089
|
[/VCM GATE REVIEW]`;
|
|
1095
1090
|
}
|
|
1096
|
-
async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp,
|
|
1097
|
-
|
|
1098
|
-
let lastError;
|
|
1099
|
-
while (Date.now() - startedAt <= options.timeoutMs) {
|
|
1091
|
+
async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs) {
|
|
1092
|
+
while (true) {
|
|
1100
1093
|
try {
|
|
1101
1094
|
return await parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp);
|
|
1102
1095
|
}
|
|
1103
1096
|
catch (error) {
|
|
1104
|
-
lastError = error;
|
|
1105
1097
|
if (!isPendingReportError(error)) {
|
|
1106
1098
|
throw error;
|
|
1107
1099
|
}
|
|
1108
1100
|
}
|
|
1109
|
-
await delay(
|
|
1101
|
+
await delay(intervalMs);
|
|
1110
1102
|
}
|
|
1111
|
-
const detail = errorMessage(lastError);
|
|
1112
|
-
throw new VcmError({
|
|
1113
|
-
code: "GATE_REVIEW_REPORT_TIMEOUT",
|
|
1114
|
-
message: `Reviewer did not produce a valid ${gate} report within ${Math.round(options.timeoutMs / 1000)}s.`,
|
|
1115
|
-
statusCode: 504,
|
|
1116
|
-
hint: detail
|
|
1117
|
-
});
|
|
1118
1103
|
}
|
|
1119
1104
|
async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
|
|
1120
1105
|
const reportPath = reportPathForGate(gate);
|
|
@@ -261,6 +261,25 @@ export function createRoundService(deps) {
|
|
|
261
261
|
return toSessionRoundState(next, timestamp);
|
|
262
262
|
});
|
|
263
263
|
},
|
|
264
|
+
async recordTerminalExit(input) {
|
|
265
|
+
return withTaskLock(input, async () => {
|
|
266
|
+
const timestamp = now();
|
|
267
|
+
const state = await load(input);
|
|
268
|
+
const next = applyTerminalExit({
|
|
269
|
+
state,
|
|
270
|
+
taskSlug: input.taskSlug,
|
|
271
|
+
role: input.role,
|
|
272
|
+
timestamp
|
|
273
|
+
});
|
|
274
|
+
if (!terminalExitWasApplied(state, next, input.role)) {
|
|
275
|
+
return toSessionRoundState(state, timestamp);
|
|
276
|
+
}
|
|
277
|
+
await save(input, next);
|
|
278
|
+
clearSettleTimer(input);
|
|
279
|
+
await updateSessionStatus(input, "stopped");
|
|
280
|
+
return toSessionRoundState(next, timestamp);
|
|
281
|
+
});
|
|
282
|
+
},
|
|
264
283
|
async setRoleRecovery(input) {
|
|
265
284
|
return withTaskLock(input, async () => {
|
|
266
285
|
const timestamp = now();
|
|
@@ -392,6 +411,16 @@ function applyStop(input) {
|
|
|
392
411
|
};
|
|
393
412
|
}
|
|
394
413
|
function applyManualInterrupt(input) {
|
|
414
|
+
return applyForcedTurnStop(input, "manual-interrupt");
|
|
415
|
+
}
|
|
416
|
+
function applyTerminalExit(input) {
|
|
417
|
+
const stopped = applyForcedTurnStop(input, "terminal-exit");
|
|
418
|
+
return stopped.currentRound?.stopReason === "terminal-exit"
|
|
419
|
+
&& stopped.roleRecovery?.role === input.role
|
|
420
|
+
? { ...stopped, roleRecovery: undefined }
|
|
421
|
+
: stopped;
|
|
422
|
+
}
|
|
423
|
+
function applyForcedTurnStop(input, stopReason) {
|
|
395
424
|
const current = input.state.currentRound;
|
|
396
425
|
if (!current || current.status === "stopped" || !current.activeTurnStartedAt || current.activeRole !== input.role) {
|
|
397
426
|
return {
|
|
@@ -407,7 +436,7 @@ function applyManualInterrupt(input) {
|
|
|
407
436
|
activeRole: input.role,
|
|
408
437
|
lastTurnEndedAt: input.timestamp,
|
|
409
438
|
stoppedAt: input.timestamp,
|
|
410
|
-
stopReason
|
|
439
|
+
stopReason,
|
|
411
440
|
settleDeadlineAt: undefined,
|
|
412
441
|
activeTurnStartedAt: undefined,
|
|
413
442
|
ccActiveMs: current.ccActiveMs + activeDurationMs,
|
|
@@ -431,6 +460,13 @@ function manualInterruptWasApplied(previous, next, role) {
|
|
|
431
460
|
&& next.currentRound?.stopReason === "manual-interrupt"
|
|
432
461
|
&& next.currentRound.status === "stopped");
|
|
433
462
|
}
|
|
463
|
+
function terminalExitWasApplied(previous, next, role) {
|
|
464
|
+
return Boolean(previous.currentRound?.status === "running"
|
|
465
|
+
&& previous.currentRound.activeTurnStartedAt
|
|
466
|
+
&& previous.currentRound.activeRole === role
|
|
467
|
+
&& next.currentRound?.stopReason === "terminal-exit"
|
|
468
|
+
&& next.currentRound.status === "stopped");
|
|
469
|
+
}
|
|
434
470
|
function toSessionRoundState(state, updatedAt) {
|
|
435
471
|
const current = state.currentRound;
|
|
436
472
|
if (!current) {
|
|
@@ -587,7 +623,9 @@ function normalizeRound(input) {
|
|
|
587
623
|
: typeof legacy.pausedAt === "string"
|
|
588
624
|
? legacy.pausedAt
|
|
589
625
|
: undefined,
|
|
590
|
-
stopReason: input.stopReason === "manual-interrupt"
|
|
626
|
+
stopReason: input.stopReason === "manual-interrupt"
|
|
627
|
+
|| input.stopReason === "runtime-recovery"
|
|
628
|
+
|| input.stopReason === "terminal-exit"
|
|
591
629
|
? input.stopReason
|
|
592
630
|
: undefined,
|
|
593
631
|
activeTurnStartedAt: typeof input.activeTurnStartedAt === "string"
|
|
@@ -45,8 +45,6 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
45
45
|
return { activeTask: null, gatewayStatus };
|
|
46
46
|
}
|
|
47
47
|
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
48
|
-
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
49
|
-
await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
|
|
50
48
|
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
51
49
|
.then((status) => status.initialized)
|
|
52
50
|
.catch(() => false);
|
|
@@ -712,6 +712,44 @@ export function createSessionService(deps) {
|
|
|
712
712
|
await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
713
713
|
return updated;
|
|
714
714
|
}
|
|
715
|
+
async function recordProjectToolProcessExit(repoRoot, current, input, persist) {
|
|
716
|
+
const timestamp = now();
|
|
717
|
+
const turnWasRunning = current.activityStatus === "running";
|
|
718
|
+
const updated = {
|
|
719
|
+
...current,
|
|
720
|
+
status: input.status,
|
|
721
|
+
activityStatus: "idle",
|
|
722
|
+
pid: undefined,
|
|
723
|
+
exitCode: input.exitCode,
|
|
724
|
+
lastTurnEndedAt: turnWasRunning ? timestamp : current.lastTurnEndedAt,
|
|
725
|
+
updatedAt: timestamp
|
|
726
|
+
};
|
|
727
|
+
deps.registry.upsert(updated);
|
|
728
|
+
await persist(deps.fs, repoRoot, updated);
|
|
729
|
+
return { record: updated, turnWasRunning };
|
|
730
|
+
}
|
|
731
|
+
async function recordTaskRoleProcessExit(repoRoot, taskSlug, role, input) {
|
|
732
|
+
const current = await getTaskRoleSessionView(repoRoot, taskSlug, role);
|
|
733
|
+
if (!current || current.id !== input.sessionId) {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
const timestamp = now();
|
|
737
|
+
const turnWasRunning = current.activityStatus === "running";
|
|
738
|
+
const updated = {
|
|
739
|
+
...current,
|
|
740
|
+
status: input.status,
|
|
741
|
+
activityStatus: "idle",
|
|
742
|
+
pid: undefined,
|
|
743
|
+
exitCode: input.exitCode,
|
|
744
|
+
lastTurnEndedAt: turnWasRunning ? timestamp : current.lastTurnEndedAt,
|
|
745
|
+
updatedAt: timestamp
|
|
746
|
+
};
|
|
747
|
+
deps.registry.upsert(updated);
|
|
748
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
749
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
750
|
+
await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
751
|
+
return { record: updated, turnWasRunning };
|
|
752
|
+
}
|
|
715
753
|
return {
|
|
716
754
|
async assertModelLaunchReady(model = "default") {
|
|
717
755
|
await getModelLaunchEnvironment(normalizeClaudeModel(model));
|
|
@@ -1113,6 +1151,27 @@ export function createSessionService(deps) {
|
|
|
1113
1151
|
}
|
|
1114
1152
|
return markTaskRoleActivityIdle(repoRoot, taskSlug, role, sessionId);
|
|
1115
1153
|
},
|
|
1154
|
+
async recordTerminalProcessExit(repoRoot, input) {
|
|
1155
|
+
const registered = deps.registry.get(input.sessionId);
|
|
1156
|
+
const role = registered?.role;
|
|
1157
|
+
const taskSlug = registered?.taskSlug;
|
|
1158
|
+
if (!role || !taskSlug) {
|
|
1159
|
+
return undefined;
|
|
1160
|
+
}
|
|
1161
|
+
if (role === TRANSLATOR_ROLE && taskSlug === PROJECT_TRANSLATOR_SCOPE) {
|
|
1162
|
+
const current = await getProjectToolSessionView(repoRoot, TRANSLATOR_ROLE);
|
|
1163
|
+
return current?.id === input.sessionId
|
|
1164
|
+
? recordProjectToolProcessExit(repoRoot, current, input, persistTranslatorSession)
|
|
1165
|
+
: undefined;
|
|
1166
|
+
}
|
|
1167
|
+
if (role === HARNESS_ENGINEER_ROLE && taskSlug === PROJECT_HARNESS_ENGINEER_SCOPE) {
|
|
1168
|
+
const current = await getProjectToolSessionView(repoRoot, HARNESS_ENGINEER_ROLE);
|
|
1169
|
+
return current?.id === input.sessionId
|
|
1170
|
+
? recordProjectToolProcessExit(repoRoot, current, input, persistHarnessEngineerSession)
|
|
1171
|
+
: undefined;
|
|
1172
|
+
}
|
|
1173
|
+
return recordTaskRoleProcessExit(repoRoot, taskSlug, role, input);
|
|
1174
|
+
},
|
|
1116
1175
|
async markRoleActivityRunning(repoRoot, taskSlug, role, expectedSessionId) {
|
|
1117
1176
|
const current = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
1118
1177
|
if (!current || (expectedSessionId && current.id !== expectedSessionId)) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
3
|
+
export function createTerminalProcessExitService(deps) {
|
|
4
|
+
let unsubscribe;
|
|
5
|
+
async function handleProcessExit(event) {
|
|
6
|
+
const repoRoot = event.session.repoRoot;
|
|
7
|
+
if (!repoRoot) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const recorded = await deps.sessionService.recordTerminalProcessExit(repoRoot, {
|
|
11
|
+
sessionId: event.session.id,
|
|
12
|
+
status: event.session.status === "crashed" ? "crashed" : "exited",
|
|
13
|
+
exitCode: event.exitCode
|
|
14
|
+
});
|
|
15
|
+
if (!recorded?.turnWasRunning || !isVcmRoleName(recorded.record.role)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
19
|
+
const task = await deps.taskService.loadTask(repoRoot, recorded.record.taskSlug);
|
|
20
|
+
await deps.roundService.recordTerminalExit({
|
|
21
|
+
repoRoot,
|
|
22
|
+
stateRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
23
|
+
stateRoot: config.stateRoot,
|
|
24
|
+
taskSlug: recorded.record.taskSlug,
|
|
25
|
+
role: recorded.record.role
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
start() {
|
|
30
|
+
if (unsubscribe) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
unsubscribe = deps.runtime.subscribeProcessExits((event) => {
|
|
34
|
+
void handleProcessExit(event).catch(() => undefined);
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
stop() {
|
|
38
|
+
unsubscribe?.();
|
|
39
|
+
unsubscribe = undefined;
|
|
40
|
+
},
|
|
41
|
+
handleProcessExit
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
|
|
2
|
-
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
3
|
-
export function createTurnReconcilerService(deps) {
|
|
4
|
-
const readEvidence = deps.readTranscriptEvidence ?? readTranscriptTurnEvidence;
|
|
5
|
-
return {
|
|
6
|
-
async reconcileTask(repoRoot, task, stateRoot) {
|
|
7
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
8
|
-
const round = await deps.roundService.getSessionRoundState({
|
|
9
|
-
repoRoot,
|
|
10
|
-
stateRepoRoot: taskRepoRoot,
|
|
11
|
-
stateRoot,
|
|
12
|
-
taskSlug: task.taskSlug
|
|
13
|
-
});
|
|
14
|
-
if (round.status !== "running"
|
|
15
|
-
|| !round.activeRole
|
|
16
|
-
|| !round.activeTurnStartedAt
|
|
17
|
-
|| round.roleRecovery) {
|
|
18
|
-
return { status: "inactive" };
|
|
19
|
-
}
|
|
20
|
-
const role = round.activeRole;
|
|
21
|
-
const session = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, role);
|
|
22
|
-
const evidence = session
|
|
23
|
-
? await readEvidence({ ...session, lastTurnStartedAt: round.activeTurnStartedAt })
|
|
24
|
-
: {};
|
|
25
|
-
if (evidence.completion) {
|
|
26
|
-
await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "Stop", {
|
|
27
|
-
vcm_reconcile_reason: "transcript-end-turn",
|
|
28
|
-
vcm_completion_id: evidence.completion.id,
|
|
29
|
-
vcm_completion_at: evidence.completion.timestamp
|
|
30
|
-
}));
|
|
31
|
-
return { status: "completed", role, reason: "transcript-end-turn" };
|
|
32
|
-
}
|
|
33
|
-
if (!session || session.status !== "running") {
|
|
34
|
-
const reason = session ? "terminal-session-exited" : "terminal-session-missing";
|
|
35
|
-
await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "StopFailure", {
|
|
36
|
-
error: reason.replaceAll("-", "_"),
|
|
37
|
-
error_details: `VCM reconciled an active turn because its ${reason.replaceAll("-", " ")}.`
|
|
38
|
-
}));
|
|
39
|
-
return { status: "failed", role, reason };
|
|
40
|
-
}
|
|
41
|
-
return { status: "active" };
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function buildReconciledHook(taskSlug, role, session, eventName, evidence) {
|
|
46
|
-
return {
|
|
47
|
-
taskSlug,
|
|
48
|
-
role,
|
|
49
|
-
event: {
|
|
50
|
-
hook_event_name: eventName,
|
|
51
|
-
...(session?.claudeSessionId ? { session_id: session.claudeSessionId } : {}),
|
|
52
|
-
...(session?.transcriptPath ? { transcript_path: session.transcriptPath } : {}),
|
|
53
|
-
...(session?.cwd ? { cwd: session.cwd } : {}),
|
|
54
|
-
...(session?.id ? { vcm_runtime_session_id: session.id } : {}),
|
|
55
|
-
vcm_reconciled: true,
|
|
56
|
-
...evidence
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
}
|