@yeaft/webchat-agent 1.0.299 → 1.0.301
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/context.js +1 -0
- package/index.js +9 -0
- package/local-runtime/server/database.js +1 -0
- package/local-runtime/server/db/connection.js +86 -0
- package/local-runtime/server/db/yeaft-project-db.js +225 -0
- package/local-runtime/server/handlers/agent-output.js +41 -5
- package/local-runtime/server/handlers/client-conversation.js +73 -0
- package/local-runtime/server/ws-utils.js +5 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +73 -78
- 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/yeaft/cli-session-runner.js +3 -2
- package/yeaft/cli.js +20 -1
- package/yeaft/conversation/search.js +13 -7
- package/yeaft/engine.js +29 -5
- package/yeaft/managed-cli.js +618 -0
- package/yeaft/session.js +7 -0
- package/yeaft/sub-agent/runner.js +6 -0
- package/yeaft/tools/disk-usage.js +243 -0
- package/yeaft/tools/glob.js +84 -28
- package/yeaft/tools/grep.js +616 -152
- package/yeaft/tools/history-search.js +12 -3
- package/yeaft/tools/index.js +2 -0
- package/yeaft/tools/process-runner.js +211 -0
- package/yeaft/tools/search-paths.js +108 -0
- package/yeaft/tools/types.js +2 -0
- package/yeaft/web-bridge.js +93 -6
|
@@ -138,12 +138,12 @@ export default defineTool({
|
|
|
138
138
|
en: `Search through past conversation history.
|
|
139
139
|
|
|
140
140
|
Searches message content for all whitespace-separated terms (case-insensitive).
|
|
141
|
-
Tool-result messages are excluded. Useful for finding previous discussions, decisions, or code snippets.
|
|
141
|
+
Inside a Session, search is limited to that Session plus sibling Sessions in the same Project on this Agent. Tool-result messages are excluded. Useful for finding previous discussions, decisions, or code snippets.
|
|
142
142
|
|
|
143
143
|
Results are returned newest-first with a bounded matching snippet and source metadata.`,
|
|
144
144
|
zh: `搜索历史对话记录。
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
在已持久化消息的正文中搜索全部空格分隔的关键词(不区分大小写)。在 Session 内仅搜索当前 Session,以及同一 Agent 上同 Project 的兄弟 Session;排除工具结果消息。用于查找之前的讨论、决策或代码片段。
|
|
147
147
|
|
|
148
148
|
结果按最新优先返回,包含有界的命中片段和来源信息。`
|
|
149
149
|
},
|
|
@@ -180,7 +180,16 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
180
180
|
|
|
181
181
|
try {
|
|
182
182
|
const telemetry = {};
|
|
183
|
-
const
|
|
183
|
+
const projectSessionIds = Array.isArray(ctx?.projectSessionIds)
|
|
184
|
+
? ctx.projectSessionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
|
|
185
|
+
: [];
|
|
186
|
+
const scopedSessionIds = ctx?.sessionId
|
|
187
|
+
? Array.from(new Set([ctx.sessionId, ...projectSessionIds]))
|
|
188
|
+
: null;
|
|
189
|
+
const results = searchMessages(yeaftDir, keyword, limit, {
|
|
190
|
+
telemetry,
|
|
191
|
+
...(scopedSessionIds ? { sessionIds: scopedSessionIds } : {}),
|
|
192
|
+
});
|
|
184
193
|
const searchTelemetry = {
|
|
185
194
|
resultCount: results.length,
|
|
186
195
|
scannedFiles: telemetry.scannedFiles || 0,
|
package/yeaft/tools/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import fileEdit from './file-edit.js';
|
|
|
37
37
|
import globTool from './glob.js';
|
|
38
38
|
import grepTool from './grep.js';
|
|
39
39
|
import listDir from './list-dir.js';
|
|
40
|
+
import diskUsage from './disk-usage.js';
|
|
40
41
|
import applyPatch from './apply-patch.js';
|
|
41
42
|
import listTasks from './list-tasks.js';
|
|
42
43
|
import readTaskLog from './read-task-log.js';
|
|
@@ -102,6 +103,7 @@ export const allTools = [
|
|
|
102
103
|
globTool,
|
|
103
104
|
grepTool,
|
|
104
105
|
listDir,
|
|
106
|
+
diskUsage,
|
|
105
107
|
applyPatch,
|
|
106
108
|
listTasks,
|
|
107
109
|
readTaskLog,
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_BYTES = 512 * 1024;
|
|
5
|
+
const DEFAULT_KILL_GRACE_MS = 250;
|
|
6
|
+
const DEFAULT_FORCE_SETTLE_MS = 1000;
|
|
7
|
+
|
|
8
|
+
function abortError(signal) {
|
|
9
|
+
if (signal?.reason instanceof Error && signal.reason.name === 'AbortError') return signal.reason;
|
|
10
|
+
const error = new Error(
|
|
11
|
+
signal?.reason instanceof Error ? signal.reason.message : 'The operation was aborted',
|
|
12
|
+
);
|
|
13
|
+
error.name = 'AbortError';
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function killProcessTree(proc, signal, platform, spawnProcessSync) {
|
|
18
|
+
if (!proc.pid) return false;
|
|
19
|
+
if (platform === 'win32') {
|
|
20
|
+
try {
|
|
21
|
+
const result = spawnProcessSync('taskkill', ['/pid', String(proc.pid), '/t', '/f'], {
|
|
22
|
+
stdio: 'ignore',
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
});
|
|
26
|
+
if (!result.error && result.status === 0) return true;
|
|
27
|
+
} catch {}
|
|
28
|
+
try { return proc.kill(signal) !== false; } catch { return false; }
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
process.kill(-proc.pid, signal);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
try { return proc.kill(signal) !== false; } catch { return false; }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute a binary directly without a shell and keep captured output bounded.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} command
|
|
42
|
+
* @param {string[]} args
|
|
43
|
+
* @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, forceSettleMs?: number, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
|
|
44
|
+
*/
|
|
45
|
+
export function runProcess(command, args, options = {}) {
|
|
46
|
+
if (options.signal?.aborted) return Promise.reject(abortError(options.signal));
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const platform = options.platform || process.platform;
|
|
50
|
+
const spawnProcess = options.spawnProcess || spawn;
|
|
51
|
+
const spawnProcessSync = options.spawnProcessSync || spawnSync;
|
|
52
|
+
const maxBytes = Number.isFinite(options.maxBytes)
|
|
53
|
+
? Math.max(0, options.maxBytes)
|
|
54
|
+
: DEFAULT_MAX_BYTES;
|
|
55
|
+
const killGraceMs = Number.isFinite(options.killGraceMs)
|
|
56
|
+
? Math.max(0, options.killGraceMs)
|
|
57
|
+
: DEFAULT_KILL_GRACE_MS;
|
|
58
|
+
const forceSettleMs = Number.isFinite(options.forceSettleMs)
|
|
59
|
+
? Math.max(1, options.forceSettleMs)
|
|
60
|
+
: DEFAULT_FORCE_SETTLE_MS;
|
|
61
|
+
const proc = spawnProcess(command, args, {
|
|
62
|
+
cwd: options.cwd,
|
|
63
|
+
env: options.env || process.env,
|
|
64
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
65
|
+
windowsHide: true,
|
|
66
|
+
detached: platform !== 'win32',
|
|
67
|
+
});
|
|
68
|
+
const stdout = [];
|
|
69
|
+
const stderr = [];
|
|
70
|
+
let stdoutBytes = 0;
|
|
71
|
+
let stderrBytes = 0;
|
|
72
|
+
let stdoutTruncated = false;
|
|
73
|
+
let stderrTruncated = false;
|
|
74
|
+
let truncated = false;
|
|
75
|
+
let settled = false;
|
|
76
|
+
let timedOut = false;
|
|
77
|
+
let aborted = false;
|
|
78
|
+
let stopRequested = false;
|
|
79
|
+
let forceRequested = false;
|
|
80
|
+
let timer = null;
|
|
81
|
+
let forceTimer = null;
|
|
82
|
+
let forceSettleTimer = null;
|
|
83
|
+
|
|
84
|
+
let onStdout;
|
|
85
|
+
let onStderr;
|
|
86
|
+
let onError;
|
|
87
|
+
let onClose;
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
if (timer) clearTimeout(timer);
|
|
90
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
91
|
+
if (forceSettleTimer) clearTimeout(forceSettleTimer);
|
|
92
|
+
timer = null;
|
|
93
|
+
forceTimer = null;
|
|
94
|
+
forceSettleTimer = null;
|
|
95
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
96
|
+
if (onStdout) proc.stdout?.off('data', onStdout);
|
|
97
|
+
if (onStderr) proc.stderr?.off('data', onStderr);
|
|
98
|
+
if (onError) proc.off('error', onError);
|
|
99
|
+
if (onClose) proc.off('close', onClose);
|
|
100
|
+
};
|
|
101
|
+
const decode = (chunks, wasTruncated, preserveCarriageReturns = false) => {
|
|
102
|
+
const decoder = new StringDecoder('utf8');
|
|
103
|
+
let value = decoder.write(Buffer.concat(chunks));
|
|
104
|
+
if (!wasTruncated) value += decoder.end();
|
|
105
|
+
return preserveCarriageReturns ? value : value.replace(/\r/g, '');
|
|
106
|
+
};
|
|
107
|
+
const finish = code => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
cleanup();
|
|
111
|
+
if (aborted) {
|
|
112
|
+
reject(abortError(options.signal));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
resolve({
|
|
116
|
+
code: timedOut ? 124 : (code ?? 1),
|
|
117
|
+
stdout: decode(stdout, stdoutTruncated, options.preserveCarriageReturns),
|
|
118
|
+
stderr: decode(stderr, stderrTruncated),
|
|
119
|
+
truncated,
|
|
120
|
+
timedOut,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const forceStop = () => {
|
|
124
|
+
if (settled || forceRequested) return;
|
|
125
|
+
forceRequested = true;
|
|
126
|
+
killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
|
|
127
|
+
forceSettleTimer = setTimeout(() => finish(null), forceSettleMs);
|
|
128
|
+
forceSettleTimer.unref?.();
|
|
129
|
+
};
|
|
130
|
+
const stop = () => {
|
|
131
|
+
if (settled || stopRequested) return;
|
|
132
|
+
stopRequested = true;
|
|
133
|
+
if (platform === 'win32') {
|
|
134
|
+
// taskkill must run while the parent PID still identifies the tree.
|
|
135
|
+
// It is already forceful, so do not wait for the direct child to exit.
|
|
136
|
+
forceRequested = true;
|
|
137
|
+
killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
|
|
138
|
+
if (!settled) {
|
|
139
|
+
forceSettleTimer = setTimeout(() => finish(null), forceSettleMs);
|
|
140
|
+
forceSettleTimer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
killProcessTree(proc, 'SIGTERM', platform, spawnProcessSync);
|
|
145
|
+
forceTimer = setTimeout(forceStop, killGraceMs);
|
|
146
|
+
forceTimer.unref?.();
|
|
147
|
+
};
|
|
148
|
+
const onAbort = () => {
|
|
149
|
+
aborted = true;
|
|
150
|
+
stop();
|
|
151
|
+
};
|
|
152
|
+
const capture = (target, chunk, isStdout) => {
|
|
153
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
154
|
+
const current = isStdout ? stdoutBytes : stderrBytes;
|
|
155
|
+
const remaining = maxBytes - current;
|
|
156
|
+
if (remaining <= 0) {
|
|
157
|
+
truncated = true;
|
|
158
|
+
if (isStdout) stdoutTruncated = true;
|
|
159
|
+
else stderrTruncated = true;
|
|
160
|
+
stop();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const bounded = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
|
|
164
|
+
target.push(bounded);
|
|
165
|
+
if (isStdout) stdoutBytes += bounded.length;
|
|
166
|
+
else stderrBytes += bounded.length;
|
|
167
|
+
if (bounded.length !== buffer.length) {
|
|
168
|
+
truncated = true;
|
|
169
|
+
if (isStdout) stdoutTruncated = true;
|
|
170
|
+
else stderrTruncated = true;
|
|
171
|
+
stop();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
onStdout = chunk => capture(stdout, chunk, true);
|
|
176
|
+
onStderr = chunk => capture(stderr, chunk, false);
|
|
177
|
+
onError = error => {
|
|
178
|
+
if (settled) return;
|
|
179
|
+
if (stopRequested) {
|
|
180
|
+
finish(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
settled = true;
|
|
184
|
+
cleanup();
|
|
185
|
+
reject(error);
|
|
186
|
+
};
|
|
187
|
+
onClose = code => {
|
|
188
|
+
if (stopRequested && !forceRequested) {
|
|
189
|
+
// The direct child is gone. Kill any process that remained in its
|
|
190
|
+
// detached group before releasing the tool call.
|
|
191
|
+
forceRequested = true;
|
|
192
|
+
killProcessTree(proc, 'SIGKILL', platform, spawnProcessSync);
|
|
193
|
+
}
|
|
194
|
+
finish(code);
|
|
195
|
+
};
|
|
196
|
+
proc.stdout.on('data', onStdout);
|
|
197
|
+
proc.stderr.on('data', onStderr);
|
|
198
|
+
proc.on('error', onError);
|
|
199
|
+
proc.on('close', onClose);
|
|
200
|
+
|
|
201
|
+
if (Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
|
|
202
|
+
timer = setTimeout(() => {
|
|
203
|
+
timedOut = true;
|
|
204
|
+
stop();
|
|
205
|
+
}, options.timeoutMs);
|
|
206
|
+
timer.unref?.();
|
|
207
|
+
}
|
|
208
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
209
|
+
if (options.signal?.aborted) onAbort();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const SEARCH_SKIP_DIRS = new Set([
|
|
4
|
+
'node_modules', '.git', '__pycache__', '.next', '.nuxt',
|
|
5
|
+
'dist', 'build', '.cache', '.venv', 'venv', '.tox',
|
|
6
|
+
'vendor', 'target', '.gradle', '.idea', '.vscode',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const SEARCH_SKIP_GLOBS = Object.freeze([
|
|
10
|
+
...[...SEARCH_SKIP_DIRS].flatMap(name => [`!${name}/**`, `!**/${name}/**`]),
|
|
11
|
+
'!.yeaft/worktrees/**',
|
|
12
|
+
'!**/.yeaft/worktrees/**',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const TYPE_EXTENSIONS = {
|
|
16
|
+
js: ['.js', '.jsx', '.mjs', '.cjs'], ts: ['.ts', '.tsx', '.mts', '.cts'],
|
|
17
|
+
py: ['.py'], rust: ['.rs'], go: ['.go'], java: ['.java'],
|
|
18
|
+
json: ['.json'], yaml: ['.yaml', '.yml'], markdown: ['.md', '.markdown'],
|
|
19
|
+
html: ['.html', '.htm'], css: ['.css'], shell: ['.sh', '.bash', '.zsh'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function isSkippedSearchDirectory(relativePath, name) {
|
|
23
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/');
|
|
24
|
+
return SEARCH_SKIP_DIRS.has(name)
|
|
25
|
+
|| normalized === '.yeaft/worktrees'
|
|
26
|
+
|| normalized.endsWith('/.yeaft/worktrees');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function expandBraces(pattern) {
|
|
30
|
+
const match = pattern.match(/\{([^{}]+)\}/);
|
|
31
|
+
if (!match) return [pattern];
|
|
32
|
+
return match[1].split(',').flatMap(part => expandBraces(
|
|
33
|
+
pattern.slice(0, match.index) + part + pattern.slice(match.index + match[0].length),
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function globToRegExp(pattern) {
|
|
38
|
+
let source = '';
|
|
39
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
40
|
+
const char = pattern[index];
|
|
41
|
+
if (char === '*' && pattern[index + 1] === '*') {
|
|
42
|
+
index += 1;
|
|
43
|
+
if (pattern[index + 1] === '/') {
|
|
44
|
+
index += 1;
|
|
45
|
+
source += '(?:.*/)?';
|
|
46
|
+
} else {
|
|
47
|
+
source += '.*';
|
|
48
|
+
}
|
|
49
|
+
} else if (char === '*') {
|
|
50
|
+
source += '[^/]*';
|
|
51
|
+
} else if (char === '?') {
|
|
52
|
+
source += '[^/]';
|
|
53
|
+
} else {
|
|
54
|
+
source += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return new RegExp(`^${source}$`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createSearchPathMatcher({ glob, type } = {}) {
|
|
61
|
+
const normalizedGlob = String(glob || '').replace(/\\/g, '/');
|
|
62
|
+
const globMatchers = normalizedGlob
|
|
63
|
+
? expandBraces(normalizedGlob).map(globToRegExp)
|
|
64
|
+
: [];
|
|
65
|
+
const matchBase = normalizedGlob && !normalizedGlob.includes('/');
|
|
66
|
+
const extensions = type ? TYPE_EXTENSIONS[type] : null;
|
|
67
|
+
|
|
68
|
+
return path => {
|
|
69
|
+
const normalized = String(path || '').replace(/\\/g, '/');
|
|
70
|
+
const candidate = matchBase ? normalized.split('/').pop() : normalized;
|
|
71
|
+
if (globMatchers.length && !globMatchers.some(matcher => matcher.test(candidate))) return false;
|
|
72
|
+
if (type && !extensions?.includes(extname(normalized).toLowerCase())) return false;
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function throwIfAborted(signal) {
|
|
78
|
+
if (!signal?.aborted) return;
|
|
79
|
+
if (signal.reason instanceof Error && signal.reason.name === 'AbortError') {
|
|
80
|
+
throw signal.reason;
|
|
81
|
+
}
|
|
82
|
+
const error = new Error(
|
|
83
|
+
signal.reason instanceof Error ? signal.reason.message : 'The operation was aborted',
|
|
84
|
+
);
|
|
85
|
+
error.name = 'AbortError';
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isAbortError(error) {
|
|
90
|
+
return error?.name === 'AbortError';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function waitForAbortable(promise, signal) {
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
if (!signal) return promise;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const onAbort = () => {
|
|
98
|
+
cleanup();
|
|
99
|
+
try { throwIfAborted(signal); } catch (error) { reject(error); }
|
|
100
|
+
};
|
|
101
|
+
const cleanup = () => signal.removeEventListener('abort', onAbort);
|
|
102
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
103
|
+
Promise.resolve(promise).then(
|
|
104
|
+
value => { cleanup(); resolve(value); },
|
|
105
|
+
error => { cleanup(); reject(error); },
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
}
|
package/yeaft/tools/types.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* @typedef {Object} ToolContext
|
|
15
15
|
* @property {AbortSignal} [signal] — cancellation signal
|
|
16
16
|
* @property {string} [yeaftDir] — Yeaft data directory
|
|
17
|
+
* @property {Promise<Array> & {toolReady?: Record<string, Promise<object>>}} [managedCliReady] — resolves after optional managed CLI setup; toolReady exposes per-command readiness
|
|
17
18
|
* @property {ReturnType<import('../runtime-platform.js').getRuntimePlatformInfo>} [runtimePlatform]
|
|
18
19
|
* — runtime OS/shell facts for platform-aware tools
|
|
19
20
|
* @property {string} [cwd] — working directory
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
* @property {object} [config] — engine config
|
|
24
25
|
* @property {import('../tasks/manager.js').TaskManager} [taskManager] — Session task manager
|
|
25
26
|
* @property {string} [sessionId] — current Session id
|
|
27
|
+
* @property {string[]} [projectSessionIds] — same-Agent sibling Session ids in the current Project
|
|
26
28
|
* @property {string} [threadId] — current Session thread id
|
|
27
29
|
* @property {string} [currentVpId] — R6: VP id of the caller (set in multi-VP groups)
|
|
28
30
|
* @property {string} [currentGroupId] — R6: group id of the caller's RoleInstance
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -589,6 +589,54 @@ const vpCurrentTodos = new Map();
|
|
|
589
589
|
* sessionHandle: object }>}
|
|
590
590
|
*/
|
|
591
591
|
const sessionContexts = new Map();
|
|
592
|
+
/** Latest server-authoritative Project identity and same-Agent siblings per Session. */
|
|
593
|
+
const projectContextBySession = new Map();
|
|
594
|
+
|
|
595
|
+
function normalizeProjectContext(value, sessionId) {
|
|
596
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.sessionIds)) return null;
|
|
597
|
+
const currentSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
598
|
+
return {
|
|
599
|
+
projectId: typeof value.projectId === 'string' && value.projectId.trim()
|
|
600
|
+
? value.projectId.trim()
|
|
601
|
+
: null,
|
|
602
|
+
projectName: typeof value.projectName === 'string' && value.projectName.trim()
|
|
603
|
+
? value.projectName.trim()
|
|
604
|
+
: null,
|
|
605
|
+
sessionIds: Array.from(new Set(value.sessionIds
|
|
606
|
+
.filter(id => typeof id === 'string' && id.trim())
|
|
607
|
+
.map(id => id.trim())
|
|
608
|
+
.filter(id => id !== currentSessionId))),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function legacyProjectContext(yeaftDir, sessionId) {
|
|
613
|
+
const project = loadProjects(yeaftDir).find(row => row.sessionIds.includes(sessionId));
|
|
614
|
+
if (!project) return null;
|
|
615
|
+
return normalizeProjectContext({
|
|
616
|
+
projectId: project.id,
|
|
617
|
+
projectName: project.name,
|
|
618
|
+
sessionIds: project.sessionIds,
|
|
619
|
+
}, sessionId);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function buildProjectSharedBlock(projectContext, summaries = '') {
|
|
623
|
+
const context = normalizeProjectContext(projectContext, null);
|
|
624
|
+
const body = typeof summaries === 'string' ? summaries.trim() : '';
|
|
625
|
+
if (!context?.projectId && !body) return '';
|
|
626
|
+
const lines = ['[Project Shared Context]'];
|
|
627
|
+
if (context?.projectId) {
|
|
628
|
+
const label = context.projectName
|
|
629
|
+
? `${context.projectName} (${context.projectId})`
|
|
630
|
+
: context.projectId;
|
|
631
|
+
lines.push(`Project: ${label}`);
|
|
632
|
+
lines.push('Sharing boundary: sibling Sessions in this Project on this Agent only.');
|
|
633
|
+
} else {
|
|
634
|
+
lines.push('Sharing boundary: sibling Sessions in the same Project on this Agent only.');
|
|
635
|
+
}
|
|
636
|
+
lines.push('Read-only memory summaries preserve each source Session identity.');
|
|
637
|
+
if (body) lines.push('', body);
|
|
638
|
+
return lines.join('\n');
|
|
639
|
+
}
|
|
592
640
|
|
|
593
641
|
function vpKey(sessionId, vpId) {
|
|
594
642
|
return `${sessionId}::${vpId}`;
|
|
@@ -1731,6 +1779,7 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
|
|
|
1731
1779
|
skillManager: session.skillManager,
|
|
1732
1780
|
mcpManager: session.mcpManager,
|
|
1733
1781
|
yeaftDir: session.yeaftDir,
|
|
1782
|
+
managedCliReady: session.managedCliReady || null,
|
|
1734
1783
|
// Share the session-shared ToolUsageStats so per-VP tool calls land
|
|
1735
1784
|
// in the same on-disk snapshot the `yeaft_fetch_tool_stats` handler
|
|
1736
1785
|
// reads. Without this, engine's record-on-tool-exec guard
|
|
@@ -2297,6 +2346,7 @@ export async function __testResetVpState() {
|
|
|
2297
2346
|
asyncTaskOwners.clear();
|
|
2298
2347
|
vpAborts.clear();
|
|
2299
2348
|
sessionContexts.clear();
|
|
2349
|
+
projectContextBySession.clear();
|
|
2300
2350
|
vpCurrentTodos.clear();
|
|
2301
2351
|
threadClassifier = defaultClassifyThread;
|
|
2302
2352
|
if (_vpUnsubscribe) {
|
|
@@ -3028,8 +3078,14 @@ const PROJECT_CONTEXT_MAX_TOKENS = 4096;
|
|
|
3028
3078
|
const PROJECT_CONTEXT_TRUNCATION_NOTICE = '\n[Summary truncated to Project context budget]';
|
|
3029
3079
|
|
|
3030
3080
|
async function sharedProjectContext(yeaftDir, sessionId, options = {}) {
|
|
3031
|
-
const
|
|
3032
|
-
|
|
3081
|
+
const requestedSiblingIds = Array.isArray(options.sessionIds)
|
|
3082
|
+
? options.sessionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
|
|
3083
|
+
: null;
|
|
3084
|
+
const project = requestedSiblingIds === null
|
|
3085
|
+
? loadProjects(yeaftDir).find(row => row.sessionIds.includes(sessionId))
|
|
3086
|
+
: null;
|
|
3087
|
+
const sourceSessionIds = requestedSiblingIds || project?.sessionIds || [];
|
|
3088
|
+
if (sourceSessionIds.length === 0) return '';
|
|
3033
3089
|
const memoryRoot = join(yeaftDir, 'memory');
|
|
3034
3090
|
const language = options.language || 'en';
|
|
3035
3091
|
const configuredBudget = Number.isFinite(options.tokenBudget) && options.tokenBudget > 0
|
|
@@ -3037,7 +3093,7 @@ async function sharedProjectContext(yeaftDir, sessionId, options = {}) {
|
|
|
3037
3093
|
: PROJECT_CONTEXT_MAX_TOKENS;
|
|
3038
3094
|
const tokenBudget = Math.min(configuredBudget, PROJECT_CONTEXT_MAX_TOKENS);
|
|
3039
3095
|
let context = '';
|
|
3040
|
-
const siblingIds =
|
|
3096
|
+
const siblingIds = sourceSessionIds
|
|
3041
3097
|
.filter(id => id !== sessionId)
|
|
3042
3098
|
.slice(0, PROJECT_CONTEXT_MAX_SIBLINGS);
|
|
3043
3099
|
for (const siblingId of siblingIds) {
|
|
@@ -3341,6 +3397,7 @@ export function handleYeaftArchiveSession(msg) {
|
|
|
3341
3397
|
try {
|
|
3342
3398
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3343
3399
|
const result = archiveSession(yeaftDir, sessionId);
|
|
3400
|
+
projectContextBySession.delete(sessionId);
|
|
3344
3401
|
invalidateGroupContext(sessionId);
|
|
3345
3402
|
sendSessionCrudResult({
|
|
3346
3403
|
op: 'archive',
|
|
@@ -3362,6 +3419,7 @@ export function handleYeaftDeleteSession(msg) {
|
|
|
3362
3419
|
try {
|
|
3363
3420
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3364
3421
|
const result = deleteSession(yeaftDir, sessionId);
|
|
3422
|
+
projectContextBySession.delete(sessionId);
|
|
3365
3423
|
removeSessionFromProjects(yeaftDir, sessionId);
|
|
3366
3424
|
ctx.assetOutbox?.removeSession(sessionId);
|
|
3367
3425
|
// Cascade: remove every persisted message stamped with this group id.
|
|
@@ -4495,6 +4553,20 @@ async function runYeaftSessionSend(msg) {
|
|
|
4495
4553
|
},
|
|
4496
4554
|
});
|
|
4497
4555
|
|
|
4556
|
+
const hasInboundProjectContext = Object.prototype.hasOwnProperty.call(msg, 'projectContext');
|
|
4557
|
+
const inboundProjectContext = normalizeProjectContext(msg.projectContext, sessionId);
|
|
4558
|
+
if (hasInboundProjectContext) {
|
|
4559
|
+
projectContextBySession.set(sessionId, inboundProjectContext || {
|
|
4560
|
+
projectId: null,
|
|
4561
|
+
projectName: null,
|
|
4562
|
+
sessionIds: [],
|
|
4563
|
+
});
|
|
4564
|
+
} else {
|
|
4565
|
+
// An old Server does not know this field. Drop any context cached from a
|
|
4566
|
+
// newer Server before falling back to this Agent's legacy projects.json.
|
|
4567
|
+
projectContextBySession.delete(sessionId);
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4498
4570
|
// Ingest user text. The coordinator persists, applies mention/fanout
|
|
4499
4571
|
// rules, and calls deliver() (== enqueueForVp) for each chosen VP —
|
|
4500
4572
|
// which both (a) emits vp_typing_start and (b) ensures a driver runs.
|
|
@@ -4519,6 +4591,9 @@ async function runYeaftSessionSend(msg) {
|
|
|
4519
4591
|
_promptParts: attachmentBundle.promptParts,
|
|
4520
4592
|
_promptSuffix: attachmentBundle.promptSuffix,
|
|
4521
4593
|
_perfTraceId: perfTraceId,
|
|
4594
|
+
_projectContext: msg.projectContext && typeof msg.projectContext === 'object'
|
|
4595
|
+
? msg.projectContext
|
|
4596
|
+
: null,
|
|
4522
4597
|
});
|
|
4523
4598
|
} catch (err) {
|
|
4524
4599
|
console.warn('[Yeaft] yeaft_session_chat: coord.ingest failed', err?.message || err);
|
|
@@ -4762,6 +4837,7 @@ export async function ensureSessionLoaded(opts = {}) {
|
|
|
4762
4837
|
skipMCP: true,
|
|
4763
4838
|
skipSkills: true,
|
|
4764
4839
|
serverMode: true,
|
|
4840
|
+
managedCliReady: ctx.managedCliReady,
|
|
4765
4841
|
});
|
|
4766
4842
|
claimRuntimeOwnership(session);
|
|
4767
4843
|
|
|
@@ -5159,12 +5235,19 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5159
5235
|
threadId,
|
|
5160
5236
|
});
|
|
5161
5237
|
if (queryOpts) {
|
|
5162
|
-
const
|
|
5238
|
+
const envelopeProjectContext = normalizeProjectContext(inboundEnvelope?._projectContext, sessionId);
|
|
5239
|
+
const projectContext = envelopeProjectContext
|
|
5240
|
+
|| projectContextBySession.get(sessionId)
|
|
5241
|
+
|| legacyProjectContext(ctx.CONFIG?.yeaftDir, sessionId);
|
|
5242
|
+
const projectSessionIds = projectContext?.sessionIds || [];
|
|
5243
|
+
queryOpts.projectSessionIds = projectSessionIds;
|
|
5244
|
+
const projectSummaries = await sharedProjectContext(ctx.CONFIG?.yeaftDir, sessionId, {
|
|
5245
|
+
sessionIds: projectSessionIds,
|
|
5163
5246
|
language: session?.config?.language,
|
|
5164
5247
|
tokenBudget: Math.max(512, Math.floor((session?.config?.messageTokenBudget || 32768) / 8)),
|
|
5165
5248
|
});
|
|
5166
|
-
|
|
5167
|
-
|
|
5249
|
+
const sharedBlock = buildProjectSharedBlock(projectContext, projectSummaries);
|
|
5250
|
+
if (sharedBlock) {
|
|
5168
5251
|
queryOpts.sessionAnnouncement = queryOpts.sessionAnnouncement
|
|
5169
5252
|
? `${queryOpts.sessionAnnouncement}\n\n${sharedBlock}`
|
|
5170
5253
|
: sharedBlock;
|
|
@@ -7007,6 +7090,7 @@ export async function resetYeaftSession() {
|
|
|
7007
7090
|
vpEngineConfigKeys.clear();
|
|
7008
7091
|
asyncTaskOwners.clear();
|
|
7009
7092
|
sessionContexts.clear();
|
|
7093
|
+
projectContextBySession.clear();
|
|
7010
7094
|
vpCurrentTodos.clear();
|
|
7011
7095
|
threadClassifier = defaultClassifyThread;
|
|
7012
7096
|
// History-dedup cache is keyed by per-session coordinator msg ids;
|
|
@@ -7030,6 +7114,7 @@ export async function resetYeaftSession() {
|
|
|
7030
7114
|
skipMCP: true,
|
|
7031
7115
|
skipSkills: true,
|
|
7032
7116
|
serverMode: true,
|
|
7117
|
+
managedCliReady: ctx.managedCliReady,
|
|
7033
7118
|
});
|
|
7034
7119
|
claimRuntimeOwnership(session);
|
|
7035
7120
|
installYeaftRuntimeBridge(session);
|
|
@@ -7289,6 +7374,8 @@ export async function handleYeaftMcpReload(msg = {}) {
|
|
|
7289
7374
|
export const __testHooks = {
|
|
7290
7375
|
loadProjects,
|
|
7291
7376
|
sharedProjectContext,
|
|
7377
|
+
buildProjectSharedBlock,
|
|
7378
|
+
normalizeProjectContext,
|
|
7292
7379
|
loadVisibleGroupHistoryPage,
|
|
7293
7380
|
projectVisibleHistoryChunkMessages,
|
|
7294
7381
|
persistInboundMessageOnceByMsgId,
|