@yeaft/webchat-agent 0.1.752 → 0.1.754
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/package.json +1 -1
- package/unify/compact/compactor.js +14 -2
- package/unify/dream-v2/runner.js +17 -1
- package/unify/dream-v2/state.js +103 -2
- package/unify/engine.js +12 -2
- package/unify/history-compact.js +41 -21
- package/unify/session.js +7 -0
- package/unify/tools/registry.js +30 -2
- package/unify/web-bridge.js +34 -7
package/package.json
CHANGED
|
@@ -62,12 +62,17 @@ export class Compactor {
|
|
|
62
62
|
* treats that as a soft failure).
|
|
63
63
|
* @param {() => number|undefined} [opts.getMaxContextTokens]
|
|
64
64
|
* Returns `config.maxContextTokens` for `shouldCompactHistory`.
|
|
65
|
+
* @param {() => string|undefined} [opts.getLanguage]
|
|
66
|
+
* Returns the live `config.language`. Threaded into
|
|
67
|
+
* `compactHistory` so the compactor's summary prompt + the
|
|
68
|
+
* "session continued" wrapper render in the user's preferred
|
|
69
|
+
* locale instead of always English.
|
|
65
70
|
* @param {(groupId: string, result: CompactedResult) => void} [opts.onCompacted]
|
|
66
71
|
* Optional sink. Bridge wires this to send the
|
|
67
72
|
* `unify_history_compacted` WS event. Default: no-op. Can be
|
|
68
73
|
* replaced post-construction via `setOnCompacted`.
|
|
69
74
|
*/
|
|
70
|
-
constructor({ summarize, getMaxContextTokens, onCompacted } = {}) {
|
|
75
|
+
constructor({ summarize, getMaxContextTokens, getLanguage, onCompacted } = {}) {
|
|
71
76
|
if (typeof summarize !== 'function') {
|
|
72
77
|
throw new TypeError('Compactor: summarize is required');
|
|
73
78
|
}
|
|
@@ -75,6 +80,9 @@ export class Compactor {
|
|
|
75
80
|
this._getMaxContextTokens = typeof getMaxContextTokens === 'function'
|
|
76
81
|
? getMaxContextTokens
|
|
77
82
|
: () => undefined;
|
|
83
|
+
this._getLanguage = typeof getLanguage === 'function'
|
|
84
|
+
? getLanguage
|
|
85
|
+
: () => undefined;
|
|
78
86
|
this._onCompacted = typeof onCompacted === 'function' ? onCompacted : () => {};
|
|
79
87
|
/** @type {Map<string, { inFlight: Promise<void>|null, pending: boolean }>} */
|
|
80
88
|
this._states = new Map();
|
|
@@ -186,7 +194,11 @@ export class Compactor {
|
|
|
186
194
|
const summarize = ({ system, prompt }) =>
|
|
187
195
|
this._summarize({ system, prompt, maxTokens: SUMMARIZER_MAX_TOKENS });
|
|
188
196
|
|
|
189
|
-
const result = await compactHistory(snapshot, {
|
|
197
|
+
const result = await compactHistory(snapshot, {
|
|
198
|
+
summarize,
|
|
199
|
+
maxContextTokens,
|
|
200
|
+
language: this._getLanguage(),
|
|
201
|
+
});
|
|
190
202
|
if (!result || !result.compacted) {
|
|
191
203
|
if (result && result.error) {
|
|
192
204
|
console.warn(
|
package/unify/dream-v2/runner.js
CHANGED
|
@@ -44,7 +44,7 @@ import { listScopes, readSummary } from '../memory/store-v2.js';
|
|
|
44
44
|
import {
|
|
45
45
|
DEFAULT_LIMITS,
|
|
46
46
|
} from './limits.js';
|
|
47
|
-
import { readGroupState, writeGroupState } from './state.js';
|
|
47
|
+
import { readGroupState, writeGroupState, writeDreamError } from './state.js';
|
|
48
48
|
import { segmentDiff, truncateMessage, estimateMessagesTokens } from './segment.js';
|
|
49
49
|
import { triageGroupSegments } from './triage.js';
|
|
50
50
|
import { mergeByTarget } from './merge.js';
|
|
@@ -148,6 +148,14 @@ export async function runDream(opts) {
|
|
|
148
148
|
} catch (err) {
|
|
149
149
|
groupsReport.push({ groupId, new: newCount, status: 'error', error: err.message });
|
|
150
150
|
onProgress({ phase: 'triage', groupId, status: 'error', error: err.message });
|
|
151
|
+
// Journal the failure on disk so operators can see WHY dream is
|
|
152
|
+
// not advancing without having to enable `config.debug`. Best-
|
|
153
|
+
// effort — `writeDreamError` swallows its own I/O errors.
|
|
154
|
+
await writeDreamError(opts.root, `group/${groupId}`, {
|
|
155
|
+
phase: 'triage',
|
|
156
|
+
message: err.message,
|
|
157
|
+
stack: err.stack,
|
|
158
|
+
});
|
|
151
159
|
continue;
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -191,6 +199,14 @@ export async function runDream(opts) {
|
|
|
191
199
|
error: err.message,
|
|
192
200
|
});
|
|
193
201
|
onProgress({ phase: 'apply', target: merged.target, status: 'error', error: err.message });
|
|
202
|
+
// Journal apply-stage failures into the target scope's directory
|
|
203
|
+
// (`<root>/<merged.target>/.dream-last-error.json`). Same rationale
|
|
204
|
+
// as the triage catch above.
|
|
205
|
+
await writeDreamError(opts.root, merged.target, {
|
|
206
|
+
phase: 'apply',
|
|
207
|
+
message: err.message,
|
|
208
|
+
stack: err.stack,
|
|
209
|
+
});
|
|
194
210
|
}
|
|
195
211
|
}
|
|
196
212
|
|
package/unify/dream-v2/state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dream-v2/state.js.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three pieces of state, tracked separately:
|
|
5
5
|
*
|
|
6
6
|
* 1. Per-group control state (used to decide whether a group enters
|
|
7
7
|
* triage and how far to advance the cursor):
|
|
@@ -31,13 +31,25 @@
|
|
|
31
31
|
* control-flow decision. We update it by replacing the existing
|
|
32
32
|
* block (if any) or appending a new one to the end of the file.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* 3. Per-scope dream-error sink (added v0.1.754):
|
|
35
|
+
*
|
|
36
|
+
* ~/.yeaft/memory/<scope>/.dream-last-error.json
|
|
37
|
+
*
|
|
38
|
+
* Most-recent-wins JSON written unconditionally on every triage
|
|
39
|
+
* or apply failure (best-effort — never throws even when the I/O
|
|
40
|
+
* itself fails). The runner used to swallow these exceptions and
|
|
41
|
+
* the only sink was a `config.debug`-gated console.log; this file
|
|
42
|
+
* gives operators on-disk evidence regardless of debug. See
|
|
43
|
+
* `writeDreamError` / `readDreamError` below for the contract.
|
|
44
|
+
*
|
|
45
|
+
* All helpers are pure I/O; no LLM, no logic beyond parsing.
|
|
35
46
|
*/
|
|
36
47
|
|
|
37
48
|
import { promises as fsp, existsSync } from 'fs';
|
|
38
49
|
import { join, dirname } from 'path';
|
|
39
50
|
|
|
40
51
|
const STATE_FILE = '.dream-state';
|
|
52
|
+
const ERROR_FILE = '.dream-last-error.json';
|
|
41
53
|
const DREAM_BLOCK_OPEN = '<!-- dream-state -->';
|
|
42
54
|
const DREAM_BLOCK_CLOSE = '<!-- /dream-state -->';
|
|
43
55
|
|
|
@@ -101,6 +113,95 @@ function parseGroupState(raw) {
|
|
|
101
113
|
return out;
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
// ─── per-scope dream error sink ────────────────────────────────
|
|
117
|
+
//
|
|
118
|
+
// Why: dream-v2 silently swallowed exceptions at the triage / apply
|
|
119
|
+
// catch sites — the only sink was `trace.event('dream_progress', evt)`
|
|
120
|
+
// and a `config.debug`-gated `console.log` in `session-wiring.js`. With
|
|
121
|
+
// `debug=false` (the default), there was no on-disk evidence that a
|
|
122
|
+
// dream pass had ever failed: no `.dream-state` (because we only write
|
|
123
|
+
// it on success), no log file, nothing. The Resident layer's continued
|
|
124
|
+
// regurgitation of the bootstrap seed was the only symptom.
|
|
125
|
+
//
|
|
126
|
+
// `writeDreamError` writes `<memoryRoot>/<scope>/.dream-last-error.json`
|
|
127
|
+
// unconditionally on every catch (best-effort — write failures must not
|
|
128
|
+
// shadow the original error). Operators can then `ls ~/.yeaft/memory/
|
|
129
|
+
// group/<id>/` and see what blew up, without having to re-enable debug.
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a memoryRoot + scope-string to the scope directory.
|
|
133
|
+
* The scope string is the same shape dream-v2 already uses internally:
|
|
134
|
+
* `'user'`, `'vp/<vpId>'`, `'group/<groupId>'`, `'feature/<id>'`, etc.
|
|
135
|
+
*
|
|
136
|
+
* Pure path-join; does NOT create the directory. The writer creates it.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} root
|
|
139
|
+
* @param {string} scope
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
export function scopeDirFor(root, scope) {
|
|
143
|
+
// Defensive: trim leading/trailing slashes so callers can pass either
|
|
144
|
+
// `'group/grp_fun'` or `/group/grp_fun/` — both land on the same dir.
|
|
145
|
+
const clean = String(scope || '').replace(/^\/+|\/+$/g, '');
|
|
146
|
+
return join(root, clean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Best-effort write of the dream-error sink. Never throws — a failed
|
|
151
|
+
* write is silently swallowed because the caller is already in an
|
|
152
|
+
* error-handling path and we must not mask the original failure.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} root — memory root, e.g. ~/.yeaft/memory
|
|
155
|
+
* @param {string} scope — `'group/<id>'` for triage failures,
|
|
156
|
+
* `merged.target` for apply failures.
|
|
157
|
+
* @param {{ phase: string, message: string, stack?: string|null, at?: string }} info
|
|
158
|
+
* @returns {Promise<void>}
|
|
159
|
+
*/
|
|
160
|
+
export async function writeDreamError(root, scope, info) {
|
|
161
|
+
try {
|
|
162
|
+
const dir = scopeDirFor(root, scope);
|
|
163
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
164
|
+
const abs = join(dir, ERROR_FILE);
|
|
165
|
+
const at = (info && info.at) || new Date().toISOString();
|
|
166
|
+
// Trim stack to the first 5 frames — enough for diagnosis, small
|
|
167
|
+
// enough that the artifact stays human-readable. Missing/empty
|
|
168
|
+
// stack collapses to `null` rather than `""` so the artifact is
|
|
169
|
+
// cleaner for operators.
|
|
170
|
+
const rawStack = info && typeof info.stack === 'string' ? info.stack : '';
|
|
171
|
+
const stackLines = rawStack ? rawStack.split('\n').slice(0, 5) : [];
|
|
172
|
+
const body = JSON.stringify({
|
|
173
|
+
at,
|
|
174
|
+
scope,
|
|
175
|
+
phase: String(info?.phase || 'unknown'),
|
|
176
|
+
message: String(info?.message || ''),
|
|
177
|
+
stack: stackLines.length > 0 ? stackLines.join('\n') : null,
|
|
178
|
+
}, null, 2) + '\n';
|
|
179
|
+
await atomicWrite(abs, body);
|
|
180
|
+
} catch {
|
|
181
|
+
// Best-effort: swallow. The caller is already handling the real
|
|
182
|
+
// error; an inability to journal it must not shadow that.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read the last dream error JSON for a scope, or null if absent. Used
|
|
188
|
+
* by the debug panel and by tests. Tolerates a malformed file by
|
|
189
|
+
* returning `{ raw: <body>, parseError: <message> }` instead of
|
|
190
|
+
* throwing.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} root
|
|
193
|
+
* @param {string} scope
|
|
194
|
+
* @returns {Promise<object|null>}
|
|
195
|
+
*/
|
|
196
|
+
export async function readDreamError(root, scope) {
|
|
197
|
+
const abs = join(scopeDirFor(root, scope), ERROR_FILE);
|
|
198
|
+
let raw;
|
|
199
|
+
try { raw = await fsp.readFile(abs, 'utf8'); }
|
|
200
|
+
catch (err) { if (err && err.code === 'ENOENT') return null; throw err; }
|
|
201
|
+
try { return JSON.parse(raw); }
|
|
202
|
+
catch (e) { return { raw, parseError: e.message }; }
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
// ─── per-scope marker (memory.md tail block) ───────────────────
|
|
105
206
|
|
|
106
207
|
/**
|
package/unify/engine.js
CHANGED
|
@@ -975,13 +975,23 @@ export class Engine {
|
|
|
975
975
|
|
|
976
976
|
const archiveIds = [];
|
|
977
977
|
|
|
978
|
+
// Language-aware summarizer prompts. The orchestrator-track summary
|
|
979
|
+
// ends up in the system prompt as a "previous conversation summary"
|
|
980
|
+
// block, so it needs to match the user's preferred language to avoid
|
|
981
|
+
// a jarring locale flip mid-context.
|
|
982
|
+
const isZh = String(this.#config.language || '').toLowerCase().startsWith('zh');
|
|
983
|
+
const summariserSystem = isZh
|
|
984
|
+
? '你是对话摘要器。请用中文写出 2–3 段简明摘要,保留决策、事实与上下文。'
|
|
985
|
+
: 'You are a conversation summarizer. Summarize concisely in 2–3 paragraphs, preserving decisions, facts, and context.';
|
|
986
|
+
const summariserPromptPrefix = isZh ? '请概括:\n\n' : 'Summarize:\n\n';
|
|
987
|
+
|
|
978
988
|
const hooks = {
|
|
979
989
|
summarise: async () => {
|
|
980
990
|
try {
|
|
981
991
|
const result = await adapter.call({
|
|
982
992
|
model: fastConfig.model,
|
|
983
|
-
system:
|
|
984
|
-
messages: [{ role: 'user', content:
|
|
993
|
+
system: summariserSystem,
|
|
994
|
+
messages: [{ role: 'user', content: `${summariserPromptPrefix}${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
|
|
985
995
|
maxTokens: 1024,
|
|
986
996
|
});
|
|
987
997
|
return (result.text || '').trim();
|
package/unify/history-compact.js
CHANGED
|
@@ -328,16 +328,22 @@ export function findCutIndex(messages, keepRecent) {
|
|
|
328
328
|
* `server/db/message-db.js`) recognise it.
|
|
329
329
|
*
|
|
330
330
|
* @param {string} summary
|
|
331
|
+
* @param {{ language?: string }} [opts]
|
|
331
332
|
* @returns {{role:'user', content:string, _compactSummary: true}}
|
|
332
333
|
*/
|
|
333
|
-
export function wrapSummaryAsUserMessage(summary) {
|
|
334
|
+
export function wrapSummaryAsUserMessage(summary, opts = {}) {
|
|
334
335
|
const body = (summary || '').trim() || '(no summary produced)';
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
'
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
const isZh = String(opts.language || '').toLowerCase().startsWith('zh');
|
|
337
|
+
const content = isZh
|
|
338
|
+
? '本会话延续自之前的对话。早期上下文已经被概括以节省空间。\n\n' +
|
|
339
|
+
'至此为止的对话摘要:\n' +
|
|
340
|
+
body +
|
|
341
|
+
'\n\n请从中断处继续对话,不要再向用户重复确认。'
|
|
342
|
+
: 'This session is being continued from a previous conversation. ' +
|
|
343
|
+
'The earlier context has been summarized for efficiency.\n\n' +
|
|
344
|
+
'Summary of conversation so far:\n' +
|
|
345
|
+
body +
|
|
346
|
+
'\n\nContinue the conversation from where it left off without asking the user any further questions.';
|
|
341
347
|
return {
|
|
342
348
|
role: 'user',
|
|
343
349
|
content,
|
|
@@ -349,24 +355,36 @@ export function wrapSummaryAsUserMessage(summary) {
|
|
|
349
355
|
* Build the prompt fed to the fast-model summarizer. Kept in code (not in
|
|
350
356
|
* a template file) because it's small and lives alongside the call site.
|
|
351
357
|
*
|
|
358
|
+
* The summarizer prompt itself is language-aware: callers pass the live
|
|
359
|
+
* `config.language` so the produced summary is written in the user's
|
|
360
|
+
* preferred language. JSON-style structural cues stay English so the
|
|
361
|
+
* summary remains easy to splice into the next turn regardless of locale.
|
|
362
|
+
*
|
|
352
363
|
* @param {Array<{role:string, content:string}>} cleanedMessages
|
|
364
|
+
* @param {{ language?: string }} [opts]
|
|
353
365
|
* @returns {{system: string, prompt: string}}
|
|
354
366
|
*/
|
|
355
|
-
export function buildSummaryPrompt(cleanedMessages) {
|
|
367
|
+
export function buildSummaryPrompt(cleanedMessages, opts = {}) {
|
|
356
368
|
const transcript = cleanedMessages
|
|
357
369
|
.map(m => `[${m.role}]\n${m.content}`)
|
|
358
370
|
.join('\n\n---\n\n');
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
'
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
'
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
371
|
+
const isZh = String(opts.language || '').toLowerCase().startsWith('zh');
|
|
372
|
+
const system = isZh
|
|
373
|
+
? '你是多 agent 群聊的对话摘要器。请用中文写出 4–8 条简明 bullet 摘要。' +
|
|
374
|
+
'保留:(1) 已做的决策,(2) 已学到的事实,(3) 用户当前目标,' +
|
|
375
|
+
'(4) 任何未解决的问题或待办事项,(5) 哪些 VP 参与了对话以及各自贡献。' +
|
|
376
|
+
'不要包含原始工具输出。不要臆测。要具体。'
|
|
377
|
+
: 'You are a conversation summarizer for a multi-agent group chat. ' +
|
|
378
|
+
'Produce a concise (4–8 short bullet points) summary of the conversation ' +
|
|
379
|
+
'so far. Preserve: (1) decisions made, (2) facts learned, (3) the user\'s ' +
|
|
380
|
+
'current goal, (4) any open questions or pending actions, (5) which VPs ' +
|
|
381
|
+
'are participating and what each contributed. Do NOT include raw tool ' +
|
|
382
|
+
'output. Do NOT speculate. Be specific.';
|
|
383
|
+
const prompt = isZh
|
|
384
|
+
? '请概括下面的对话。只输出摘要正文,不要前言。\n\n' + transcript
|
|
385
|
+
: 'Summarize the following conversation. Output ONLY the summary, no ' +
|
|
386
|
+
'preamble.\n\n' +
|
|
387
|
+
transcript;
|
|
370
388
|
return { system, prompt };
|
|
371
389
|
}
|
|
372
390
|
|
|
@@ -384,6 +402,7 @@ export function buildSummaryPrompt(cleanedMessages) {
|
|
|
384
402
|
* maxContextTokens?: number,
|
|
385
403
|
* tokenFraction?: number,
|
|
386
404
|
* hardTokenCeiling?: number,
|
|
405
|
+
* language?: string,
|
|
387
406
|
* }} options
|
|
388
407
|
* @returns {Promise<{
|
|
389
408
|
* messages: Array<object>,
|
|
@@ -407,6 +426,7 @@ export async function compactHistory(messages, options) {
|
|
|
407
426
|
maxContextTokens,
|
|
408
427
|
tokenFraction,
|
|
409
428
|
hardTokenCeiling,
|
|
429
|
+
language,
|
|
410
430
|
} = options || {};
|
|
411
431
|
|
|
412
432
|
if (typeof summarize !== 'function') {
|
|
@@ -460,7 +480,7 @@ export async function compactHistory(messages, options) {
|
|
|
460
480
|
|
|
461
481
|
let summaryText = '';
|
|
462
482
|
if (cleaned.length > 0) {
|
|
463
|
-
const { system, prompt } = buildSummaryPrompt(cleaned);
|
|
483
|
+
const { system, prompt } = buildSummaryPrompt(cleaned, { language });
|
|
464
484
|
try {
|
|
465
485
|
summaryText = (await summarize({ system, prompt })) || '';
|
|
466
486
|
} catch (err) {
|
|
@@ -498,7 +518,7 @@ export async function compactHistory(messages, options) {
|
|
|
498
518
|
}
|
|
499
519
|
}
|
|
500
520
|
|
|
501
|
-
const summaryMsg = wrapSummaryAsUserMessage(summaryText);
|
|
521
|
+
const summaryMsg = wrapSummaryAsUserMessage(summaryText, { language });
|
|
502
522
|
|
|
503
523
|
// Defensive pair-sanitize: the cut at `cutIdx` lands at a user-message
|
|
504
524
|
// boundary so an `[assistant(toolCalls), tool…]` arc is not split, but
|
package/unify/session.js
CHANGED
|
@@ -315,6 +315,13 @@ export async function loadSession(options = {}) {
|
|
|
315
315
|
engine.summarizeForCompact({ system, prompt, maxTokens }),
|
|
316
316
|
getMaxContextTokens: () =>
|
|
317
317
|
typeof config.maxContextTokens === 'number' ? config.maxContextTokens : undefined,
|
|
318
|
+
// Live-read: `config.language` is mutated in place by
|
|
319
|
+
// `engine.setLanguage()` (which broadcastLanguageChange fans out to
|
|
320
|
+
// every per-VP engine). The compactor must see the post-broadcast
|
|
321
|
+
// value, not a boot-time snapshot, so the summary prompt + the
|
|
322
|
+
// "session continued" wrapper render in the user's current locale.
|
|
323
|
+
getLanguage: () =>
|
|
324
|
+
typeof config.language === 'string' ? config.language : undefined,
|
|
318
325
|
});
|
|
319
326
|
|
|
320
327
|
// ─── 9a. Create dream scheduler ────────────
|
package/unify/tools/registry.js
CHANGED
|
@@ -61,16 +61,44 @@ function localizeVisibleText(value, language, toolName) {
|
|
|
61
61
|
].join('\n');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Walk a JSON Schema `parameters` object and localize the human-readable
|
|
66
|
+
* `description` strings to the requested language. All other schema bits
|
|
67
|
+
* (`type`, `enum`, `required`, `items`, nested `properties`, etc.) are
|
|
68
|
+
* preserved by value.
|
|
69
|
+
*
|
|
70
|
+
* Critical correctness rule: a JSON Schema can have a *property named*
|
|
71
|
+
* `description` whose value is itself a sub-schema (e.g.
|
|
72
|
+
* `properties: { description: { type: 'string', description: 'Detailed...' }}`).
|
|
73
|
+
* In that case the OUTER `description` key holds the sub-schema and must
|
|
74
|
+
* be recursed into; only the INNER `description: 'Detailed...'` value
|
|
75
|
+
* (which is a string) should be localized.
|
|
76
|
+
*
|
|
77
|
+
* Previous bug: this walker localized ANY value under a `description`
|
|
78
|
+
* key, including sub-schema objects. `localizeVisibleText` then ran
|
|
79
|
+
* `String(value)` on the object, producing the literal string
|
|
80
|
+
* `'[object Object]'`. GPT-5's strict schema validator rejected the
|
|
81
|
+
* resulting `{ description: '[object Object]' }` with
|
|
82
|
+
* `"'[object Object]' is not of type 'object', 'boolean'"`. This made
|
|
83
|
+
* FeatureCreate (and any tool whose schema contains a property named
|
|
84
|
+
* `description`) unusable in zh locale on strict providers.
|
|
85
|
+
*
|
|
86
|
+
* The fix: only treat a `description` value as localizable text when it
|
|
87
|
+
* is actually a string. Object/array values under a `description` key
|
|
88
|
+
* are sub-schemas and must be recursed into normally.
|
|
89
|
+
*/
|
|
64
90
|
function localizeParameters(parameters, language, toolName) {
|
|
65
91
|
const lang = normalizeLanguage(language);
|
|
66
92
|
if (lang !== 'zh' || !parameters || typeof parameters !== 'object') return parameters;
|
|
67
93
|
if (Array.isArray(parameters)) return parameters.map(v => localizeParameters(v, lang, toolName));
|
|
68
94
|
const out = {};
|
|
69
95
|
for (const [key, value] of Object.entries(parameters)) {
|
|
70
|
-
if (key === 'description') {
|
|
96
|
+
if (key === 'description' && typeof value === 'string') {
|
|
71
97
|
out[key] = localizeVisibleText(value, lang, toolName);
|
|
72
|
-
} else {
|
|
98
|
+
} else if (value && typeof value === 'object') {
|
|
73
99
|
out[key] = localizeParameters(value, lang, toolName);
|
|
100
|
+
} else {
|
|
101
|
+
out[key] = value;
|
|
74
102
|
}
|
|
75
103
|
}
|
|
76
104
|
return out;
|
package/unify/web-bridge.js
CHANGED
|
@@ -2418,28 +2418,55 @@ export function __testGetRegisteredThreadIds() {
|
|
|
2418
2418
|
export const __testRaceWithEscalation = raceWithEscalation;
|
|
2419
2419
|
|
|
2420
2420
|
/**
|
|
2421
|
-
* Manual dream trigger
|
|
2421
|
+
* Manual dream trigger.
|
|
2422
|
+
*
|
|
2423
|
+
* Two call shapes, both routed through this single handler:
|
|
2424
|
+
*
|
|
2425
|
+
* { type: 'unify_dream_trigger', vpId } — per-VP trigger (legacy
|
|
2426
|
+
* VP-detail page button). Fires an unscoped dream pass; the result
|
|
2427
|
+
* event is tagged with `vpId` so the per-VP store row updates.
|
|
2428
|
+
*
|
|
2429
|
+
* { type: 'unify_dream_trigger', groupId } — per-GROUP trigger (new
|
|
2430
|
+
* in v0.1.754 — added so users can manually kick dream for a group
|
|
2431
|
+
* after seeing the Resident layer stuck on the bootstrap seed).
|
|
2432
|
+
* Fires a scope-filtered pass via `triggerDreamForScopes(['group/X'])`
|
|
2433
|
+
* so unrelated groups don't get processed; the result event is
|
|
2434
|
+
* tagged with `groupId` for the per-group UI row.
|
|
2435
|
+
*
|
|
2436
|
+
* Backwards-compat: when neither field is set, defaults to `vpId='default'`
|
|
2437
|
+
* which matches the pre-v0.1.754 behavior.
|
|
2422
2438
|
*/
|
|
2423
2439
|
export async function handleUnifyDreamTrigger(msg = {}) {
|
|
2440
|
+
// Resolve tag up-front so EVERY outbound envelope (including the
|
|
2441
|
+
// scheduler-uninitialised early-return below) carries `groupId` /
|
|
2442
|
+
// `vpId`. Without this the frontend's `applyDreamResult` couldn't
|
|
2443
|
+
// route the error event back to the right row and the per-group
|
|
2444
|
+
// "Run dream now" button would stay stuck on "Running…" forever
|
|
2445
|
+
// (review feedback from PR #757).
|
|
2446
|
+
const groupId = typeof msg.groupId === 'string' && msg.groupId ? msg.groupId : null;
|
|
2447
|
+
const vpId = !groupId ? (msg.vpId || 'default') : null;
|
|
2448
|
+
const tag = groupId ? { groupId } : { vpId };
|
|
2449
|
+
|
|
2424
2450
|
if (!session?.dreamScheduler) {
|
|
2425
2451
|
sendToServer({
|
|
2426
2452
|
type: 'unify_dream_result',
|
|
2453
|
+
...tag,
|
|
2427
2454
|
success: false,
|
|
2428
2455
|
error: 'Dream scheduler not initialized — session not loaded.',
|
|
2429
2456
|
});
|
|
2430
2457
|
return;
|
|
2431
2458
|
}
|
|
2432
2459
|
|
|
2433
|
-
const vpId = msg.vpId || 'default';
|
|
2434
|
-
|
|
2435
2460
|
try {
|
|
2436
2461
|
sendToServer({
|
|
2437
2462
|
type: 'unify_dream_status',
|
|
2438
|
-
|
|
2463
|
+
...tag,
|
|
2439
2464
|
status: 'running',
|
|
2440
2465
|
});
|
|
2441
2466
|
|
|
2442
|
-
const result =
|
|
2467
|
+
const result = groupId
|
|
2468
|
+
? await session.dreamScheduler.triggerDreamForScopes([`group/${groupId}`])
|
|
2469
|
+
: await session.dreamScheduler.triggerDreamNow();
|
|
2443
2470
|
|
|
2444
2471
|
// fix/dream-cadence-and-ui-trigger: derive a single "entries
|
|
2445
2472
|
// created" count for the UI bubble. The runner returns a richer
|
|
@@ -2457,7 +2484,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2457
2484
|
// PR #743.
|
|
2458
2485
|
sendToServer({
|
|
2459
2486
|
type: 'unify_dream_result',
|
|
2460
|
-
|
|
2487
|
+
...tag,
|
|
2461
2488
|
...result,
|
|
2462
2489
|
success: !result.error && !result.skipped,
|
|
2463
2490
|
entriesCreated,
|
|
@@ -2466,7 +2493,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2466
2493
|
} catch (err) {
|
|
2467
2494
|
sendToServer({
|
|
2468
2495
|
type: 'unify_dream_result',
|
|
2469
|
-
|
|
2496
|
+
...tag,
|
|
2470
2497
|
success: false,
|
|
2471
2498
|
error: err?.message || String(err),
|
|
2472
2499
|
});
|