groove-dev 0.27.112 → 0.27.115
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/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +3 -0
- package/moe-training/client/parsers/codex.js +3 -3
- package/moe-training/client/parsers/gemini.js +2 -2
- package/moe-training/client/step-classifier.js +2 -2
- package/moe-training/test/client/step-classifier.test.js +63 -7
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +75 -15
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +36 -10
- package/node_modules/@groove-dev/daemon/src/teams.js +100 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BKCiOUDb.js} +593 -593
- package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -8
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
- package/node_modules/moe-training/client/parsers/codex.js +3 -3
- package/node_modules/moe-training/client/parsers/gemini.js +2 -2
- package/node_modules/moe-training/client/step-classifier.js +2 -2
- package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +43 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +75 -15
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +36 -10
- package/packages/daemon/src/teams.js +100 -6
- package/packages/daemon/src/tunnel-manager.js +75 -43
- package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BKCiOUDb.js} +593 -593
- package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/packages/gui/src/components/layout/status-bar.jsx +43 -45
- package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/stores/groove.js +57 -8
- package/packages/gui/src/views/agents.jsx +31 -3
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +106 -3
- package/TRAINING_DATA_v2.md +0 -9
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Embedding Service Diagnostic — Central Command Rebuild
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
After the Central Command rebuild, `session_embedding` is `null` on all SESSION_CLOSE envelopes and `domain_tags` show keyword-mode confidence (~0.067) instead of embedding-mode confidence (~0.3-0.5). Embeddings were working correctly before the rebuild (TRAINING_DATA_v3 had full 384-dim vectors on every session).
|
|
6
|
+
|
|
7
|
+
The endpoint responds to direct curl tests from a developer machine, but the Groove client is falling back to keyword mode during live sessions.
|
|
8
|
+
|
|
9
|
+
## What "working" looked like (v3 data)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
domain_tags: primary=react_frontend (0.4932)
|
|
13
|
+
session_embedding: 384d, model=sentence-transformers/all-MiniLM-L6-v2, nonzero=377/384
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What's happening now (v4 data)
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
domain_tags: primary=nlp_llm (0.0769) ← keyword mode
|
|
20
|
+
session_embedding: null ← embed() returns null in keyword mode
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How the client decides between keyword and embedding mode
|
|
24
|
+
|
|
25
|
+
On every agent spawn, the `DomainTagger.init()` runs this sequence:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
1. POST to EMBEDDING_SERVICE_URL with {"input": "health check", "model": "sentence-transformers/all-MiniLM-L6-v2"}
|
|
29
|
+
2. If response is 200 OK:
|
|
30
|
+
a. Switch to "http" mode
|
|
31
|
+
b. Call _buildCentroids() — embeds all 40 domain descriptions (40 sequential POST calls)
|
|
32
|
+
c. If all succeeds → ready, embedding mode active
|
|
33
|
+
3. If ANYTHING fails (network error, non-200, timeout, _buildCentroids() error):
|
|
34
|
+
→ Fall back to "keyword" mode silently
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The client **never logs** why it fell back — it catches all errors silently. So the failure is invisible.
|
|
38
|
+
|
|
39
|
+
## Likely failure points (in order of probability)
|
|
40
|
+
|
|
41
|
+
### 1. `_buildCentroids()` is failing
|
|
42
|
+
|
|
43
|
+
Even if the health probe succeeds, `init()` then makes **40 sequential embed calls** to build domain centroids. If any single one fails, the whole init throws and falls back to keyword mode. The entire block is wrapped in a try/catch:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(this._serviceUrl, { ... }); // health probe
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
this._mode = this._leafRegistry?.length ? 'registry' : 'http';
|
|
50
|
+
await this._buildCentroids(); // ← 40 more calls, if this throws → keyword mode
|
|
51
|
+
this._ready = true;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// silently falls through to keyword mode
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**To check:** Look at Central Command logs during a Groove session. You should see ~41 POST requests to `/v1/embed` in quick succession (1 health probe + 40 centroid builds). If you see fewer, one is failing.
|
|
60
|
+
|
|
61
|
+
### 2. Timeout under load
|
|
62
|
+
|
|
63
|
+
The health probe has a **5-second timeout**. The centroid embed calls have a **10-second timeout** each. If the ONNX model is cold or the server is under load, individual calls might be timing out.
|
|
64
|
+
|
|
65
|
+
**To check:** Add timing logs to the `/v1/embed` endpoint:
|
|
66
|
+
```javascript
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
const vector = await embed(text);
|
|
69
|
+
console.log(`[embed] ${Date.now() - start}ms "${text.slice(0, 50)}..."`);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Response format mismatch
|
|
73
|
+
|
|
74
|
+
The client parses the response as:
|
|
75
|
+
```javascript
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
const embedding = data?.data?.[0]?.embedding;
|
|
78
|
+
if (!Array.isArray(embedding)) {
|
|
79
|
+
// returns null — "Invalid embedding response format"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The expected response is **exactly**:
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"data": [
|
|
87
|
+
{
|
|
88
|
+
"embedding": [0.0123, -0.0456, ...384 floats],
|
|
89
|
+
"index": 0
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"model": "sentence-transformers/all-MiniLM-L6-v2"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**To check:** Verify the response structure. Common mistakes:
|
|
97
|
+
- Returning `{ "embedding": [...] }` instead of `{ "data": [{ "embedding": [...] }] }`
|
|
98
|
+
- Returning the vector as a nested array `[[0.01, ...]]` instead of flat `[0.01, ...]`
|
|
99
|
+
- Returning Float32Array or typed array instead of plain JSON numbers
|
|
100
|
+
|
|
101
|
+
### 4. CORS or request rejection
|
|
102
|
+
|
|
103
|
+
The Groove daemon makes requests from `localhost` (or wherever it runs). If the rebuilt server has stricter CORS, different firewall rules, or middleware that rejects requests without a specific User-Agent or Origin header, the fetch would fail.
|
|
104
|
+
|
|
105
|
+
**To check:** The original Central Command had `Access-Control-Allow-Origin: *`. Verify the rebuild has the same.
|
|
106
|
+
|
|
107
|
+
### 5. URL path mismatch
|
|
108
|
+
|
|
109
|
+
The client hits exactly: `POST https://api.groovedev.ai/v1/embed`
|
|
110
|
+
|
|
111
|
+
**To check:** Verify the route is mounted at `/v1/embed`, not `/embed` or `/v1/embeddings` or `/api/v1/embed`.
|
|
112
|
+
|
|
113
|
+
## Quick diagnostic commands
|
|
114
|
+
|
|
115
|
+
Run these on the Central Command server:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# 1. Verify endpoint exists and responds correctly
|
|
119
|
+
curl -s -X POST http://localhost:8443/v1/embed \
|
|
120
|
+
-H "Content-Type: application/json" \
|
|
121
|
+
-d '{"input": "Python programming debugging testing"}' \
|
|
122
|
+
| python3 -c "
|
|
123
|
+
import sys, json
|
|
124
|
+
d = json.load(sys.stdin)
|
|
125
|
+
v = d.get('data', [{}])[0].get('embedding')
|
|
126
|
+
if v and isinstance(v, list):
|
|
127
|
+
print(f'OK: {len(v)}-dim vector, type={type(v[0]).__name__}')
|
|
128
|
+
else:
|
|
129
|
+
print(f'BROKEN: response structure = {json.dumps(d)[:200]}')"
|
|
130
|
+
|
|
131
|
+
# 2. Simulate the full init sequence (health probe + 40 centroids)
|
|
132
|
+
python3 -c "
|
|
133
|
+
import requests, time
|
|
134
|
+
url = 'http://localhost:8443/v1/embed'
|
|
135
|
+
domains = [
|
|
136
|
+
'Python programming, debugging, testing with pytest',
|
|
137
|
+
'TypeScript and Node.js backend, Express frameworks',
|
|
138
|
+
'React frontend development, JSX components, hooks',
|
|
139
|
+
# ...add all 40 domain descriptions from domain-tagger.js
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Health probe
|
|
143
|
+
start = time.time()
|
|
144
|
+
r = requests.post(url, json={'input': 'health check', 'model': 'sentence-transformers/all-MiniLM-L6-v2'}, timeout=5)
|
|
145
|
+
print(f'Health probe: {r.status_code} in {time.time()-start:.1f}s')
|
|
146
|
+
|
|
147
|
+
# Centroid batch
|
|
148
|
+
fails = 0
|
|
149
|
+
for i, desc in enumerate(domains):
|
|
150
|
+
try:
|
|
151
|
+
r = requests.post(url, json={'input': desc}, timeout=10)
|
|
152
|
+
d = r.json()
|
|
153
|
+
v = d.get('data', [{}])[0].get('embedding')
|
|
154
|
+
if not v or not isinstance(v, list) or len(v) != 384:
|
|
155
|
+
print(f' Domain {i}: BAD RESPONSE')
|
|
156
|
+
fails += 1
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f' Domain {i}: FAILED - {e}')
|
|
159
|
+
fails += 1
|
|
160
|
+
|
|
161
|
+
print(f'Centroid build: {len(domains)-fails}/{len(domains)} succeeded, {fails} failed')
|
|
162
|
+
if fails > 0:
|
|
163
|
+
print('THIS IS WHY EMBEDDINGS ARE NULL - any failure causes full fallback to keyword mode')
|
|
164
|
+
"
|
|
165
|
+
|
|
166
|
+
# 3. Check response time under sequential load
|
|
167
|
+
for i in $(seq 1 10); do
|
|
168
|
+
time curl -s -X POST http://localhost:8443/v1/embed \
|
|
169
|
+
-H "Content-Type: application/json" \
|
|
170
|
+
-d "{\"input\": \"test embedding call $i\"}" > /dev/null
|
|
171
|
+
done
|
|
172
|
+
|
|
173
|
+
# 4. Check server logs during a live Groove session
|
|
174
|
+
# Start a Groove agent and watch for /v1/embed requests in the Central Command logs
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## What success looks like
|
|
178
|
+
|
|
179
|
+
After fix, a SESSION_CLOSE envelope should show:
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"metadata": {
|
|
183
|
+
"domain_tags": {
|
|
184
|
+
"primary": { "domain": "react_frontend", "confidence": 0.49 },
|
|
185
|
+
"secondary": { "domain": "vue_frontend", "confidence": 0.47 },
|
|
186
|
+
"tertiary": { "domain": "typescript_node", "confidence": 0.40 }
|
|
187
|
+
},
|
|
188
|
+
"session_embedding": {
|
|
189
|
+
"model": "sentence-transformers/all-MiniLM-L6-v2",
|
|
190
|
+
"vector": [0.0123, -0.0456, "...384 floats"],
|
|
191
|
+
"source_text": "frontend\nI'll scaffold the project..."
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Confidence scores in the 0.25-0.55 range (cosine similarity), NOT 0.06-0.08 (keyword hit ratio).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"envelope_id":"env_70dfa132-575b-48a3-becc-a2ea8a75aecc","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"9f2d5a6369edcb2545181101574e61e02c65e939e253c068d398d2cbf8f20cd4","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"fullstack","agent_id":"e18a8fa4","task_complexity":"medium","team_size":2,"session_quality":0,"groove_version":"0.27.113","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777354900.276,"content":"Let me trace the exact URL the client is using.","token_count":12},{"step":2,"type":"action","timestamp":1777354901.244,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'EMBEDDING_SERVICE_URL\\|CENTRAL_COMMAND_URL' moe-training/shared/constants.js"},"content":"Using Bash","token_count":37},{"step":3,"type":"observation","timestamp":1777354901.808,"content":"41:export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';\n42:export const EMBEDDING_SERVICE_URL = process.env.EMBEDDING_SERVICE_URL || `${CENTRAL_COMMAND_URL}/v1/embed`;\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":71,"is_error":false,"tool":"Bash","token_count":69},{"step":4,"type":"action","timestamp":1777354905.484,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'serviceUrl\\|EMBEDDING' moe-training/client/domain-tagger.js | head -10"},"content":"Using Bash","token_count":36},{"step":5,"type":"observation","timestamp":1777354905.528,"content":"3:import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';\n176: this._serviceUrl = options.serviceUrl !== undefined ? options.serviceUrl : EMBEDDING_SERVICE_URL;\n208: if (this._serviceUrl) {\n210: const res = await fetch(this._serviceUrl, {\n361: const res = await fetch(this._serviceUrl, {\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":97,"is_error":false,"tool":"Bash","token_count":95},{"step":6,"type":"thought","timestamp":1777354911.293,"content":"The constant looks correct — `https://api.groovedev.ai/v1/embed`, no trailing slash. But let me check what the DomainTagger actually receives at runtime. The `TrajectoryCapture` creates the DomainTagger — let me see if it passes any options.","token_count":61},{"step":7,"type":"action","timestamp":1777354911.644,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'DomainTagger\\|domainTagger\\|_domainTagger' moe-training/client/trajectory-capture.js"},"content":"Using Bash","token_count":40},{"step":8,"type":"observation","timestamp":1777354911.695,"content":"11:import { DomainTagger } from './domain-tagger.js';\n48: this._domainTagger = new DomainTagger();\n49: await this._domainTagger.init();\n299: if (this._domainTagger) {\n304: const routingText = DomainTagger.buildRoutingText(role, firstPrompt, remainingThoughts);\n305: ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);\n306: ctx.metadata.session_embedding = await this._domainTagger.embed(routingText);\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":129,"is_error":false,"tool":"Bash","token_count":127},{"step":9,"type":"thought","timestamp":1777354916.313,"content":"`new DomainTagger()` — no options passed, so it uses the default `EMBEDDING_SERVICE_URL`. Now let me check where the daemon actually imports and uses this. The `moe-training` module is consumed by the daemon.","token_count":52},{"step":10,"type":"action","timestamp":1777354916.852,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -rn 'TrajectoryCapture\\|trajectory-capture\\|moe-training' packages/daemon/src/ | grep -v node_modules"},"content":"Using Bash","token_count":43},{"step":11,"type":"observation","timestamp":1777354916.946,"content":"packages/daemon/src/index.js:46:import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';\npackages/daemon/src/index.js:594: this._initTrajectoryCapture().catch(() => {});\npackages/daemon/src/index.js:680: async _initTrajectoryCapture() {\npackages/daemon/src/index.js:685: this.trajectoryCapture = new TrajectoryCapture({\npackages/daemon/src/api.js:18:import { ConsentManager } from '../../../moe-training/client/index.js';\npackages/daemon/src/api.js:4939: await daemon._initTrajectoryCapture();\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":155,"is_error":false,"tool":"Bash","token_count":153},{"step":12,"type":"action","timestamp":1777354920.727,"tool":"Read","arguments":{"file_path":"~/Desktop/groove-project/groove/packages/daemon/src/index.js","offset":678,"limit":25},"content":"Using Read","token_count":28},{"step":13,"type":"observation","timestamp":1777354920.728,"content":"678\t }\n679\t\n680\t async _initTrajectoryCapture() {\n681\t try {\n682\t if (!ConsentManager.isCaptureEnabled()) return;\n683\t const pkgPath = new URL('../package.json', import.meta.url);\n684\t const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;\n685\t this.trajectoryCapture = new TrajectoryCapture({\n686\t centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',\n687\t grooveVersion: version,\n688\t });\n689\t this.trajectoryCapture.init();\n690\t } catch (e) {\n691\t // Training capture is never critical\n692\t }\n693\t }\n694\t\n695\t _startGarbageCollector() {\n696\t // Run once on startup, then every 24 hours\n697\t this._gc();\n698\t this._gcInterval = setInterval(() => this._gc(), 24 * 60 * 60 * 1000);\n699\t\n700\t // Periodic state save — crash protection (every 30s)\n701\t this._stateSaveInterval = setInterval(async () => {\n702\t try { this.state.set('agents', this.registry.getAll()); await this.state.save(); } catch {}","truncated":false,"original_token_count":255,"is_error":false,"tool":"Read","token_count":255},{"step":14,"type":"thought","timestamp":1777354925.525,"content":"Line 689 — `this.trajectoryCapture.init()` — no `await`. That's not the bug here though. Let me look at what `TrajectoryCapture.init()` does with the DomainTagger.","token_count":41},{"step":15,"type":"action","timestamp":1777354926.407,"tool":"Read","arguments":{"file_path":"~/Desktop/groove-project/groove/moe-training/client/trajectory-capture.js","offset":38,"limit":16},"content":"Using Read","token_count":31},{"step":16,"type":"observation","timestamp":1777354926.407,"content":"38\t async init() {\n39\t if (!ConsentManager.isCaptureEnabled()) {\n40\t this._enabled = false;\n41\t return;\n42\t }\n43\t this._enabled = true;\n44\t this._scrubber = new PIIScrubber();\n45\t this._attestation = new SessionAttestation(this._centralCommandUrl);\n46\t this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);\n47\t this._transmissionQueue.start();\n48\t this._domainTagger = new DomainTagger();\n49\t await this._domainTagger.init();\n50\t this._offlineRetryTimer = setInterval(() => {\n51\t this._retryOfflineQueue();\n52\t }, OFFLINE_RETRY_INTERVAL_MS);\n53\t }","truncated":false,"original_token_count":155,"is_error":false,"tool":"Read","token_count":155},{"step":17,"type":"thought","timestamp":1777354935.764,"content":"Wait — line 689 in `index.js`: `this.trajectoryCapture.init()` — **no await**. That means `init()` runs as a fire-and-forget promise. The DomainTagger's `init()` calls `await this._domainTagger.init()` which does 41 HTTP requests. But the daemon doesn't await the TrajectoryCapture's init.\n\nThat's fine in theory — the DomainTagger should still eventually resolve. But let me check: if `init()` isn't awaited and an agent spawns before the DomainTagger finishes its 41 embed calls, the `_domainTagger` might be set but not yet `_ready`. Let me look at the timing.\n\nActually, that's not the issue either — `onAgentSpawn` doesn't check `_domainTagger.ready`. The domain tagging only happens in `_closeAgent`, by which point init would have long completed.\n\nLet me look at this from another angle — is the `EMBEDDING_SERVICE_URL` constant actually making it into the published npm package?","token_count":222},{"step":18,"type":"action","timestamp":1777354936.676,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { EMBEDDING_SERVICE_URL, CENTRAL_COMMAND_URL } from './moe-training/shared/constants.js';\nconsole.log('CENTRAL_COMMAND_URL:', CENTRAL_COMMAND_URL);\nconsole.log('EMBEDDING_SERVICE_URL:', EMBEDDING_SERVICE_URL);\n\""},"content":"Using Bash","token_count":74},{"step":19,"type":"observation","timestamp":1777354936.764,"content":"CENTRAL_COMMAND_URL: https://api.groovedev.ai\nEMBEDDING_SERVICE_URL: https://api.groovedev.ai/v1/embed\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":44,"is_error":false,"tool":"Bash","token_count":42},{"step":20,"type":"action","timestamp":1777354948.816,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('serviceUrl:', t._serviceUrl);\nconsole.log('mode:', t._mode);\nconsole.log('ready:', t._ready);\n\""},"content":"Using Bash","token_count":73},{"step":21,"type":"observation","timestamp":1777354948.897,"content":"serviceUrl: https://api.groovedev.ai/v1/embed\nmode: null\nready: false\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":36,"is_error":false,"tool":"Bash","token_count":34},{"step":22,"type":"thought","timestamp":1777354952.865,"content":"URL is correct. Let me test the full init flow.","token_count":12},{"step":23,"type":"action","timestamp":1777354956.383,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('serviceUrl:', t._serviceUrl);\nconsole.time('init');\ntry {\n await t.init();\n console.timeEnd('init');\n console.log('mode:', t.mode);\n console.log('ready:', t.ready);\n console.log('lastError:', t.lastError);\n if (t._centroids) {\n console.log('centroids:', Object.keys(t._centroids).length);\n }\n const result = await t.tag('Build a React frontend with TypeScript and Tailwind CSS');\n console.log('tag result:', JSON.stringify(result));\n const emb = await t.embed('Build a React frontend with TypeScript');\n console.log('embed:', emb ? emb.vector.length + 'd' : 'null');\n} catch (e) {\n console.error('ERROR:', e.message);\n}\n\" 2>&1","timeout":60000},"content":"Using Bash","token_count":217},{"step":24,"type":"observation","timestamp":1777354961.386,"content":"serviceUrl: https://api.groovedev.ai/v1/embed\ninit: 4.706s\nmode: http\nready: true\nlastError: null\ncentroids: 40\ntag result: {\"primary\":{\"domain\":\"react_frontend\",\"confidence\":0.685},\"secondary\":{\"domain\":\"typescript_node\",\"confidence\":0.4633},\"tertiary\":{\"domain\":\"vue_frontend\",\"confidence\":0.3264}}\nembed: 384d\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":97,"is_error":false,"tool":"Bash","token_count":94},{"step":25,"type":"thought","timestamp":1777354969.631,"content":"It works perfectly from this machine — `http` mode, 40 centroids, 0.685 confidence on React, 384d embedding. The code is correct.\n\nThe issue is that the **running daemon** was started before these changes were committed. The daemon process has the old code in memory where `DomainTagger` didn't import `EMBEDDING_SERVICE_URL` and defaulted `serviceUrl` to `null`.\n\nYou need to restart the Groove daemon to pick up the new code. The changes are in the files but the running Node process is still using the old version.","token_count":130},{"step":26,"type":"action","timestamp":1777354971.472,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('Confirmed: serviceUrl =', t._serviceUrl);\nconsole.log('This proves the code is correct.');\nconsole.log('If live sessions show keyword mode, the running daemon has stale code in memory.');\nconsole.log('Fix: restart the daemon (groove stop && groove start)');\n\""},"content":"Using Bash","token_count":114},{"step":27,"type":"observation","timestamp":1777354971.549,"content":"Confirmed: serviceUrl = https://api.groovedev.ai/v1/embed\nThis proves the code is correct.\nIf live sessions show keyword mode, the running daemon has stale code in memory.\nFix: restart the daemon (groove stop && groove start)\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":75,"is_error":false,"tool":"Bash","token_count":73},{"step":28,"type":"thought","timestamp":1777354976.411,"content":"Mystery solved. The code works — `http` mode, 40 centroids built in 4.7s, 0.685 confidence, 384d embeddings. The running daemon just needs a restart to pick up the new `EMBEDDING_SERVICE_URL` default. Run `groove stop && groove start` and the next session will have full embeddings.","token_count":71},{"step":29,"type":"resolution","timestamp":1777354976.457,"content":"Mystery solved. The code works — `http` mode, 40 centroids built in 4.7s, 0.685 confidence, 384d embeddings. The running daemon just needs a restart to pick up the new `EMBEDDING_SERVICE_URL` default. Run `groove stop && groove start` and the next session will have full embeddings.","token_count":71}]}
|
|
2
|
+
{"envelope_id":"env_8c59a800-92c2-486e-aab2-d251958883da","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","type":"SESSION_CLOSE","attestation":{"session_hmac":"3fd6f3891108e43eb7482a0361fc5789ecfb9fdf40f637a63edb01e154c6f74f","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"fullstack","agent_id":"e18a8fa4","task_complexity":"medium","team_size":2,"session_quality":70,"groove_version":"0.27.113","leaf_context":null,"domain_tags":{"primary":{"domain":"linux_admin","confidence":0.0769},"secondary":{"domain":"nlp_llm","confidence":0.0769},"tertiary":{"domain":"react_frontend","confidence":0.0667}},"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":70,"quality_tier":"TIER_A","quality_tier_reason":"high_quality_no_errors","user_interventions":0,"total_steps":29,"total_chunks":1,"total_tokens":2462,"duration_seconds":86,"files_modified":0,"errors_encountered":0,"errors_recovered":0,"coordination_events":0,"training_eligible":true,"training_exclusion_reason":null}}
|
|
3
|
+
{"envelope_id":"env_eafaf868-6b85-46c8-b687-43a576aedc50","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","type":"USER_FEEDBACK","attestation":{"session_hmac":"a60cc84ce26d8714749bbaedd3d51a8fb153d008e3cb396d3413740d6970454c","sequence":2,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"feedback":{"signal":"accepted","timestamp":1777354977.009,"context":"session completed successfully with no user interventions","target_step":29,"revision_rounds":0,"delta_summary":null}}
|
|
@@ -57,15 +57,15 @@ export class CodexParser {
|
|
|
57
57
|
if (item.type === 'command_execution') {
|
|
58
58
|
const rawOutput = item.aggregated_output || '';
|
|
59
59
|
if (item.exit_code !== 0) {
|
|
60
|
-
return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
60
|
+
return { type: 'error', is_error: true, content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
61
61
|
}
|
|
62
62
|
const obs = truncateObservation(rawOutput);
|
|
63
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
63
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
64
64
|
}
|
|
65
65
|
if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
|
|
66
66
|
const rawOutput = item.output || item.content || '';
|
|
67
67
|
const obs = truncateObservation(rawOutput);
|
|
68
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
68
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
@@ -64,11 +64,11 @@ export class GeminiParser {
|
|
|
64
64
|
const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
|
|
65
65
|
const rawText = contentParts.map((p) => p.text || '').join('');
|
|
66
66
|
const obs = truncateObservation(rawText);
|
|
67
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
67
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
case 'error': {
|
|
71
|
-
return { type: 'error', content: jsonEvent.message || 'Unknown error' };
|
|
71
|
+
return { type: 'error', is_error: true, content: jsonEvent.message || 'Unknown error' };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
case 'agent_end': {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
const ERROR_SIGNAL_RE =
|
|
3
|
+
const ERROR_SIGNAL_RE = /(?:^|\n)\s*(?:error[ :\[]\S|Error[ :\[]\S|ERROR[ :\[]\S)|(?:exit code [1-9]|non-zero exit|ENOENT|EACCES|EPERM|Command failed)|(?:(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError):\s)|(?:Cannot find module|Module not found|ModuleNotFoundError|ImportError:)|(?:FATAL|PANIC|Traceback \(most recent)|(?:error TS\d{4}:)/;
|
|
4
4
|
const FIX_SIGNAL_RE = /\b(?:fix|correcting|I see the issue|let me fix|the (?:issue|problem|bug) (?:is|was)|instead I should|my mistake)\b/i;
|
|
5
5
|
|
|
6
6
|
const CORRECTION_RE = /\b(?:no[,. ](?:that|not|don't|wrong)|that'?s (?:not|wrong|incorrect)|don'?t do that|stop (?:doing|that)|instead (?:of|do)|undo|revert|go back|try (?:again|differently)|you (?:broke|missed|forgot))\b/i;
|
|
@@ -54,7 +54,7 @@ export class StepClassifier {
|
|
|
54
54
|
|
|
55
55
|
const content = step.content || '';
|
|
56
56
|
|
|
57
|
-
if (
|
|
57
|
+
if (step.type === 'observation' && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
|
|
58
58
|
step.type = 'error';
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -132,11 +132,11 @@ describe('StepClassifier', () => {
|
|
|
132
132
|
assert.equal(StepClassifier.countUserInterventions(steps), 0);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it('reclassifies action
|
|
135
|
+
it('never reclassifies action to error', () => {
|
|
136
136
|
const classifier = new StepClassifier();
|
|
137
137
|
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
138
138
|
const result = classifier.onStep(step);
|
|
139
|
-
assert.equal(result.type, '
|
|
139
|
+
assert.equal(result.type, 'action');
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it('reclassifies observation with error content to error', () => {
|
|
@@ -215,19 +215,75 @@ describe('StepClassifier', () => {
|
|
|
215
215
|
assert.equal(result.type, 'error');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('
|
|
218
|
+
it('does not reclassify observation containing bare word "error" in source code', () => {
|
|
219
219
|
const classifier = new StepClassifier();
|
|
220
|
-
const step = { type: '
|
|
220
|
+
const step = { type: 'observation', content: 'function handleError(err) { console.error(err); }' };
|
|
221
221
|
const result = classifier.onStep(step);
|
|
222
|
-
assert.equal(result.type, '
|
|
222
|
+
assert.equal(result.type, 'observation');
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
it('
|
|
225
|
+
it('does not reclassify observation with "0 errors" or "found 0 vulnerabilities"', () => {
|
|
226
226
|
const classifier = new StepClassifier();
|
|
227
|
-
const step = { type: '
|
|
227
|
+
const step = { type: 'observation', content: 'Build succeeded\n0 errors, 0 warnings\nfound 0 vulnerabilities' };
|
|
228
|
+
const result = classifier.onStep(step);
|
|
229
|
+
assert.equal(result.type, 'observation');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('does not reclassify observation reading a file that mentions exceptions', () => {
|
|
233
|
+
const classifier = new StepClassifier();
|
|
234
|
+
const step = { type: 'observation', content: '{"scripts": {"build": "tsc && vite build"}, "name": "my-app"}' };
|
|
235
|
+
const result = classifier.onStep(step);
|
|
236
|
+
assert.equal(result.type, 'observation');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('reclassifies observation with real TypeScript build error', () => {
|
|
240
|
+
const classifier = new StepClassifier();
|
|
241
|
+
const step = { type: 'observation', content: 'src/main.ts(1,8): error TS2882: Cannot find module' };
|
|
242
|
+
const result = classifier.onStep(step);
|
|
243
|
+
assert.equal(result.type, 'error');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('reclassifies observation with Python traceback', () => {
|
|
247
|
+
const classifier = new StepClassifier();
|
|
248
|
+
const step = { type: 'observation', content: 'Traceback (most recent call last):\n File "main.py", line 5' };
|
|
249
|
+
const result = classifier.onStep(step);
|
|
250
|
+
assert.equal(result.type, 'error');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('reclassifies observation with actual TypeError message', () => {
|
|
254
|
+
const classifier = new StepClassifier();
|
|
255
|
+
const step = { type: 'observation', content: 'TypeError: Cannot read properties of undefined (reading "map")' };
|
|
256
|
+
const result = classifier.onStep(step);
|
|
257
|
+
assert.equal(result.type, 'error');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('reclassifies observation with exit code failure', () => {
|
|
261
|
+
const classifier = new StepClassifier();
|
|
262
|
+
const step = { type: 'observation', content: 'Process exited with exit code 1' };
|
|
263
|
+
const result = classifier.onStep(step);
|
|
264
|
+
assert.equal(result.type, 'error');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('reclassifies observation with ModuleNotFoundError', () => {
|
|
268
|
+
const classifier = new StepClassifier();
|
|
269
|
+
const step = { type: 'observation', content: 'ModuleNotFoundError: No module named requests' };
|
|
228
270
|
const result = classifier.onStep(step);
|
|
229
271
|
assert.equal(result.type, 'error');
|
|
230
272
|
});
|
|
273
|
+
|
|
274
|
+
it('preserves action type even with error keywords', () => {
|
|
275
|
+
const classifier = new StepClassifier();
|
|
276
|
+
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
277
|
+
const result = classifier.onStep(step);
|
|
278
|
+
assert.equal(result.type, 'action');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('preserves action type regardless of is_error flag', () => {
|
|
282
|
+
const classifier = new StepClassifier();
|
|
283
|
+
const step = { type: 'action', content: 'ENOENT: no such file', is_error: true };
|
|
284
|
+
const result = classifier.onStep(step);
|
|
285
|
+
assert.equal(result.type, 'action');
|
|
286
|
+
});
|
|
231
287
|
});
|
|
232
288
|
|
|
233
289
|
describe('StepClassifier.classifyIntent', () => {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// GROOVE CLI — team commands
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { createInterface } from 'readline';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { apiCall } from '../client.js';
|
|
6
7
|
|
|
8
|
+
function confirm(prompt) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim().toLowerCase() === 'y'); });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export async function teamCreate(name) {
|
|
8
16
|
try {
|
|
9
17
|
const team = await apiCall('POST', '/api/teams', { name });
|
|
@@ -37,9 +45,43 @@ export async function teamList() {
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
export async function teamDelete(id) {
|
|
48
|
+
const ok = await confirm(` This will archive the team directory. Continue? [y/N] `);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
try {
|
|
41
54
|
await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}`);
|
|
42
|
-
console.log(chalk.green(`
|
|
55
|
+
console.log(chalk.green(` Archived team "${id}"`) + chalk.dim(' — restore with `groove team restore <id>`'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(chalk.red(' Failed:'), err.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function teamArchived() {
|
|
63
|
+
try {
|
|
64
|
+
const { archived } = await apiCall('GET', '/api/teams/archived');
|
|
65
|
+
if (archived.length === 0) {
|
|
66
|
+
console.log(chalk.dim(' No archived teams.'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.bold(`\n Archived Teams (${archived.length})\n`));
|
|
70
|
+
for (const t of archived) {
|
|
71
|
+
const date = t.deletedAt ? new Date(t.deletedAt).toLocaleDateString() : 'unknown';
|
|
72
|
+
console.log(` ${chalk.bold(t.originalName || t.id)} — archive-id: ${t.id} — deleted ${date} — ${t.agentCount} agent(s)`);
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
} catch {
|
|
76
|
+
console.error(chalk.red(' Cannot connect to daemon.'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function teamRestore(id) {
|
|
82
|
+
try {
|
|
83
|
+
const team = await apiCall('POST', `/api/teams/archived/${encodeURIComponent(id)}/restore`);
|
|
84
|
+
console.log(chalk.green(` Restored team "${team.name}"`) + ` (new id: ${team.id})`);
|
|
43
85
|
} catch (err) {
|
|
44
86
|
console.error(chalk.red(' Failed:'), err.message);
|
|
45
87
|
process.exit(1);
|
|
@@ -1100,6 +1100,30 @@ export function createApi(app, daemon) {
|
|
|
1100
1100
|
}
|
|
1101
1101
|
});
|
|
1102
1102
|
|
|
1103
|
+
app.get('/api/teams/archived', (req, res) => {
|
|
1104
|
+
res.json({ archived: daemon.teams.listArchived() });
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
app.post('/api/teams/archived/:id/restore', (req, res) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const team = daemon.teams.restore(req.params.id);
|
|
1110
|
+
daemon.audit.log('team.restore', { archivedId: req.params.id, newId: team.id, name: team.name });
|
|
1111
|
+
res.json(team);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
res.status(400).json({ error: err.message });
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
app.delete('/api/teams/archived/:id', (req, res) => {
|
|
1118
|
+
try {
|
|
1119
|
+
daemon.teams.purge(req.params.id);
|
|
1120
|
+
daemon.audit.log('team.purge', { archivedId: req.params.id });
|
|
1121
|
+
res.json({ ok: true });
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
res.status(400).json({ error: err.message });
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1103
1127
|
app.patch('/api/teams/:id', (req, res) => {
|
|
1104
1128
|
try {
|
|
1105
1129
|
if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
|
|
@@ -3855,6 +3879,48 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3855
3879
|
// --- Preview Proxy (same-origin iframe support) ---
|
|
3856
3880
|
// Forwards HTTP requests to the dev server so the GUI can iframe the preview
|
|
3857
3881
|
// without cross-origin issues. WebSocket upgrade for HMR is handled in index.js.
|
|
3882
|
+
|
|
3883
|
+
function rewriteAbsoluteUrls(body, proxyBase) {
|
|
3884
|
+
let out = body;
|
|
3885
|
+
// HTML attributes: src, href, action, poster
|
|
3886
|
+
out = out.replace(/((?:src|href|action|poster)\s*=\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3887
|
+
// JS imports: from '/' and import('/')
|
|
3888
|
+
out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3889
|
+
out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3890
|
+
// CSS url()
|
|
3891
|
+
out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3892
|
+
return out;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
const REWRITABLE_TYPES = ['text/html', 'application/javascript', 'text/javascript', 'text/css'];
|
|
3896
|
+
|
|
3897
|
+
function handleProxyResponse(proxyRes, res, proxyBase) {
|
|
3898
|
+
const fwdHeaders = { ...proxyRes.headers };
|
|
3899
|
+
delete fwdHeaders['content-security-policy'];
|
|
3900
|
+
delete fwdHeaders['x-frame-options'];
|
|
3901
|
+
|
|
3902
|
+
const ct = (fwdHeaders['content-type'] || '').toLowerCase();
|
|
3903
|
+
const shouldRewrite = REWRITABLE_TYPES.some((t) => ct.includes(t));
|
|
3904
|
+
|
|
3905
|
+
if (!shouldRewrite) {
|
|
3906
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3907
|
+
proxyRes.pipe(res);
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
const chunks = [];
|
|
3912
|
+
proxyRes.on('data', (c) => chunks.push(c));
|
|
3913
|
+
proxyRes.on('end', () => {
|
|
3914
|
+
let body = Buffer.concat(chunks).toString('utf8');
|
|
3915
|
+
body = rewriteAbsoluteUrls(body, proxyBase);
|
|
3916
|
+
const buf = Buffer.from(body, 'utf8');
|
|
3917
|
+
fwdHeaders['content-length'] = buf.length;
|
|
3918
|
+
delete fwdHeaders['transfer-encoding'];
|
|
3919
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3920
|
+
res.end(buf);
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3858
3924
|
app.all('/api/preview/:teamId/proxy/*', (req, res) => {
|
|
3859
3925
|
const entry = daemon.preview?.get(req.params.teamId);
|
|
3860
3926
|
if (!entry || !entry.url) return res.status(404).json({ error: 'No active preview for this team' });
|
|
@@ -3865,9 +3931,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3865
3931
|
const proxyPath = req.params[0] || '';
|
|
3866
3932
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3867
3933
|
const fullPath = '/' + proxyPath + search;
|
|
3934
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3868
3935
|
|
|
3869
3936
|
const headers = { ...req.headers };
|
|
3870
3937
|
delete headers.host;
|
|
3938
|
+
delete headers['accept-encoding'];
|
|
3871
3939
|
headers.host = targetUrl.host;
|
|
3872
3940
|
|
|
3873
3941
|
const proxyReq = httpRequest({
|
|
@@ -3876,13 +3944,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3876
3944
|
path: fullPath,
|
|
3877
3945
|
method: req.method,
|
|
3878
3946
|
headers,
|
|
3879
|
-
}, (proxyRes) =>
|
|
3880
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3881
|
-
delete fwdHeaders['content-security-policy'];
|
|
3882
|
-
delete fwdHeaders['x-frame-options'];
|
|
3883
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3884
|
-
proxyRes.pipe(res);
|
|
3885
|
-
});
|
|
3947
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3886
3948
|
|
|
3887
3949
|
proxyReq.on('error', (err) => {
|
|
3888
3950
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -3899,9 +3961,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3899
3961
|
try { targetUrl = new URL(entry.devUrl || entry.url); } catch { return res.status(500).json({ error: 'Invalid preview URL' }); }
|
|
3900
3962
|
|
|
3901
3963
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3964
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3902
3965
|
|
|
3903
3966
|
const headers = { ...req.headers };
|
|
3904
3967
|
delete headers.host;
|
|
3968
|
+
delete headers['accept-encoding'];
|
|
3905
3969
|
headers.host = targetUrl.host;
|
|
3906
3970
|
|
|
3907
3971
|
const proxyReq = httpRequest({
|
|
@@ -3910,13 +3974,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3910
3974
|
path: '/' + search,
|
|
3911
3975
|
method: req.method,
|
|
3912
3976
|
headers,
|
|
3913
|
-
}, (proxyRes) =>
|
|
3914
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3915
|
-
delete fwdHeaders['content-security-policy'];
|
|
3916
|
-
delete fwdHeaders['x-frame-options'];
|
|
3917
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3918
|
-
proxyRes.pipe(res);
|
|
3919
|
-
});
|
|
3977
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3920
3978
|
|
|
3921
3979
|
proxyReq.on('error', (err) => {
|
|
3922
3980
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -6183,13 +6241,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
6183
6241
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
6184
6242
|
env: tagEnv,
|
|
6185
6243
|
});
|
|
6244
|
+
daemon._networkCheckProc = proc;
|
|
6186
6245
|
let stdout = '';
|
|
6187
6246
|
let stderr = '';
|
|
6188
6247
|
proc.stdout.on('data', (c) => { stdout += c.toString(); });
|
|
6189
6248
|
proc.stderr.on('data', (c) => { stderr += c.toString(); });
|
|
6190
6249
|
const timeout = setTimeout(() => { safeKill(proc, 'SIGTERM'); }, 10_000);
|
|
6191
|
-
proc.on('error', () => { clearTimeout(timeout); resolvePromise(null); });
|
|
6250
|
+
proc.on('error', () => { clearTimeout(timeout); daemon._networkCheckProc = null; resolvePromise(null); });
|
|
6192
6251
|
proc.on('close', (code) => {
|
|
6252
|
+
daemon._networkCheckProc = null;
|
|
6193
6253
|
clearTimeout(timeout);
|
|
6194
6254
|
if (code !== 0) return resolvePromise(null);
|
|
6195
6255
|
const tags = [];
|