vibe-coding-master 0.7.22 → 0.7.23
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/server.js +1 -2
- package/dist/backend/services/claude-transcript-reply.js +5 -16
- package/dist/backend/services/claude-transcript-service.js +18 -2
- package/dist/backend/services/session-service.js +9 -6
- package/dist/backend/services/turn-reconciler-service.js +1 -65
- package/package.json +1 -1
package/dist/backend/server.js
CHANGED
|
@@ -393,8 +393,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
393
393
|
const turnReconciler = createTurnReconcilerService({
|
|
394
394
|
sessionService,
|
|
395
395
|
roundService,
|
|
396
|
-
claudeHookService
|
|
397
|
-
runtime
|
|
396
|
+
claudeHookService
|
|
398
397
|
});
|
|
399
398
|
const runtimeCoordinator = createRuntimeCoordinatorService({
|
|
400
399
|
appSettings,
|
|
@@ -42,7 +42,8 @@ export async function readTranscriptTextEvents(transcriptPath) {
|
|
|
42
42
|
id: event.id,
|
|
43
43
|
timestamp: event.timestamp,
|
|
44
44
|
text: event.text,
|
|
45
|
-
stopReason: event.stopReason
|
|
45
|
+
stopReason: event.stopReason,
|
|
46
|
+
...(event.isSidechain ? { isSidechain: true } : {})
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -56,12 +57,10 @@ export async function readTranscriptTurnEvidence(session) {
|
|
|
56
57
|
return {};
|
|
57
58
|
}
|
|
58
59
|
let raw;
|
|
59
|
-
let modifiedAt;
|
|
60
60
|
let handle;
|
|
61
61
|
try {
|
|
62
62
|
handle = await open(transcriptPath, "r");
|
|
63
63
|
const metadata = await handle.stat();
|
|
64
|
-
modifiedAt = metadata.mtime.toISOString();
|
|
65
64
|
const readLength = Math.min(metadata.size, TRANSCRIPT_EVIDENCE_TAIL_BYTES);
|
|
66
65
|
const readOffset = Math.max(0, metadata.size - readLength);
|
|
67
66
|
const buffer = Buffer.alloc(readLength);
|
|
@@ -79,7 +78,6 @@ export async function readTranscriptTurnEvidence(session) {
|
|
|
79
78
|
await handle?.close().catch(() => undefined);
|
|
80
79
|
}
|
|
81
80
|
const turnStartedAtMs = timestampMs(session.lastTurnStartedAt);
|
|
82
|
-
let lastActivityAt;
|
|
83
81
|
let completion;
|
|
84
82
|
for (const line of raw.split("\n")) {
|
|
85
83
|
if (!line.trim()) {
|
|
@@ -93,10 +91,7 @@ export async function readTranscriptTurnEvidence(session) {
|
|
|
93
91
|
continue;
|
|
94
92
|
}
|
|
95
93
|
const timestamp = typeof record.timestamp === "string" ? record.timestamp : undefined;
|
|
96
|
-
if (
|
|
97
|
-
lastActivityAt = timestamp;
|
|
98
|
-
}
|
|
99
|
-
if (record.type !== "assistant" || !timestamp) {
|
|
94
|
+
if (record.type !== "assistant" || record.isSidechain === true || !timestamp) {
|
|
100
95
|
continue;
|
|
101
96
|
}
|
|
102
97
|
const message = record.message;
|
|
@@ -115,13 +110,7 @@ export async function readTranscriptTurnEvidence(session) {
|
|
|
115
110
|
};
|
|
116
111
|
}
|
|
117
112
|
}
|
|
118
|
-
|
|
119
|
-
lastActivityAt = modifiedAt;
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
...(lastActivityAt ? { lastActivityAt } : {}),
|
|
123
|
-
...(completion ? { completion } : {})
|
|
124
|
-
};
|
|
113
|
+
return completion ? { completion } : {};
|
|
125
114
|
}
|
|
126
115
|
/** True for a text event that completed a turn (assistant stopped of its own accord). */
|
|
127
116
|
export function isFinalTurnTextEvent(event) {
|
|
@@ -138,7 +127,7 @@ export function selectLatestTurnReply(events, session, maxLength = MAX_TURN_REPL
|
|
|
138
127
|
}
|
|
139
128
|
const startMs = timestampMs(session.lastTurnStartedAt);
|
|
140
129
|
const endMs = timestampMs(session.lastTurnEndedAt);
|
|
141
|
-
const finalEvents = events.filter(isFinalTurnTextEvent);
|
|
130
|
+
const finalEvents = events.filter((event) => !event.isSidechain && isFinalTurnTextEvent(event));
|
|
142
131
|
const selected = startMs === undefined
|
|
143
132
|
? finalEvents.slice(-1)
|
|
144
133
|
: finalEvents.filter((event) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync, watch as fsWatch } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
const DEFAULT_TAIL_POLL_INTERVAL_MS = 1000;
|
|
5
5
|
/**
|
|
6
6
|
* Adapted from CodingForMoney/cc-pm's transcript tailer.
|
|
@@ -204,7 +204,9 @@ export function createClaudeTranscriptService() {
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
export function resolveExistingClaudeTranscriptPath(session) {
|
|
207
|
-
const sessionPath =
|
|
207
|
+
const sessionPath = transcriptPathMatchesSession(session.transcriptPath, session.claudeSessionId)
|
|
208
|
+
? existingFile(session.transcriptPath)
|
|
209
|
+
: undefined;
|
|
208
210
|
if (sessionPath) {
|
|
209
211
|
return sessionPath;
|
|
210
212
|
}
|
|
@@ -214,6 +216,11 @@ export function resolveExistingClaudeTranscriptPath(session) {
|
|
|
214
216
|
}
|
|
215
217
|
return findClaudeTranscriptPathBySessionId(session.claudeSessionId, session.claudeConfigDir);
|
|
216
218
|
}
|
|
219
|
+
function transcriptPathMatchesSession(transcriptPath, claudeSessionId) {
|
|
220
|
+
return Boolean(transcriptPath
|
|
221
|
+
&& (!claudeSessionId
|
|
222
|
+
|| basename(transcriptPath) === `${claudeSessionId}.jsonl`));
|
|
223
|
+
}
|
|
217
224
|
export function findClaudeTranscriptPathBySessionId(claudeSessionId, configDir) {
|
|
218
225
|
const root = claudeProjectsRoot(configDir);
|
|
219
226
|
let projectDirs;
|
|
@@ -296,6 +303,7 @@ export function parseAssistantContent(line) {
|
|
|
296
303
|
const rawTools = [];
|
|
297
304
|
const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : new Date().toISOString();
|
|
298
305
|
const uuid = typeof obj.uuid === "string" ? obj.uuid : undefined;
|
|
306
|
+
const isSidechain = obj.isSidechain === true;
|
|
299
307
|
const rawStopReason = message?.stop_reason;
|
|
300
308
|
const stopReason = typeof rawStopReason === "string" ? rawStopReason : undefined;
|
|
301
309
|
for (const entry of content) {
|
|
@@ -352,6 +360,7 @@ export function parseAssistantContent(line) {
|
|
|
352
360
|
timestamp,
|
|
353
361
|
text,
|
|
354
362
|
id: uuid ?? `${timestamp}-${text.slice(0, 16)}`,
|
|
363
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
355
364
|
...(stopReason !== undefined ? { stopReason } : {})
|
|
356
365
|
});
|
|
357
366
|
}
|
|
@@ -362,6 +371,7 @@ export function parseAssistantContent(line) {
|
|
|
362
371
|
timestamp,
|
|
363
372
|
text,
|
|
364
373
|
id: uuid ? `${uuid}#thinking` : `${timestamp}-thinking-${text.slice(0, 16)}`,
|
|
374
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
365
375
|
...(stopReason !== undefined ? { stopReason } : {})
|
|
366
376
|
});
|
|
367
377
|
}
|
|
@@ -370,6 +380,7 @@ export function parseAssistantContent(line) {
|
|
|
370
380
|
kind: "question",
|
|
371
381
|
timestamp,
|
|
372
382
|
id: question.id,
|
|
383
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
373
384
|
question: question.payload
|
|
374
385
|
});
|
|
375
386
|
}
|
|
@@ -378,6 +389,7 @@ export function parseAssistantContent(line) {
|
|
|
378
389
|
kind: "todo",
|
|
379
390
|
timestamp,
|
|
380
391
|
id: todo.id,
|
|
392
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
381
393
|
todo: todo.payload
|
|
382
394
|
});
|
|
383
395
|
}
|
|
@@ -386,6 +398,7 @@ export function parseAssistantContent(line) {
|
|
|
386
398
|
kind: "agent",
|
|
387
399
|
timestamp,
|
|
388
400
|
id: agent.id,
|
|
401
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
389
402
|
agent: agent.payload
|
|
390
403
|
});
|
|
391
404
|
}
|
|
@@ -394,6 +407,7 @@ export function parseAssistantContent(line) {
|
|
|
394
407
|
kind: "tool_use",
|
|
395
408
|
timestamp,
|
|
396
409
|
id: tool.id,
|
|
410
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
397
411
|
toolUse: tool.payload
|
|
398
412
|
});
|
|
399
413
|
}
|
|
@@ -406,6 +420,7 @@ function parseUserToolResults(obj) {
|
|
|
406
420
|
return [];
|
|
407
421
|
}
|
|
408
422
|
const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : new Date().toISOString();
|
|
423
|
+
const isSidechain = obj.isSidechain === true;
|
|
409
424
|
const out = [];
|
|
410
425
|
for (const entry of content) {
|
|
411
426
|
if (!entry || typeof entry !== "object") {
|
|
@@ -423,6 +438,7 @@ function parseUserToolResults(obj) {
|
|
|
423
438
|
kind: "tool_result",
|
|
424
439
|
timestamp,
|
|
425
440
|
id: `${toolUseId}#result`,
|
|
441
|
+
...(isSidechain ? { isSidechain: true } : {}),
|
|
426
442
|
toolResult: {
|
|
427
443
|
tool_use_id: toolUseId,
|
|
428
444
|
content: block.content,
|
|
@@ -1172,16 +1172,17 @@ function toRoleSessionRecordView(record, runtime) {
|
|
|
1172
1172
|
};
|
|
1173
1173
|
}
|
|
1174
1174
|
export function matchesRoleHookSession(record, input) {
|
|
1175
|
+
if (input.runtimeSessionId) {
|
|
1176
|
+
return record.id === input.runtimeSessionId;
|
|
1177
|
+
}
|
|
1175
1178
|
if (!record.claudeSessionId
|
|
1176
1179
|
&& !record.transcriptPath
|
|
1177
1180
|
&& input.eventName !== "UserPromptSubmit") {
|
|
1178
1181
|
return false;
|
|
1179
1182
|
}
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
if (record.runtimeSessionToken) {
|
|
1184
|
-
return record.runtimeSessionToken === input.runtimeSessionToken;
|
|
1183
|
+
if (record.runtimeSessionToken
|
|
1184
|
+
&& record.runtimeSessionToken !== input.runtimeSessionToken) {
|
|
1185
|
+
return false;
|
|
1185
1186
|
}
|
|
1186
1187
|
if (!record.claudeSessionId && !record.transcriptPath) {
|
|
1187
1188
|
return input.eventName === "UserPromptSubmit";
|
|
@@ -1189,7 +1190,9 @@ export function matchesRoleHookSession(record, input) {
|
|
|
1189
1190
|
if (input.sessionId && record.claudeSessionId === input.sessionId) {
|
|
1190
1191
|
return true;
|
|
1191
1192
|
}
|
|
1192
|
-
if (input.transcriptPath
|
|
1193
|
+
if (input.transcriptPath
|
|
1194
|
+
&& record.transcriptPath
|
|
1195
|
+
&& samePath(record.transcriptPath, input.transcriptPath)) {
|
|
1193
1196
|
return true;
|
|
1194
1197
|
}
|
|
1195
1198
|
return false;
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
|
|
2
2
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
3
|
-
export const TURN_STALL_THRESHOLD_MS = 30 * 60_000;
|
|
4
|
-
export const TURN_INTERRUPT_GRACE_MS = 10_000;
|
|
5
3
|
export function createTurnReconcilerService(deps) {
|
|
6
|
-
const now = deps.now ?? (() => new Date().toISOString());
|
|
7
|
-
const stallThresholdMs = deps.stallThresholdMs ?? TURN_STALL_THRESHOLD_MS;
|
|
8
|
-
const interruptGraceMs = deps.interruptGraceMs ?? TURN_INTERRUPT_GRACE_MS;
|
|
9
4
|
const readEvidence = deps.readTranscriptEvidence ?? readTranscriptTurnEvidence;
|
|
10
|
-
const pendingInterrupts = new Map();
|
|
11
5
|
return {
|
|
12
6
|
async reconcileTask(repoRoot, task, stateRoot) {
|
|
13
7
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -21,17 +15,14 @@ export function createTurnReconcilerService(deps) {
|
|
|
21
15
|
|| !round.activeRole
|
|
22
16
|
|| !round.activeTurnStartedAt
|
|
23
17
|
|| round.roleRecovery) {
|
|
24
|
-
clearTaskInterrupts(repoRoot, task.taskSlug);
|
|
25
18
|
return { status: "inactive" };
|
|
26
19
|
}
|
|
27
20
|
const role = round.activeRole;
|
|
28
|
-
const interruptKey = `${repoRoot}:${task.taskSlug}:${role}`;
|
|
29
21
|
const session = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, role);
|
|
30
22
|
const evidence = session
|
|
31
23
|
? await readEvidence({ ...session, lastTurnStartedAt: round.activeTurnStartedAt })
|
|
32
24
|
: {};
|
|
33
25
|
if (evidence.completion) {
|
|
34
|
-
pendingInterrupts.delete(interruptKey);
|
|
35
26
|
await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "Stop", {
|
|
36
27
|
vcm_reconcile_reason: "transcript-end-turn",
|
|
37
28
|
vcm_completion_id: evidence.completion.id,
|
|
@@ -40,7 +31,6 @@ export function createTurnReconcilerService(deps) {
|
|
|
40
31
|
return { status: "completed", role, reason: "transcript-end-turn" };
|
|
41
32
|
}
|
|
42
33
|
if (!session || session.status !== "running") {
|
|
43
|
-
pendingInterrupts.delete(interruptKey);
|
|
44
34
|
const reason = session ? "terminal-session-exited" : "terminal-session-missing";
|
|
45
35
|
await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "StopFailure", {
|
|
46
36
|
error: reason.replaceAll("-", "_"),
|
|
@@ -48,44 +38,9 @@ export function createTurnReconcilerService(deps) {
|
|
|
48
38
|
}));
|
|
49
39
|
return { status: "failed", role, reason };
|
|
50
40
|
}
|
|
51
|
-
|
|
52
|
-
round.activeTurnStartedAt,
|
|
53
|
-
session.lastHookEventAt,
|
|
54
|
-
session.lastOutputAt,
|
|
55
|
-
evidence.lastActivityAt
|
|
56
|
-
]);
|
|
57
|
-
const currentTime = now();
|
|
58
|
-
if (!isStale(lastActivityAt, currentTime, stallThresholdMs)) {
|
|
59
|
-
pendingInterrupts.delete(interruptKey);
|
|
60
|
-
return { status: "active" };
|
|
61
|
-
}
|
|
62
|
-
const pendingInterrupt = pendingInterrupts.get(interruptKey);
|
|
63
|
-
if (!pendingInterrupt || pendingInterrupt.turnStartedAt !== round.activeTurnStartedAt) {
|
|
64
|
-
deps.runtime.write(session.id, "\u0003");
|
|
65
|
-
pendingInterrupts.set(interruptKey, {
|
|
66
|
-
turnStartedAt: round.activeTurnStartedAt,
|
|
67
|
-
requestedAt: currentTime
|
|
68
|
-
});
|
|
69
|
-
return { status: "active" };
|
|
70
|
-
}
|
|
71
|
-
if (!isStale(pendingInterrupt.requestedAt, currentTime, interruptGraceMs)) {
|
|
72
|
-
return { status: "active" };
|
|
73
|
-
}
|
|
74
|
-
pendingInterrupts.delete(interruptKey);
|
|
75
|
-
await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "StopFailure", {
|
|
76
|
-
error: "turn_stalled",
|
|
77
|
-
error_details: `No hook, terminal output, or transcript activity was observed for ${stallThresholdMs}ms.`
|
|
78
|
-
}));
|
|
79
|
-
return { status: "failed", role, reason: "turn-stalled" };
|
|
41
|
+
return { status: "active" };
|
|
80
42
|
}
|
|
81
43
|
};
|
|
82
|
-
function clearTaskInterrupts(repoRoot, taskSlug) {
|
|
83
|
-
for (const key of pendingInterrupts.keys()) {
|
|
84
|
-
if (key.startsWith(`${repoRoot}:${taskSlug}:`)) {
|
|
85
|
-
pendingInterrupts.delete(key);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
44
|
}
|
|
90
45
|
function buildReconciledHook(taskSlug, role, session, eventName, evidence) {
|
|
91
46
|
return {
|
|
@@ -102,22 +57,3 @@ function buildReconciledHook(taskSlug, role, session, eventName, evidence) {
|
|
|
102
57
|
}
|
|
103
58
|
};
|
|
104
59
|
}
|
|
105
|
-
function latestTimestamp(values) {
|
|
106
|
-
return values.reduce((latest, value) => {
|
|
107
|
-
if (!value) {
|
|
108
|
-
return latest;
|
|
109
|
-
}
|
|
110
|
-
const valueMs = Date.parse(value);
|
|
111
|
-
const latestMs = latest ? Date.parse(latest) : Number.NaN;
|
|
112
|
-
return Number.isFinite(valueMs) && (!Number.isFinite(latestMs) || valueMs > latestMs)
|
|
113
|
-
? value
|
|
114
|
-
: latest;
|
|
115
|
-
}, undefined);
|
|
116
|
-
}
|
|
117
|
-
function isStale(lastActivityAt, currentTime, thresholdMs) {
|
|
118
|
-
const activityMs = lastActivityAt ? Date.parse(lastActivityAt) : Number.NaN;
|
|
119
|
-
const currentMs = Date.parse(currentTime);
|
|
120
|
-
return Number.isFinite(activityMs)
|
|
121
|
-
&& Number.isFinite(currentMs)
|
|
122
|
-
&& currentMs - activityMs >= thresholdMs;
|
|
123
|
-
}
|