vibe-coding-master 0.0.14 → 0.0.15

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.
@@ -1,20 +1,24 @@
1
1
  import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
- import { ROLE_NAMES } from "../../shared/constants.js";
3
+ import { ROLE_NAMES, isRoleName } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
7
- import { renderManualStagePrompt, renderMessageEnvelope } from "../templates/message-envelope.js";
7
+ import { renderMessageEnvelope } from "../templates/message-envelope.js";
8
8
  const PM_ROLE = "project-manager";
9
9
  const PM_TO_ROLE_TYPES = new Set(["task", "question", "review-request", "revise", "cancel"]);
10
10
  const ROLE_TO_PM_TYPES = new Set(["result", "question", "blocked", "finding"]);
11
- const MARK_ALL_DONE_STATUSES = new Set([
11
+ const OPEN_MESSAGE_STATUSES = new Set([
12
12
  "pending_approval",
13
13
  "queued",
14
- "staged",
15
- "delivering",
16
14
  "submitted",
17
- "delivered",
15
+ "failed",
16
+ "delivery_failed",
17
+ "response_ready_missing_result"
18
+ ]);
19
+ const MUTABLE_ROUTE_MESSAGE_STATUSES = new Set([
20
+ "pending_approval",
21
+ "queued",
18
22
  "failed",
19
23
  "delivery_failed",
20
24
  "response_ready_missing_result"
@@ -29,153 +33,145 @@ export function createMessageService(deps) {
29
33
  return {
30
34
  taskSlug: input.taskSlug,
31
35
  mode: "manual",
32
- paused: false,
33
36
  updatedAt: now()
34
37
  };
35
38
  }
36
- return deps.fs.readJson(statePath);
39
+ const state = await deps.fs.readJson(statePath);
40
+ return {
41
+ taskSlug: state.taskSlug,
42
+ mode: state.mode,
43
+ updatedAt: state.updatedAt
44
+ };
37
45
  }
38
- async function sendMessageLocked(input) {
46
+ async function scanLocked(input) {
39
47
  await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
40
- validateMessagePolicy(input.fromRole, input.toRole, input.type);
41
48
  const timestamp = now();
42
- const message = {
43
- id: id(),
44
- taskSlug: input.taskSlug,
45
- fromRole: input.fromRole,
46
- toRole: input.toRole,
47
- type: input.type,
48
- body: input.body,
49
- artifactRefs: input.artifactRefs ?? [],
50
- parentMessageId: input.parentMessageId,
51
- status: "queued",
52
- createdAt: timestamp
53
- };
54
- message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
55
- let messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
56
- if (isRoleActor(input.fromRole)) {
57
- const acknowledged = acknowledgeActiveMessages(messages, input.fromRole, timestamp);
58
- for (const acknowledgedMessage of acknowledged) {
59
- await appendMessageSnapshot(deps.fs, input, acknowledgedMessage);
49
+ const base = toRouteContext(input);
50
+ const state = await getOrchestrationState(input);
51
+ const messagesPath = getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug);
52
+ let messages = await readLatestMessages(deps.fs, messagesPath);
53
+ const updates = [];
54
+ if (input.stoppedRole) {
55
+ const acknowledged = acknowledgeActiveMessages(messages, input.stoppedRole, timestamp);
56
+ for (const message of acknowledged) {
57
+ await appendMessageSnapshot(deps.fs, input, message);
60
58
  }
59
+ updates.push(...acknowledged);
61
60
  messages = applyMessageSnapshots(messages, acknowledged);
62
61
  }
63
- const state = await getOrchestrationState(input);
64
- const result = await routeOutboundMessage(input, message, messages, state, timestamp);
65
- await appendMessageSnapshot(deps.fs, input, result.message);
66
- if (isRoleActor(input.fromRole)) {
67
- await deliverNextQueuedMessage(input, input.fromRole);
62
+ const pendingFiles = (await listRouteFiles(deps.fs, base)).filter((routeFile) => routeFile.pending);
63
+ const candidates = selectDispatchCandidates(pendingFiles, input.stoppedRole);
64
+ const results = [];
65
+ const deliveredTargets = new Set();
66
+ for (const routeFile of candidates) {
67
+ if (deliveredTargets.has(routeFile.toRole)) {
68
+ continue;
69
+ }
70
+ const result = await dispatchRouteFile(input, routeFile, messages, state, timestamp);
71
+ results.push(result);
72
+ await appendMessageSnapshot(deps.fs, input, result.message);
73
+ messages = applyMessageSnapshots(messages, [result.message]);
74
+ if (result.delivered) {
75
+ deliveredTargets.add(routeFile.toRole);
76
+ }
68
77
  }
69
- return result;
78
+ return results;
79
+ }
80
+ async function listPendingRouteFiles(input) {
81
+ const context = toRouteContext(input);
82
+ const all = await listRouteFiles(deps.fs, context);
83
+ return all.filter((routeFile) => routeFile.pending);
70
84
  }
71
- async function routeOutboundMessage(input, message, existingMessages, state, timestamp) {
72
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
85
+ async function dispatchRouteFile(input, routeFile, existingMessages, state, timestamp) {
86
+ validateMessagePolicy(routeFile.fromRole, routeFile.toRole, routeFile.type);
87
+ const existingOpen = findOpenMessageForRoute(existingMessages, routeFile.path);
88
+ const message = {
89
+ ...(existingOpen ?? {
90
+ id: id(),
91
+ taskSlug: input.taskSlug,
92
+ createdAt: timestamp
93
+ }),
94
+ fromRole: routeFile.fromRole,
95
+ toRole: routeFile.toRole,
96
+ type: routeFile.type,
97
+ body: routeFile.body,
98
+ artifactRefs: routeFile.artifactRefs,
99
+ bodyPath: routeFile.path,
100
+ routePath: routeFile.path,
101
+ status: "queued",
102
+ queuedBehindMessageId: undefined,
103
+ failureReason: undefined
104
+ };
105
+ const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, routeFile.toRole);
73
106
  if (!session || session.status !== "running") {
74
- const queued = {
75
- ...message,
76
- status: "queued",
77
- failureReason: `${input.toRole} session is not running.`
107
+ return {
108
+ message: {
109
+ ...message,
110
+ status: "queued",
111
+ failureReason: `${routeFile.toRole} session is not running.`
112
+ },
113
+ delivered: false,
114
+ requiresUserApproval: false,
115
+ clearedRouteFile: false
78
116
  };
79
- return { message: queued, delivered: false, requiresUserApproval: false };
80
117
  }
81
118
  if (state.mode === "manual") {
82
- const pending = {
83
- ...message,
84
- status: "pending_approval"
85
- };
86
- return { message: pending, delivered: false, requiresUserApproval: true };
87
- }
88
- if (state.paused) {
89
- const queued = {
90
- ...message,
91
- status: "queued",
92
- failureReason: "Auto orchestration is paused."
119
+ return {
120
+ message: {
121
+ ...message,
122
+ status: "pending_approval"
123
+ },
124
+ delivered: false,
125
+ requiresUserApproval: true,
126
+ clearedRouteFile: false
93
127
  };
94
- return { message: queued, delivered: false, requiresUserApproval: false };
95
128
  }
96
- const activeMessage = findActiveMessageForRole(existingMessages, input.toRole);
129
+ const activeMessage = findActiveMessageForRole(existingMessages, routeFile.toRole);
97
130
  if (activeMessage) {
98
- const queued = {
99
- ...message,
100
- status: "queued",
101
- queuedBehindMessageId: activeMessage.id,
102
- failureReason: `${input.toRole} is still handling VCM message ${activeMessage.id}.`
131
+ return {
132
+ message: {
133
+ ...message,
134
+ status: "queued",
135
+ queuedBehindMessageId: activeMessage.id,
136
+ failureReason: `${routeFile.toRole} is still handling VCM message ${activeMessage.id}.`
137
+ },
138
+ delivered: false,
139
+ requiresUserApproval: false,
140
+ clearedRouteFile: false
103
141
  };
104
- return { message: queued, delivered: false, requiresUserApproval: false };
105
- }
106
- const delivered = await deliverMessageToSession(session.id, message, timestamp);
107
- return { message: delivered, delivered: true, requiresUserApproval: false };
108
- }
109
- async function deliverNextQueuedMessage(input, targetRole) {
110
- const state = await getOrchestrationState(input);
111
- if (state.mode !== "auto" || state.paused) {
112
- return undefined;
113
- }
114
- const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
115
- if (findActiveMessageForRole(messages, targetRole)) {
116
- return undefined;
117
- }
118
- const nextMessage = messages.find((message) => message.toRole === targetRole && message.status === "queued");
119
- if (!nextMessage) {
120
- return undefined;
121
142
  }
122
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, targetRole);
123
- if (!session || session.status !== "running") {
124
- return undefined;
125
- }
126
- const delivered = await deliverMessageToSession(session.id, nextMessage, now());
127
- await appendMessageSnapshot(deps.fs, input, delivered);
128
- return delivered;
129
- }
130
- async function deliverMessageToSession(sessionId, message, timestamp) {
131
- const delivering = {
143
+ const submitted = {
132
144
  ...message,
133
- status: "delivering",
145
+ status: "submitted",
134
146
  deliveredAt: timestamp,
135
- queuedBehindMessageId: undefined,
136
- failureReason: undefined
147
+ submittedAt: timestamp
148
+ };
149
+ await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(submitted));
150
+ await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole);
151
+ await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
152
+ return {
153
+ message: submitted,
154
+ delivered: true,
155
+ requiresUserApproval: false,
156
+ clearedRouteFile: true
137
157
  };
138
- await submitTerminalInput(deps.runtime, sessionId, renderMessageEnvelope(delivering));
139
- return delivering;
140
158
  }
141
159
  return {
142
160
  listMessages(input) {
143
161
  return readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
144
162
  },
145
- async sendMessage(input) {
146
- return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => sendMessageLocked(input));
163
+ listPendingRouteFiles(input) {
164
+ return listPendingRouteFiles(input);
147
165
  },
148
- async confirmPromptSubmitted(input) {
149
- const messageId = extractSubmittedMessageId(input.prompt);
150
- if (!messageId) {
151
- return undefined;
152
- }
153
- const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
154
- const message = messages.find((candidate) => candidate.id === messageId && candidate.toRole === input.role);
155
- if (!message) {
156
- return undefined;
157
- }
158
- if (message.status === "submitted" || message.status === "acknowledged") {
159
- return message;
160
- }
161
- if (!isSubmittableMessageStatus(message.status)) {
162
- return undefined;
163
- }
164
- const submitted = {
165
- ...message,
166
- status: "submitted",
167
- submittedAt: now(),
168
- failureReason: undefined
169
- };
170
- await appendMessageSnapshot(deps.fs, input, submitted);
171
- return submitted;
166
+ async scanAndDispatchPendingRouteFiles(input) {
167
+ return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => scanLocked(input));
172
168
  },
173
169
  async markAllDone(input) {
174
170
  return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
175
171
  const timestamp = now();
176
172
  const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
177
173
  const updated = messages
178
- .filter((message) => MARK_ALL_DONE_STATUSES.has(message.status))
174
+ .filter((message) => OPEN_MESSAGE_STATUSES.has(message.status))
179
175
  .map((message) => ({
180
176
  ...message,
181
177
  status: "acknowledged",
@@ -186,6 +182,11 @@ export function createMessageService(deps) {
186
182
  for (const message of updated) {
187
183
  await appendMessageSnapshot(deps.fs, input, message);
188
184
  }
185
+ if (input.clearRouteFiles) {
186
+ for (const routeFile of await listPendingRouteFiles(input)) {
187
+ await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
188
+ }
189
+ }
189
190
  const latest = applyMessageSnapshots(messages, updated);
190
191
  return {
191
192
  taskSlug: input.taskSlug,
@@ -194,40 +195,6 @@ export function createMessageService(deps) {
194
195
  };
195
196
  });
196
197
  },
197
- async stageMessage(input) {
198
- const message = await getMessageOrThrow(deps.fs, input);
199
- const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, message.toRole);
200
- if (!session || session.status !== "running") {
201
- throw new VcmError({
202
- code: "MESSAGE_TARGET_NOT_RUNNING",
203
- message: `${message.toRole} session is not running.`,
204
- statusCode: 409,
205
- hint: `Start the ${message.toRole} session before staging this message.`
206
- });
207
- }
208
- const staged = {
209
- ...message,
210
- status: "staged",
211
- stagedAt: now(),
212
- failureReason: undefined
213
- };
214
- deps.runtime.write(session.id, renderManualStagePrompt(staged));
215
- await appendMessageSnapshot(deps.fs, input, staged);
216
- return staged;
217
- },
218
- approveMessage(input) {
219
- return this.stageMessage(input);
220
- },
221
- async rejectMessage(input) {
222
- const message = await getMessageOrThrow(deps.fs, input);
223
- const rejected = {
224
- ...message,
225
- status: "rejected",
226
- failureReason: undefined
227
- };
228
- await appendMessageSnapshot(deps.fs, input, rejected);
229
- return rejected;
230
- },
231
198
  async getOrchestrationState(input) {
232
199
  return getOrchestrationState(input);
233
200
  },
@@ -236,7 +203,6 @@ export function createMessageService(deps) {
236
203
  const next = {
237
204
  ...current,
238
205
  mode: input.mode ?? current.mode,
239
- paused: input.paused ?? current.paused,
240
206
  updatedAt: now()
241
207
  };
242
208
  await deps.fs.writeJsonAtomic(getOrchestrationStatePath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), next);
@@ -244,6 +210,132 @@ export function createMessageService(deps) {
244
210
  }
245
211
  };
246
212
  }
213
+ function toRouteContext(input) {
214
+ return {
215
+ repoRoot: input.repoRoot,
216
+ taskRepoRoot: input.taskRepoRoot,
217
+ handoffDir: input.handoffDir
218
+ };
219
+ }
220
+ async function listRouteFiles(fs, input) {
221
+ const repoRoot = input.taskRepoRoot ?? input.repoRoot;
222
+ const routeDir = path.posix.join(input.handoffDir, "messages");
223
+ const absoluteMessagesDir = resolveRepoPath(repoRoot, routeDir);
224
+ if (!(await fs.pathExists(absoluteMessagesDir))) {
225
+ return [];
226
+ }
227
+ const entries = await fs.readDir(absoluteMessagesDir);
228
+ const routeFiles = [];
229
+ for (const entry of entries.filter((candidate) => candidate.endsWith(".md")).sort()) {
230
+ const route = parseRouteFileName(entry);
231
+ if (!route) {
232
+ continue;
233
+ }
234
+ const relativePath = path.posix.join(routeDir, entry);
235
+ const content = await fs.readText(resolveRepoPath(repoRoot, relativePath));
236
+ const parsed = parseRouteFileContent(content, route.fromRole, route.toRole);
237
+ routeFiles.push({
238
+ path: relativePath,
239
+ fromRole: route.fromRole,
240
+ toRole: route.toRole,
241
+ type: parsed.type,
242
+ body: parsed.body,
243
+ artifactRefs: parsed.artifactRefs,
244
+ exists: true,
245
+ pending: parsed.body.trim().length > 0
246
+ });
247
+ }
248
+ return routeFiles;
249
+ }
250
+ function parseRouteFileName(fileName) {
251
+ for (const fromRole of ROLE_NAMES) {
252
+ for (const toRole of ROLE_NAMES) {
253
+ if (fromRole === toRole) {
254
+ continue;
255
+ }
256
+ if (fileName === `${fromRole}-${toRole}.md`) {
257
+ return { fromRole, toRole };
258
+ }
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+ function parseRouteFileContent(content, fromRole, toRole) {
264
+ const { frontmatter, body } = splitFrontmatter(content);
265
+ const type = parseMessageType(frontmatter.type) ?? getDefaultMessageType(fromRole, toRole);
266
+ const artifactRefs = parseArtifactRefs(frontmatter);
267
+ return {
268
+ type,
269
+ body: body.trim(),
270
+ artifactRefs
271
+ };
272
+ }
273
+ function splitFrontmatter(content) {
274
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
275
+ if (!match) {
276
+ return { frontmatter: {}, body: content };
277
+ }
278
+ const frontmatter = {};
279
+ for (const line of match[1].split(/\r?\n/)) {
280
+ const delimiter = line.indexOf(":");
281
+ if (delimiter <= 0) {
282
+ continue;
283
+ }
284
+ const key = line.slice(0, delimiter).trim();
285
+ const value = line.slice(delimiter + 1).trim();
286
+ if (key) {
287
+ frontmatter[key] = value;
288
+ }
289
+ }
290
+ return {
291
+ frontmatter,
292
+ body: content.slice(match[0].length)
293
+ };
294
+ }
295
+ function parseMessageType(value) {
296
+ const validTypes = [
297
+ "user-request",
298
+ "task",
299
+ "question",
300
+ "blocked",
301
+ "result",
302
+ "finding",
303
+ "review-request",
304
+ "revise",
305
+ "cancel"
306
+ ];
307
+ return value && validTypes.includes(value)
308
+ ? value
309
+ : undefined;
310
+ }
311
+ function parseArtifactRefs(frontmatter) {
312
+ const refs = frontmatter.artifact_refs ?? frontmatter.artifactRefs ?? frontmatter.related_artifact;
313
+ if (!refs) {
314
+ return [];
315
+ }
316
+ return refs
317
+ .split(",")
318
+ .map((entry) => entry.trim())
319
+ .filter(Boolean);
320
+ }
321
+ function getDefaultMessageType(fromRole, toRole) {
322
+ if (fromRole === PM_ROLE && toRole !== PM_ROLE) {
323
+ return "task";
324
+ }
325
+ if (fromRole !== PM_ROLE && toRole === PM_ROLE) {
326
+ return "result";
327
+ }
328
+ return "question";
329
+ }
330
+ function selectDispatchCandidates(routeFiles, stoppedRole) {
331
+ const candidates = stoppedRole
332
+ ? routeFiles.filter((routeFile) => routeFile.fromRole === stoppedRole || routeFile.toRole === stoppedRole)
333
+ : routeFiles;
334
+ return [...candidates].sort((left, right) => {
335
+ const updated = (left.updatedAt ?? "").localeCompare(right.updatedAt ?? "");
336
+ return updated !== 0 ? updated : left.path.localeCompare(right.path);
337
+ });
338
+ }
247
339
  function validateMessagePolicy(fromRole, toRole, type) {
248
340
  if (!ROLE_NAMES.includes(toRole)) {
249
341
  throw new VcmError({
@@ -252,17 +344,7 @@ function validateMessagePolicy(fromRole, toRole, type) {
252
344
  statusCode: 400
253
345
  });
254
346
  }
255
- if (fromRole === "user") {
256
- if (toRole === PM_ROLE && type === "user-request") {
257
- return;
258
- }
259
- throw new VcmError({
260
- code: "MESSAGE_POLICY_DENIED",
261
- message: "User messages can only target project-manager as user-request.",
262
- statusCode: 403
263
- });
264
- }
265
- if (!ROLE_NAMES.includes(fromRole)) {
347
+ if (!isRoleName(String(fromRole))) {
266
348
  throw new VcmError({
267
349
  code: "MESSAGE_SENDER_UNKNOWN",
268
350
  message: `Unknown sender role: ${fromRole}`,
@@ -279,7 +361,7 @@ function validateMessagePolicy(fromRole, toRole, type) {
279
361
  code: "MESSAGE_POLICY_DENIED",
280
362
  message: `${fromRole} cannot send ${type} messages to ${toRole}.`,
281
363
  statusCode: 403,
282
- hint: "Use project-manager as the orchestration hub."
364
+ hint: "Use project-manager as the orchestration hub unless this task explicitly allows a peer route."
283
365
  });
284
366
  }
285
367
  async function withTaskLock(locks, key, run) {
@@ -295,15 +377,18 @@ async function withTaskLock(locks, key, run) {
295
377
  }
296
378
  }
297
379
  }
298
- function isRoleActor(actor) {
299
- return ROLE_NAMES.includes(actor);
380
+ function findOpenMessageForRoute(messages, routePath) {
381
+ return [...messages]
382
+ .reverse()
383
+ .find((message) => (message.routePath ?? message.bodyPath) === routePath && MUTABLE_ROUTE_MESSAGE_STATUSES.has(message.status));
300
384
  }
301
385
  function findActiveMessageForRole(messages, role) {
302
- return messages.find((message) => message.toRole === role && isActiveMessageStatus(message.status));
386
+ return messages.find((message) => message.toRole === role &&
387
+ message.status === "submitted");
303
388
  }
304
389
  function acknowledgeActiveMessages(messages, role, timestamp) {
305
390
  return messages
306
- .filter((message) => message.toRole === role && isActiveMessageStatus(message.status))
391
+ .filter((message) => message.toRole === role && message.status === "submitted")
307
392
  .map((message) => ({
308
393
  ...message,
309
394
  status: "acknowledged",
@@ -311,16 +396,6 @@ function acknowledgeActiveMessages(messages, role, timestamp) {
311
396
  failureReason: undefined
312
397
  }));
313
398
  }
314
- function isActiveMessageStatus(status) {
315
- return status === "delivering" || status === "submitted" || status === "delivered" || status === "staged";
316
- }
317
- function isSubmittableMessageStatus(status) {
318
- return status === "delivering" || status === "delivered" || status === "staged" || status === "pending_approval";
319
- }
320
- function extractSubmittedMessageId(prompt) {
321
- const match = prompt.match(/\bmsg_[A-Za-z0-9_-]+\b/);
322
- return match?.[0];
323
- }
324
399
  function applyMessageSnapshots(messages, snapshots) {
325
400
  if (snapshots.length === 0) {
326
401
  return messages;
@@ -331,43 +406,6 @@ function applyMessageSnapshots(messages, snapshots) {
331
406
  }
332
407
  return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
333
408
  }
334
- async function writeMessageBody(fs, repoRoot, handoffDir, message) {
335
- const bodyPath = path.posix.join(handoffDir, "messages", `${message.id}.md`);
336
- await fs.writeText(resolveRepoPath(repoRoot, bodyPath), renderMessageBodyFile(message));
337
- return bodyPath;
338
- }
339
- function renderMessageBodyFile(message) {
340
- const artifactRefs = message.artifactRefs.length > 0
341
- ? message.artifactRefs.map((artifact) => `- ${artifact}`).join("\n")
342
- : "- none";
343
- return `# VCM Message ${message.id}
344
-
345
- - Task: ${message.taskSlug}
346
- - From: ${message.fromRole}
347
- - To: ${message.toRole}
348
- - Type: ${message.type}
349
-
350
- ## Body
351
-
352
- ${message.body}
353
-
354
- ## Artifact Refs
355
-
356
- ${artifactRefs}
357
- `;
358
- }
359
- async function getMessageOrThrow(fs, input) {
360
- const messages = await readLatestMessages(fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
361
- const message = messages.find((candidate) => candidate.id === input.messageId);
362
- if (!message) {
363
- throw new VcmError({
364
- code: "MESSAGE_MISSING",
365
- message: `Message does not exist: ${input.messageId}`,
366
- statusCode: 404
367
- });
368
- }
369
- return message;
370
- }
371
409
  async function readLatestMessages(fs, messagesPath) {
372
410
  if (!(await fs.pathExists(messagesPath))) {
373
411
  return [];
@@ -96,12 +96,12 @@ function toRoleTurnState(role, sessions) {
96
96
  }
97
97
  function getLatestDeliveredMessage(messages) {
98
98
  return messages
99
- .filter((message) => message.status === "delivering" || message.status === "submitted" || message.status === "delivered" || message.status === "staged")
99
+ .filter((message) => message.status === "submitted")
100
100
  .sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
101
101
  .at(-1);
102
102
  }
103
103
  function getMessageDeliveredAt(message) {
104
- return message?.submittedAt ?? message?.deliveredAt ?? message?.stagedAt ?? message?.createdAt ?? "";
104
+ return message?.submittedAt ?? message?.deliveredAt ?? message?.createdAt ?? "";
105
105
  }
106
106
  function getCompletedAtForDelivery(roleState, deliveredAt) {
107
107
  if (roleState.status !== "idle" || !roleState.lastAnswerEndedAt) {
@@ -41,9 +41,9 @@ export function createSessionService(deps) {
41
41
  cwd: taskRepoRoot,
42
42
  env: {
43
43
  VCM_API_URL: deps.apiUrl,
44
- VCM_CTL_COMMAND: deps.vcmctlCommand,
45
44
  VCM_TASK_SLUG: taskSlug,
46
- VCM_ROLE: role
45
+ VCM_ROLE: role,
46
+ VCM_SESSION_ID: claudeSessionId
47
47
  },
48
48
  cols: input.cols,
49
49
  rows: input.rows,
@@ -165,14 +165,9 @@ export function createSessionService(deps) {
165
165
  const timestamp = now();
166
166
  const updated = {
167
167
  ...current,
168
- activityStatus: input.eventName === "UserPromptSubmit" ? "running" : "idle",
168
+ activityStatus: "idle",
169
169
  lastHookEventAt: timestamp,
170
- lastPromptSubmittedAt: input.eventName === "UserPromptSubmit"
171
- ? timestamp
172
- : current.lastPromptSubmittedAt,
173
- lastStopAt: input.eventName === "Stop"
174
- ? timestamp
175
- : current.lastStopAt,
170
+ lastStopAt: timestamp,
176
171
  updatedAt: timestamp
177
172
  };
178
173
  deps.registry.upsert(updated);
@@ -180,6 +175,25 @@ export function createSessionService(deps) {
180
175
  const task = await deps.taskService.loadTask(repoRoot, input.taskSlug);
181
176
  await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
182
177
  return updated;
178
+ },
179
+ async markRoleActivityRunning(repoRoot, taskSlug, role) {
180
+ const current = await this.getRoleSession(repoRoot, taskSlug, role);
181
+ if (!current) {
182
+ return undefined;
183
+ }
184
+ const timestamp = now();
185
+ const updated = {
186
+ ...current,
187
+ activityStatus: "running",
188
+ lastPromptSubmittedAt: timestamp,
189
+ lastHookEventAt: timestamp,
190
+ updatedAt: timestamp
191
+ };
192
+ deps.registry.upsert(updated);
193
+ const config = await deps.projectService.loadConfig(repoRoot);
194
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
195
+ await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
196
+ return updated;
183
197
  }
184
198
  };
185
199
  }
@@ -106,3 +106,6 @@ TBD
106
106
  TBD
107
107
  `;
108
108
  }
109
+ export function renderMessageRouteTemplate() {
110
+ return "";
111
+ }