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.
@@ -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 (timestamp && isLaterTimestamp(timestamp, lastActivityAt)) {
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
- if (isLaterTimestamp(modifiedAt, lastActivityAt)) {
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 = existingFile(session.transcriptPath);
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 (input.runtimeSessionId) {
1181
- return record.id === input.runtimeSessionId;
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 && record.transcriptPath === 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
- const lastActivityAt = latestTimestamp([
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.7.22",
3
+ "version": "0.7.23",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [