vibe-coding-master 0.0.15 → 0.0.16
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/README.md +11 -9
- package/dist/backend/api/claude-hook-routes.js +3 -0
- package/dist/backend/services/claude-hook-service.js +110 -48
- package/dist/backend/services/harness-service.js +4 -4
- package/dist/backend/services/message-service.js +69 -10
- package/dist/backend/services/round-service.js +1 -1
- package/dist/backend/services/session-service.js +4 -2
- package/dist-frontend/assets/{index-QhzvzTMZ.js → index-CvtyKEfS.js} +42 -42
- package/dist-frontend/index.html +1 -1
- package/docs/product-design.md +10 -9
- package/docs/v1-architecture-design.md +9 -7
- package/docs/v1-implementation-plan.md +10 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ The same file stores recent repository paths. The translation API key is stored
|
|
|
206
206
|
|
|
207
207
|
The sidebar `Settings` section also stores the UI theme preference in this file. The default is `system`, which follows the OS/browser color-scheme preference; users can cycle between `System`, `Light`, and `Dark`.
|
|
208
208
|
|
|
209
|
-
The same sidebar also has a `Round alert` toggle. It is on by default and controls the in-app prompt plus
|
|
209
|
+
The same sidebar also has a `Round alert` toggle. It is on by default and controls the in-app prompt plus a soft two-note completion chime that fires when VCM detects that the current full conversation round is complete. The `Try alert` button triggers the same local prompt and sound for testing.
|
|
210
210
|
|
|
211
211
|
Translation behavior:
|
|
212
212
|
|
|
@@ -258,7 +258,7 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
|
|
|
258
258
|
|
|
259
259
|
`.ai/vcm/` is the active VCM local control area, and `.claude/worktrees/` is the Claude-compatible task worktree area. The base repo keeps the task index; each task runtime repo keeps its own session, message, orchestration, and translation state.
|
|
260
260
|
|
|
261
|
-
VCM also JSON-merges `.claude/settings.json` to install
|
|
261
|
+
VCM also JSON-merges `.claude/settings.json` to install Claude Code `UserPromptSubmit` and `Stop` hooks. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion.
|
|
262
262
|
|
|
263
263
|
After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
|
|
264
264
|
|
|
@@ -276,7 +276,8 @@ Claude Code role
|
|
|
276
276
|
-> ends the Claude Code turn
|
|
277
277
|
-> Stop hook calls VCM backend directly
|
|
278
278
|
-> VCM scans pending route files
|
|
279
|
-
-> VCM validates, snapshots, and dispatches pending messages
|
|
279
|
+
-> VCM validates, snapshots, and dispatches pending messages as delivering
|
|
280
|
+
-> UserPromptSubmit hook confirms accepted VCM messages as submitted
|
|
280
281
|
```
|
|
281
282
|
|
|
282
283
|
Examples:
|
|
@@ -319,24 +320,25 @@ When it is on, VCM is in auto mode:
|
|
|
319
320
|
- Roles write pending route files under `.ai/vcm/handoffs/messages/`.
|
|
320
321
|
- On Claude Code `Stop`, VCM scans pending route files.
|
|
321
322
|
- If the target role session is running, idle, and has no active message, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
|
|
322
|
-
- After successful terminal
|
|
323
|
+
- After successful terminal write, VCM snapshots the body as `delivering`.
|
|
324
|
+
- Claude Code `UserPromptSubmit` confirms that the prompt was accepted; VCM then marks the message `submitted` and clears the source route file if it still contains the same message.
|
|
323
325
|
- When the GUI observes a newly submitted auto message, it switches the active role tab to that message's target role so the user can watch the next step.
|
|
324
|
-
- VCM enforces per-role turn-taking: each target role can have at most one in-flight `submitted` message.
|
|
326
|
+
- VCM enforces per-role turn-taking: each target role can have at most one in-flight `delivering` or `submitted` message.
|
|
325
327
|
- Additional pending files to a busy target role remain non-empty and are not written to that terminal.
|
|
326
328
|
- When the target role later reaches `Stop`, VCM scans again and may deliver the next pending route file.
|
|
327
|
-
- If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears open `pending_approval`, `queued`, `submitted`, and failed message records to `acknowledged`, and clears pending route files.
|
|
329
|
+
- If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears open `pending_approval`, `queued`, `delivering`, `submitted`, and failed message records to `acknowledged`, and clears pending route files.
|
|
328
330
|
|
|
329
|
-
VCM Harness injects
|
|
331
|
+
VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs become `running` when Claude Code accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. The terminal process status is still tracked separately. The injected role rules require a role to end its turn after writing or updating a message route file, rather than polling, looping, or waiting for another role inside the same Claude Code turn.
|
|
330
332
|
|
|
331
333
|
The implementation keeps only the active manual/auto orchestration mode. It does not expose pause/resume, stage/approve/reject, or a separate agent-facing message CLI.
|
|
332
334
|
|
|
333
335
|
## Round Completion Alerts
|
|
334
336
|
|
|
335
|
-
VCM detects conversation completion from hook-driven role activity state, not PTY silence.
|
|
337
|
+
VCM detects conversation completion from hook-driven role activity state, not PTY silence. `UserPromptSubmit` marks a role `running`, and `Stop` marks that role `idle` with a stop timestamp.
|
|
336
338
|
|
|
337
339
|
For role chains, VCM waits for the latest active message's target role to reach hook `Stop`. For example, if PM sends work to Coder and Coder sends a result back to PM, the round is not complete when Coder finishes; it is complete only after PM reaches `Stop` for the final response. If no VCM role message is involved, the latest direct role `Stop` can complete the round.
|
|
338
340
|
|
|
339
|
-
When `Round alert` is enabled, the frontend polls the task round state, deduplicates each completion id, shows a small `Round complete` prompt, and plays
|
|
341
|
+
When `Round alert` is enabled, the frontend polls the task round state, deduplicates each completion id, shows a small `Round complete` prompt, and plays the local completion chime.
|
|
340
342
|
|
|
341
343
|
## Resume Behavior
|
|
342
344
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export function registerClaudeHookRoutes(app, deps) {
|
|
2
|
+
app.post("/api/hooks/claude-code", async (request) => {
|
|
3
|
+
return deps.claudeHookService.handleHook(request.body);
|
|
4
|
+
});
|
|
2
5
|
app.post("/api/hooks/claude-code/stop", async (request) => {
|
|
3
6
|
return deps.claudeHookService.handleStopHook(request.body);
|
|
4
7
|
});
|
|
@@ -2,64 +2,126 @@ import { isRoleName } from "../../shared/constants.js";
|
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
4
|
export function createClaudeHookService(deps) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
statusCode: 400
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
const eventName = parseStopEvent(input.event.hook_event_name);
|
|
15
|
-
const project = await deps.projectService.getCurrentProject();
|
|
16
|
-
if (!project) {
|
|
17
|
-
throw new VcmError({
|
|
18
|
-
code: "PROJECT_NOT_CONNECTED",
|
|
19
|
-
message: "Connect a repository before accepting Claude Code hooks.",
|
|
20
|
-
statusCode: 409
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
const session = await deps.sessionService.recordClaudeHookEvent(project.repoRoot, {
|
|
24
|
-
taskSlug: input.taskSlug,
|
|
25
|
-
role: input.role,
|
|
26
|
-
eventName,
|
|
27
|
-
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
28
|
-
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
29
|
-
cwd: stringOrUndefined(input.event.cwd)
|
|
5
|
+
async function getHookContext(input) {
|
|
6
|
+
if (!isRoleName(input.role)) {
|
|
7
|
+
throw new VcmError({
|
|
8
|
+
code: "HOOK_ROLE_INVALID",
|
|
9
|
+
message: `Unknown hook role: ${input.role}`,
|
|
10
|
+
statusCode: 400
|
|
30
11
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
stateRoot: config.stateRoot,
|
|
39
|
-
handoffDir: task.handoffDir,
|
|
40
|
-
taskSlug: input.taskSlug,
|
|
41
|
-
stoppedRole: input.role
|
|
12
|
+
}
|
|
13
|
+
const project = await deps.projectService.getCurrentProject();
|
|
14
|
+
if (!project) {
|
|
15
|
+
throw new VcmError({
|
|
16
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
17
|
+
message: "Connect a repository before accepting Claude Code hooks.",
|
|
18
|
+
statusCode: 409
|
|
42
19
|
});
|
|
43
|
-
return {
|
|
44
|
-
ok: true,
|
|
45
|
-
eventName,
|
|
46
|
-
taskSlug: input.taskSlug,
|
|
47
|
-
role: input.role,
|
|
48
|
-
sessionUpdated: Boolean(session),
|
|
49
|
-
dispatchedCount: dispatched.filter((result) => result.delivered).length
|
|
50
|
-
};
|
|
51
20
|
}
|
|
21
|
+
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
22
|
+
const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
|
|
23
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
24
|
+
return {
|
|
25
|
+
project,
|
|
26
|
+
config,
|
|
27
|
+
task,
|
|
28
|
+
taskRepoRoot
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function handleUserPromptSubmitHook(input) {
|
|
32
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
33
|
+
if (eventName !== "UserPromptSubmit") {
|
|
34
|
+
throwUnsupportedEvent(eventName);
|
|
35
|
+
}
|
|
36
|
+
const context = await getHookContext(input);
|
|
37
|
+
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
38
|
+
taskSlug: input.taskSlug,
|
|
39
|
+
role: input.role,
|
|
40
|
+
eventName,
|
|
41
|
+
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
42
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
43
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
44
|
+
});
|
|
45
|
+
const submitted = await deps.messageService.confirmPromptSubmitted({
|
|
46
|
+
repoRoot: context.project.repoRoot,
|
|
47
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
48
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
49
|
+
stateRoot: context.config.stateRoot,
|
|
50
|
+
handoffDir: context.task.handoffDir,
|
|
51
|
+
taskSlug: input.taskSlug,
|
|
52
|
+
role: input.role,
|
|
53
|
+
prompt: stringOrUndefined(input.event.prompt)
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
eventName,
|
|
58
|
+
taskSlug: input.taskSlug,
|
|
59
|
+
role: input.role,
|
|
60
|
+
sessionUpdated: Boolean(session),
|
|
61
|
+
dispatchedCount: 0,
|
|
62
|
+
submittedMessageId: submitted?.id
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function handleStopHook(input) {
|
|
66
|
+
const eventName = parseHookEvent(input.event.hook_event_name ?? "Stop");
|
|
67
|
+
if (eventName !== "Stop") {
|
|
68
|
+
throwUnsupportedEvent(eventName);
|
|
69
|
+
}
|
|
70
|
+
const context = await getHookContext(input);
|
|
71
|
+
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
72
|
+
taskSlug: input.taskSlug,
|
|
73
|
+
role: input.role,
|
|
74
|
+
eventName,
|
|
75
|
+
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
76
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
77
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
78
|
+
});
|
|
79
|
+
const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles({
|
|
80
|
+
repoRoot: context.project.repoRoot,
|
|
81
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
82
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
83
|
+
stateRoot: context.config.stateRoot,
|
|
84
|
+
handoffDir: context.task.handoffDir,
|
|
85
|
+
taskSlug: input.taskSlug,
|
|
86
|
+
stoppedRole: input.role
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
eventName,
|
|
91
|
+
taskSlug: input.taskSlug,
|
|
92
|
+
role: input.role,
|
|
93
|
+
sessionUpdated: Boolean(session),
|
|
94
|
+
dispatchedCount: dispatched.filter((result) => result.delivered).length
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
async handleHook(input) {
|
|
99
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
100
|
+
if (eventName === "UserPromptSubmit") {
|
|
101
|
+
return handleUserPromptSubmitHook(input);
|
|
102
|
+
}
|
|
103
|
+
return handleStopHook(input);
|
|
104
|
+
},
|
|
105
|
+
handleStopHook
|
|
52
106
|
};
|
|
53
107
|
}
|
|
54
|
-
function
|
|
55
|
-
if (value ===
|
|
56
|
-
return
|
|
108
|
+
function parseHookEvent(value) {
|
|
109
|
+
if (value === "UserPromptSubmit" || value === "Stop") {
|
|
110
|
+
return value;
|
|
57
111
|
}
|
|
58
112
|
throw new VcmError({
|
|
59
113
|
code: "HOOK_EVENT_UNSUPPORTED",
|
|
60
114
|
message: `Unsupported Claude Code hook event: ${String(value)}`,
|
|
61
115
|
statusCode: 400,
|
|
62
|
-
hint: "VCM accepts Stop hooks only."
|
|
116
|
+
hint: "VCM accepts UserPromptSubmit and Stop hooks only."
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function throwUnsupportedEvent(eventName) {
|
|
120
|
+
throw new VcmError({
|
|
121
|
+
code: "HOOK_EVENT_UNSUPPORTED",
|
|
122
|
+
message: `Unsupported Claude Code hook event: ${eventName}`,
|
|
123
|
+
statusCode: 400,
|
|
124
|
+
hint: "Use the matching VCM hook endpoint for this event."
|
|
63
125
|
});
|
|
64
126
|
}
|
|
65
127
|
function stringOrUndefined(value) {
|
|
@@ -9,8 +9,8 @@ export const VCM_HARNESS_VERSION = 1;
|
|
|
9
9
|
const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
|
|
10
10
|
const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
|
|
11
11
|
const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
12
|
-
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code
|
|
13
|
-
const VCM_HOOK_EVENTS = ["Stop"];
|
|
12
|
+
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
13
|
+
const VCM_HOOK_EVENTS = ["UserPromptSubmit", "Stop"];
|
|
14
14
|
const HARNESS_FILES = [
|
|
15
15
|
{
|
|
16
16
|
kind: "root-claude",
|
|
@@ -227,7 +227,7 @@ async function analyzeClaudeSettingsFile(fs, repoRoot) {
|
|
|
227
227
|
path: CLAUDE_SETTINGS_PATH,
|
|
228
228
|
action,
|
|
229
229
|
reason: exists
|
|
230
|
-
? "Claude Code hook settings do not contain the VCM Stop hook bridge."
|
|
230
|
+
? "Claude Code hook settings do not contain the VCM UserPromptSubmit/Stop hook bridge."
|
|
231
231
|
: "Claude Code hook settings are missing; VCM will create them."
|
|
232
232
|
},
|
|
233
233
|
nextContent: action === "ok" ? undefined : nextContent
|
|
@@ -294,7 +294,7 @@ function isVcmHookMatcher(value) {
|
|
|
294
294
|
if (!isPlainObject(hook)) {
|
|
295
295
|
return false;
|
|
296
296
|
}
|
|
297
|
-
return typeof hook.command === "string" && hook.command.includes("/api/hooks/claude-code
|
|
297
|
+
return typeof hook.command === "string" && hook.command.includes("/api/hooks/claude-code");
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
function isPlainObject(value) {
|
|
@@ -11,6 +11,7 @@ const ROLE_TO_PM_TYPES = new Set(["result", "question", "blocked", "finding"]);
|
|
|
11
11
|
const OPEN_MESSAGE_STATUSES = new Set([
|
|
12
12
|
"pending_approval",
|
|
13
13
|
"queued",
|
|
14
|
+
"delivering",
|
|
14
15
|
"submitted",
|
|
15
16
|
"failed",
|
|
16
17
|
"delivery_failed",
|
|
@@ -55,6 +56,7 @@ export function createMessageService(deps) {
|
|
|
55
56
|
const acknowledged = acknowledgeActiveMessages(messages, input.stoppedRole, timestamp);
|
|
56
57
|
for (const message of acknowledged) {
|
|
57
58
|
await appendMessageSnapshot(deps.fs, input, message);
|
|
59
|
+
await clearRouteFileIfStillMatchesMessage(deps.fs, input, message);
|
|
58
60
|
}
|
|
59
61
|
updates.push(...acknowledged);
|
|
60
62
|
messages = applyMessageSnapshots(messages, acknowledged);
|
|
@@ -140,20 +142,18 @@ export function createMessageService(deps) {
|
|
|
140
142
|
clearedRouteFile: false
|
|
141
143
|
};
|
|
142
144
|
}
|
|
143
|
-
const
|
|
145
|
+
const delivering = {
|
|
144
146
|
...message,
|
|
145
|
-
status: "
|
|
146
|
-
deliveredAt: timestamp
|
|
147
|
-
submittedAt: timestamp
|
|
147
|
+
status: "delivering",
|
|
148
|
+
deliveredAt: timestamp
|
|
148
149
|
};
|
|
149
|
-
await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(
|
|
150
|
+
await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivering));
|
|
150
151
|
await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole);
|
|
151
|
-
await deps.fs.writeText(resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, routeFile.path), "");
|
|
152
152
|
return {
|
|
153
|
-
message:
|
|
153
|
+
message: delivering,
|
|
154
154
|
delivered: true,
|
|
155
155
|
requiresUserApproval: false,
|
|
156
|
-
clearedRouteFile:
|
|
156
|
+
clearedRouteFile: false
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
return {
|
|
@@ -166,6 +166,30 @@ export function createMessageService(deps) {
|
|
|
166
166
|
async scanAndDispatchPendingRouteFiles(input) {
|
|
167
167
|
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), () => scanLocked(input));
|
|
168
168
|
},
|
|
169
|
+
async confirmPromptSubmitted(input) {
|
|
170
|
+
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
171
|
+
const timestamp = now();
|
|
172
|
+
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
173
|
+
const messageId = extractVcmMessageId(input.prompt);
|
|
174
|
+
if (!messageId) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const message = findDeliveringMessageForPrompt(messages, input.role, messageId);
|
|
178
|
+
if (!message) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const submitted = {
|
|
182
|
+
...message,
|
|
183
|
+
status: "submitted",
|
|
184
|
+
submittedAt: timestamp,
|
|
185
|
+
queuedBehindMessageId: undefined,
|
|
186
|
+
failureReason: undefined
|
|
187
|
+
};
|
|
188
|
+
await appendMessageSnapshot(deps.fs, input, submitted);
|
|
189
|
+
await clearRouteFileIfStillMatchesMessage(deps.fs, input, submitted);
|
|
190
|
+
return submitted;
|
|
191
|
+
});
|
|
192
|
+
},
|
|
169
193
|
async markAllDone(input) {
|
|
170
194
|
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
171
195
|
const timestamp = now();
|
|
@@ -384,18 +408,53 @@ function findOpenMessageForRoute(messages, routePath) {
|
|
|
384
408
|
}
|
|
385
409
|
function findActiveMessageForRole(messages, role) {
|
|
386
410
|
return messages.find((message) => message.toRole === role &&
|
|
387
|
-
message.status === "submitted");
|
|
411
|
+
(message.status === "delivering" || message.status === "submitted"));
|
|
388
412
|
}
|
|
389
413
|
function acknowledgeActiveMessages(messages, role, timestamp) {
|
|
390
414
|
return messages
|
|
391
|
-
.filter((message) => message.toRole === role && message.status === "submitted")
|
|
415
|
+
.filter((message) => message.toRole === role && (message.status === "delivering" || message.status === "submitted"))
|
|
392
416
|
.map((message) => ({
|
|
393
417
|
...message,
|
|
394
418
|
status: "acknowledged",
|
|
419
|
+
submittedAt: message.submittedAt ?? message.deliveredAt ?? timestamp,
|
|
395
420
|
acknowledgedAt: timestamp,
|
|
396
421
|
failureReason: undefined
|
|
397
422
|
}));
|
|
398
423
|
}
|
|
424
|
+
function findDeliveringMessageForPrompt(messages, role, messageId) {
|
|
425
|
+
const delivering = messages.filter((message) => message.toRole === role &&
|
|
426
|
+
message.status === "delivering" &&
|
|
427
|
+
message.id === messageId);
|
|
428
|
+
return delivering.sort((left, right) => getMessageDeliveryTime(left).localeCompare(getMessageDeliveryTime(right))).at(-1);
|
|
429
|
+
}
|
|
430
|
+
function extractVcmMessageId(prompt) {
|
|
431
|
+
return prompt?.match(/^\s*id:\s*(\S+)\s*$/m)?.[1];
|
|
432
|
+
}
|
|
433
|
+
function getMessageDeliveryTime(message) {
|
|
434
|
+
return message.deliveredAt ?? message.createdAt;
|
|
435
|
+
}
|
|
436
|
+
async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
|
|
437
|
+
if (!message.routePath || !isRoleName(String(message.fromRole))) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const absolutePath = resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, message.routePath);
|
|
441
|
+
if (!(await fs.pathExists(absolutePath))) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const routeContent = await fs.readText(absolutePath);
|
|
445
|
+
const parsed = parseRouteFileContent(routeContent, message.fromRole, message.toRole);
|
|
446
|
+
if (parsed.body.trim() === message.body.trim() &&
|
|
447
|
+
parsed.type === message.type &&
|
|
448
|
+
arraysEqual(parsed.artifactRefs, message.artifactRefs)) {
|
|
449
|
+
await fs.writeText(absolutePath, "");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function arraysEqual(left, right) {
|
|
453
|
+
if (left.length !== right.length) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
return left.every((value, index) => value === right[index]);
|
|
457
|
+
}
|
|
399
458
|
function applyMessageSnapshots(messages, snapshots) {
|
|
400
459
|
if (snapshots.length === 0) {
|
|
401
460
|
return messages;
|
|
@@ -96,7 +96,7 @@ function toRoleTurnState(role, sessions) {
|
|
|
96
96
|
}
|
|
97
97
|
function getLatestDeliveredMessage(messages) {
|
|
98
98
|
return messages
|
|
99
|
-
.filter((message) => message.status === "submitted")
|
|
99
|
+
.filter((message) => message.status === "delivering" || message.status === "submitted")
|
|
100
100
|
.sort((left, right) => getMessageDeliveredAt(left).localeCompare(getMessageDeliveredAt(right)))
|
|
101
101
|
.at(-1);
|
|
102
102
|
}
|
|
@@ -163,11 +163,13 @@ export function createSessionService(deps) {
|
|
|
163
163
|
return undefined;
|
|
164
164
|
}
|
|
165
165
|
const timestamp = now();
|
|
166
|
+
const isStop = input.eventName === "Stop";
|
|
166
167
|
const updated = {
|
|
167
168
|
...current,
|
|
168
|
-
activityStatus: "idle",
|
|
169
|
+
activityStatus: isStop ? "idle" : "running",
|
|
169
170
|
lastHookEventAt: timestamp,
|
|
170
|
-
lastStopAt: timestamp,
|
|
171
|
+
lastStopAt: isStop ? timestamp : current.lastStopAt,
|
|
172
|
+
lastPromptSubmittedAt: isStop ? current.lastPromptSubmittedAt : timestamp,
|
|
171
173
|
updatedAt: timestamp
|
|
172
174
|
};
|
|
173
175
|
deps.registry.upsert(updated);
|