groove-dev 0.27.75 → 0.27.78
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/MOE_TRAINING_PIPELINE.md +216 -12
- package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
- package/moe-training/client/consent.js +96 -0
- package/moe-training/client/envelope-builder.js +56 -0
- package/moe-training/client/index.js +10 -0
- package/moe-training/client/parsers/claude-code.js +110 -0
- package/moe-training/client/parsers/codex.js +80 -0
- package/moe-training/client/parsers/gemini.js +80 -0
- package/moe-training/client/parsers/grok.js +16 -0
- package/moe-training/client/parsers/index.js +20 -0
- package/moe-training/client/scrubber.js +126 -0
- package/moe-training/client/session-attestation.js +114 -0
- package/moe-training/client/step-classifier.js +51 -0
- package/moe-training/client/trajectory-capture.js +227 -0
- package/moe-training/client/transmission-queue.js +93 -0
- package/moe-training/package-lock.json +1266 -0
- package/moe-training/package.json +20 -0
- package/moe-training/server/enrichment.js +24 -0
- package/moe-training/server/index.js +119 -0
- package/moe-training/server/ledger.js +110 -0
- package/moe-training/server/routes/ingest.js +96 -0
- package/moe-training/server/routes/sessions.js +43 -0
- package/moe-training/server/routes/stats.js +31 -0
- package/moe-training/server/scoring.js +63 -0
- package/moe-training/server/session-registry.js +156 -0
- package/moe-training/server/stats.js +129 -0
- package/moe-training/server/stitcher.js +69 -0
- package/moe-training/server/storage.js +147 -0
- package/moe-training/server/verifier.js +102 -0
- package/moe-training/shared/constants.js +30 -0
- package/moe-training/shared/crypto.js +45 -0
- package/moe-training/shared/envelope-schema.js +220 -0
- package/moe-training/test/client/consent.test.js +121 -0
- package/moe-training/test/client/envelope-builder.test.js +107 -0
- package/moe-training/test/client/parsers/claude-code.test.js +119 -0
- package/moe-training/test/client/parsers/codex.test.js +83 -0
- package/moe-training/test/client/parsers/gemini.test.js +99 -0
- package/moe-training/test/client/scrubber.test.js +133 -0
- package/moe-training/test/client/session-attestation-security.test.js +95 -0
- package/moe-training/test/client/step-classifier.test.js +88 -0
- package/moe-training/test/integration/handshake.test.js +260 -0
- package/moe-training/test/server/ingest-security.test.js +166 -0
- package/moe-training/test/server/ledger.test.js +131 -0
- package/moe-training/test/server/scoring.test.js +242 -0
- package/moe-training/test/server/session-registry.test.js +125 -0
- package/moe-training/test/server/stitcher.test.js +157 -0
- package/moe-training/test/server/verifier.test.js +232 -0
- package/moe-training/test/shared/crypto.test.js +87 -0
- package/moe-training/test/shared/envelope-schema.test.js +351 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/agent-loop.js +48 -5
- package/node_modules/@groove-dev/daemon/src/api.js +77 -0
- package/node_modules/@groove-dev/daemon/src/index.js +61 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
- package/node_modules/@groove-dev/daemon/src/preview.js +14 -0
- package/node_modules/@groove-dev/daemon/src/process.js +203 -1
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
- package/node_modules/@groove-dev/daemon/src/state.js +20 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-CAT9SCJi.js → index-BJgEJ9lZ.js} +1700 -1704
- package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.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/app.css +12 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +32 -27
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +26 -24
- package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +34 -6
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +19 -4
- package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +91 -57
- package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/agent-loop.js +48 -5
- package/packages/daemon/src/api.js +77 -0
- package/packages/daemon/src/index.js +61 -0
- package/packages/daemon/src/journalist.js +64 -21
- package/packages/daemon/src/preview.js +14 -0
- package/packages/daemon/src/process.js +203 -1
- package/packages/daemon/src/providers/grok.js +15 -0
- package/packages/daemon/src/state.js +20 -1
- package/packages/gui/dist/assets/{index-CAT9SCJi.js → index-BJgEJ9lZ.js} +1700 -1704
- package/packages/gui/dist/assets/index-kbR5tOHu.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +12 -0
- package/packages/gui/src/components/chat/chat-input.jsx +32 -27
- package/packages/gui/src/components/chat/chat-messages.jsx +26 -24
- package/packages/gui/src/components/preview/preview-toolbar.jsx +34 -6
- package/packages/gui/src/components/preview/preview-workspace.jsx +19 -4
- package/packages/gui/src/components/preview/screenshot-overlay.jsx +91 -57
- package/packages/gui/src/stores/groove.js +32 -0
- package/packages/gui/src/views/settings.jsx +167 -1
- package/welcome.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +0 -1
- package/packages/gui/dist/assets/index-CVzz6zyb.css +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { platform, arch, cpus, totalmem, hostname, release, endianness } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { generateECDHKeypair, deriveSharedSecret, signEnvelope, computeAppHash } from '../shared/crypto.js';
|
|
6
|
+
|
|
7
|
+
export class SessionAttestation {
|
|
8
|
+
constructor(centralCommandUrl) {
|
|
9
|
+
this._centralCommandUrl = centralCommandUrl;
|
|
10
|
+
this._sessions = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async openSession(sessionId, metadata) {
|
|
14
|
+
const keypair = generateECDHKeypair();
|
|
15
|
+
let appVersionHash = '';
|
|
16
|
+
try {
|
|
17
|
+
appVersionHash = computeAppHash(new URL(import.meta.url).pathname);
|
|
18
|
+
} catch {
|
|
19
|
+
appVersionHash = 'unknown';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const body = {
|
|
23
|
+
session_id: sessionId,
|
|
24
|
+
public_key: keypair.publicKey,
|
|
25
|
+
app_version_hash: appVersionHash,
|
|
26
|
+
provider: metadata.provider,
|
|
27
|
+
model: metadata.model_engine,
|
|
28
|
+
machine_fingerprint: SessionAttestation.getMachineFingerprint(),
|
|
29
|
+
groove_version: metadata.groove_version,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${this._centralCommandUrl}/v1/sessions/open`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
signal: AbortSignal.timeout(10_000),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
this._sessions.set(sessionId, { keypair, sharedSecret: null, sequence: 0, appVersionHash, offline: true });
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
const sharedSecret = deriveSharedSecret(keypair.privateKey, data.server_public_key);
|
|
47
|
+
this._sessions.set(sessionId, { keypair, sharedSecret, sequence: 0, appVersionHash, offline: false });
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
this._sessions.set(sessionId, { keypair, sharedSecret: null, sequence: 0, appVersionHash, offline: true });
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
signEnvelope(sessionId, envelope) {
|
|
56
|
+
const session = this._sessions.get(sessionId);
|
|
57
|
+
if (!session) return envelope;
|
|
58
|
+
|
|
59
|
+
if (session.offline || !session.sharedSecret) {
|
|
60
|
+
envelope.attestation = {
|
|
61
|
+
session_hmac: 'OFFLINE',
|
|
62
|
+
sequence: session.sequence++,
|
|
63
|
+
app_version_hash: session.appVersionHash,
|
|
64
|
+
};
|
|
65
|
+
return envelope;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const forSigning = { ...envelope };
|
|
69
|
+
delete forSigning.attestation;
|
|
70
|
+
const envelopeBytes = JSON.stringify(forSigning);
|
|
71
|
+
const hmac = signEnvelope(session.sharedSecret, envelopeBytes, session.sequence);
|
|
72
|
+
envelope.attestation = {
|
|
73
|
+
session_hmac: hmac,
|
|
74
|
+
sequence: session.sequence++,
|
|
75
|
+
app_version_hash: session.appVersionHash,
|
|
76
|
+
};
|
|
77
|
+
return envelope;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async closeSession(sessionId) {
|
|
81
|
+
const session = this._sessions.get(sessionId);
|
|
82
|
+
if (!session || session.offline) {
|
|
83
|
+
this._sessions.delete(sessionId);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await fetch(`${this._centralCommandUrl}/v1/sessions/close`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ session_id: sessionId }),
|
|
92
|
+
signal: AbortSignal.timeout(10_000),
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
// fail silent
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this._sessions.delete(sessionId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static getMachineFingerprint() {
|
|
102
|
+
const signals = [
|
|
103
|
+
platform(),
|
|
104
|
+
arch(),
|
|
105
|
+
cpus()[0]?.model || '',
|
|
106
|
+
String(totalmem()),
|
|
107
|
+
hostname(),
|
|
108
|
+
String(cpus().length),
|
|
109
|
+
release(),
|
|
110
|
+
endianness(),
|
|
111
|
+
];
|
|
112
|
+
return createHash('sha256').update(signals.join('|')).digest('hex');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
export class StepClassifier {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.hasAgentActed = false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
classifyUserMessage(text) {
|
|
9
|
+
if (!this.hasAgentActed) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
type: 'correction',
|
|
14
|
+
content: text,
|
|
15
|
+
source: 'user',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
classifyCoordinationEvent(event) {
|
|
20
|
+
return {
|
|
21
|
+
type: 'coordination',
|
|
22
|
+
coordination_id: event.coordination_id || event.id || '',
|
|
23
|
+
direction: event.direction || (event.source ? 'inbound' : 'outbound'),
|
|
24
|
+
target_agent: event.target_agent || event.targetAgent || '',
|
|
25
|
+
protocol: event.protocol || 'knock',
|
|
26
|
+
content: event.content || event.message || '',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onStep(step) {
|
|
31
|
+
if (step.type === 'action') {
|
|
32
|
+
this.hasAgentActed = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static detectErrorRecovery(steps) {
|
|
37
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
38
|
+
if (steps[i].type === 'error') {
|
|
39
|
+
for (let j = i + 1; j < steps.length; j++) {
|
|
40
|
+
if (steps[j].type === 'resolution') return true;
|
|
41
|
+
if (steps[j].type === 'error') break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static countUserInterventions(steps) {
|
|
49
|
+
return steps.filter((s) => s.type === 'correction').length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { ConsentManager } from './consent.js';
|
|
5
|
+
import { PIIScrubber } from './scrubber.js';
|
|
6
|
+
import { getParser } from './parsers/index.js';
|
|
7
|
+
import { StepClassifier } from './step-classifier.js';
|
|
8
|
+
import { EnvelopeBuilder } from './envelope-builder.js';
|
|
9
|
+
import { SessionAttestation } from './session-attestation.js';
|
|
10
|
+
import { TransmissionQueue } from './transmission-queue.js';
|
|
11
|
+
import { CHUNK_TIMEOUT_MS, CENTRAL_COMMAND_URL } from '../shared/constants.js';
|
|
12
|
+
|
|
13
|
+
export class TrajectoryCapture {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this._centralCommandUrl = options.centralCommandUrl || CENTRAL_COMMAND_URL;
|
|
16
|
+
this._grooveVersion = options.grooveVersion || '0.0.0';
|
|
17
|
+
this._enabled = false;
|
|
18
|
+
this._scrubber = null;
|
|
19
|
+
this._attestation = null;
|
|
20
|
+
this._transmissionQueue = null;
|
|
21
|
+
this._contexts = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
init() {
|
|
25
|
+
if (!ConsentManager.isCaptureEnabled()) {
|
|
26
|
+
this._enabled = false;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this._enabled = true;
|
|
30
|
+
this._scrubber = new PIIScrubber();
|
|
31
|
+
this._attestation = new SessionAttestation(this._centralCommandUrl);
|
|
32
|
+
this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);
|
|
33
|
+
this._transmissionQueue.start();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async onAgentSpawn(agentId, provider, model, role, teamSize) {
|
|
37
|
+
if (!this._enabled) return;
|
|
38
|
+
|
|
39
|
+
const parser = getParser(provider);
|
|
40
|
+
if (!parser) return;
|
|
41
|
+
|
|
42
|
+
const sessionId = `sess_${randomUUID()}`;
|
|
43
|
+
const contributorId = ConsentManager.getOrCreateUserId();
|
|
44
|
+
const metadata = {
|
|
45
|
+
model_engine: model,
|
|
46
|
+
provider,
|
|
47
|
+
agent_role: role,
|
|
48
|
+
agent_id: agentId,
|
|
49
|
+
task_complexity: 'medium',
|
|
50
|
+
team_size: teamSize || 1,
|
|
51
|
+
session_quality: 0,
|
|
52
|
+
groove_version: this._grooveVersion,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const builder = new EnvelopeBuilder(sessionId, contributorId, metadata);
|
|
56
|
+
const classifier = new StepClassifier();
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
|
|
59
|
+
const ctx = {
|
|
60
|
+
sessionId,
|
|
61
|
+
parser,
|
|
62
|
+
builder,
|
|
63
|
+
classifier,
|
|
64
|
+
metadata,
|
|
65
|
+
stepCount: 0,
|
|
66
|
+
chunkCount: 0,
|
|
67
|
+
totalTokens: 0,
|
|
68
|
+
errorsEncountered: 0,
|
|
69
|
+
errorsRecovered: 0,
|
|
70
|
+
filesModified: 0,
|
|
71
|
+
coordinationEvents: 0,
|
|
72
|
+
startTime,
|
|
73
|
+
chunkTimer: null,
|
|
74
|
+
allSteps: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
ctx.chunkTimer = setInterval(() => {
|
|
78
|
+
this._flushContext(agentId);
|
|
79
|
+
}, CHUNK_TIMEOUT_MS);
|
|
80
|
+
|
|
81
|
+
this._contexts.set(agentId, ctx);
|
|
82
|
+
|
|
83
|
+
await this._attestation.openSession(sessionId, metadata);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onStdoutLine(agentId, jsonLine) {
|
|
87
|
+
if (!this._enabled) return;
|
|
88
|
+
const ctx = this._contexts.get(agentId);
|
|
89
|
+
if (!ctx) return;
|
|
90
|
+
|
|
91
|
+
let jsonEvent;
|
|
92
|
+
try {
|
|
93
|
+
jsonEvent = typeof jsonLine === 'string' ? JSON.parse(jsonLine) : jsonLine;
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parsed = ctx.parser.parseEvent(jsonEvent);
|
|
99
|
+
if (!parsed) return;
|
|
100
|
+
|
|
101
|
+
const events = Array.isArray(parsed) ? parsed : [parsed];
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
this._processStep(agentId, ctx, event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tokens = ctx.parser.extractTokens(jsonEvent);
|
|
107
|
+
if (tokens) {
|
|
108
|
+
ctx.totalTokens += (tokens.input || 0) + (tokens.output || 0);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onUserMessage(agentId, text) {
|
|
113
|
+
if (!this._enabled) return;
|
|
114
|
+
const ctx = this._contexts.get(agentId);
|
|
115
|
+
if (!ctx) return;
|
|
116
|
+
|
|
117
|
+
const classified = ctx.classifier.classifyUserMessage(text);
|
|
118
|
+
if (!classified) return;
|
|
119
|
+
|
|
120
|
+
this._processStep(agentId, ctx, classified);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onCoordinationEvent(agentId, event) {
|
|
124
|
+
if (!this._enabled) return;
|
|
125
|
+
const ctx = this._contexts.get(agentId);
|
|
126
|
+
if (!ctx) return;
|
|
127
|
+
|
|
128
|
+
const classified = ctx.classifier.classifyCoordinationEvent(event);
|
|
129
|
+
ctx.coordinationEvents++;
|
|
130
|
+
this._processStep(agentId, ctx, classified);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async onAgentComplete(agentId, outcome) {
|
|
134
|
+
await this._closeAgent(agentId, outcome?.status || 'SUCCESS', outcome);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async onAgentCrash(agentId, error) {
|
|
138
|
+
await this._closeAgent(agentId, 'CRASH', { error: error?.message || String(error) });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async shutdown() {
|
|
142
|
+
for (const agentId of this._contexts.keys()) {
|
|
143
|
+
await this._closeAgent(agentId, 'SHUTDOWN');
|
|
144
|
+
}
|
|
145
|
+
if (this._transmissionQueue) {
|
|
146
|
+
await this._transmissionQueue.stop();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_processStep(agentId, ctx, event) {
|
|
151
|
+
ctx.classifier.onStep(event);
|
|
152
|
+
|
|
153
|
+
if (event.content && typeof event.content === 'string') {
|
|
154
|
+
event.content = this._scrubber.scrub(event.content);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const step = {
|
|
158
|
+
step: ++ctx.stepCount,
|
|
159
|
+
type: event.type,
|
|
160
|
+
timestamp: Date.now() / 1000,
|
|
161
|
+
...event,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (event.type === 'error') ctx.errorsEncountered++;
|
|
165
|
+
ctx.allSteps.push(step);
|
|
166
|
+
|
|
167
|
+
const envelope = ctx.builder.addStep(step);
|
|
168
|
+
if (envelope) {
|
|
169
|
+
this._signAndTransmit(ctx.sessionId, envelope);
|
|
170
|
+
ctx.chunkCount++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_flushContext(agentId) {
|
|
175
|
+
const ctx = this._contexts.get(agentId);
|
|
176
|
+
if (!ctx) return;
|
|
177
|
+
const envelope = ctx.builder.flush();
|
|
178
|
+
if (envelope) {
|
|
179
|
+
this._signAndTransmit(ctx.sessionId, envelope);
|
|
180
|
+
ctx.chunkCount++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async _closeAgent(agentId, status, extra) {
|
|
185
|
+
const ctx = this._contexts.get(agentId);
|
|
186
|
+
if (!ctx) return;
|
|
187
|
+
|
|
188
|
+
if (ctx.chunkTimer) clearInterval(ctx.chunkTimer);
|
|
189
|
+
|
|
190
|
+
this._flushContext(agentId);
|
|
191
|
+
|
|
192
|
+
const hasRecovery = StepClassifier.detectErrorRecovery(ctx.allSteps);
|
|
193
|
+
if (hasRecovery) ctx.errorsRecovered++;
|
|
194
|
+
|
|
195
|
+
const closeEnvelope = ctx.builder.buildSessionClose({
|
|
196
|
+
status,
|
|
197
|
+
user_interventions: StepClassifier.countUserInterventions(ctx.allSteps),
|
|
198
|
+
total_steps: ctx.stepCount,
|
|
199
|
+
total_chunks: ctx.chunkCount,
|
|
200
|
+
total_tokens: ctx.totalTokens,
|
|
201
|
+
duration_seconds: Math.round((Date.now() - ctx.startTime) / 1000),
|
|
202
|
+
files_modified: extra?.files_modified || ctx.filesModified,
|
|
203
|
+
errors_encountered: ctx.errorsEncountered,
|
|
204
|
+
errors_recovered: ctx.errorsRecovered,
|
|
205
|
+
coordination_events: ctx.coordinationEvents,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this._signAndTransmit(ctx.sessionId, closeEnvelope);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await this._attestation.closeSession(ctx.sessionId);
|
|
212
|
+
} catch {
|
|
213
|
+
// fail silent
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this._contexts.delete(agentId);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_signAndTransmit(sessionId, envelope) {
|
|
220
|
+
try {
|
|
221
|
+
const signed = this._attestation.signEnvelope(sessionId, envelope);
|
|
222
|
+
this._transmissionQueue.enqueue(signed);
|
|
223
|
+
} catch {
|
|
224
|
+
// fail silent
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { MAX_QUEUE_SIZE } from '../shared/constants.js';
|
|
4
|
+
|
|
5
|
+
export class TransmissionQueue {
|
|
6
|
+
constructor(centralCommandUrl, maxSize = MAX_QUEUE_SIZE) {
|
|
7
|
+
this._centralCommandUrl = centralCommandUrl;
|
|
8
|
+
this._maxSize = maxSize;
|
|
9
|
+
this._queue = [];
|
|
10
|
+
this._offlineQueue = [];
|
|
11
|
+
this._running = false;
|
|
12
|
+
this._drainPromise = null;
|
|
13
|
+
|
|
14
|
+
if (process.env.NODE_ENV === 'production' && !centralCommandUrl.startsWith('https://')) {
|
|
15
|
+
console.warn('[TransmissionQueue] WARNING: centralCommandUrl does not use HTTPS in production');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
enqueue(signedEnvelope) {
|
|
20
|
+
if (this._queue.length >= this._maxSize) return;
|
|
21
|
+
if (signedEnvelope?.attestation?.session_hmac === 'OFFLINE') {
|
|
22
|
+
this._offlineQueue.push(signedEnvelope);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this._queue.push(signedEnvelope);
|
|
26
|
+
if (this._running) this._kick();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
replayOfflineQueue(sessionAttestation) {
|
|
30
|
+
const pending = this._offlineQueue.splice(0);
|
|
31
|
+
for (const envelope of pending) {
|
|
32
|
+
const sessionId = envelope.session_id;
|
|
33
|
+
const resigned = sessionAttestation.signEnvelope(sessionId, envelope);
|
|
34
|
+
if (resigned?.attestation?.session_hmac === 'OFFLINE') {
|
|
35
|
+
this._offlineQueue.push(resigned);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
this._queue.push(resigned);
|
|
39
|
+
}
|
|
40
|
+
if (this._running && this._queue.length > 0) this._kick();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
start() {
|
|
44
|
+
if (this._running) return;
|
|
45
|
+
this._running = true;
|
|
46
|
+
this._kick();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async stop() {
|
|
50
|
+
this._running = false;
|
|
51
|
+
if (this._drainPromise) {
|
|
52
|
+
await this._drainPromise;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_kick() {
|
|
57
|
+
if (this._drainPromise) return;
|
|
58
|
+
this._drainPromise = this._drain().finally(() => {
|
|
59
|
+
this._drainPromise = null;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _drain() {
|
|
64
|
+
while (this._running && this._queue.length > 0) {
|
|
65
|
+
const envelope = this._queue[0];
|
|
66
|
+
let success = false;
|
|
67
|
+
|
|
68
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${this._centralCommandUrl}/v1/training/ingest`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(envelope),
|
|
74
|
+
signal: AbortSignal.timeout(30_000),
|
|
75
|
+
});
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
success = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// network error
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!this._running) return;
|
|
85
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 60_000);
|
|
86
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
87
|
+
if (!this._running) return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this._queue.shift();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|