@yeaft/webchat-agent 0.1.924 → 0.1.926
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/index.js +13 -1
- package/package.json +1 -1
- package/yeaft/cli.js +19 -4
- package/yeaft/config.js +31 -7
- package/yeaft/debug-trace.js +1 -1
- package/yeaft/dream/state.js +13 -5
- package/yeaft/engine.js +7 -2
- package/yeaft/init.js +0 -1
- package/yeaft/llm/models-dev.js +93 -0
- package/yeaft/migrate/sessions.js +61 -4
- package/yeaft/models.js +108 -54
- package/yeaft/sessions/ids.js +2 -2
- package/yeaft/sessions/seed-default.js +11 -11
- package/yeaft/sessions/session-crud.js +19 -16
package/index.js
CHANGED
|
@@ -324,10 +324,22 @@ process.on('SIGTERM', () => {
|
|
|
324
324
|
process.exit(0);
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
-
// 启动 -
|
|
327
|
+
// 启动 - 先确保依赖,再检测能力,预热 models.dev 缓存,再连接
|
|
328
328
|
(async () => {
|
|
329
329
|
await ensureDependencies();
|
|
330
330
|
await ensureYeaftSkills();
|
|
331
331
|
ctx.agentCapabilities = await detectCapabilities();
|
|
332
|
+
// Prime the models.dev community catalog so the Yeaft engine's *synchronous*
|
|
333
|
+
// hot path (engine.js / config.js / cli.js all read context-window inline)
|
|
334
|
+
// can resolve real per-model limits without bubbling async up through every
|
|
335
|
+
// call site. Failure here is non-fatal: stale disk cache → DEFAULT 200K
|
|
336
|
+
// fall-through keeps the agent boot-able offline.
|
|
337
|
+
try {
|
|
338
|
+
const { fetchModelsDev } = await import('./yeaft/llm/models-dev.js');
|
|
339
|
+
await fetchModelsDev({ yeaftDir: YEAFT_DIR });
|
|
340
|
+
console.log('[Agent] models.dev cache primed');
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.warn(`[Agent] models.dev prime failed (will use config/defaults): ${err?.message || err}`);
|
|
343
|
+
}
|
|
332
344
|
connect();
|
|
333
345
|
})();
|
package/package.json
CHANGED
package/yeaft/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ import { join } from 'path';
|
|
|
28
28
|
import { loadConfig } from './config.js';
|
|
29
29
|
import { DebugTrace } from './debug-trace.js';
|
|
30
30
|
import { loadSession } from './session.js';
|
|
31
|
-
import { listModels, resolveModel, parseModelRef } from './models.js';
|
|
31
|
+
import { listModels, resolveModel, parseModelRef, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
32
32
|
import { buildSystemPrompt } from './prompts.js';
|
|
33
33
|
import { searchMessages } from './conversation/search.js';
|
|
34
34
|
import { ConversationStore } from './conversation/persist.js';
|
|
@@ -488,13 +488,15 @@ async function runREPL(config, args) {
|
|
|
488
488
|
session.config.model = modelRef;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
// Re-resolve model info from registry
|
|
491
|
+
// Re-resolve model info from registry (adapter / baseUrl /
|
|
492
|
+
// thinking metadata). Token limits come from the resolver
|
|
493
|
+
// ladder — models.dev snapshot first, config fallback.
|
|
492
494
|
const newModelInfo = resolveModel(session.config.model);
|
|
493
495
|
if (newModelInfo) {
|
|
494
|
-
session.config.maxContextTokens = newModelInfo.contextWindow;
|
|
495
|
-
session.config.maxOutputTokens = newModelInfo.maxOutputTokens;
|
|
496
496
|
session.config.modelInfo = newModelInfo;
|
|
497
497
|
}
|
|
498
|
+
session.config.maxContextTokens = resolveContextWindow(session.config.model, session.config);
|
|
499
|
+
session.config.maxOutputTokens = resolveMaxOutputTokens(session.config.model, session.config);
|
|
498
500
|
const providerStr = providerName ? ` (provider: ${providerName})` : '';
|
|
499
501
|
console.log(`Model switched to: ${session.config.model}${providerStr}`);
|
|
500
502
|
console.log('Note: Model change takes effect on next query.');
|
|
@@ -766,6 +768,19 @@ async function runOnce(config, args) {
|
|
|
766
768
|
async function main() {
|
|
767
769
|
const args = parseArgs(process.argv);
|
|
768
770
|
|
|
771
|
+
// Prime the models.dev community catalog so resolveContextWindow /
|
|
772
|
+
// resolveMaxOutputTokens — called synchronously from loadConfig and the
|
|
773
|
+
// engine hot path — can return real per-model limits instead of falling
|
|
774
|
+
// through to DEFAULT. Failure is non-fatal: stale disk cache or DEFAULT
|
|
775
|
+
// keeps the CLI usable offline. Mirrors the prime in agent/index.js.
|
|
776
|
+
try {
|
|
777
|
+
const { fetchModelsDev } = await import('./llm/models-dev.js');
|
|
778
|
+
const dir = args.dir || process.env.YEAFT_DIR || null;
|
|
779
|
+
await fetchModelsDev({ yeaftDir: dir });
|
|
780
|
+
} catch {
|
|
781
|
+
// best-effort; resolver falls back to config / DEFAULT.
|
|
782
|
+
}
|
|
783
|
+
|
|
769
784
|
// Load config with CLI overrides (for --trace queries and dry-run, no session needed)
|
|
770
785
|
const config = loadConfig({
|
|
771
786
|
model: args.model,
|
package/yeaft/config.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { existsSync, readFileSync } from 'fs';
|
|
23
23
|
import { join } from 'path';
|
|
24
24
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
25
|
-
import { resolveModel, parseModelRef, normalizeProviderModels } from './models.js';
|
|
25
|
+
import { resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
26
26
|
|
|
27
27
|
/** Default configuration values. */
|
|
28
28
|
const DEFAULTS = {
|
|
@@ -250,8 +250,15 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
250
250
|
if (modelInfo) {
|
|
251
251
|
config.adapter = modelInfo.adapter === 'anthropic' ? 'anthropic' : 'openai';
|
|
252
252
|
if (!config.baseUrl) config.baseUrl = modelInfo.baseUrl;
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
// Resolve token limits via the resolver ladder (models.dev → config →
|
|
254
|
+
// DEFAULT). Only fill in when the caller left the slot at the default
|
|
255
|
+
// — explicit env / CLI overrides win.
|
|
256
|
+
if (config.maxContextTokens === DEFAULTS.maxContextTokens) {
|
|
257
|
+
config.maxContextTokens = resolveContextWindow(config.model, config);
|
|
258
|
+
}
|
|
259
|
+
if (config.maxOutputTokens === DEFAULTS.maxOutputTokens) {
|
|
260
|
+
config.maxOutputTokens = resolveMaxOutputTokens(config.model, config);
|
|
261
|
+
}
|
|
255
262
|
} else {
|
|
256
263
|
if (config.apiKey) config.adapter = 'anthropic';
|
|
257
264
|
else if (config.openaiApiKey) config.adapter = 'openai';
|
|
@@ -305,9 +312,19 @@ export function loadConfig(overrides = {}) {
|
|
|
305
312
|
fastModelId = parsed.modelId;
|
|
306
313
|
}
|
|
307
314
|
|
|
308
|
-
// Resolve model info for
|
|
315
|
+
// Resolve model info for adapter/baseUrl/thinking metadata. Token limits
|
|
316
|
+
// (contextWindow / maxOutputTokens) are NOT read from here — they live in
|
|
317
|
+
// models.dev and are resolved via resolveContextWindow / resolveMaxOutputTokens
|
|
318
|
+
// a few lines below so the live models.dev snapshot is the source of truth.
|
|
309
319
|
const modelInfo = resolveModel(model);
|
|
310
320
|
|
|
321
|
+
// Pre-resolve token limits once so we can both write them onto config and
|
|
322
|
+
// pass `config` to the resolver chain consistently below.
|
|
323
|
+
const resolvedMaxContext = overrides.maxContextTokens ?? jsonConfig.maxContextTokens
|
|
324
|
+
?? resolveContextWindow(model, { modelInfo });
|
|
325
|
+
const resolvedMaxOutput = overrides.maxOutputTokens ?? jsonConfig.maxOutputTokens
|
|
326
|
+
?? resolveMaxOutputTokens(model, { modelInfo });
|
|
327
|
+
|
|
311
328
|
const config = {
|
|
312
329
|
// Model
|
|
313
330
|
model: overrides.model || model,
|
|
@@ -325,9 +342,16 @@ export function loadConfig(overrides = {}) {
|
|
|
325
342
|
debug: overrides.debug !== undefined ? overrides.debug : (jsonConfig.debug ?? DEFAULTS.debug),
|
|
326
343
|
dir,
|
|
327
344
|
|
|
328
|
-
// Token limits
|
|
329
|
-
|
|
330
|
-
|
|
345
|
+
// Token limits. Resolution order:
|
|
346
|
+
// 1. CLI override (overrides.*)
|
|
347
|
+
// 2. ~/.yeaft/config.json explicit value
|
|
348
|
+
// 3. resolveContextWindow / resolveMaxOutputTokens — which themselves
|
|
349
|
+
// walk: per-provider override → models.dev snapshot → DEFAULT.
|
|
350
|
+
// Anything that needs the *live* number for a model the user picks at
|
|
351
|
+
// runtime (mid-session model switch, e.g.) should call the resolvers
|
|
352
|
+
// directly rather than read these fields.
|
|
353
|
+
maxContextTokens: resolvedMaxContext,
|
|
354
|
+
maxOutputTokens: resolvedMaxOutput,
|
|
331
355
|
messageTokenBudget: overrides.messageTokenBudget ?? jsonConfig.messageTokenBudget ?? DEFAULTS.messageTokenBudget,
|
|
332
356
|
maxContinueTurns: overrides.maxContinueTurns ?? jsonConfig.maxContinueTurns ?? DEFAULTS.maxContinueTurns,
|
|
333
357
|
|
package/yeaft/debug-trace.js
CHANGED
|
@@ -529,7 +529,7 @@ export class DebugTrace {
|
|
|
529
529
|
const target = typeof data.target === 'string' ? data.target : '';
|
|
530
530
|
if (sessionId) {
|
|
531
531
|
const isBroadcast = !evtGroupId && !target;
|
|
532
|
-
const isThisGroup = evtGroupId === sessionId || target === `group/${sessionId}`;
|
|
532
|
+
const isThisGroup = evtGroupId === sessionId || target === `group/${sessionId}` || target === `session/${sessionId}`;
|
|
533
533
|
if (!isBroadcast && !isThisGroup) continue;
|
|
534
534
|
}
|
|
535
535
|
dreamEvents.push({
|
package/yeaft/dream/state.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 1. Per-group control state (used to decide whether a group enters
|
|
7
7
|
* triage and how far to advance the cursor):
|
|
8
8
|
*
|
|
9
|
-
* ~/.yeaft/memory/
|
|
9
|
+
* ~/.yeaft/memory/session/<id>/.dream-state
|
|
10
10
|
*
|
|
11
11
|
* A 3-line text file:
|
|
12
12
|
*
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* empty / null / 0. The file is rewritten atomically every dream.
|
|
19
19
|
*
|
|
20
20
|
* The virtual `_no-group/` group lives at the same path layout
|
|
21
|
-
* (`
|
|
21
|
+
* (`session/_no-group/.dream-state`) and uses the same accessor.
|
|
22
22
|
*
|
|
23
23
|
* 2. Per-scope observability marker, embedded inside the scope's
|
|
24
24
|
* `memory.md` between two HTML comments at the file's tail:
|
|
@@ -63,11 +63,19 @@ const DREAM_BLOCK_CLOSE = '<!-- /dream-state -->';
|
|
|
63
63
|
* @returns {Promise<{ lastDreamMessageId: string|null, lastDreamAt: string|null, messageCount: number }>}
|
|
64
64
|
*/
|
|
65
65
|
export async function readGroupState(root, sessionId) {
|
|
66
|
-
const abs = join(root, '
|
|
66
|
+
const abs = join(root, 'session', sessionId, STATE_FILE);
|
|
67
|
+
const legacyAbs = join(root, 'group', sessionId, STATE_FILE);
|
|
67
68
|
const empty = { lastDreamMessageId: null, lastDreamAt: null, messageCount: 0 };
|
|
68
69
|
let raw;
|
|
69
70
|
try { raw = await fsp.readFile(abs, 'utf8'); }
|
|
70
|
-
catch (err) {
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (!err || err.code !== 'ENOENT') throw err;
|
|
73
|
+
try { raw = await fsp.readFile(legacyAbs, 'utf8'); }
|
|
74
|
+
catch (legacyErr) {
|
|
75
|
+
if (legacyErr && legacyErr.code === 'ENOENT') return empty;
|
|
76
|
+
throw legacyErr;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
71
79
|
return parseGroupState(raw);
|
|
72
80
|
}
|
|
73
81
|
|
|
@@ -80,7 +88,7 @@ export async function readGroupState(root, sessionId) {
|
|
|
80
88
|
* @param {{ lastDreamMessageId?: string|null, lastDreamAt?: string|null, messageCount?: number }} state
|
|
81
89
|
*/
|
|
82
90
|
export async function writeGroupState(root, sessionId, state) {
|
|
83
|
-
const dir = join(root, '
|
|
91
|
+
const dir = join(root, 'session', sessionId);
|
|
84
92
|
await fsp.mkdir(dir, { recursive: true });
|
|
85
93
|
const abs = join(dir, STATE_FILE);
|
|
86
94
|
const body =
|
package/yeaft/engine.js
CHANGED
|
@@ -37,6 +37,7 @@ import { runStopHooks } from './stop-hooks.js';
|
|
|
37
37
|
const MAIN_THREAD_ID = 'main';
|
|
38
38
|
import { pickEffort, parseEffortPrefix } from './effort.js';
|
|
39
39
|
import { DEFAULT_CONTEXT_WINDOW, normalizeEffort, resolveContextWindow, resolveModel } from './models.js';
|
|
40
|
+
import { lookupModelLimitSync } from './llm/models-dev.js';
|
|
40
41
|
import { countTurns } from './turn-utils.js';
|
|
41
42
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
42
43
|
import { resolveThinking } from './router/thinking.js';
|
|
@@ -186,7 +187,11 @@ export function shouldAllowGroupReflection({
|
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
const contextWindow = resolveContextWindow(model, config);
|
|
189
|
-
|
|
190
|
+
// Telemetry: did the resolver hit either of its top non-default rungs?
|
|
191
|
+
// Used by `usedFallbackContextWindow` below — if neither models.dev nor
|
|
192
|
+
// the global config provided a number, we fell through to DEFAULT and
|
|
193
|
+
// callers may want to surface that to the user.
|
|
194
|
+
const hasModelsDevContext = !!lookupModelLimitSync(model, resolveModel(model)?.provider || null)?.context;
|
|
190
195
|
const hasConfigContext = Number.isFinite(config?.maxContextTokens) && config.maxContextTokens > 0;
|
|
191
196
|
const threshold = Math.floor(contextWindow * GROUP_CONTEXT_PRESSURE_RATIO);
|
|
192
197
|
const tokenEstimate = estimateMessagesTokens(system, messages);
|
|
@@ -204,7 +209,7 @@ export function shouldAllowGroupReflection({
|
|
|
204
209
|
contextWindow,
|
|
205
210
|
ratio: GROUP_CONTEXT_PRESSURE_RATIO,
|
|
206
211
|
turnCount,
|
|
207
|
-
usedFallbackContextWindow: !
|
|
212
|
+
usedFallbackContextWindow: !hasModelsDevContext && !hasConfigContext && contextWindow === DEFAULT_CONTEXT_WINDOW,
|
|
208
213
|
};
|
|
209
214
|
}
|
|
210
215
|
|
package/yeaft/init.js
CHANGED
package/yeaft/llm/models-dev.js
CHANGED
|
@@ -163,6 +163,76 @@ export async function listProviderModels(providerId, { yeaftDir = null } = {}) {
|
|
|
163
163
|
return Object.keys(models);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Synchronously look up a model's `{context, output}` limits from whatever
|
|
168
|
+
* models.dev snapshot is currently warmed in memory.
|
|
169
|
+
*
|
|
170
|
+
* The yeaft engine query loop is synchronous (engine.js, config.js, cli.js
|
|
171
|
+
* all read the context window inline). To avoid bubbling async up through
|
|
172
|
+
* every gate, the agent boot script primes `fetchModelsDev()` once so this
|
|
173
|
+
* function can read straight from `_memCache` afterwards.
|
|
174
|
+
*
|
|
175
|
+
* If the cache is empty (boot prime failed or test never warmed it), or the
|
|
176
|
+
* model isn't present in any provider entry, returns `null` — callers fall
|
|
177
|
+
* back to the next rung of their resolver ladder.
|
|
178
|
+
*
|
|
179
|
+
* Collision policy: a given model id can appear under multiple providers
|
|
180
|
+
* (e.g. `qwen3-32b` lives under 7 providers in the live models.dev snapshot
|
|
181
|
+
* with context limits ranging 32K–131K). Behavior:
|
|
182
|
+
*
|
|
183
|
+
* • If `providerHint` is given AND that provider lists the model, we use
|
|
184
|
+
* that provider's numbers verbatim — the caller knew which gateway it
|
|
185
|
+
* was talking to.
|
|
186
|
+
* • Otherwise we take the MIN of every provider's `context` and `output`.
|
|
187
|
+
* Context is a ceiling: under-shooting risks an early compact (bad);
|
|
188
|
+
* over-shooting risks an LLMContextError mid-query (worse). Min picks
|
|
189
|
+
* the safer side. Users who know better can pin numbers explicitly via
|
|
190
|
+
* `providers[].models[].contextWindow` in `~/.yeaft/config.json`.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} modelId
|
|
193
|
+
* @param {string|null} [providerHint] Provider id matching the models.dev
|
|
194
|
+
* top-level key (e.g. 'anthropic', 'openai', 'google', 'deepseek'). The
|
|
195
|
+
* MODEL_REGISTRY's `provider` field is aligned to these ids deliberately.
|
|
196
|
+
* @returns {{ context?: number, output?: number } | null}
|
|
197
|
+
*/
|
|
198
|
+
export function lookupModelLimitSync(modelId, providerHint = null) {
|
|
199
|
+
if (!modelId || !_memCache || typeof _memCache !== 'object') return null;
|
|
200
|
+
|
|
201
|
+
// Hit the hint first if it actually lists this model.
|
|
202
|
+
if (providerHint && typeof providerHint === 'string') {
|
|
203
|
+
const provEntry = _memCache[providerHint];
|
|
204
|
+
const m = provEntry?.models?.[modelId];
|
|
205
|
+
if (m && m.limit && typeof m.limit === 'object') {
|
|
206
|
+
const out = {};
|
|
207
|
+
if (Number.isFinite(m.limit.context) && m.limit.context > 0) out.context = m.limit.context;
|
|
208
|
+
if (Number.isFinite(m.limit.output) && m.limit.output > 0) out.output = m.limit.output;
|
|
209
|
+
if (out.context !== undefined || out.output !== undefined) return out;
|
|
210
|
+
}
|
|
211
|
+
// Hint missed — fall through to the scan rather than returning null,
|
|
212
|
+
// because the model genuinely might live under another provider id
|
|
213
|
+
// (e.g. a deepseek model surfaced via a relay).
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Scan every provider; collect context/output values, return MIN.
|
|
217
|
+
let minCtx = null;
|
|
218
|
+
let minOut = null;
|
|
219
|
+
for (const provId of Object.keys(_memCache)) {
|
|
220
|
+
const m = _memCache[provId]?.models?.[modelId];
|
|
221
|
+
if (!m || !m.limit || typeof m.limit !== 'object') continue;
|
|
222
|
+
if (Number.isFinite(m.limit.context) && m.limit.context > 0) {
|
|
223
|
+
minCtx = minCtx === null ? m.limit.context : Math.min(minCtx, m.limit.context);
|
|
224
|
+
}
|
|
225
|
+
if (Number.isFinite(m.limit.output) && m.limit.output > 0) {
|
|
226
|
+
minOut = minOut === null ? m.limit.output : Math.min(minOut, m.limit.output);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (minCtx === null && minOut === null) return null;
|
|
230
|
+
const result = {};
|
|
231
|
+
if (minCtx !== null) result.context = minCtx;
|
|
232
|
+
if (minOut !== null) result.output = minOut;
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
166
236
|
/**
|
|
167
237
|
* Reset the in-memory cache. Test seam.
|
|
168
238
|
*/
|
|
@@ -171,3 +241,26 @@ export function _resetMemCache() {
|
|
|
171
241
|
_memCachePath = null;
|
|
172
242
|
_memCacheTime = 0;
|
|
173
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Inject a snapshot into the in-memory cache without going through the
|
|
247
|
+
* fetch/disk path. Test seam: callers that want to exercise
|
|
248
|
+
* `lookupModelLimitSync` deterministically can seed any shape they need.
|
|
249
|
+
*
|
|
250
|
+
* Pass a falsy snapshot to fully reset (equivalent to {@link _resetMemCache}).
|
|
251
|
+
* This avoids the foot-gun where `_setMemCacheForTest(null)` would leave
|
|
252
|
+
* `_memCachePath` and `_memCacheTime` stamped to a fake `__test__` value,
|
|
253
|
+
* confusing any subsequent `fetchModelsDev` call about whether the cache
|
|
254
|
+
* was ever populated.
|
|
255
|
+
*
|
|
256
|
+
* @param {object|null} snapshot models.dev-shaped data, or null/undefined to reset.
|
|
257
|
+
*/
|
|
258
|
+
export function _setMemCacheForTest(snapshot) {
|
|
259
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
260
|
+
_resetMemCache();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
_memCache = snapshot;
|
|
264
|
+
_memCachePath = '__test__';
|
|
265
|
+
_memCacheTime = Date.now();
|
|
266
|
+
}
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
readFileSync,
|
|
44
44
|
renameSync,
|
|
45
45
|
statSync,
|
|
46
|
+
rmSync,
|
|
46
47
|
unlinkSync,
|
|
47
48
|
writeFileSync,
|
|
48
49
|
} from 'fs';
|
|
@@ -50,7 +51,7 @@ import { join } from 'path';
|
|
|
50
51
|
import { openSegmentIndex } from '../memory/index-db.js';
|
|
51
52
|
|
|
52
53
|
const SENTINEL = '.yeaft-migration.done';
|
|
53
|
-
const SENTINEL_VERSION =
|
|
54
|
+
const SENTINEL_VERSION = 3;
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Run the sessions migration. No-op when sentinel exists.
|
|
@@ -216,9 +217,13 @@ export function migrateSessions(yeaftDir) {
|
|
|
216
217
|
// so that any pre-rename row still on disk gets the new key shape.
|
|
217
218
|
const frontmatterRewrites = rewriteAllMessageFrontmatter(yeaftDir, warnings);
|
|
218
219
|
|
|
219
|
-
// 8.
|
|
220
|
-
//
|
|
221
|
-
//
|
|
220
|
+
// 8. Cleanup: if this is rerunning after a v2 sentinel, legacy groups/ or
|
|
221
|
+
// chats/ directories may have been recreated. Merge non-duplicate files
|
|
222
|
+
// into sessions/<id>, then remove empty legacy directories.
|
|
223
|
+
const cleanup = cleanupLegacySessionDirs(yeaftDir, warnings);
|
|
224
|
+
moved += cleanup.moved;
|
|
225
|
+
|
|
226
|
+
// 9. Sentinel — version 3 = consolidated migration plus legacy cleanup.
|
|
222
227
|
writeFileSync(sentinel, JSON.stringify({
|
|
223
228
|
version: SENTINEL_VERSION,
|
|
224
229
|
migratedAt: new Date().toISOString(),
|
|
@@ -410,6 +415,58 @@ function listDirs(root) {
|
|
|
410
415
|
return out;
|
|
411
416
|
}
|
|
412
417
|
|
|
418
|
+
function cleanupLegacySessionDirs(yeaftDir, warnings) {
|
|
419
|
+
const sessionsRoot = join(yeaftDir, 'sessions');
|
|
420
|
+
let moved = 0;
|
|
421
|
+
for (const legacyName of ['groups', 'chats']) {
|
|
422
|
+
const legacyRoot = join(yeaftDir, legacyName);
|
|
423
|
+
if (!existsSync(legacyRoot)) continue;
|
|
424
|
+
for (const id of listDirs(legacyRoot)) {
|
|
425
|
+
const src = join(legacyRoot, id);
|
|
426
|
+
const dst = join(sessionsRoot, id);
|
|
427
|
+
if (!existsSync(dst)) continue;
|
|
428
|
+
mergeLegacyDirIntoSession(src, dst, warnings, `${legacyName}/${id}`);
|
|
429
|
+
removeDirIfEmpty(src, warnings, `${legacyName}/${id}`);
|
|
430
|
+
if (!existsSync(src)) moved++;
|
|
431
|
+
}
|
|
432
|
+
removeDirIfEmpty(legacyRoot, warnings, legacyName);
|
|
433
|
+
}
|
|
434
|
+
return { moved };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mergeLegacyDirIntoSession(src, dst, warnings, label) {
|
|
438
|
+
if (!existsSync(src) || !existsSync(dst)) return;
|
|
439
|
+
let entries = [];
|
|
440
|
+
try { entries = readdirSync(src, { withFileTypes: true }); }
|
|
441
|
+
catch (err) { warnings.push(`${label}: failed to scan legacy dir: ${err.message}`); return; }
|
|
442
|
+
|
|
443
|
+
for (const ent of entries) {
|
|
444
|
+
const from = join(src, ent.name);
|
|
445
|
+
const to = join(dst, ent.name);
|
|
446
|
+
if (!existsSync(to)) {
|
|
447
|
+
try { renameSync(from, to); }
|
|
448
|
+
catch (err) { warnings.push(`${label}: failed to move ${ent.name}: ${err.message}`); }
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (ent.isDirectory()) {
|
|
452
|
+
mergeLegacyDirIntoSession(from, to, warnings, `${label}/${ent.name}`);
|
|
453
|
+
removeDirIfEmpty(from, warnings, `${label}/${ent.name}`);
|
|
454
|
+
} else {
|
|
455
|
+
warnings.push(`${label}: kept duplicate legacy file ${ent.name}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function removeDirIfEmpty(dir, warnings, label) {
|
|
461
|
+
if (!existsSync(dir)) return;
|
|
462
|
+
try {
|
|
463
|
+
const entries = readdirSync(dir);
|
|
464
|
+
if (entries.length === 0) rmSync(dir, { recursive: true, force: true });
|
|
465
|
+
} catch (err) {
|
|
466
|
+
warnings.push(`${label}: failed to remove empty legacy dir: ${err.message}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
413
470
|
function rewriteGroupMetaToSessionMeta(sessionDir, warnings) {
|
|
414
471
|
const oldPath = join(sessionDir, 'group.json');
|
|
415
472
|
const newPath = join(sessionDir, 'meta.json');
|
package/yeaft/models.js
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* models.js — Model ID registry for Yeaft
|
|
2
|
+
* models.js — Model ID registry for Yeaft
|
|
3
3
|
*
|
|
4
|
-
* Maps model IDs (e.g. "gpt-5", "claude-sonnet-4-20250514") to
|
|
5
|
-
*
|
|
4
|
+
* Maps model IDs (e.g. "gpt-5", "claude-sonnet-4-20250514") to the *adapter*
|
|
5
|
+
* metadata Yeaft needs to dispatch requests: which protocol (anthropic vs
|
|
6
|
+
* openai-responses), which base URL, which thinking/reasoning capabilities.
|
|
6
7
|
*
|
|
7
8
|
* Yeaft does not provide its own models. The "model" field is always a
|
|
8
9
|
* model ID from an external provider. This registry lets Yeaft auto-detect
|
|
9
10
|
* the correct adapter and endpoint from just the model ID, so users only
|
|
10
11
|
* need to set YEAFT_MODEL=gpt-5 without configuring adapter/baseUrl separately.
|
|
11
12
|
*
|
|
13
|
+
* **Token limits (contextWindow / maxOutput) are NOT stored here.** They are
|
|
14
|
+
* resolved at runtime from the models.dev community catalog
|
|
15
|
+
* (`agent/yeaft/llm/models-dev.js`), with a per-provider config override
|
|
16
|
+
* (`~/.yeaft/config.json: providers[].models[].contextWindow`) and a
|
|
17
|
+
* conservative DEFAULT as the final rung. See {@link resolveContextWindow}
|
|
18
|
+
* and {@link resolveMaxOutputTokens} for the full ladder.
|
|
19
|
+
*
|
|
12
20
|
* Unknown model IDs return null — caller falls back to env-based detection.
|
|
13
21
|
*/
|
|
14
22
|
|
|
23
|
+
import { lookupModelLimitSync } from './llm/models-dev.js';
|
|
24
|
+
|
|
15
25
|
/**
|
|
16
26
|
* @typedef {Object} ModelInfo
|
|
17
|
-
* @property {'anthropic' | 'openai' | 'deepseek' | 'google'} provider — Which provider this model belongs to
|
|
27
|
+
* @property {'anthropic' | 'openai' | 'deepseek' | 'google'} provider — Which provider this model belongs to.
|
|
28
|
+
* These ids are intentionally aligned with the top-level keys in the models.dev
|
|
29
|
+
* catalog so they can be used as a `providerHint` to {@link lookupModelLimitSync}.
|
|
18
30
|
* @property {'anthropic' | 'chat-completions'} adapter — Which wire protocol to use
|
|
19
31
|
* @property {string} baseUrl — Official API endpoint base URL
|
|
20
|
-
* @property {number} contextWindow — Max context tokens
|
|
21
|
-
* @property {number} maxOutputTokens — Max output tokens
|
|
22
32
|
* @property {string} displayName — Human-readable model name
|
|
23
33
|
* @property {boolean} [supportsThinking] — task-327a: model supports thinking/reasoning effort.
|
|
24
34
|
* @property {'anthropic' | 'openai-reasoning' | 'none'} [thinkingProtocol] — task-327a:
|
|
@@ -39,8 +49,6 @@ export const MODEL_REGISTRY = new Map([
|
|
|
39
49
|
provider: 'anthropic',
|
|
40
50
|
adapter: 'anthropic',
|
|
41
51
|
baseUrl: 'https://api.anthropic.com',
|
|
42
|
-
contextWindow: 200000,
|
|
43
|
-
maxOutputTokens: 16384,
|
|
44
52
|
displayName: 'Claude Sonnet 4',
|
|
45
53
|
// task-327a: extended thinking supported; budget caps at 32K on Sonnet.
|
|
46
54
|
supportsThinking: true,
|
|
@@ -52,8 +60,6 @@ export const MODEL_REGISTRY = new Map([
|
|
|
52
60
|
provider: 'anthropic',
|
|
53
61
|
adapter: 'anthropic',
|
|
54
62
|
baseUrl: 'https://api.anthropic.com',
|
|
55
|
-
contextWindow: 200000,
|
|
56
|
-
maxOutputTokens: 16384,
|
|
57
63
|
displayName: 'Claude Opus 4',
|
|
58
64
|
// task-327a: PM decision — Opus max budget = 64K.
|
|
59
65
|
supportsThinking: true,
|
|
@@ -65,8 +71,6 @@ export const MODEL_REGISTRY = new Map([
|
|
|
65
71
|
provider: 'anthropic',
|
|
66
72
|
adapter: 'anthropic',
|
|
67
73
|
baseUrl: 'https://api.anthropic.com',
|
|
68
|
-
contextWindow: 200000,
|
|
69
|
-
maxOutputTokens: 8192,
|
|
70
74
|
displayName: 'Claude Haiku 3',
|
|
71
75
|
// task-327a: Haiku 3 does not support extended thinking — effort is dropped.
|
|
72
76
|
supportsThinking: false,
|
|
@@ -78,17 +82,12 @@ export const MODEL_REGISTRY = new Map([
|
|
|
78
82
|
provider: 'openai',
|
|
79
83
|
adapter: 'openai-responses',
|
|
80
84
|
baseUrl: 'https://api.openai.com/v1',
|
|
81
|
-
contextWindow: 256000,
|
|
82
|
-
maxOutputTokens: 16384,
|
|
83
85
|
displayName: 'GPT-5',
|
|
84
86
|
// task-327a: GPT-5 supports reasoning.effort (low/medium/high). No 'max'.
|
|
85
87
|
supportsThinking: true,
|
|
86
88
|
thinkingProtocol: 'openai-reasoning',
|
|
87
89
|
defaultEffort: null,
|
|
88
90
|
}],
|
|
89
|
-
// gpt-5-mini/-nano/-pro: keep id + family/protocol metadata so they appear
|
|
90
|
-
// as known models, but do NOT hardcode context/maxOutput — the real limits
|
|
91
|
-
// should come from provider config (user-supplied) instead of guesses.
|
|
92
91
|
['gpt-5-mini', {
|
|
93
92
|
provider: 'openai',
|
|
94
93
|
adapter: 'openai-responses',
|
|
@@ -111,40 +110,30 @@ export const MODEL_REGISTRY = new Map([
|
|
|
111
110
|
provider: 'openai',
|
|
112
111
|
adapter: 'openai-responses',
|
|
113
112
|
baseUrl: 'https://api.openai.com/v1',
|
|
114
|
-
contextWindow: 272000,
|
|
115
|
-
maxOutputTokens: 16384,
|
|
116
113
|
displayName: 'GPT-5.4',
|
|
117
114
|
}],
|
|
118
115
|
['gpt-4.1', {
|
|
119
116
|
provider: 'openai',
|
|
120
117
|
adapter: 'openai-responses',
|
|
121
118
|
baseUrl: 'https://api.openai.com/v1',
|
|
122
|
-
contextWindow: 1047576,
|
|
123
|
-
maxOutputTokens: 32768,
|
|
124
119
|
displayName: 'GPT-4.1',
|
|
125
120
|
}],
|
|
126
121
|
['gpt-4.1-mini', {
|
|
127
122
|
provider: 'openai',
|
|
128
123
|
adapter: 'openai-responses',
|
|
129
124
|
baseUrl: 'https://api.openai.com/v1',
|
|
130
|
-
contextWindow: 1047576,
|
|
131
|
-
maxOutputTokens: 16384,
|
|
132
125
|
displayName: 'GPT-4.1 Mini',
|
|
133
126
|
}],
|
|
134
127
|
['gpt-4.1-nano', {
|
|
135
128
|
provider: 'openai',
|
|
136
129
|
adapter: 'openai-responses',
|
|
137
130
|
baseUrl: 'https://api.openai.com/v1',
|
|
138
|
-
contextWindow: 1047576,
|
|
139
|
-
maxOutputTokens: 16384,
|
|
140
131
|
displayName: 'GPT-4.1 Nano',
|
|
141
132
|
}],
|
|
142
133
|
['o3', {
|
|
143
134
|
provider: 'openai',
|
|
144
135
|
adapter: 'openai-responses',
|
|
145
136
|
baseUrl: 'https://api.openai.com/v1',
|
|
146
|
-
contextWindow: 200000,
|
|
147
|
-
maxOutputTokens: 100000,
|
|
148
137
|
displayName: 'o3',
|
|
149
138
|
// task-327a: o-series reasoning models use reasoning.effort.
|
|
150
139
|
supportsThinking: true,
|
|
@@ -155,8 +144,6 @@ export const MODEL_REGISTRY = new Map([
|
|
|
155
144
|
provider: 'openai',
|
|
156
145
|
adapter: 'openai-responses',
|
|
157
146
|
baseUrl: 'https://api.openai.com/v1',
|
|
158
|
-
contextWindow: 200000,
|
|
159
|
-
maxOutputTokens: 100000,
|
|
160
147
|
displayName: 'o4-mini',
|
|
161
148
|
supportsThinking: true,
|
|
162
149
|
thinkingProtocol: 'openai-reasoning',
|
|
@@ -168,16 +155,12 @@ export const MODEL_REGISTRY = new Map([
|
|
|
168
155
|
provider: 'deepseek',
|
|
169
156
|
adapter: 'openai-responses',
|
|
170
157
|
baseUrl: 'https://api.deepseek.com',
|
|
171
|
-
contextWindow: 131072,
|
|
172
|
-
maxOutputTokens: 8192,
|
|
173
158
|
displayName: 'DeepSeek Chat',
|
|
174
159
|
}],
|
|
175
160
|
['deepseek-reasoner', {
|
|
176
161
|
provider: 'deepseek',
|
|
177
162
|
adapter: 'openai-responses',
|
|
178
163
|
baseUrl: 'https://api.deepseek.com',
|
|
179
|
-
contextWindow: 131072,
|
|
180
|
-
maxOutputTokens: 8192,
|
|
181
164
|
displayName: 'DeepSeek Reasoner',
|
|
182
165
|
}],
|
|
183
166
|
|
|
@@ -186,16 +169,12 @@ export const MODEL_REGISTRY = new Map([
|
|
|
186
169
|
provider: 'google',
|
|
187
170
|
adapter: 'openai-responses',
|
|
188
171
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
189
|
-
contextWindow: 1048576,
|
|
190
|
-
maxOutputTokens: 65536,
|
|
191
172
|
displayName: 'Gemini 2.5 Pro',
|
|
192
173
|
}],
|
|
193
174
|
['gemini-2.5-flash', {
|
|
194
175
|
provider: 'google',
|
|
195
176
|
adapter: 'openai-responses',
|
|
196
177
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
197
|
-
contextWindow: 1048576,
|
|
198
|
-
maxOutputTokens: 65536,
|
|
199
178
|
displayName: 'Gemini 2.5 Flash',
|
|
200
179
|
}],
|
|
201
180
|
]);
|
|
@@ -214,9 +193,9 @@ export function resolveModel(modelName) {
|
|
|
214
193
|
}
|
|
215
194
|
|
|
216
195
|
/**
|
|
217
|
-
* Default context window when
|
|
218
|
-
*
|
|
219
|
-
*
|
|
196
|
+
* Default context window when no upstream source has a value. 200K is a
|
|
197
|
+
* conservative middle-ground — most modern production models (Claude,
|
|
198
|
+
* GPT-5, Gemini) have ≥ 128K.
|
|
220
199
|
*
|
|
221
200
|
* Single source of truth: callers (engine.js pre-flight guard,
|
|
222
201
|
* tools/registry.js per-result cap) MUST use this constant or
|
|
@@ -225,40 +204,106 @@ export function resolveModel(modelName) {
|
|
|
225
204
|
export const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
226
205
|
|
|
227
206
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
|
|
207
|
+
* Default per-call output cap when no upstream source has a value. This is
|
|
208
|
+
* the floor the adapters use to size `max_tokens`; production models give us
|
|
209
|
+
* more, but 16K is enough to make progress on any single turn without
|
|
210
|
+
* tripping a provider-side reject.
|
|
211
|
+
*/
|
|
212
|
+
export const DEFAULT_MAX_OUTPUT_TOKENS = 16_384;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolve the live context window for a model.
|
|
216
|
+
*
|
|
217
|
+
* Fallback ladder (first match wins):
|
|
218
|
+
* 1. `config.modelInfo.contextWindow` — per-provider override originating
|
|
219
|
+
* from `~/.yeaft/config.json: providers[].models[].contextWindow`. The
|
|
220
|
+
* caller is responsible for passing the relevant entry; loadConfig
|
|
221
|
+
* threads it through `config.modelInfo` already.
|
|
222
|
+
* 2. {@link lookupModelLimitSync} against the warmed models.dev snapshot,
|
|
223
|
+
* hinted by the MODEL_REGISTRY provider when available so collisions
|
|
224
|
+
* across providers resolve to the right entry.
|
|
225
|
+
* 3. `config.maxContextTokens` — global ceiling from config / CLI.
|
|
226
|
+
* 4. {@link DEFAULT_CONTEXT_WINDOW}.
|
|
233
227
|
*
|
|
234
228
|
* Used by the per-tool-result cap and the pre-flight token guard so the
|
|
235
229
|
* defense layers always see the same number, regardless of which seam
|
|
236
230
|
* resolves it first.
|
|
237
231
|
*
|
|
238
232
|
* @param {string} modelName
|
|
239
|
-
* @param {{ maxContextTokens?: number }} [config]
|
|
233
|
+
* @param {{ maxContextTokens?: number, modelInfo?: { contextWindow?: number } } | null} [config]
|
|
240
234
|
* @returns {number}
|
|
241
235
|
*/
|
|
242
236
|
export function resolveContextWindow(modelName, config) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
237
|
+
// Rung 1: per-provider config override threaded via config.modelInfo.
|
|
238
|
+
const overrideCtx = config?.modelInfo?.contextWindow;
|
|
239
|
+
if (Number.isFinite(overrideCtx) && overrideCtx > 0) return overrideCtx;
|
|
240
|
+
|
|
241
|
+
// Rung 2: models.dev snapshot (warmed at agent boot).
|
|
242
|
+
const reg = MODEL_REGISTRY.get(modelName);
|
|
243
|
+
const limit = lookupModelLimitSync(modelName, reg?.provider || null);
|
|
244
|
+
if (limit && Number.isFinite(limit.context) && limit.context > 0) {
|
|
245
|
+
return limit.context;
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
// Rung 3: global config ceiling.
|
|
247
249
|
const cfg = config && Number.isFinite(config.maxContextTokens) && config.maxContextTokens > 0
|
|
248
250
|
? config.maxContextTokens : null;
|
|
249
251
|
if (cfg !== null) return cfg;
|
|
252
|
+
|
|
253
|
+
// Rung 4: default.
|
|
250
254
|
return DEFAULT_CONTEXT_WINDOW;
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
/**
|
|
254
|
-
*
|
|
258
|
+
* Resolve the live per-call output cap for a model. Same ladder as
|
|
259
|
+
* {@link resolveContextWindow} but for the `output` axis:
|
|
260
|
+
* 1. `config.modelInfo.maxOutput` override
|
|
261
|
+
* 2. models.dev `limit.output`
|
|
262
|
+
* 3. `config.maxOutputTokens`
|
|
263
|
+
* 4. {@link DEFAULT_MAX_OUTPUT_TOKENS}
|
|
264
|
+
*
|
|
265
|
+
* @param {string} modelName
|
|
266
|
+
* @param {{ maxOutputTokens?: number, modelInfo?: { maxOutput?: number, maxOutputTokens?: number } } | null} [config]
|
|
267
|
+
* @returns {number}
|
|
268
|
+
*/
|
|
269
|
+
export function resolveMaxOutputTokens(modelName, config) {
|
|
270
|
+
const overrideOut = config?.modelInfo?.maxOutput
|
|
271
|
+
?? config?.modelInfo?.maxOutputTokens;
|
|
272
|
+
if (Number.isFinite(overrideOut) && overrideOut > 0) return overrideOut;
|
|
273
|
+
|
|
274
|
+
const reg = MODEL_REGISTRY.get(modelName);
|
|
275
|
+
const limit = lookupModelLimitSync(modelName, reg?.provider || null);
|
|
276
|
+
if (limit && Number.isFinite(limit.output) && limit.output > 0) {
|
|
277
|
+
return limit.output;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const cfg = config && Number.isFinite(config.maxOutputTokens) && config.maxOutputTokens > 0
|
|
281
|
+
? config.maxOutputTokens : null;
|
|
282
|
+
if (cfg !== null) return cfg;
|
|
283
|
+
|
|
284
|
+
return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* List all known models with their resolved token limits.
|
|
289
|
+
*
|
|
290
|
+
* Token limits are resolved at call time — they reflect whatever models.dev
|
|
291
|
+
* snapshot is currently warmed, plus the DEFAULT fallback for models the
|
|
292
|
+
* snapshot doesn't cover. The returned objects therefore mirror what
|
|
293
|
+
* `resolveContextWindow` / `resolveMaxOutputTokens` would return for the
|
|
294
|
+
* same model id.
|
|
255
295
|
*
|
|
256
296
|
* @returns {{ name: string, adapter: string, baseUrl: string, contextWindow: number, maxOutputTokens: number, displayName: string }[]}
|
|
257
297
|
*/
|
|
258
298
|
export function listModels() {
|
|
259
299
|
const result = [];
|
|
260
300
|
for (const [name, info] of MODEL_REGISTRY) {
|
|
261
|
-
result.push({
|
|
301
|
+
result.push({
|
|
302
|
+
name,
|
|
303
|
+
...info,
|
|
304
|
+
contextWindow: resolveContextWindow(name, null),
|
|
305
|
+
maxOutputTokens: resolveMaxOutputTokens(name, null),
|
|
306
|
+
});
|
|
262
307
|
}
|
|
263
308
|
return result;
|
|
264
309
|
}
|
|
@@ -484,11 +529,16 @@ export function serializeModelForPersistence(entry) {
|
|
|
484
529
|
*
|
|
485
530
|
* Lookup order:
|
|
486
531
|
* 1. provider-config fields (contextWindow / maxOutput) — highest priority
|
|
487
|
-
* 2.
|
|
532
|
+
* 2. models.dev snapshot (`limit.{context,output}`) via the synchronous
|
|
533
|
+
* reader, hinted by the MODEL_REGISTRY provider when known so cross-
|
|
534
|
+
* provider id collisions resolve to the right entry.
|
|
488
535
|
* 3. undefined if neither has any info
|
|
489
536
|
*
|
|
490
537
|
* Returns a normalized shape: `{ id, contextWindow?, maxOutput?, provider?, adapter?, baseUrl?, displayName? }`.
|
|
491
538
|
*
|
|
539
|
+
* Note: token limits are intentionally NOT read from MODEL_REGISTRY — those
|
|
540
|
+
* fields were removed when models.dev became the source of truth.
|
|
541
|
+
*
|
|
492
542
|
* @param {string} model
|
|
493
543
|
* @param {{ id?: string, contextWindow?: number, maxOutput?: number } | null} [providerConfig]
|
|
494
544
|
* @returns {object | undefined}
|
|
@@ -499,8 +549,12 @@ export function getModelInfo(model, providerConfig) {
|
|
|
499
549
|
const overrideCtx = coercePositiveInt(providerConfig?.contextWindow);
|
|
500
550
|
const overrideMax = coercePositiveInt(providerConfig?.maxOutput);
|
|
501
551
|
|
|
502
|
-
|
|
503
|
-
|
|
552
|
+
// Pull the models.dev numbers as the second rung. Hint with the registry's
|
|
553
|
+
// provider id when available — same alignment trick the resolveContext-
|
|
554
|
+
// Window ladder uses.
|
|
555
|
+
const limit = lookupModelLimitSync(model, reg?.provider || null);
|
|
556
|
+
const ctx = overrideCtx ?? (limit && Number.isFinite(limit.context) && limit.context > 0 ? limit.context : undefined);
|
|
557
|
+
const max = overrideMax ?? (limit && Number.isFinite(limit.output) && limit.output > 0 ? limit.output : undefined);
|
|
504
558
|
|
|
505
559
|
// If we have literally no information, return undefined
|
|
506
560
|
if (!reg && ctx === undefined && max === undefined) return undefined;
|
package/yeaft/sessions/ids.js
CHANGED
|
@@ -40,13 +40,13 @@ export function nextMsgId() {
|
|
|
40
40
|
export function nextSessionId(slug = 'default') {
|
|
41
41
|
// Slug-tolerant: lowercase a-z0-9_- only, capped at 24 chars so the
|
|
42
42
|
// total id stays compact after the suffix is appended.
|
|
43
|
-
const safe = String(slug).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').slice(0, 24) || '
|
|
43
|
+
const safe = String(slug).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').slice(0, 24) || 'session';
|
|
44
44
|
// Append 8 crockford-base32 chars (~40 bits) so re-creating a session
|
|
45
45
|
// with the same display name yields a fresh id instead of throwing
|
|
46
46
|
// `duplicate` on the existsSync check in session-crud.js. The
|
|
47
47
|
// `duplicate` branch is now a true defensive guard rather than the
|
|
48
48
|
// first-collision footgun it used to be.
|
|
49
|
-
return `
|
|
49
|
+
return `session_${safe}_${randEncoded(8)}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* seed-default.js — First-boot default
|
|
2
|
+
* seed-default.js — First-boot default session (architecture §10 D1).
|
|
3
3
|
*
|
|
4
|
-
* When multi-VP mode is first enabled for a user, seed a default
|
|
5
|
-
* the provided roster (typically `[defaultVpId]`). Idempotent: if the
|
|
4
|
+
* When multi-VP mode is first enabled for a user, seed a default session with
|
|
5
|
+
* the provided roster (typically `[defaultVpId]`). Idempotent: if the session
|
|
6
6
|
* already exists on disk, returns the existing handle without overwriting.
|
|
7
7
|
*
|
|
8
8
|
* Separation from group-store.createSession:
|
|
9
9
|
* - createSession throws on duplicate; seed returns the existing handle.
|
|
10
|
-
* - seed picks a stable id `
|
|
11
|
-
* - seed is the only place that writes the "default
|
|
10
|
+
* - seed picks a stable id `session_default` so UI can deep-link to it.
|
|
11
|
+
* - seed is the only place that writes the "default session exists" side
|
|
12
12
|
* effect during the bootstrap flow.
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -18,20 +18,20 @@ import { homedir } from 'os';
|
|
|
18
18
|
import { openSession, createSession, loadSessionMeta } from './session-store.js';
|
|
19
19
|
import { seedSummaryIfMissingSync } from '../memory/store.js';
|
|
20
20
|
|
|
21
|
-
export const DEFAULT_SESSION_ID = '
|
|
21
|
+
export const DEFAULT_SESSION_ID = 'session_default';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Default memory root used when callers don't pass `options.memoryRoot`.
|
|
25
|
-
* See `
|
|
25
|
+
* See `sessions/session-crud.js` and `vp/vp-crud.js` for the same default;
|
|
26
26
|
* production code threads `<yeaftDir>/memory` through to keep test/prod
|
|
27
27
|
* isolation honest.
|
|
28
28
|
*/
|
|
29
29
|
const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Build the default-
|
|
32
|
+
* Build the default-session seed summary body. Pulled into a helper so
|
|
33
33
|
* tests can pin the exact format. Mirrors `buildSessionSeedSummary` in
|
|
34
|
-
* `
|
|
34
|
+
* `session-crud.js` shape, with the "Default session" wording reserved for
|
|
35
35
|
* the bootstrap path.
|
|
36
36
|
*
|
|
37
37
|
* @param {{ name?: string, roster?: string[], defaultVpId?: string|null }} spec
|
|
@@ -42,7 +42,7 @@ export function buildDefaultSessionSeedSummary(spec) {
|
|
|
42
42
|
const roster = Array.isArray(spec?.roster) ? spec.roster : [];
|
|
43
43
|
const defaultVpId = spec?.defaultVpId || null;
|
|
44
44
|
const lines = [`# ${name}`, ''];
|
|
45
|
-
lines.push(`Default
|
|
45
|
+
lines.push(`Default session with ${roster.length} member${roster.length === 1 ? '' : 's'}.`);
|
|
46
46
|
if (roster.length > 0) lines.push('', `**Members:** ${roster.join(', ')}`);
|
|
47
47
|
if (defaultVpId) lines.push('', `**Default VP:** ${defaultVpId}`);
|
|
48
48
|
return lines.join('\n').trim();
|
|
@@ -77,7 +77,7 @@ export function seedDefaultSession(yeaftDir, spec = {}) {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
// Seed Layer-A resident summary so the very first session — even on a
|
|
80
|
-
// brand-new install where only `
|
|
80
|
+
// brand-new install where only `session_default` exists — renders a non-
|
|
81
81
|
// empty memory section in the system prompt. No-op once Dream-v2 (or
|
|
82
82
|
// createSessionFromSpec) has already written one. Best-effort: a memory-
|
|
83
83
|
// root permission failure must NOT break the bootstrap flow.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* session-crud.js — High-level Session CRUD API (task-334m).
|
|
3
3
|
*
|
|
4
|
-
* Wraps the primitives from
|
|
4
|
+
* Wraps the primitives from session-store.js + roster.js into the 5 operations
|
|
5
5
|
* wired to WS events (§Δ10 334m + R6 §Δ31.2):
|
|
6
|
-
* createSessionFromSpec — wizard "create new
|
|
6
|
+
* createSessionFromSpec — wizard "create new session" (empty or user-picked roster)
|
|
7
7
|
* renameSession — update meta.name; preserves roster / defaultVpId
|
|
8
8
|
* archiveSession — rename dir to `.archived-<ts>-<id>` (soft delete)
|
|
9
9
|
* addMember — roster.addVp + save; sets defaultVpId if first
|
|
10
10
|
* removeMember — roster.removeVp + save; clears/rotates defaultVpId
|
|
11
11
|
*
|
|
12
12
|
* Plus the D1 bootstrap helper:
|
|
13
|
-
* ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO
|
|
14
|
-
* disk, seed `
|
|
13
|
+
* ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO session exists on
|
|
14
|
+
* disk, seed `session_default` with roster = every VP in the library, and
|
|
15
15
|
* defaultVpId = alphabetically first vpId. No-op when ≥1 group present.
|
|
16
16
|
*
|
|
17
17
|
* Hard constraints (PM):
|
|
@@ -250,21 +250,21 @@ export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
|
|
|
250
250
|
|
|
251
251
|
/** Build a safe group id from a display name (slug + ulid-lite suffix). */
|
|
252
252
|
export function makeSessionId(name) {
|
|
253
|
-
const slug = String(name || '
|
|
253
|
+
const slug = String(name || 'session')
|
|
254
254
|
.toLowerCase()
|
|
255
255
|
.replace(/[^a-z0-9]+/g, '-')
|
|
256
256
|
.replace(/^-+|-+$/g, '')
|
|
257
|
-
.slice(0, 24) || '
|
|
257
|
+
.slice(0, 24) || 'session';
|
|
258
258
|
return nextSessionId(slug);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* (B) D1 seed — called at boot (or when multi-VP is first enabled). Idempotent:
|
|
263
|
-
* returns `{seeded:false}` if any
|
|
264
|
-
* `
|
|
263
|
+
* returns `{seeded:false}` if any session already exists on disk (including
|
|
264
|
+
* `session_default`). When empty, seeds with roster = full VP library, sorted
|
|
265
265
|
* alphabetically; defaultVpId = roster[0].
|
|
266
266
|
*
|
|
267
|
-
* When the VP library is also empty, we still seed an empty-roster
|
|
267
|
+
* When the VP library is also empty, we still seed an empty-roster session so
|
|
268
268
|
* the UI has somewhere to land — but defaultVpId is null and downstream
|
|
269
269
|
* message send will return `no_default_vp` until the user adds a VP.
|
|
270
270
|
*/
|
|
@@ -355,7 +355,7 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
|
355
355
|
saveSessionConfig(yeaftDir, id, spec.config);
|
|
356
356
|
}
|
|
357
357
|
} catch (err) {
|
|
358
|
-
console.warn(`[
|
|
358
|
+
console.warn(`[session-crud] failed to seed config.json for ${id}:`, err?.message || err);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// Seed Layer-A resident summary so the first session has memory content
|
|
@@ -363,12 +363,12 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
|
363
363
|
// Best-effort: a memory-root permission failure must NOT break group create.
|
|
364
364
|
try {
|
|
365
365
|
seedSummaryIfMissingSync(
|
|
366
|
-
{ kind: '
|
|
366
|
+
{ kind: 'session', id },
|
|
367
367
|
buildSessionSeedSummary({ name, roster, defaultVpId }),
|
|
368
368
|
{ root: memoryRoot },
|
|
369
369
|
);
|
|
370
370
|
} catch (err) {
|
|
371
|
-
console.warn(`[
|
|
371
|
+
console.warn(`[session-crud] failed to seed summary.md for ${id}:`, err?.message || err);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
return meta;
|
|
@@ -501,9 +501,12 @@ export function deleteSession(yeaftDir, sessionId, options = {}) {
|
|
|
501
501
|
// starts clean. Best-effort — never let memory cleanup fail the CRUD op.
|
|
502
502
|
// Runs unconditionally so the idempotent path also clears stale memory.
|
|
503
503
|
try {
|
|
504
|
+
removeScopeDirSync({ kind: 'session', id: sessionId }, { root: memoryRoot });
|
|
505
|
+
// Legacy pre-session memory scopes used memory/group/<id>. Delete both so
|
|
506
|
+
// idempotent removal clears stale summaries for old grp_* sessions too.
|
|
504
507
|
removeScopeDirSync({ kind: 'group', id: sessionId }, { root: memoryRoot });
|
|
505
508
|
} catch (err) {
|
|
506
|
-
console.warn(`[
|
|
509
|
+
console.warn(`[session-crud] failed to remove memory dir for ${sessionId}:`, err?.message || err);
|
|
507
510
|
}
|
|
508
511
|
|
|
509
512
|
unregisterSessionWorkDir(yeaftDir, sessionId);
|
|
@@ -516,9 +519,9 @@ export function deleteSession(yeaftDir, sessionId, options = {}) {
|
|
|
516
519
|
}
|
|
517
520
|
|
|
518
521
|
/**
|
|
519
|
-
* Sweep any leftover `.archived-*` directories under
|
|
522
|
+
* Sweep any leftover `.archived-*` directories under sessions/ that are
|
|
520
523
|
* orphans of the old soft-archive flow. Used at boot so users don't see
|
|
521
|
-
* ghost
|
|
524
|
+
* ghost sessions in subsequent loads. Returns the list of removed paths.
|
|
522
525
|
*/
|
|
523
526
|
export function purgeArchivedSessions(yeaftDir) {
|
|
524
527
|
const root = sessionsRoot(yeaftDir);
|