@yeaft/webchat-agent 1.0.414 → 1.0.415
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/connection/index.js +12 -0
- package/index.js +1 -1
- package/local-runtime/server/client-protocol.js +14 -0
- package/local-runtime/server/context.js +3 -2
- package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
- package/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-misc.js +21 -4
- package/local-runtime/server/handlers/client-workbench.js +222 -41
- package/local-runtime/server/workbench-correlation.js +184 -0
- package/local-runtime/server/workbench-route.js +180 -0
- package/local-runtime/server/ws-agent.js +4 -0
- package/local-runtime/server/ws-client.js +25 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -135
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/terminal.js +167 -30
- package/workbench/file-ops.js +21 -20
- package/workbench/file-search.js +4 -3
- package/workbench/git-ops.js +23 -22
- package/workbench/request-routing.js +16 -0
- package/yeaft/cli.js +57 -1
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function workbenchRequestRouting(source) {
|
|
2
|
+
return {
|
|
3
|
+
...(source?._workbenchRequestId ? { _workbenchRequestId: source._workbenchRequestId } : {}),
|
|
4
|
+
...(source?.workbenchRouteKey ? { workbenchRouteKey: source.workbenchRouteKey } : {}),
|
|
5
|
+
...(source?.workbenchWorkspaceGeneration
|
|
6
|
+
? { workbenchWorkspaceGeneration: source.workbenchWorkspaceGeneration }
|
|
7
|
+
: {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sendWorkbenchResult(ctx, request, result) {
|
|
12
|
+
ctx.sendToServer({
|
|
13
|
+
...result,
|
|
14
|
+
...workbenchRequestRouting(request),
|
|
15
|
+
});
|
|
16
|
+
}
|
package/yeaft/cli.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { createInterface } from 'readline';
|
|
27
27
|
import { randomUUID } from 'node:crypto';
|
|
28
|
+
import { realpathSync } from 'node:fs';
|
|
28
29
|
import { resolve } from 'node:path';
|
|
29
30
|
import { fileURLToPath } from 'node:url';
|
|
30
31
|
import { join } from 'path';
|
|
@@ -35,13 +36,16 @@ import { listModels, resolveModel, parseModelRef, resolveContextWindow, resolveM
|
|
|
35
36
|
import { buildSystemPrompt } from './prompts.js';
|
|
36
37
|
import { searchMessages } from './conversation/search.js';
|
|
37
38
|
import { ConversationStore } from './conversation/persist.js';
|
|
38
|
-
import { snapshotSessions } from './sessions/session-crud.js';
|
|
39
|
+
import { sessionsRoot, snapshotSessions } from './sessions/session-crud.js';
|
|
39
40
|
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
41
|
+
import { createSession } from './sessions/session-store.js';
|
|
42
|
+
import { addOrUpdateManifestSession, withSessionManifestLock } from './sessions/session-manifest.js';
|
|
40
43
|
import { validateSessionId } from './sessions/ids.js';
|
|
41
44
|
import {
|
|
42
45
|
createJsonlWriter,
|
|
43
46
|
JsonlInput,
|
|
44
47
|
normalizeStreamRoutingIntent,
|
|
48
|
+
normalizeStreamSessionBootstrap,
|
|
45
49
|
runStreamTurn,
|
|
46
50
|
runStreamSessionTurn,
|
|
47
51
|
} from './stdio-protocol.js';
|
|
@@ -900,6 +904,7 @@ async function runStreamJson(config, args) {
|
|
|
900
904
|
let taskEventIntakeOpen = true;
|
|
901
905
|
let hadError = false;
|
|
902
906
|
let singleEngineTail = Promise.resolve();
|
|
907
|
+
let streamSessionBootstrapOpen = false;
|
|
903
908
|
|
|
904
909
|
const loadStreamHistory = () => conversationStore.loadRecentBySession(sessionId, 20).map(message => ({
|
|
905
910
|
role: message.role,
|
|
@@ -1033,6 +1038,16 @@ async function runStreamJson(config, args) {
|
|
|
1033
1038
|
|
|
1034
1039
|
configureStreamEngine(engine, null);
|
|
1035
1040
|
sessionRunner = createCliSessionRunner({ loaded, sessionId, workDir, configureEngine: configureStreamEngine });
|
|
1041
|
+
if (sessionRunner) {
|
|
1042
|
+
snapshotSessions(loaded.yeaftDir);
|
|
1043
|
+
const meta = sessionRunner.meta;
|
|
1044
|
+
if (meta) addOrUpdateManifestSession(
|
|
1045
|
+
loaded.yeaftDir,
|
|
1046
|
+
meta,
|
|
1047
|
+
join(sessionsRoot(loaded.yeaftDir), sessionId),
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
streamSessionBootstrapOpen = !sessionRunner;
|
|
1036
1051
|
|
|
1037
1052
|
write({
|
|
1038
1053
|
type: 'system',
|
|
@@ -1091,6 +1106,47 @@ async function runStreamJson(config, args) {
|
|
|
1091
1106
|
};
|
|
1092
1107
|
|
|
1093
1108
|
const runPrompt = async (prompt, message = null) => {
|
|
1109
|
+
if (streamSessionBootstrapOpen) {
|
|
1110
|
+
streamSessionBootstrapOpen = false;
|
|
1111
|
+
const bootstrap = normalizeStreamSessionBootstrap(message);
|
|
1112
|
+
if (bootstrap) {
|
|
1113
|
+
let workspaceKey = '';
|
|
1114
|
+
try { workspaceKey = realpathSync(resolve(workDir)); } catch { /* leave empty */ }
|
|
1115
|
+
withSessionManifestLock(loaded.yeaftDir, () => {
|
|
1116
|
+
sessionRunner = createCliSessionRunner({
|
|
1117
|
+
loaded,
|
|
1118
|
+
sessionId,
|
|
1119
|
+
workDir,
|
|
1120
|
+
configureEngine: configureStreamEngine,
|
|
1121
|
+
});
|
|
1122
|
+
if (sessionRunner) return;
|
|
1123
|
+
const handle = createSession(sessionsRoot(loaded.yeaftDir), {
|
|
1124
|
+
id: sessionId,
|
|
1125
|
+
name: sessionId,
|
|
1126
|
+
roster: bootstrap.roster,
|
|
1127
|
+
defaultVpId: bootstrap.defaultVpId,
|
|
1128
|
+
workDir,
|
|
1129
|
+
workspaceKey,
|
|
1130
|
+
});
|
|
1131
|
+
handle.close();
|
|
1132
|
+
});
|
|
1133
|
+
if (!sessionRunner) {
|
|
1134
|
+
sessionRunner = createCliSessionRunner({
|
|
1135
|
+
loaded,
|
|
1136
|
+
sessionId,
|
|
1137
|
+
workDir,
|
|
1138
|
+
configureEngine: configureStreamEngine,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
if (!sessionRunner) throw new Error(`Failed to initialize formal stream-json Session ${sessionId}`);
|
|
1142
|
+
const meta = sessionRunner.meta;
|
|
1143
|
+
if (meta) addOrUpdateManifestSession(
|
|
1144
|
+
loaded.yeaftDir,
|
|
1145
|
+
meta,
|
|
1146
|
+
join(sessionsRoot(loaded.yeaftDir), sessionId),
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1094
1150
|
const routingIntent = normalizeStreamRoutingIntent(message);
|
|
1095
1151
|
if (routingIntent && !sessionRunner) {
|
|
1096
1152
|
throw new Error('stream-json VP selectors require an existing formal Session with a persisted roster');
|
|
@@ -15,18 +15,25 @@
|
|
|
15
15
|
import {
|
|
16
16
|
cpSync,
|
|
17
17
|
existsSync,
|
|
18
|
+
linkSync,
|
|
18
19
|
mkdirSync,
|
|
19
20
|
readFileSync,
|
|
20
21
|
readdirSync,
|
|
21
22
|
rmSync,
|
|
22
23
|
statSync,
|
|
24
|
+
unlinkSync,
|
|
23
25
|
writeFileSync,
|
|
24
26
|
} from 'fs';
|
|
27
|
+
import { randomUUID } from 'node:crypto';
|
|
25
28
|
import { join } from 'path';
|
|
26
29
|
import { loadSessionMeta } from './session-store.js';
|
|
30
|
+
import { writeAtomic } from '../storage/atomic.js';
|
|
27
31
|
|
|
28
32
|
export const SESSIONS_MANIFEST_FILE = 'sessions-manifest.json';
|
|
29
33
|
const MANIFEST_VERSION = 1;
|
|
34
|
+
const MANIFEST_LOCK_FILE = 'sessions-manifest.lock';
|
|
35
|
+
const MANIFEST_LOCK_WAIT_MS = 5_000;
|
|
36
|
+
const lockWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
30
37
|
|
|
31
38
|
export function sessionManifestPath(yeaftDir) {
|
|
32
39
|
return join(yeaftDir, SESSIONS_MANIFEST_FILE);
|
|
@@ -62,6 +69,10 @@ export function loadSessionsManifest(yeaftDir) {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
export function writeSessionsManifest(yeaftDir, sessions) {
|
|
72
|
+
return withManifestLock(yeaftDir, () => writeSessionsManifestUnlocked(yeaftDir, sessions));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeSessionsManifestUnlocked(yeaftDir, sessions) {
|
|
65
76
|
if (!yeaftDir) throw new Error('yeaftDir required');
|
|
66
77
|
mkdirSync(yeaftDir, { recursive: true });
|
|
67
78
|
const manifest = {
|
|
@@ -69,7 +80,7 @@ export function writeSessionsManifest(yeaftDir, sessions) {
|
|
|
69
80
|
generatedAt: new Date().toISOString(),
|
|
70
81
|
sessions: dedupeSessions(sessions).sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || ''))),
|
|
71
82
|
};
|
|
72
|
-
|
|
83
|
+
writeAtomic(sessionManifestPath(yeaftDir), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
73
84
|
return manifest;
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -171,21 +182,114 @@ export function ensureSessionsManifest(yeaftDir, options) {
|
|
|
171
182
|
}
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
return withManifestLock(yeaftDir, () => {
|
|
186
|
+
const current = loadSessionsManifest(yeaftDir);
|
|
187
|
+
const manifest = writeSessionsManifestUnlocked(yeaftDir, [
|
|
188
|
+
...(current?.sessions || []),
|
|
189
|
+
...buildManifestFromLocalSessions(yeaftDir, root),
|
|
190
|
+
]);
|
|
191
|
+
return {
|
|
192
|
+
created: !current,
|
|
193
|
+
migrated,
|
|
194
|
+
skipped,
|
|
195
|
+
migratedIds,
|
|
196
|
+
skippedIds,
|
|
197
|
+
manifest,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
176
200
|
}
|
|
177
201
|
|
|
178
202
|
export function addOrUpdateManifestSession(yeaftDir, meta, dir) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
return withManifestLock(yeaftDir, () => {
|
|
204
|
+
const current = loadSessionsManifest(yeaftDir);
|
|
205
|
+
const recovered = current?.sessions
|
|
206
|
+
|| buildManifestFromLocalSessions(yeaftDir, join(yeaftDir, 'sessions'));
|
|
207
|
+
const rows = recovered.filter(row => row.id !== meta.id);
|
|
208
|
+
rows.push(manifestRowFromMeta(meta, dir));
|
|
209
|
+
return writeSessionsManifestUnlocked(yeaftDir, rows);
|
|
210
|
+
});
|
|
183
211
|
}
|
|
184
212
|
|
|
185
213
|
export function removeManifestSession(yeaftDir, sessionId) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
214
|
+
return withManifestLock(yeaftDir, () => {
|
|
215
|
+
const current = loadSessionsManifest(yeaftDir);
|
|
216
|
+
if (!current) return null;
|
|
217
|
+
return writeSessionsManifestUnlocked(yeaftDir, current.sessions.filter(row => row.id !== sessionId));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function withSessionManifestLock(yeaftDir, operation) {
|
|
222
|
+
if (!yeaftDir) throw new Error('yeaftDir required');
|
|
223
|
+
mkdirSync(yeaftDir, { recursive: true });
|
|
224
|
+
const lockFile = join(yeaftDir, MANIFEST_LOCK_FILE);
|
|
225
|
+
const deadline = Date.now() + MANIFEST_LOCK_WAIT_MS;
|
|
226
|
+
const token = randomUUID();
|
|
227
|
+
const ownerFile = `${lockFile}.owner.${process.pid}.${token}`;
|
|
228
|
+
const owner = { pid: process.pid, token, ownerFile };
|
|
229
|
+
writeFileSync(ownerFile, `${JSON.stringify(owner)}\n`, { flag: 'wx' });
|
|
230
|
+
for (;;) {
|
|
231
|
+
try {
|
|
232
|
+
linkSync(ownerFile, lockFile);
|
|
233
|
+
break;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error?.code !== 'EEXIST') {
|
|
236
|
+
try { unlinkSync(ownerFile); } catch {}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
reapDeadManifestLock(lockFile);
|
|
240
|
+
if (Date.now() >= deadline) {
|
|
241
|
+
try { unlinkSync(ownerFile); } catch {}
|
|
242
|
+
throw new Error('Timed out waiting for Session manifest lock');
|
|
243
|
+
}
|
|
244
|
+
Atomics.wait(lockWaitArray, 0, 0, 25);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return operation();
|
|
249
|
+
} finally {
|
|
250
|
+
try {
|
|
251
|
+
const current = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
252
|
+
if (current.token === owner.token) unlinkSync(lockFile);
|
|
253
|
+
} catch {
|
|
254
|
+
// A missing lock means another process already recovered this owner.
|
|
255
|
+
}
|
|
256
|
+
try { unlinkSync(ownerFile); } catch {}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function withManifestLock(yeaftDir, operation) {
|
|
261
|
+
return withSessionManifestLock(yeaftDir, operation);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function reapDeadManifestLock(lockFile) {
|
|
265
|
+
let observed;
|
|
266
|
+
try {
|
|
267
|
+
observed = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (!Number.isInteger(observed.pid) || typeof observed.token !== 'string') return;
|
|
272
|
+
try {
|
|
273
|
+
process.kill(observed.pid, 0);
|
|
274
|
+
return;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error?.code === 'EPERM') return;
|
|
277
|
+
}
|
|
278
|
+
const claim = `${lockFile}.reap.${process.pid}.${randomUUID()}`;
|
|
279
|
+
try {
|
|
280
|
+
linkSync(lockFile, claim);
|
|
281
|
+
const claimed = JSON.parse(readFileSync(claim, 'utf8'));
|
|
282
|
+
if (claimed.token !== observed.token) return;
|
|
283
|
+
unlinkSync(lockFile);
|
|
284
|
+
const expectedOwnerFile = `${lockFile}.owner.${observed.pid}.${observed.token}`;
|
|
285
|
+
if (observed.ownerFile === expectedOwnerFile) {
|
|
286
|
+
try { unlinkSync(expectedOwnerFile); } catch {}
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// Another process either recovered or replaced the observed lock.
|
|
290
|
+
} finally {
|
|
291
|
+
try { unlinkSync(claim); } catch {}
|
|
292
|
+
}
|
|
189
293
|
}
|
|
190
294
|
|
|
191
295
|
function manifestRowFromMeta(meta, dir) {
|
package/yeaft/stdio-protocol.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isReservedVpId, validateVpId } from './sessions/ids.js';
|
|
3
4
|
|
|
4
5
|
const TERMINAL_STOP_REASONS = new Set([
|
|
5
6
|
'end_turn', 'max_tokens', 'stop_sequence', 'aborted', 'error', 'tool_handoff', 'plan_recorded',
|
|
@@ -78,6 +79,62 @@ export function normalizeStreamRoutingIntent(message) {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Read the opt-in formal Session seed supplied by an integration's first
|
|
84
|
+
* stream-json prompt. Existing Sessions keep their persisted roster; this only
|
|
85
|
+
* establishes a canonical roster for a previously unknown session id.
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeStreamSessionBootstrap(message) {
|
|
88
|
+
if (!message || typeof message !== 'object') return null;
|
|
89
|
+
const hasRoster = Object.hasOwn(message, 'roster');
|
|
90
|
+
const hasVps = Object.hasOwn(message, 'vps');
|
|
91
|
+
const hasDefault = Object.hasOwn(message, 'defaultVpId');
|
|
92
|
+
if (!hasRoster && !hasVps && !hasDefault) return null;
|
|
93
|
+
if (!hasRoster && !hasVps) {
|
|
94
|
+
throw new Error('stream-json defaultVpId requires roster or vps');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const readRoster = (key) => {
|
|
98
|
+
const value = message[key];
|
|
99
|
+
if (!Array.isArray(value)) throw new Error(`stream-json ${key} must be an array`);
|
|
100
|
+
return value.slice();
|
|
101
|
+
};
|
|
102
|
+
const roster = hasRoster ? readRoster('roster') : readRoster('vps');
|
|
103
|
+
if (hasRoster && hasVps) {
|
|
104
|
+
const vps = readRoster('vps');
|
|
105
|
+
if (vps.length !== roster.length || vps.some((vpId, index) => vpId !== roster[index])) {
|
|
106
|
+
throw new Error('stream-json roster and vps must contain the same VP ids in the same order');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
for (const vpId of roster) {
|
|
112
|
+
const verdict = validateVpId(vpId);
|
|
113
|
+
if (!verdict.ok || isReservedVpId(vpId)) {
|
|
114
|
+
throw new Error(`stream-json roster contains invalid VP id ${JSON.stringify(vpId)} (${verdict.reason || 'reserved'})`);
|
|
115
|
+
}
|
|
116
|
+
if (seen.has(vpId)) throw new Error(`stream-json roster contains duplicate VP id ${vpId}`);
|
|
117
|
+
seen.add(vpId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let defaultVpId = roster[0] || null;
|
|
121
|
+
if (hasDefault && message.defaultVpId != null) {
|
|
122
|
+
defaultVpId = message.defaultVpId;
|
|
123
|
+
const verdict = validateVpId(defaultVpId);
|
|
124
|
+
if (!verdict.ok || isReservedVpId(defaultVpId)) {
|
|
125
|
+
throw new Error(`stream-json defaultVpId is invalid (${verdict.reason || 'reserved'})`);
|
|
126
|
+
}
|
|
127
|
+
if (!seen.has(defaultVpId)) {
|
|
128
|
+
throw new Error(`stream-json defaultVpId ${defaultVpId} is not in roster`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
roster: Object.freeze(roster),
|
|
134
|
+
defaultVpId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
81
138
|
export function createJsonlWriter(output = process.stdout) {
|
|
82
139
|
return event => { output.write(`${JSON.stringify(event)}\n`); };
|
|
83
140
|
}
|