@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.
@@ -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
- writeFileSync(sessionManifestPath(yeaftDir), `${JSON.stringify(manifest, null, 2)}\n`);
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
- const manifest = writeSessionsManifest(yeaftDir, buildManifestFromLocalSessions(yeaftDir, root));
175
- return { created: true, migrated, skipped, migratedIds, skippedIds, manifest };
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
- const current = loadSessionsManifest(yeaftDir) || { sessions: [] };
180
- const rows = current.sessions.filter(row => row.id !== meta.id);
181
- rows.push(manifestRowFromMeta(meta, dir));
182
- return writeSessionsManifest(yeaftDir, rows);
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
- const current = loadSessionsManifest(yeaftDir);
187
- if (!current) return null;
188
- return writeSessionsManifest(yeaftDir, current.sessions.filter(row => row.id !== sessionId));
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) {
@@ -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
  }