vibe-coding-master 0.4.41 → 0.5.0
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/api/task-routes.js +7 -0
- package/dist/backend/api/translation-routes.js +11 -0
- package/dist/backend/errors.js +2 -0
- package/dist/backend/gateway/gateway-service.js +31 -61
- package/dist/backend/server.js +11 -1
- package/dist/backend/services/round-service.js +34 -0
- package/dist/backend/services/task-launch-service.js +91 -0
- package/dist/backend/services/translation-service.js +87 -0
- package/dist/backend/services/translation-worker-service.js +228 -72
- package/dist-frontend/assets/index-BaDS9Ohj.js +96 -0
- package/dist-frontend/index.html +1 -1
- package/docs/ARCHITECTURE.md +123 -0
- package/docs/TESTING.md +121 -73
- package/docs/known-issues.md +155 -0
- package/package.json +1 -1
- package/dist-frontend/assets/index-CsxS5H0d.js +0 -96
- package/docs/claude-code-translation-plan.md +0 -1268
- package/docs/full-harness-baseline.md +0 -160
- package/docs/gate-review-gates.md +0 -132
- package/docs/gateway-design.md +0 -813
- package/docs/v0.2-implementation-plan.md +0 -408
- package/docs/v0.4-harness-optimization-plan.md +0 -664
|
@@ -83,6 +83,13 @@ export function registerTaskRoutes(app, deps) {
|
|
|
83
83
|
throw error;
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
|
+
app.post("/api/tasks/:taskSlug/one-click-start", async (request) => {
|
|
87
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
88
|
+
return deps.taskLaunchService.startTaskRoleSessions(project.repoRoot, {
|
|
89
|
+
taskSlug: request.params.taskSlug,
|
|
90
|
+
requireFreshStart: true
|
|
91
|
+
});
|
|
92
|
+
});
|
|
86
93
|
app.post("/api/tasks/:taskSlug/cleanup", async (request) => {
|
|
87
94
|
const project = await requireCurrentProject(deps.projectService);
|
|
88
95
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
@@ -17,6 +17,17 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
17
17
|
const project = await requireCurrentProject(deps.projectService);
|
|
18
18
|
return deps.translationService.pollSessionEvents(request.params.sessionId, Number(request.query.after ?? "1"), request.query.limit === undefined ? undefined : Number(request.query.limit), { repoRoot: project.repoRoot });
|
|
19
19
|
});
|
|
20
|
+
app.get("/api/tasks/:taskSlug/translation/feed", async (request) => {
|
|
21
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
22
|
+
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
23
|
+
return deps.translationService.pollTaskFeed({
|
|
24
|
+
repoRoot: project.repoRoot,
|
|
25
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
26
|
+
taskSlug: request.params.taskSlug,
|
|
27
|
+
after: Number(request.query.after ?? "1"),
|
|
28
|
+
limit: request.query.limit === undefined ? undefined : Number(request.query.limit)
|
|
29
|
+
});
|
|
30
|
+
});
|
|
20
31
|
app.post("/api/tasks/:taskSlug/sessions/:role/translation/input", async (request) => {
|
|
21
32
|
const project = await requireCurrentProject(deps.projectService);
|
|
22
33
|
const role = parseRole(request.params.role);
|
package/dist/backend/errors.js
CHANGED
|
@@ -2,12 +2,14 @@ export class VcmError extends Error {
|
|
|
2
2
|
code;
|
|
3
3
|
statusCode;
|
|
4
4
|
hint;
|
|
5
|
+
details;
|
|
5
6
|
constructor(input) {
|
|
6
7
|
super(input.message);
|
|
7
8
|
this.name = "VcmError";
|
|
8
9
|
this.code = input.code;
|
|
9
10
|
this.statusCode = input.statusCode ?? 400;
|
|
10
11
|
this.hint = input.hint;
|
|
12
|
+
this.details = input.details;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
export function toVcmError(error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
5
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
@@ -430,78 +430,48 @@ export function createGatewayService(deps) {
|
|
|
430
430
|
taskSlug,
|
|
431
431
|
title
|
|
432
432
|
});
|
|
433
|
-
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
434
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
435
433
|
const preferences = await deps.appSettings.getPreferences();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const existing = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, definition.name);
|
|
461
|
-
if (existing?.status === "running") {
|
|
462
|
-
startedRoles.push(definition.name);
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (existing?.claudeSessionId) {
|
|
466
|
-
await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
|
|
470
|
-
}
|
|
471
|
-
startedRoles.push(definition.name);
|
|
472
|
-
}
|
|
473
|
-
catch (error) {
|
|
474
|
-
const settings = await deps.settings.loadSettings();
|
|
475
|
-
await deps.settings.saveSettings({
|
|
476
|
-
...settings,
|
|
477
|
-
currentProjectId: project.repoRoot,
|
|
478
|
-
currentTaskSlug: task.taskSlug,
|
|
479
|
-
translationEnabled: preferences.translationEnabled,
|
|
480
|
-
updatedAt: now()
|
|
481
|
-
});
|
|
434
|
+
// Persist the gateway's current project/task selection. Done after a partial
|
|
435
|
+
// start (so the phone stays pointed at the new task) and on success.
|
|
436
|
+
const persistTaskSelection = async () => {
|
|
437
|
+
const settings = await deps.settings.loadSettings();
|
|
438
|
+
await deps.settings.saveSettings({
|
|
439
|
+
...settings,
|
|
440
|
+
currentProjectId: project.repoRoot,
|
|
441
|
+
currentTaskSlug: task.taskSlug,
|
|
442
|
+
translationEnabled: preferences.translationEnabled,
|
|
443
|
+
updatedAt: now()
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
// Reuse the shared backend launch orchestration (roster + mode + skip/resume/
|
|
447
|
+
// start). A freshly created task has no sessions, so requireFreshStart is false.
|
|
448
|
+
let launch;
|
|
449
|
+
try {
|
|
450
|
+
launch = await deps.taskLaunchService.startTaskRoleSessions(project.repoRoot, {
|
|
451
|
+
taskSlug: task.taskSlug,
|
|
452
|
+
requireFreshStart: false
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (error instanceof VcmError && error.code === "TASK_ONE_CLICK_PARTIAL_START") {
|
|
457
|
+
await persistTaskSelection();
|
|
482
458
|
throw new VcmError({
|
|
483
459
|
code: "GATEWAY_TASK_PARTIAL_START",
|
|
484
|
-
message: `Task was created, but ${
|
|
460
|
+
message: `Task was created, but ${error.message}`,
|
|
485
461
|
statusCode: 409,
|
|
486
|
-
hint:
|
|
462
|
+
hint: error.hint
|
|
487
463
|
});
|
|
488
464
|
}
|
|
465
|
+
throw error;
|
|
489
466
|
}
|
|
490
|
-
|
|
491
|
-
await deps.settings.saveSettings({
|
|
492
|
-
...settings,
|
|
493
|
-
currentProjectId: project.repoRoot,
|
|
494
|
-
currentTaskSlug: task.taskSlug,
|
|
495
|
-
translationEnabled: preferences.translationEnabled,
|
|
496
|
-
updatedAt: now()
|
|
497
|
-
});
|
|
467
|
+
await persistTaskSelection();
|
|
498
468
|
return [
|
|
499
469
|
`Task created and initialized: ${task.taskSlug}`,
|
|
500
470
|
`branch: ${task.branch}`,
|
|
501
471
|
`worktree: ${task.worktreePath}`,
|
|
502
|
-
`orchestration: ${
|
|
472
|
+
`orchestration: ${launch.orchestration.mode}`,
|
|
503
473
|
`translation: ${preferences.translationEnabled ? "on" : "off"}`,
|
|
504
|
-
`sessions: ${startedRoles.join(", ")}`
|
|
474
|
+
`sessions: ${launch.startedRoles.join(", ")}`
|
|
505
475
|
].join("\n");
|
|
506
476
|
}
|
|
507
477
|
async function closeTaskPrompt() {
|
package/dist/backend/server.js
CHANGED
|
@@ -33,6 +33,7 @@ import { createRoundService } from "./services/round-service.js";
|
|
|
33
33
|
import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
|
|
34
34
|
import { createStatusService } from "./services/status-service.js";
|
|
35
35
|
import { createTaskService } from "./services/task-service.js";
|
|
36
|
+
import { createTaskLaunchService } from "./services/task-launch-service.js";
|
|
36
37
|
import { createTranslationService } from "./services/translation-service.js";
|
|
37
38
|
import { createDiagnosticsService } from "./services/diagnostics-service.js";
|
|
38
39
|
import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
|
|
@@ -109,6 +110,7 @@ export async function createServer(deps, options = {}) {
|
|
|
109
110
|
sessionService: deps.sessionService,
|
|
110
111
|
statusService: deps.statusService,
|
|
111
112
|
messageService: deps.messageService,
|
|
113
|
+
taskLaunchService: deps.taskLaunchService,
|
|
112
114
|
translationService: deps.translationService,
|
|
113
115
|
roundService: deps.roundService
|
|
114
116
|
});
|
|
@@ -232,6 +234,13 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
232
234
|
sessionService,
|
|
233
235
|
taskService
|
|
234
236
|
});
|
|
237
|
+
const taskLaunchService = createTaskLaunchService({
|
|
238
|
+
projectService,
|
|
239
|
+
taskService,
|
|
240
|
+
appSettings,
|
|
241
|
+
sessionService,
|
|
242
|
+
messageService
|
|
243
|
+
});
|
|
235
244
|
const roundService = createRoundService({
|
|
236
245
|
fs,
|
|
237
246
|
sessionService,
|
|
@@ -286,7 +295,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
286
295
|
projectService,
|
|
287
296
|
taskService,
|
|
288
297
|
sessionService,
|
|
289
|
-
|
|
298
|
+
taskLaunchService,
|
|
290
299
|
translationService,
|
|
291
300
|
roundService,
|
|
292
301
|
runtime,
|
|
@@ -337,6 +346,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
337
346
|
commandDispatcher,
|
|
338
347
|
claudeHookService,
|
|
339
348
|
messageService,
|
|
349
|
+
taskLaunchService,
|
|
340
350
|
gateReviewService,
|
|
341
351
|
translationWorkerService,
|
|
342
352
|
roundService,
|
|
@@ -383,6 +383,7 @@ function toSessionRoundState(state, updatedAt) {
|
|
|
383
383
|
totalCcActiveMs: state.totalCcActiveMs,
|
|
384
384
|
currentRoundCcActiveMs: 0,
|
|
385
385
|
roleRecovery: state.roleRecovery,
|
|
386
|
+
flowPause: computeFlowPause(undefined, state.roleRecovery),
|
|
386
387
|
roles: [],
|
|
387
388
|
updatedAt
|
|
388
389
|
};
|
|
@@ -411,10 +412,43 @@ function toSessionRoundState(state, updatedAt) {
|
|
|
411
412
|
totalCcActiveMs: state.totalCcActiveMs + activeDurationMs,
|
|
412
413
|
currentRoundCcActiveMs,
|
|
413
414
|
roleRecovery: state.roleRecovery,
|
|
415
|
+
flowPause: computeFlowPause(current, state.roleRecovery),
|
|
414
416
|
roles: current.roles,
|
|
415
417
|
updatedAt
|
|
416
418
|
};
|
|
417
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Authoritative flow-pause predicate (single source of truth). Returns a paused
|
|
422
|
+
* VcmFlowPauseState exactly when a real round has ended and the auto flow has not
|
|
423
|
+
* advanced and we are not mid active-recovery — mirroring the decision the GUI
|
|
424
|
+
* previously derived. Reason is `role-recovery-failed` when recovery has failed,
|
|
425
|
+
* otherwise `stopped-no-next-turn`. Returns a non-paused state (or undefined) when
|
|
426
|
+
* the flow is not paused. The frontend consumes this instead of re-deriving it.
|
|
427
|
+
*
|
|
428
|
+
* Intended logic (to implement):
|
|
429
|
+
* const recovering = roleRecovery?.status === "waiting" || roleRecovery?.status === "retrying";
|
|
430
|
+
* const paused = !!current && current.status === "stopped" && !!current.id && !recovering;
|
|
431
|
+
* if (!paused) return undefined; // or { paused: false }
|
|
432
|
+
* return {
|
|
433
|
+
* paused: true,
|
|
434
|
+
* reason: roleRecovery?.status === "failed" ? "role-recovery-failed" : "stopped-no-next-turn",
|
|
435
|
+
* role: current.activeRole,
|
|
436
|
+
* since: current.stoppedAt ?? current.lastTurnEndedAt
|
|
437
|
+
* };
|
|
438
|
+
*/
|
|
439
|
+
function computeFlowPause(current, roleRecovery) {
|
|
440
|
+
const recovering = roleRecovery?.status === "waiting" || roleRecovery?.status === "retrying";
|
|
441
|
+
const paused = Boolean(current) && current.status === "stopped" && Boolean(current.id) && !recovering;
|
|
442
|
+
if (!paused) {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
paused: true,
|
|
447
|
+
reason: roleRecovery?.status === "failed" ? "role-recovery-failed" : "stopped-no-next-turn",
|
|
448
|
+
role: current.activeRole,
|
|
449
|
+
since: current.stoppedAt ?? current.lastTurnEndedAt
|
|
450
|
+
};
|
|
451
|
+
}
|
|
418
452
|
function normalizeRoundFile(input, taskSlug, updatedAt) {
|
|
419
453
|
const legacy = input;
|
|
420
454
|
return {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
|
+
const ONE_CLICK_SESSION_COLS = 100;
|
|
5
|
+
const ONE_CLICK_SESSION_ROWS = 28;
|
|
6
|
+
export function createTaskLaunchService(deps) {
|
|
7
|
+
async function assertNoExistingRoleSessions(repoRoot, taskSlug) {
|
|
8
|
+
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
9
|
+
if (sessions.some((session) => VCM_ROLE_NAMES.some((role) => role === session.role))) {
|
|
10
|
+
throw new VcmError({
|
|
11
|
+
code: "TASK_ONE_CLICK_REQUIRES_FRESH_START",
|
|
12
|
+
message: "One-click start is only available before any role session has started.",
|
|
13
|
+
statusCode: 409
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function applyOrchestrationMode(repoRoot, taskSlug, mode) {
|
|
18
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
19
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
20
|
+
return deps.messageService.updateOrchestrationState({
|
|
21
|
+
repoRoot,
|
|
22
|
+
stateRepoRoot: getTaskRuntimeRepoRoot(task),
|
|
23
|
+
stateRoot: config.stateRoot,
|
|
24
|
+
taskSlug,
|
|
25
|
+
mode
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function composeRoleDefinitions(gateReviewerEnabled) {
|
|
29
|
+
return [
|
|
30
|
+
...CORE_VCM_ROLE_DEFINITIONS,
|
|
31
|
+
...(gateReviewerEnabled ? [GATE_REVIEWER_ROLE_DEFINITION] : [])
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
// Skip a running role, resume one that has a prior Claude session, otherwise
|
|
35
|
+
// start it fresh — using the launch-template entry's permission/model/effort.
|
|
36
|
+
async function launchRole(repoRoot, taskSlug, role, roleTemplate) {
|
|
37
|
+
const sessionInput = {
|
|
38
|
+
cols: ONE_CLICK_SESSION_COLS,
|
|
39
|
+
rows: ONE_CLICK_SESSION_ROWS,
|
|
40
|
+
permissionMode: roleTemplate.permissionMode,
|
|
41
|
+
model: roleTemplate.model,
|
|
42
|
+
effort: roleTemplate.effort
|
|
43
|
+
};
|
|
44
|
+
const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
45
|
+
if (existing?.status === "running") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (existing?.claudeSessionId) {
|
|
49
|
+
await deps.sessionService.resumeRoleSession(repoRoot, taskSlug, role, sessionInput);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await deps.sessionService.startRoleSession(repoRoot, taskSlug, role, sessionInput);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
async startTaskRoleSessions(repoRoot, input) {
|
|
56
|
+
const { taskSlug, requireFreshStart } = input;
|
|
57
|
+
if (requireFreshStart) {
|
|
58
|
+
await assertNoExistingRoleSessions(repoRoot, taskSlug);
|
|
59
|
+
}
|
|
60
|
+
const preferences = await deps.appSettings.getPreferences();
|
|
61
|
+
const template = preferences.launchTemplate;
|
|
62
|
+
const orchestration = await applyOrchestrationMode(repoRoot, taskSlug, template.autoOrchestration ? "auto" : "manual");
|
|
63
|
+
const gateReview = await deps.appSettings.getGateReviewSettings(repoRoot, taskSlug);
|
|
64
|
+
const roleDefinitions = composeRoleDefinitions(gateReview.enabled);
|
|
65
|
+
const startedRoles = [];
|
|
66
|
+
for (const definition of roleDefinitions) {
|
|
67
|
+
try {
|
|
68
|
+
await launchRole(repoRoot, taskSlug, definition.name, template.roles[definition.name]);
|
|
69
|
+
startedRoles.push(definition.name);
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
throw partialStartError(definition.name, startedRoles, cause);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
76
|
+
return { taskSlug, orchestration, startedRoles, sessions };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function partialStartError(failedRole, startedRoles, cause) {
|
|
81
|
+
return new VcmError({
|
|
82
|
+
code: "TASK_ONE_CLICK_PARTIAL_START",
|
|
83
|
+
message: `${failedRole} failed to start.`,
|
|
84
|
+
statusCode: 409,
|
|
85
|
+
hint: errorMessage(cause),
|
|
86
|
+
details: { startedRoles: [...startedRoles], failedRole }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function errorMessage(error) {
|
|
90
|
+
return error instanceof Error ? error.message : String(error);
|
|
91
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
3
|
import { TRANSLATION_ENTRY_RETENTION_LIMIT } from "../../shared/types/translation.js";
|
|
3
4
|
import { VcmError } from "../errors.js";
|
|
4
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -11,12 +12,14 @@ const TRANSLATION_PROVIDER = "claude-code";
|
|
|
11
12
|
const TRANSLATION_MODEL = "translator";
|
|
12
13
|
const OUTPUT_TRANSLATION_BATCH_DELAY_MS = 10000;
|
|
13
14
|
const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
|
|
15
|
+
const TRANSLATION_TASK_FEED_RETENTION_LIMIT = 2000;
|
|
14
16
|
export function createTranslationService(deps) {
|
|
15
17
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
16
18
|
const id = deps.id ?? (() => `tr_${Date.now()}_${Math.random().toString(16).slice(2)}`);
|
|
17
19
|
const outputBatchDelayMs = Math.max(0, deps.outputBatchDelayMs ?? OUTPUT_TRANSLATION_BATCH_DELAY_MS);
|
|
18
20
|
const queues = createTranslationQueueRegistry();
|
|
19
21
|
const sessionStates = new Map();
|
|
22
|
+
const taskFeeds = new Map();
|
|
20
23
|
async function loadConfig() {
|
|
21
24
|
const preferences = await deps.appSettings.getPreferences();
|
|
22
25
|
return {
|
|
@@ -79,9 +82,49 @@ export function createTranslationService(deps) {
|
|
|
79
82
|
createdAt: now()
|
|
80
83
|
};
|
|
81
84
|
state.events.push(event);
|
|
85
|
+
appendTaskFeedEvent(sessionId, state, event);
|
|
82
86
|
void persistEvents(state);
|
|
83
87
|
return event;
|
|
84
88
|
}
|
|
89
|
+
function appendTaskFeedEvent(sessionId, state, event) {
|
|
90
|
+
if (!state.repoRoot || !state.taskSlug || !state.role) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const feed = getTaskFeed(state.repoRoot, state.taskSlug);
|
|
94
|
+
const sessionEventKey = getTaskFeedSessionEventKey(sessionId, event);
|
|
95
|
+
if (feed.seenSessionEvents.has(sessionEventKey)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
feed.seenSessionEvents.add(sessionEventKey);
|
|
99
|
+
feed.events.push({
|
|
100
|
+
seq: feed.nextSeq++,
|
|
101
|
+
sessionId,
|
|
102
|
+
role: state.role,
|
|
103
|
+
event
|
|
104
|
+
});
|
|
105
|
+
if (feed.events.length > TRANSLATION_TASK_FEED_RETENTION_LIMIT) {
|
|
106
|
+
feed.events = feed.events.slice(-TRANSLATION_TASK_FEED_RETENTION_LIMIT);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function syncTaskFeedFromSessionState(sessionId, state) {
|
|
110
|
+
for (const event of state.events) {
|
|
111
|
+
appendTaskFeedEvent(sessionId, state, event);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function getTaskFeed(repoRoot, taskSlug) {
|
|
115
|
+
const key = getTaskFeedKey(repoRoot, taskSlug);
|
|
116
|
+
const current = taskFeeds.get(key);
|
|
117
|
+
if (current) {
|
|
118
|
+
return current;
|
|
119
|
+
}
|
|
120
|
+
const created = {
|
|
121
|
+
events: [],
|
|
122
|
+
nextSeq: 1,
|
|
123
|
+
seenSessionEvents: new Set()
|
|
124
|
+
};
|
|
125
|
+
taskFeeds.set(key, created);
|
|
126
|
+
return created;
|
|
127
|
+
}
|
|
85
128
|
async function prepareCache(input) {
|
|
86
129
|
const state = getState(input.sessionId);
|
|
87
130
|
state.repoRoot = input.repoRoot;
|
|
@@ -99,6 +142,7 @@ export function createTranslationService(deps) {
|
|
|
99
142
|
state.cacheLoaded = true;
|
|
100
143
|
pruneTranslationEntries(input.sessionId);
|
|
101
144
|
}
|
|
145
|
+
syncTaskFeedFromSessionState(input.sessionId, state);
|
|
102
146
|
await deps.fs.ensureDir(path.dirname(cachePath));
|
|
103
147
|
return state;
|
|
104
148
|
}
|
|
@@ -629,6 +673,43 @@ export function createTranslationService(deps) {
|
|
|
629
673
|
events
|
|
630
674
|
};
|
|
631
675
|
},
|
|
676
|
+
async pollTaskFeed(input) {
|
|
677
|
+
const cursor = Number.isFinite(input.after) ? Math.max(1, Math.floor(input.after)) : 1;
|
|
678
|
+
const maxEvents = Math.min(Math.max(1, Math.floor(input.limit ?? 500)), 1000);
|
|
679
|
+
const roleSessions = await deps.sessionService.listRoleSessions(input.repoRoot, input.taskSlug);
|
|
680
|
+
const feedSessions = [];
|
|
681
|
+
for (const roleSession of roleSessions) {
|
|
682
|
+
if (!isVcmRoleName(roleSession.role)) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const state = await prepareCache({
|
|
686
|
+
repoRoot: input.taskRepoRoot,
|
|
687
|
+
baseRepoRoot: input.repoRoot,
|
|
688
|
+
taskSlug: input.taskSlug,
|
|
689
|
+
role: roleSession.role,
|
|
690
|
+
sessionId: roleSession.id
|
|
691
|
+
});
|
|
692
|
+
if (roleSession.status === "running") {
|
|
693
|
+
startTranscriptTail(roleSession);
|
|
694
|
+
}
|
|
695
|
+
feedSessions.push({
|
|
696
|
+
sessionId: roleSession.id,
|
|
697
|
+
role: roleSession.role,
|
|
698
|
+
status: state.status
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
const feed = getTaskFeed(input.taskRepoRoot, input.taskSlug);
|
|
702
|
+
const events = feed.events
|
|
703
|
+
.filter((event) => event.seq >= cursor)
|
|
704
|
+
.slice(0, maxEvents);
|
|
705
|
+
const nextCursor = events.length > 0 ? (events.at(-1)?.seq ?? cursor) + 1 : cursor;
|
|
706
|
+
return {
|
|
707
|
+
taskSlug: input.taskSlug,
|
|
708
|
+
nextCursor,
|
|
709
|
+
sessions: feedSessions,
|
|
710
|
+
events
|
|
711
|
+
};
|
|
712
|
+
},
|
|
632
713
|
async recordConversationBoundary(input) {
|
|
633
714
|
const config = await loadConfig();
|
|
634
715
|
const state = await prepareCache({
|
|
@@ -1060,6 +1141,12 @@ function getTranscriptSessionKey(roleSession) {
|
|
|
1060
1141
|
roleSession.transcriptPath
|
|
1061
1142
|
].join("\n");
|
|
1062
1143
|
}
|
|
1144
|
+
function getTaskFeedKey(repoRoot, taskSlug) {
|
|
1145
|
+
return `${repoRoot}\n${taskSlug}`;
|
|
1146
|
+
}
|
|
1147
|
+
function getTaskFeedSessionEventKey(sessionId, event) {
|
|
1148
|
+
return `${sessionId}:${event.seq}`;
|
|
1149
|
+
}
|
|
1063
1150
|
function formatStructuredTranscriptEvent(event) {
|
|
1064
1151
|
if (event.kind === "question") {
|
|
1065
1152
|
return event.question.questions.map((question, index) => {
|