vibe-coding-master 0.5.6 → 0.6.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/gateway-routes.js +5 -0
- package/dist/backend/gateway/gateway-service.js +34 -6
- package/dist/backend/gateway/gateway-settings-service.js +2 -1
- package/dist/backend/services/session-service.js +78 -1
- package/dist-frontend/assets/{index-BL1E27kN.js → index-D5LBogZG.js} +35 -35
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -23,4 +23,9 @@ export function registerGatewayRoutes(app, deps) {
|
|
|
23
23
|
app.post("/api/gateway/binding/reset", async () => {
|
|
24
24
|
return deps.gatewayService.resetBinding();
|
|
25
25
|
});
|
|
26
|
+
// Arm/disarm the runtime channel-connection switch (process-local, not
|
|
27
|
+
// persisted). Returns the updated GatewayStatus.
|
|
28
|
+
app.put("/api/gateway/connection", async (request) => {
|
|
29
|
+
return deps.gatewayService.setConnectionEnabled(request.body.enabled);
|
|
30
|
+
});
|
|
26
31
|
}
|
|
@@ -30,10 +30,20 @@ export function createGatewayService(deps) {
|
|
|
30
30
|
let qrLogin = null;
|
|
31
31
|
let larkRegistrationState = null;
|
|
32
32
|
let lastFailedTranslation = null;
|
|
33
|
+
// Runtime channel-connection arming switch. Not persisted: every process starts
|
|
34
|
+
// disarmed. `ensurePolling` is the single chokepoint that reads it, so no
|
|
35
|
+
// self-heal path can connect the channel while this is false.
|
|
36
|
+
let connectionEnabled = false;
|
|
33
37
|
function isRunning() {
|
|
34
38
|
return Boolean(pollAbort && !pollAbort.signal.aborted);
|
|
35
39
|
}
|
|
36
40
|
async function ensurePolling() {
|
|
41
|
+
// Single chokepoint for the runtime connection switch: while disarmed, no
|
|
42
|
+
// auto-connect path (boot, getStatus/reconcile self-heal, QR success,
|
|
43
|
+
// updateSettings) may start the poll loop.
|
|
44
|
+
if (!connectionEnabled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
37
47
|
if (isRunning()) {
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
@@ -720,7 +730,7 @@ export function createGatewayService(deps) {
|
|
|
720
730
|
await enableGatewayTranslationRuntime();
|
|
721
731
|
}
|
|
722
732
|
await ensurePolling();
|
|
723
|
-
return deps.settings.expose(settings, isRunning());
|
|
733
|
+
return deps.settings.expose(settings, isRunning(), connectionEnabled);
|
|
724
734
|
},
|
|
725
735
|
async updateSettings(input) {
|
|
726
736
|
const gatewayStartInput = input.enabled === true
|
|
@@ -745,15 +755,31 @@ export function createGatewayService(deps) {
|
|
|
745
755
|
else {
|
|
746
756
|
await stopPolling();
|
|
747
757
|
}
|
|
748
|
-
return deps.settings.expose(settings, isRunning());
|
|
758
|
+
return deps.settings.expose(settings, isRunning(), connectionEnabled);
|
|
749
759
|
},
|
|
750
760
|
async resetBinding() {
|
|
751
761
|
await stopPolling();
|
|
762
|
+
// Disarm the connection switch on reset so no connection survives a
|
|
763
|
+
// binding reset.
|
|
764
|
+
connectionEnabled = false;
|
|
752
765
|
lastFailedTranslation = null;
|
|
753
766
|
qrLogin = null;
|
|
754
767
|
larkRegistrationState = null;
|
|
755
768
|
const settings = await deps.settings.resetBinding();
|
|
756
|
-
return deps.settings.expose(settings, isRunning());
|
|
769
|
+
return deps.settings.expose(settings, isRunning(), connectionEnabled);
|
|
770
|
+
},
|
|
771
|
+
async setConnectionEnabled(enabled) {
|
|
772
|
+
connectionEnabled = enabled;
|
|
773
|
+
// Armed → connect now if an account is configured; disarmed → abort the
|
|
774
|
+
// poll loop (the Lark WS closes via the abort path in waitForUpdates).
|
|
775
|
+
if (enabled) {
|
|
776
|
+
await ensurePolling();
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
await stopPolling();
|
|
780
|
+
}
|
|
781
|
+
const settings = await deps.settings.loadSettings();
|
|
782
|
+
return deps.settings.expose(settings, isRunning(), connectionEnabled);
|
|
757
783
|
},
|
|
758
784
|
async startQrLogin() {
|
|
759
785
|
const settings = await deps.settings.loadSettings();
|
|
@@ -921,7 +947,7 @@ export function createGatewayService(deps) {
|
|
|
921
947
|
});
|
|
922
948
|
larkRegistrationState = null;
|
|
923
949
|
await ensurePolling();
|
|
924
|
-
const status = deps.settings.expose(settings, isRunning());
|
|
950
|
+
const status = deps.settings.expose(settings, isRunning(), connectionEnabled);
|
|
925
951
|
return {
|
|
926
952
|
status: "confirmed",
|
|
927
953
|
appIdConfigured: Boolean(result.appId),
|
|
@@ -985,7 +1011,7 @@ export function createGatewayService(deps) {
|
|
|
985
1011
|
});
|
|
986
1012
|
larkRegistrationState = null;
|
|
987
1013
|
await ensurePolling();
|
|
988
|
-
const status = deps.settings.expose(settings, isRunning());
|
|
1014
|
+
const status = deps.settings.expose(settings, isRunning(), connectionEnabled);
|
|
989
1015
|
return {
|
|
990
1016
|
status: "confirmed",
|
|
991
1017
|
appIdConfigured: true,
|
|
@@ -1013,7 +1039,9 @@ export function createGatewayService(deps) {
|
|
|
1013
1039
|
const settings = await deps.settings.loadSettings();
|
|
1014
1040
|
const account = toAccount(settings);
|
|
1015
1041
|
const boundUserId = settings.binding.boundUserId;
|
|
1016
|
-
|
|
1042
|
+
// A disarmed gateway never touches the channel: skip the outbound push.
|
|
1043
|
+
// The latest reply was already cached above and replays on the next /start.
|
|
1044
|
+
if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
|
|
1017
1045
|
return;
|
|
1018
1046
|
}
|
|
1019
1047
|
const cursorKey = `${input.taskSlug}:project-manager:${input.session.claudeSessionId}`;
|
|
@@ -79,11 +79,12 @@ export function createGatewaySettingsService(deps) {
|
|
|
79
79
|
updatedAt: now()
|
|
80
80
|
});
|
|
81
81
|
},
|
|
82
|
-
expose(settings, running = false) {
|
|
82
|
+
expose(settings, running = false, connectionEnabled = false) {
|
|
83
83
|
return {
|
|
84
84
|
version: 1,
|
|
85
85
|
enabled: settings.enabled,
|
|
86
86
|
running,
|
|
87
|
+
connectionEnabled,
|
|
87
88
|
channel: settings.channel,
|
|
88
89
|
translationEnabled: settings.translationEnabled,
|
|
89
90
|
currentProjectId: settings.currentProjectId,
|
|
@@ -15,6 +15,15 @@ const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
|
15
15
|
const PROJECT_TRANSLATOR_SCOPE = "__project__";
|
|
16
16
|
const PROJECT_HARNESS_ENGINEER_SCOPE = "__project_harness_engineer__";
|
|
17
17
|
const PROJECT_TOOL_CD_ENTER_DELAY_MS = 500;
|
|
18
|
+
// Project tool sessions launch a Claude Code TUI inside a PTY. The PTY reports
|
|
19
|
+
// "running" the instant it is spawned, which is earlier than the moment the TUI
|
|
20
|
+
// can actually accept pasted input. These bounds drive a quiescence-based
|
|
21
|
+
// readiness wait (first output seen, then no further output for a short window)
|
|
22
|
+
// used before any programmatic input, and as the liveness probe that detects a
|
|
23
|
+
// resume-by-id launch that died before becoming usable.
|
|
24
|
+
const SESSION_READY_POLL_INTERVAL_MS = 100;
|
|
25
|
+
const SESSION_READY_QUIESCENT_POLLS = 3;
|
|
26
|
+
const SESSION_READY_MAX_POLLS = 60;
|
|
18
27
|
export function createSessionService(deps) {
|
|
19
28
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
20
29
|
async function readCurrentHarnessRevision(repoRoot) {
|
|
@@ -201,6 +210,17 @@ export function createSessionService(deps) {
|
|
|
201
210
|
};
|
|
202
211
|
deps.registry.upsert(record);
|
|
203
212
|
await persistTranslatorSession(deps.fs, repoRoot, record);
|
|
213
|
+
if (launchMode === "resume") {
|
|
214
|
+
if ((await waitForSessionInputReady(record.id)) === "exited") {
|
|
215
|
+
// Resume by claudeSessionId failed (Claude could not reopen the session
|
|
216
|
+
// and the process exited). Drop the stale id and rebuild a fresh session
|
|
217
|
+
// so a broken id cannot wedge auto-reconcile on the same resume forever.
|
|
218
|
+
deps.registry.remove(record.id);
|
|
219
|
+
await clearPersistedTranslatorSession(deps.fs, repoRoot);
|
|
220
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
221
|
+
}
|
|
222
|
+
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true }));
|
|
223
|
+
}
|
|
204
224
|
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
|
|
205
225
|
}
|
|
206
226
|
async function launchProjectHarnessEngineerSession(repoRoot, input, launchMode) {
|
|
@@ -288,6 +308,16 @@ export function createSessionService(deps) {
|
|
|
288
308
|
};
|
|
289
309
|
deps.registry.upsert(record);
|
|
290
310
|
await persistHarnessEngineerSession(deps.fs, repoRoot, record);
|
|
311
|
+
if (launchMode === "resume") {
|
|
312
|
+
if ((await waitForSessionInputReady(record.id)) === "exited") {
|
|
313
|
+
// Resume by claudeSessionId failed; drop the stale id and rebuild a fresh
|
|
314
|
+
// session so a broken id cannot wedge auto-reconcile on the same resume.
|
|
315
|
+
deps.registry.remove(record.id);
|
|
316
|
+
await clearPersistedHarnessEngineerSession(deps.fs, repoRoot);
|
|
317
|
+
return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
|
|
318
|
+
}
|
|
319
|
+
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true }));
|
|
320
|
+
}
|
|
291
321
|
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
|
|
292
322
|
}
|
|
293
323
|
async function resolveProjectToolTaskContext(repoRoot, input, roleLabel) {
|
|
@@ -306,7 +336,42 @@ export function createSessionService(deps) {
|
|
|
306
336
|
taskRepoRoot: getTaskRuntimeRepoRoot(task)
|
|
307
337
|
};
|
|
308
338
|
}
|
|
309
|
-
|
|
339
|
+
// Wait until a freshly spawned/resumed session's TUI is ready to receive
|
|
340
|
+
// programmatic input, or until it has clearly failed to start. Readiness is
|
|
341
|
+
// inferred from terminal output quiescence reported by the runtime: once the
|
|
342
|
+
// session has emitted output and then stayed quiet for SESSION_READY_QUIESCENT_POLLS
|
|
343
|
+
// consecutive polls, the TUI prompt is treated as input-ready. The wait is
|
|
344
|
+
// capped at SESSION_READY_MAX_POLLS so a perpetually chatty (or perpetually
|
|
345
|
+
// silent) session still proceeds best-effort. Returns "exited" if the runtime
|
|
346
|
+
// session is gone or no longer running, which a resume launch treats as a
|
|
347
|
+
// resume-by-id failure.
|
|
348
|
+
async function waitForSessionInputReady(sessionId) {
|
|
349
|
+
let sawOutput = false;
|
|
350
|
+
let lastOutputAt;
|
|
351
|
+
let quietPolls = 0;
|
|
352
|
+
for (let poll = 0; poll < SESSION_READY_MAX_POLLS; poll += 1) {
|
|
353
|
+
const live = deps.runtime.getSession(sessionId);
|
|
354
|
+
if (!live || isExitedStatus(live.status)) {
|
|
355
|
+
return "exited";
|
|
356
|
+
}
|
|
357
|
+
if (live.lastOutputAt) {
|
|
358
|
+
if (!sawOutput || live.lastOutputAt !== lastOutputAt) {
|
|
359
|
+
sawOutput = true;
|
|
360
|
+
lastOutputAt = live.lastOutputAt;
|
|
361
|
+
quietPolls = 0;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
quietPolls += 1;
|
|
365
|
+
if (quietPolls >= SESSION_READY_QUIESCENT_POLLS) {
|
|
366
|
+
return "ready";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
await delay(SESSION_READY_POLL_INTERVAL_MS);
|
|
371
|
+
}
|
|
372
|
+
return "ready";
|
|
373
|
+
}
|
|
374
|
+
async function migrateRunningProjectToolSessionCwd(repoRoot, session, targetCwd, options = {}) {
|
|
310
375
|
if (session.role !== TRANSLATOR_ROLE
|
|
311
376
|
&& session.role !== HARNESS_ENGINEER_ROLE) {
|
|
312
377
|
return session;
|
|
@@ -318,6 +383,9 @@ export function createSessionService(deps) {
|
|
|
318
383
|
if (!runtimeSession || runtimeSession.status !== "running") {
|
|
319
384
|
return session;
|
|
320
385
|
}
|
|
386
|
+
if (!options.alreadyReady && (await waitForSessionInputReady(session.id)) === "exited") {
|
|
387
|
+
return session;
|
|
388
|
+
}
|
|
321
389
|
assertSafeCwdTarget(targetCwd);
|
|
322
390
|
const timestamp = now();
|
|
323
391
|
await submitTerminalInput(deps.runtime, session.id, formatClaudeCdCommand(targetCwd), {
|
|
@@ -1293,3 +1361,12 @@ function formatClaudeCdCommand(targetCwd) {
|
|
|
1293
1361
|
// newline is the only unsafe character and is rejected by assertSafeCwdTarget.
|
|
1294
1362
|
return `/cd ${targetCwd}`;
|
|
1295
1363
|
}
|
|
1364
|
+
function isExitedStatus(status) {
|
|
1365
|
+
return status === "exited" || status === "crashed" || status === "missing";
|
|
1366
|
+
}
|
|
1367
|
+
function delay(ms) {
|
|
1368
|
+
if (ms <= 0) {
|
|
1369
|
+
return Promise.resolve();
|
|
1370
|
+
}
|
|
1371
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1372
|
+
}
|