@yeaft/webchat-agent 0.1.953 → 0.1.955
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/yeaft/engine.js +5 -4
- package/yeaft/prompts.js +32 -11
- package/yeaft/sessions/session-config.js +10 -10
- package/yeaft/sessions/session-crud.js +16 -13
- package/yeaft/sessions/session-store.js +44 -29
- package/yeaft/web-bridge.js +11 -6
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -1339,7 +1339,7 @@ export class Engine {
|
|
|
1339
1339
|
* string-prompt shape (no regression for existing callers).
|
|
1340
1340
|
* @yields {EngineEvent}
|
|
1341
1341
|
*/
|
|
1342
|
-
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
|
|
1342
|
+
async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null } = {}) {
|
|
1343
1343
|
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
1344
1344
|
yield {
|
|
1345
1345
|
type: 'error',
|
|
@@ -1402,7 +1402,7 @@ export class Engine {
|
|
|
1402
1402
|
|
|
1403
1403
|
try {
|
|
1404
1404
|
this.#currentThreadId = threadId || MAIN_THREAD_ID;
|
|
1405
|
-
yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy });
|
|
1405
|
+
yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy });
|
|
1406
1406
|
} finally {
|
|
1407
1407
|
if (signal) {
|
|
1408
1408
|
try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
|
|
@@ -1422,7 +1422,7 @@ export class Engine {
|
|
|
1422
1422
|
* in a try/finally without indenting the whole loop.
|
|
1423
1423
|
* @private
|
|
1424
1424
|
*/
|
|
1425
|
-
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null }) {
|
|
1425
|
+
async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null }) {
|
|
1426
1426
|
|
|
1427
1427
|
const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
|
|
1428
1428
|
? collabToolPolicy
|
|
@@ -1511,12 +1511,13 @@ export class Engine {
|
|
|
1511
1511
|
: [];
|
|
1512
1512
|
|
|
1513
1513
|
// ─── Active Scope (DESIGN-PROMPT §3 ④) ──────────────────────
|
|
1514
|
-
// Structured per-turn scope summary:
|
|
1514
|
+
// Structured per-turn scope summary: session + vp + members + envelope routing
|
|
1515
1515
|
// info. Long-form scope content lives in AMS — this block carries
|
|
1516
1516
|
// only IDs + tiny labels. (Feature scope retired 2026-05-13.)
|
|
1517
1517
|
const activeScope = {
|
|
1518
1518
|
sessionId: sessionId || '',
|
|
1519
1519
|
vpId: ownVpIdForAms || '',
|
|
1520
|
+
members: Array.isArray(sessionMembers) ? sessionMembers : [],
|
|
1520
1521
|
envelope: inboundEnvelope || null,
|
|
1521
1522
|
};
|
|
1522
1523
|
|
package/yeaft/prompts.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Concept layering (DESIGN-PROMPT §3):
|
|
11
11
|
* ① Identity — VP persona body (or Yeaft fallback)
|
|
12
|
-
* ② Rules —
|
|
12
|
+
* ② Rules — session announcement, date, mode template, tools,
|
|
13
13
|
* tool-guidance, skills, common rules
|
|
14
14
|
* ③ Memory — single block produced upstream by the AMS render
|
|
15
15
|
* outlet and threaded through here as `memoryInjection`
|
|
16
16
|
* ④ Active Scope — structured per-turn scope summary
|
|
17
|
-
* (
|
|
17
|
+
* (session / vp / members / envelope IDs)
|
|
18
18
|
*
|
|
19
19
|
* The compact summary, user_profile, and core_memory blocks that used to
|
|
20
20
|
* live inside the system prompt are GONE. Compact summary is now part of
|
|
@@ -256,11 +256,11 @@ export function normalizePromptLanguage(language) {
|
|
|
256
256
|
*
|
|
257
257
|
* Prompt structure (DESIGN-PROMPT §3):
|
|
258
258
|
* ① Identity — Core identity (persona or Yeaft fallback)
|
|
259
|
-
* ② Rules —
|
|
259
|
+
* ② Rules — Session announcement, date, mode, tools, guidance, skills
|
|
260
260
|
* ③ Memory — Single block produced by the AMS render outlet
|
|
261
261
|
* (callers pass it as `memoryInjection`).
|
|
262
262
|
* ④ Active Scope — Structured per-turn scope summary
|
|
263
|
-
* (
|
|
263
|
+
* (session / vp / members / envelope IDs).
|
|
264
264
|
* (The previous standalone user_profile / core_memory blocks are
|
|
265
265
|
* gone — those signals now arrive through AMS Resident. Task
|
|
266
266
|
* context (`taskCtx`) was wired into Active Scope by task-334e
|
|
@@ -270,6 +270,7 @@ export function normalizePromptLanguage(language) {
|
|
|
270
270
|
* @param {object} [activeScope] — structured scope summary for this turn
|
|
271
271
|
* @param {string} [activeScope.sessionId]
|
|
272
272
|
* @param {string} [activeScope.vpId]
|
|
273
|
+
* @param {string[]} [activeScope.members] current session roster
|
|
273
274
|
* @param {object} [activeScope.envelope] inbound routing info (sender, intent)
|
|
274
275
|
*
|
|
275
276
|
* @param {{
|
|
@@ -337,8 +338,8 @@ export function buildSystemPrompt({
|
|
|
337
338
|
parts.push(`${docHeader}\n${introLine}${docText}`);
|
|
338
339
|
}
|
|
339
340
|
|
|
340
|
-
// ─── 1.5
|
|
341
|
-
// When a
|
|
341
|
+
// ─── 1.5 Session Announcement (CLAUDE.md-style shared prefix) ───
|
|
342
|
+
// When a session has set an announcement, every VP in the session sees it
|
|
342
343
|
// near the top of the system prompt — before tools, memory, mode-specific
|
|
343
344
|
// instructions. Empty/whitespace = no block emitted.
|
|
344
345
|
const annText = (typeof sessionAnnouncement === 'string') ? sessionAnnouncement.trim() : '';
|
|
@@ -482,9 +483,10 @@ function hasCjk(text) {
|
|
|
482
483
|
*
|
|
483
484
|
* Schema:
|
|
484
485
|
* ## active_scope
|
|
485
|
-
*
|
|
486
|
-
* vp: <vpId>
|
|
487
|
-
*
|
|
486
|
+
* session: <sessionId> (omitted when missing)
|
|
487
|
+
* vp: <vpId> (omitted when missing)
|
|
488
|
+
* members: <vpId>, <vpId> (omitted when missing)
|
|
489
|
+
* envelope: from=<sender> intent=<intent> (omitted when no envelope)
|
|
488
490
|
*
|
|
489
491
|
* Returns '' when the input has no useful field — we don't emit an empty
|
|
490
492
|
* header. (`featureId`/`featureTitle` fields were removed 2026-05-13 along
|
|
@@ -493,6 +495,7 @@ function hasCjk(text) {
|
|
|
493
495
|
* @param {object} [activeScope]
|
|
494
496
|
* @param {string} [activeScope.sessionId]
|
|
495
497
|
* @param {string} [activeScope.vpId]
|
|
498
|
+
* @param {string[]} [activeScope.members] current session roster
|
|
496
499
|
* @param {object} [activeScope.envelope] inbound routing summary
|
|
497
500
|
* @param {object} lang
|
|
498
501
|
* @returns {string}
|
|
@@ -501,16 +504,19 @@ function renderActiveScope(activeScope, lang) {
|
|
|
501
504
|
if (!activeScope || typeof activeScope !== 'object') return '';
|
|
502
505
|
|
|
503
506
|
const lines = [];
|
|
504
|
-
const
|
|
507
|
+
const session = typeof activeScope.sessionId === 'string' && activeScope.sessionId.trim()
|
|
505
508
|
? activeScope.sessionId.trim()
|
|
506
509
|
: '';
|
|
507
|
-
if (
|
|
510
|
+
if (session) lines.push(`session: ${session}`);
|
|
508
511
|
|
|
509
512
|
const vp = typeof activeScope.vpId === 'string' && activeScope.vpId.trim()
|
|
510
513
|
? activeScope.vpId.trim()
|
|
511
514
|
: '';
|
|
512
515
|
if (vp) lines.push(`vp: ${vp}`);
|
|
513
516
|
|
|
517
|
+
const membersLine = renderSessionMembersLine(activeScope.members);
|
|
518
|
+
if (membersLine) lines.push(`members: ${membersLine}`);
|
|
519
|
+
|
|
514
520
|
const envLine = renderEnvelopeLine(activeScope.envelope);
|
|
515
521
|
if (envLine) lines.push(`envelope: ${envLine}`);
|
|
516
522
|
|
|
@@ -519,6 +525,21 @@ function renderActiveScope(activeScope, lang) {
|
|
|
519
525
|
return `${lang.activeScopeHeader}\n${lines.join('\n')}`;
|
|
520
526
|
}
|
|
521
527
|
|
|
528
|
+
|
|
529
|
+
function renderSessionMembersLine(members) {
|
|
530
|
+
if (!Array.isArray(members)) return '';
|
|
531
|
+
const clean = [];
|
|
532
|
+
const seen = new Set();
|
|
533
|
+
for (const member of members) {
|
|
534
|
+
if (typeof member !== 'string') continue;
|
|
535
|
+
const id = member.trim();
|
|
536
|
+
if (!id || seen.has(id)) continue;
|
|
537
|
+
seen.add(id);
|
|
538
|
+
clean.push(id);
|
|
539
|
+
}
|
|
540
|
+
return clean.join(', ');
|
|
541
|
+
}
|
|
542
|
+
|
|
522
543
|
/**
|
|
523
544
|
* Render a one-line envelope summary. Pulls the small set of routing
|
|
524
545
|
* fields we surface to the LLM (sender, intent, originating user) and
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* session-config.js — Per-session selected model override state.
|
|
3
3
|
*
|
|
4
|
-
* Each
|
|
4
|
+
* Each session may carry its header-selected model override in `config.json` at
|
|
5
5
|
* ~/.yeaft/sessions/<sessionId>/config.json
|
|
6
6
|
*
|
|
7
7
|
* v1 schema (intentionally tiny — extend via additive keys only):
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* "model": "my-proxy/claude-sonnet-4-20250514" // optional
|
|
10
10
|
* }
|
|
11
11
|
*
|
|
12
|
-
* Missing file →
|
|
12
|
+
* Missing file or `{}` → no session-level override. Missing field → fall back to user-level
|
|
13
13
|
* config (`~/.yeaft/config.json` via loadConfig()). Resolution is a
|
|
14
14
|
* shallow overlay for send-time effective config.
|
|
15
15
|
*
|
|
@@ -25,7 +25,7 @@ import { sessionsRoot, resolveSessionYeaftDir } from './session-crud.js';
|
|
|
25
25
|
|
|
26
26
|
const CONFIG_FILE = 'config.json';
|
|
27
27
|
|
|
28
|
-
/** Whitelist of persisted
|
|
28
|
+
/** Whitelist of persisted session model-override fields. Reject everything else. */
|
|
29
29
|
const ALLOWED_KEYS = new Set(['model']);
|
|
30
30
|
|
|
31
31
|
export class SessionConfigError extends Error {
|
|
@@ -37,18 +37,18 @@ export class SessionConfigError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Resolve the on-disk path for a
|
|
41
|
-
* per-
|
|
42
|
-
* keep their config alongside the
|
|
40
|
+
* Resolve the on-disk path for a session's config.json. Honours the
|
|
41
|
+
* per-session workDir registry so sessions bound to a project directory
|
|
42
|
+
* keep their config alongside the session metadata.
|
|
43
43
|
*/
|
|
44
44
|
export function sessionConfigPath(yeaftDir, sessionId) {
|
|
45
45
|
if (!yeaftDir) return null;
|
|
46
|
-
const
|
|
47
|
-
return join(sessionsRoot(
|
|
46
|
+
const sessionYeaftDir = resolveSessionYeaftDir(yeaftDir, sessionId);
|
|
47
|
+
return join(sessionsRoot(sessionYeaftDir), sessionId, CONFIG_FILE);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Read a
|
|
51
|
+
* Read a session's config.json. Returns `{}` when the file is missing or
|
|
52
52
|
* corrupt — callers fall back to user-level defaults via
|
|
53
53
|
* `resolveSessionConfig`. We never auto-write on read.
|
|
54
54
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Plus the D1 bootstrap helper:
|
|
13
13
|
* ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO session exists on
|
|
14
14
|
* disk, seed `session_default` with roster = every VP in the library, and
|
|
15
|
-
* defaultVpId = alphabetically first vpId. No-op when ≥1
|
|
15
|
+
* defaultVpId = alphabetically first vpId. No-op when ≥1 session present.
|
|
16
16
|
*
|
|
17
17
|
* Hard constraints (PM):
|
|
18
18
|
* (a) We don't touch 334o storage primitives (storage/index.js) — we call
|
|
@@ -202,13 +202,15 @@ export function scanWorkdirSessions(workDir) {
|
|
|
202
202
|
* Register `(sessionId, workDir)` in the central registry so the next
|
|
203
203
|
* `snapshotSessions()` includes this session.
|
|
204
204
|
*
|
|
205
|
-
* Validates that `<workDir>/.yeaft/sessions/<sessionId>/
|
|
206
|
-
* and is parseable.
|
|
205
|
+
* Validates that `<workDir>/.yeaft/sessions/<sessionId>/session.json`
|
|
206
|
+
* exists and is parseable, with legacy `group.json` as a read fallback.
|
|
207
|
+
* Throws:
|
|
207
208
|
* - `not_found` — the session dir is not on disk at this workdir
|
|
208
|
-
* - `corrupt_meta` — the dir exists but
|
|
209
|
-
* / can't be parsed. Surfaced as a distinct
|
|
210
|
-
* UI can tell the user "the file is broken"
|
|
211
|
-
* "you picked the wrong workdir" (review
|
|
209
|
+
* - `corrupt_meta` — the dir exists but session metadata is missing /
|
|
210
|
+
* unreadable / can't be parsed. Surfaced as a distinct
|
|
211
|
+
* code so the UI can tell the user "the file is broken"
|
|
212
|
+
* instead of "you picked the wrong workdir" (review
|
|
213
|
+
* finding I1).
|
|
212
214
|
*
|
|
213
215
|
* Idempotent: if the same `(sessionId, workDir)` is already registered,
|
|
214
216
|
* we still rewrite the entry (with the normalized path) and return the
|
|
@@ -227,7 +229,7 @@ export function restoreSessionToRegistry(defaultYeaftDir, sessionId, workDir) {
|
|
|
227
229
|
const dir = join(sessionsRoot(groupYeaftDir), sessionId);
|
|
228
230
|
if (!existsSync(dir)) throw new SessionCrudError('not_found', sessionId);
|
|
229
231
|
const meta = loadSessionMeta(dir);
|
|
230
|
-
if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `
|
|
232
|
+
if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `session metadata missing or unreadable at ${dir} (expected session.json or legacy group.json)`);
|
|
231
233
|
registerSessionWorkDir(defaultYeaftDir, sessionId, normalized);
|
|
232
234
|
return { ...meta, workDir: normalized };
|
|
233
235
|
}
|
|
@@ -344,11 +346,12 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
|
|
|
344
346
|
handle.close();
|
|
345
347
|
if (normalizedWorkDir) registerSessionWorkDir(yeaftDir, id, normalizedWorkDir);
|
|
346
348
|
|
|
347
|
-
// Per-
|
|
348
|
-
// config.json so
|
|
349
|
-
//
|
|
350
|
-
// wizard spec (currently just `config.model`)
|
|
351
|
-
// the engine cache picks them up on the very first
|
|
349
|
+
// Per-session config (v1: model only). We always create an empty
|
|
350
|
+
// config.json so hand-editing tools can find a session-level override
|
|
351
|
+
// stub. An empty object means "no session override; use global config".
|
|
352
|
+
// Initial overrides from the wizard spec (currently just `config.model`)
|
|
353
|
+
// are persisted here so the engine cache picks them up on the very first
|
|
354
|
+
// turn.
|
|
352
355
|
try {
|
|
353
356
|
ensureSessionConfigFile(yeaftDir, id);
|
|
354
357
|
if (spec && spec.config && typeof spec.config === 'object') {
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* session-store.js — Per-session persistent store for task-334b.
|
|
3
3
|
*
|
|
4
4
|
* Layout (see architecture §2):
|
|
5
|
-
* ~/.yeaft/sessions/<
|
|
6
|
-
*
|
|
5
|
+
* ~/.yeaft/sessions/<session-id>/
|
|
6
|
+
* session.json # { id, name, roster: [vpId...], defaultVpId, createdAt }
|
|
7
7
|
* messages/ # JSONL size-rotation log (334o openLog)
|
|
8
8
|
* 000001.jsonl
|
|
9
9
|
* index.json
|
|
10
10
|
* tasks/ # populated by 334n — reserved here
|
|
11
11
|
* vps/ # populated by 334c RoleInstance runtime — reserved here
|
|
12
12
|
*
|
|
13
|
-
* This module owns only the
|
|
14
|
-
* logic lives in roster.js so coordinator and
|
|
13
|
+
* This module owns only the session.json + messages/ log. Roster mutation
|
|
14
|
+
* logic lives in roster.js so coordinator and session-store both compose it.
|
|
15
|
+
* Legacy `group.json` is read as a compatibility alias for sessions created
|
|
16
|
+
* before the storage terminology was fixed; new writes are always session.json.
|
|
15
17
|
*
|
|
16
18
|
* Hard constraint: the store does not parse @-mentions, does not dispatch,
|
|
17
19
|
* and has no knowledge of VP/RoleInstance. It is pure persistence over 334o.
|
|
@@ -28,15 +30,16 @@ import { join } from 'path';
|
|
|
28
30
|
import { writeAtomic, openLog } from '../storage/index.js';
|
|
29
31
|
import { nextMsgId, isReservedVpId, ReservedVpIdError, validateVpId, InvalidVpIdError } from './ids.js';
|
|
30
32
|
|
|
31
|
-
const
|
|
33
|
+
export const SESSION_META_FILE = 'session.json';
|
|
34
|
+
export const LEGACY_GROUP_META_FILE = 'group.json';
|
|
32
35
|
const MESSAGES_DIR = 'messages';
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
* Load (or create) the directory for a single
|
|
38
|
+
* Load (or create) the directory for a single session.
|
|
36
39
|
*
|
|
37
|
-
* @param {string} sessionsRoot e.g. `${yeaftDir}/
|
|
40
|
+
* @param {string} sessionsRoot e.g. `${yeaftDir}/sessions`
|
|
38
41
|
* @param {string} sessionId
|
|
39
|
-
* @returns {
|
|
42
|
+
* @returns {SessionHandle}
|
|
40
43
|
*/
|
|
41
44
|
export function openSession(sessionsRoot, sessionId) {
|
|
42
45
|
if (!sessionId || typeof sessionId !== 'string') {
|
|
@@ -47,7 +50,7 @@ export function openSession(sessionsRoot, sessionId) {
|
|
|
47
50
|
|
|
48
51
|
let meta = loadSessionMeta(dir);
|
|
49
52
|
if (!meta) {
|
|
50
|
-
// Fresh
|
|
53
|
+
// Fresh session — caller must call createSession() next; we return a handle
|
|
51
54
|
// with meta=null so createSession() can write the initial file.
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -60,14 +63,14 @@ export function openSession(sessionsRoot, sessionId) {
|
|
|
60
63
|
id: sessionId,
|
|
61
64
|
/** Return current meta (reads fresh from memory after last save). */
|
|
62
65
|
getMeta() { return meta ? structuredClone(meta) : null; },
|
|
63
|
-
/** Overwrite
|
|
66
|
+
/** Overwrite session.json atomically. */
|
|
64
67
|
saveMeta(next) {
|
|
65
68
|
validateMeta(next);
|
|
66
69
|
meta = next;
|
|
67
|
-
writeAtomic(join(dir,
|
|
70
|
+
writeAtomic(join(dir, SESSION_META_FILE), JSON.stringify(meta, null, 2));
|
|
68
71
|
},
|
|
69
72
|
/**
|
|
70
|
-
* Append a message to the
|
|
73
|
+
* Append a message to the session log. Assigns an id if absent.
|
|
71
74
|
* Returns the stored record (with id + ts).
|
|
72
75
|
*
|
|
73
76
|
* Structural invariant: NO field on `record` may start with `_`.
|
|
@@ -113,14 +116,14 @@ export function openSession(sessionsRoot, sessionId) {
|
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
/**
|
|
116
|
-
* Create a fresh
|
|
117
|
-
* @returns {
|
|
119
|
+
* Create a fresh session on disk. Fails if session metadata already exists.
|
|
120
|
+
* @returns {SessionHandle}
|
|
118
121
|
*/
|
|
119
122
|
export function createSession(sessionsRoot, spec) {
|
|
120
123
|
if (!spec || !spec.id) throw new Error('createSession: spec.id required');
|
|
121
124
|
const h = openSession(sessionsRoot, spec.id);
|
|
122
125
|
if (h.getMeta()) {
|
|
123
|
-
throw new Error(`
|
|
126
|
+
throw new Error(`session ${spec.id} already exists`);
|
|
124
127
|
}
|
|
125
128
|
const roster = Array.isArray(spec.roster) ? spec.roster.slice() : [];
|
|
126
129
|
for (const v of roster) {
|
|
@@ -146,15 +149,19 @@ export function createSession(sessionsRoot, spec) {
|
|
|
146
149
|
return h;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Non-destructive load — returns null if session metadata is missing/corrupt.
|
|
154
|
+
* Reads canonical session.json first, then legacy group.json for disk
|
|
155
|
+
* compatibility. Callers that later save the handle will write session.json.
|
|
156
|
+
*/
|
|
150
157
|
export function loadSessionMeta(dir) {
|
|
151
|
-
const path =
|
|
152
|
-
if (!
|
|
158
|
+
const path = resolveSessionMetaPath(dir);
|
|
159
|
+
if (!path) return null;
|
|
153
160
|
try {
|
|
154
161
|
const raw = readFileSync(path, 'utf8');
|
|
155
162
|
const parsed = JSON.parse(raw);
|
|
156
163
|
validateMeta(parsed);
|
|
157
|
-
// Legacy
|
|
164
|
+
// Legacy sessions created before optional fields were added are
|
|
158
165
|
// forward-compat: missing fields read back as safe empty strings.
|
|
159
166
|
if (typeof parsed.announcement !== 'string') parsed.announcement = '';
|
|
160
167
|
if (typeof parsed.workDir !== 'string') parsed.workDir = '';
|
|
@@ -164,7 +171,7 @@ export function loadSessionMeta(dir) {
|
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
/** List every
|
|
174
|
+
/** List every session directory under `sessionsRoot`. */
|
|
168
175
|
export function listSessions(sessionsRoot) {
|
|
169
176
|
if (!existsSync(sessionsRoot)) return [];
|
|
170
177
|
const out = [];
|
|
@@ -182,25 +189,25 @@ export function listSessions(sessionsRoot) {
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
function validateMeta(meta) {
|
|
185
|
-
if (!meta || typeof meta !== 'object') throw new Error('
|
|
186
|
-
if (!meta.id || typeof meta.id !== 'string') throw new Error('
|
|
187
|
-
if (!Array.isArray(meta.roster)) throw new Error('
|
|
192
|
+
if (!meta || typeof meta !== 'object') throw new Error('session.json must be object');
|
|
193
|
+
if (!meta.id || typeof meta.id !== 'string') throw new Error('session.id required');
|
|
194
|
+
if (!Array.isArray(meta.roster)) throw new Error('session.roster must be array');
|
|
188
195
|
for (const v of meta.roster) {
|
|
189
|
-
if (typeof v !== 'string') throw new Error('
|
|
196
|
+
if (typeof v !== 'string') throw new Error('session.roster must be string[]');
|
|
190
197
|
}
|
|
191
198
|
if (meta.defaultVpId != null && typeof meta.defaultVpId !== 'string') {
|
|
192
|
-
throw new Error('
|
|
199
|
+
throw new Error('session.defaultVpId must be string|null');
|
|
193
200
|
}
|
|
194
201
|
if (meta.announcement != null && typeof meta.announcement !== 'string') {
|
|
195
|
-
throw new Error('
|
|
202
|
+
throw new Error('session.announcement must be string');
|
|
196
203
|
}
|
|
197
204
|
if (meta.workDir != null && typeof meta.workDir !== 'string') {
|
|
198
|
-
throw new Error('
|
|
205
|
+
throw new Error('session.workDir must be string');
|
|
199
206
|
}
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
/**
|
|
203
|
-
* @typedef {Object}
|
|
210
|
+
* @typedef {Object} SessionHandle
|
|
204
211
|
* @property {string} dir
|
|
205
212
|
* @property {string} id
|
|
206
213
|
* @property {() => any} getMeta
|
|
@@ -210,3 +217,11 @@ function validateMeta(meta) {
|
|
|
210
217
|
* @property {(first:string,last:string)=>Generator<any>} readMessageRange
|
|
211
218
|
* @property {() => void} close
|
|
212
219
|
*/
|
|
220
|
+
|
|
221
|
+
function resolveSessionMetaPath(dir) {
|
|
222
|
+
const canonical = join(dir, SESSION_META_FILE);
|
|
223
|
+
if (existsSync(canonical)) return canonical;
|
|
224
|
+
const legacy = join(dir, LEGACY_GROUP_META_FILE);
|
|
225
|
+
if (existsSync(legacy)) return legacy;
|
|
226
|
+
return null;
|
|
227
|
+
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -2619,10 +2619,10 @@ function resolveCollabToolPolicy(sessionMeta) {
|
|
|
2619
2619
|
}
|
|
2620
2620
|
|
|
2621
2621
|
export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope, threadId = 'main' }) {
|
|
2622
|
-
// Read the
|
|
2623
|
-
// announcement injection.
|
|
2624
|
-
//
|
|
2625
|
-
//
|
|
2622
|
+
// Read the session meta once and reuse for defaultVpId fallback,
|
|
2623
|
+
// announcement injection, and roster prompt context. Calling getMeta()
|
|
2624
|
+
// twice per turn is wasteful — and (more importantly) opens a window
|
|
2625
|
+
// where a concurrent session edit
|
|
2626
2626
|
// could land between the two reads, giving the engine a defaultVpId
|
|
2627
2627
|
// from one snapshot and an announcement from a newer one.
|
|
2628
2628
|
let sessionMeta = null;
|
|
@@ -2660,13 +2660,18 @@ export function buildVpQueryOpts({ vpId, sessionCoordinator, sessionId, envelope
|
|
|
2660
2660
|
}
|
|
2661
2661
|
const collabToolPolicy = resolveCollabToolPolicy(sessionMeta);
|
|
2662
2662
|
if (collabToolPolicy) out.collabToolPolicy = collabToolPolicy;
|
|
2663
|
-
|
|
2663
|
+
if (sessionMeta && Array.isArray(sessionMeta.roster)) {
|
|
2664
|
+
out.sessionMembers = sessionMeta.roster
|
|
2665
|
+
.filter(v => typeof v === 'string' && v.trim())
|
|
2666
|
+
.map(v => v.trim());
|
|
2667
|
+
}
|
|
2668
|
+
// task-334-session-editor: surface the session announcement to the engine so
|
|
2664
2669
|
// buildWorkerPrompt can inject it as a CLAUDE.md-style shared prefix.
|
|
2665
2670
|
// Empty/missing reads as '' and prompts.js skips the section.
|
|
2666
2671
|
if (sessionMeta && typeof sessionMeta.announcement === 'string') {
|
|
2667
2672
|
out.sessionAnnouncement = sessionMeta.announcement;
|
|
2668
2673
|
}
|
|
2669
|
-
// Surface the
|
|
2674
|
+
// Surface the session's configured working directory so the engine can
|
|
2670
2675
|
// resolve CLAUDE.md / AGENTS.md at that path and inject it as a
|
|
2671
2676
|
// [Project Doc] block above the announcement. Groups with no workDir
|
|
2672
2677
|
// skip the block silently (matches the announcement contract).
|