@yeaft/webchat-agent 0.1.873 → 0.1.875
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/connection/message-router.js +20 -13
- package/package.json +1 -1
- package/providers/copilot-models.js +105 -89
- package/providers/copilot.js +3 -1
- package/yeaft/attachments.js +2 -2
- package/yeaft/cli.js +13 -13
- package/yeaft/compact/compactor.js +20 -20
- package/yeaft/conversation/persist.js +95 -95
- package/yeaft/debug-trace.js +12 -12
- package/yeaft/dream-v2/apply.js +15 -15
- package/yeaft/dream-v2/merge.js +12 -12
- package/yeaft/dream-v2/prompts/{extract-group.md → extract-session.md} +12 -12
- package/yeaft/dream-v2/prompts/index.js +3 -3
- package/yeaft/dream-v2/prompts/triage-pass1.md +1 -1
- package/yeaft/dream-v2/runner.js +37 -37
- package/yeaft/dream-v2/segment.js +3 -3
- package/yeaft/dream-v2/session-wiring.js +22 -22
- package/yeaft/dream-v2/state.js +7 -7
- package/yeaft/dream-v2/triage.js +22 -22
- package/yeaft/engine.js +65 -65
- package/yeaft/memory/ams-registry.js +22 -22
- package/yeaft/memory/seed-backfill.js +9 -9
- package/yeaft/memory/store-v2.js +27 -27
- package/yeaft/prompts.js +9 -9
- package/yeaft/routing/loop-guard.js +14 -14
- package/yeaft/routing/router.js +5 -5
- package/yeaft/session.js +5 -5
- package/yeaft/sessions/coordinator.js +96 -20
- package/yeaft/{groups → sessions}/ids.js +3 -3
- package/yeaft/{groups → sessions}/index.js +28 -28
- package/yeaft/sessions/pre-flow.js +178 -42
- package/yeaft/{groups → sessions}/seed-default.js +19 -19
- package/yeaft/{groups/group-config.js → sessions/session-config.js} +29 -29
- package/yeaft/{groups/group-crud.js → sessions/session-crud.js} +113 -113
- package/yeaft/sessions/session-store.js +85 -154
- package/yeaft/stop-hooks.js +4 -4
- package/yeaft/tools/todo-write.js +1 -1
- package/yeaft/tools/types.js +2 -2
- package/yeaft/vp/registry.js +1 -1
- package/yeaft/vp/vp-crud.js +1 -1
- package/yeaft/vp-status-broker.js +28 -28
- package/yeaft/web-bridge.js +411 -398
- package/yeaft/groups/coordinator.js +0 -221
- package/yeaft/groups/group-store.js +0 -212
- package/yeaft/groups/pre-flow.js +0 -329
- /package/yeaft/{groups → sessions}/feature-flag.js +0 -0
- /package/yeaft/{groups → sessions}/project-doc.js +0 -0
- /package/yeaft/{groups → sessions}/roster.js +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Persistence is identity-only:
|
|
11
11
|
*
|
|
12
|
-
* ~/.yeaft/memory/groups/<
|
|
12
|
+
* ~/.yeaft/memory/groups/<sessionId>/ams.json
|
|
13
13
|
* {
|
|
14
14
|
* "version": 1,
|
|
15
15
|
* "ownVpId": "alice"|null,
|
|
@@ -55,9 +55,9 @@ export const DEFAULT_GROUP_KEY = 'default';
|
|
|
55
55
|
* Group-keyed in-memory cache + disk persistence for AMS instances.
|
|
56
56
|
*
|
|
57
57
|
* Lifecycle:
|
|
58
|
-
* - getOrCreate(
|
|
58
|
+
* - getOrCreate(sessionId, {ownVpId}) — returns the cached AMS or loads
|
|
59
59
|
* from disk; falls through to a fresh empty AMS on cold start.
|
|
60
|
-
* - persist(
|
|
60
|
+
* - persist(sessionId) — writes the current cached AMS to disk.
|
|
61
61
|
* - persistAll() — convenience for shutdown.
|
|
62
62
|
*
|
|
63
63
|
* The registry is intentionally narrow: it does not mutate the AMS
|
|
@@ -78,15 +78,15 @@ export class AmsRegistry {
|
|
|
78
78
|
/**
|
|
79
79
|
* Resolve the on-disk path for a group's ams.json.
|
|
80
80
|
*
|
|
81
|
-
* `
|
|
81
|
+
* `sessionId` is trusted: `nextSessionId()` (groups/ids.js) emits ids matching
|
|
82
82
|
* `grp_[a-z0-9_-]+`, and the single-VP path uses the literal
|
|
83
83
|
* `DEFAULT_GROUP_KEY`. No defensive escaping is needed.
|
|
84
84
|
*
|
|
85
|
-
* @param {string}
|
|
85
|
+
* @param {string} sessionId
|
|
86
86
|
* @returns {string}
|
|
87
87
|
*/
|
|
88
|
-
amsPath(
|
|
89
|
-
const key = String(
|
|
88
|
+
amsPath(sessionId) {
|
|
89
|
+
const key = String(sessionId || DEFAULT_GROUP_KEY);
|
|
90
90
|
return join(this.yeaftDir, 'memory', 'groups', key, 'ams.json');
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -107,12 +107,12 @@ export class AmsRegistry {
|
|
|
107
107
|
* Loads persisted state from disk if any; on cold start returns an
|
|
108
108
|
* empty AMS keyed to the supplied ownVpId.
|
|
109
109
|
*
|
|
110
|
-
* @param {string|null|undefined}
|
|
110
|
+
* @param {string|null|undefined} sessionId
|
|
111
111
|
* @param {{ ownVpId?: string|null }} [opts]
|
|
112
112
|
* @returns {ActiveMemorySet}
|
|
113
113
|
*/
|
|
114
|
-
getOrCreate(
|
|
115
|
-
const key =
|
|
114
|
+
getOrCreate(sessionId, opts = {}) {
|
|
115
|
+
const key = sessionId || DEFAULT_GROUP_KEY;
|
|
116
116
|
const cached = this._cache.get(key);
|
|
117
117
|
if (cached) return cached.ams;
|
|
118
118
|
|
|
@@ -131,11 +131,11 @@ export class AmsRegistry {
|
|
|
131
131
|
* group. Engine consults this on first AMS access so a reactivated group
|
|
132
132
|
* doesn't re-run `runAdjust` on its first turn back online.
|
|
133
133
|
*
|
|
134
|
-
* @param {string|null|undefined}
|
|
134
|
+
* @param {string|null|undefined} sessionId
|
|
135
135
|
* @returns {boolean}
|
|
136
136
|
*/
|
|
137
|
-
adjustRanThisSession(
|
|
138
|
-
const key =
|
|
137
|
+
adjustRanThisSession(sessionId) {
|
|
138
|
+
const key = sessionId || DEFAULT_GROUP_KEY;
|
|
139
139
|
return this._cache.get(key)?.adjustRanThisSession === true;
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -144,11 +144,11 @@ export class AmsRegistry {
|
|
|
144
144
|
* own — call `persist()` to flush). Engine flips this true after
|
|
145
145
|
* `runAdjust` actually ran.
|
|
146
146
|
*
|
|
147
|
-
* @param {string|null|undefined}
|
|
147
|
+
* @param {string|null|undefined} sessionId
|
|
148
148
|
* @param {boolean} value
|
|
149
149
|
*/
|
|
150
|
-
setAdjustRanThisSession(
|
|
151
|
-
const key =
|
|
150
|
+
setAdjustRanThisSession(sessionId, value) {
|
|
151
|
+
const key = sessionId || DEFAULT_GROUP_KEY;
|
|
152
152
|
const entry = this._cache.get(key);
|
|
153
153
|
if (entry) entry.adjustRanThisSession = Boolean(value);
|
|
154
154
|
}
|
|
@@ -157,10 +157,10 @@ export class AmsRegistry {
|
|
|
157
157
|
* Mark a group's AMS as dirty so the next persist() actually writes.
|
|
158
158
|
* The engine calls this after `runAdjust` mutates membership.
|
|
159
159
|
*
|
|
160
|
-
* @param {string|null|undefined}
|
|
160
|
+
* @param {string|null|undefined} sessionId
|
|
161
161
|
*/
|
|
162
|
-
markDirty(
|
|
163
|
-
this._dirty.add(
|
|
162
|
+
markDirty(sessionId) {
|
|
163
|
+
this._dirty.add(sessionId || DEFAULT_GROUP_KEY);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
@@ -171,12 +171,12 @@ export class AmsRegistry {
|
|
|
171
171
|
* entry so subsequent `adjustRanThisSession()` reads see the latest flag
|
|
172
172
|
* without a round-trip through disk.
|
|
173
173
|
*
|
|
174
|
-
* @param {string|null|undefined}
|
|
174
|
+
* @param {string|null|undefined} sessionId
|
|
175
175
|
* @param {{ force?: boolean, adjustRanThisSession?: boolean }} [opts]
|
|
176
176
|
* @returns {boolean} true if the file was written
|
|
177
177
|
*/
|
|
178
|
-
persist(
|
|
179
|
-
const key =
|
|
178
|
+
persist(sessionId, opts = {}) {
|
|
179
|
+
const key = sessionId || DEFAULT_GROUP_KEY;
|
|
180
180
|
const entry = this._cache.get(key);
|
|
181
181
|
if (!entry) return false;
|
|
182
182
|
if (!opts.force && !this._dirty.has(key)) return false;
|
|
@@ -150,11 +150,11 @@ export function backfillVpSummaries({ libDir, root = DEFAULT_MEMORY_ROOT }) {
|
|
|
150
150
|
/**
|
|
151
151
|
* Build a synthetic group summary from group.json on disk.
|
|
152
152
|
*
|
|
153
|
-
* @param {string}
|
|
153
|
+
* @param {string} sessionDir
|
|
154
154
|
* @returns {string|null}
|
|
155
155
|
*/
|
|
156
|
-
function readGroupSummaryBody(
|
|
157
|
-
const metaPath = join(
|
|
156
|
+
function readGroupSummaryBody(sessionDir) {
|
|
157
|
+
const metaPath = join(sessionDir, 'group.json');
|
|
158
158
|
if (!existsSync(metaPath)) return null;
|
|
159
159
|
let meta;
|
|
160
160
|
try { meta = JSON.parse(readFileSync(metaPath, 'utf-8')); } catch { return null; }
|
|
@@ -236,20 +236,20 @@ export function migrateLegacyVpSummaries({ libDir, root = DEFAULT_MEMORY_ROOT })
|
|
|
236
236
|
export function backfillGroupSummaries({ yeaftDir, root = DEFAULT_MEMORY_ROOT }) {
|
|
237
237
|
let scanned = 0;
|
|
238
238
|
let seeded = 0;
|
|
239
|
-
const
|
|
240
|
-
if (!existsSync(
|
|
239
|
+
const sessionsRoot = join(yeaftDir, 'sessions');
|
|
240
|
+
if (!existsSync(sessionsRoot)) return { scanned, seeded };
|
|
241
241
|
let entries;
|
|
242
|
-
try { entries = readdirSync(
|
|
242
|
+
try { entries = readdirSync(sessionsRoot); } catch { return { scanned, seeded }; }
|
|
243
243
|
for (const name of entries) {
|
|
244
244
|
if (name.startsWith('.')) continue;
|
|
245
|
-
const
|
|
245
|
+
const sessionDir = join(sessionsRoot, name);
|
|
246
246
|
let isDir = false;
|
|
247
|
-
try { isDir = statSync(
|
|
247
|
+
try { isDir = statSync(sessionDir).isDirectory(); } catch { /* skip */ }
|
|
248
248
|
if (!isDir) continue;
|
|
249
249
|
scanned++;
|
|
250
250
|
const summaryPath = join(root, 'group', name, 'summary.md');
|
|
251
251
|
if (readIfPresent(summaryPath)) continue;
|
|
252
|
-
const body = readGroupSummaryBody(
|
|
252
|
+
const body = readGroupSummaryBody(sessionDir);
|
|
253
253
|
if (!body) continue;
|
|
254
254
|
try {
|
|
255
255
|
writeAtomicSync(summaryPath, body);
|
package/yeaft/memory/store-v2.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ~/.yeaft/memory/
|
|
9
9
|
* user/ memory.md summary.md
|
|
10
10
|
* vp/<vpId>/ memory.md summary.md
|
|
11
|
-
* group/<
|
|
11
|
+
* group/<sessionId>/ memory.md summary.md
|
|
12
12
|
* feature/<featureId>/ memory.md summary.md
|
|
13
13
|
* topic/<l1>[/<l2>]/ memory.md summary.md (≤ 2 levels)
|
|
14
14
|
*
|
|
@@ -74,7 +74,7 @@ export const SCOPE_KINDS = Object.freeze([
|
|
|
74
74
|
* @typedef {Object} Scope
|
|
75
75
|
* @property {ScopeKind} kind
|
|
76
76
|
* @property {string} [id] — required for group; for group-vp / group-feature the per-kind id
|
|
77
|
-
* @property {string} [
|
|
77
|
+
* @property {string} [sessionId] — required for every group-* kind
|
|
78
78
|
* @property {string[]} [path] — required for group-topic; 1–2 segments
|
|
79
79
|
*/
|
|
80
80
|
|
|
@@ -98,33 +98,33 @@ export function scopeDir(scope) {
|
|
|
98
98
|
assertSafeSegment(scope.id, 'group.id');
|
|
99
99
|
return `group/${scope.id}`;
|
|
100
100
|
case 'group-user': {
|
|
101
|
-
if (!scope.
|
|
102
|
-
assertSafeSegment(scope.
|
|
103
|
-
return `group/${scope.
|
|
101
|
+
if (!scope.sessionId) throw new Error('scopeDir: group-user scope requires sessionId');
|
|
102
|
+
assertSafeSegment(scope.sessionId, 'group-user.sessionId');
|
|
103
|
+
return `group/${scope.sessionId}/user`;
|
|
104
104
|
}
|
|
105
105
|
case 'group-vp': {
|
|
106
|
-
if (!scope.
|
|
106
|
+
if (!scope.sessionId) throw new Error('scopeDir: group-vp scope requires sessionId');
|
|
107
107
|
if (!scope.id) throw new Error('scopeDir: group-vp scope requires id');
|
|
108
|
-
assertSafeSegment(scope.
|
|
108
|
+
assertSafeSegment(scope.sessionId, 'group-vp.sessionId');
|
|
109
109
|
assertSafeSegment(scope.id, 'group-vp.id');
|
|
110
|
-
return `group/${scope.
|
|
110
|
+
return `group/${scope.sessionId}/vp/${scope.id}`;
|
|
111
111
|
}
|
|
112
112
|
case 'group-feature': {
|
|
113
|
-
if (!scope.
|
|
113
|
+
if (!scope.sessionId) throw new Error('scopeDir: group-feature scope requires sessionId');
|
|
114
114
|
if (!scope.id) throw new Error('scopeDir: group-feature scope requires id');
|
|
115
|
-
assertSafeSegment(scope.
|
|
115
|
+
assertSafeSegment(scope.sessionId, 'group-feature.sessionId');
|
|
116
116
|
assertSafeSegment(scope.id, 'group-feature.id');
|
|
117
|
-
return `group/${scope.
|
|
117
|
+
return `group/${scope.sessionId}/feature/${scope.id}`;
|
|
118
118
|
}
|
|
119
119
|
case 'group-topic': {
|
|
120
|
-
if (!scope.
|
|
121
|
-
assertSafeSegment(scope.
|
|
120
|
+
if (!scope.sessionId) throw new Error('scopeDir: group-topic scope requires sessionId');
|
|
121
|
+
assertSafeSegment(scope.sessionId, 'group-topic.sessionId');
|
|
122
122
|
const segs = Array.isArray(scope.path) ? scope.path : [];
|
|
123
123
|
if (segs.length === 0 || segs.length > 2) {
|
|
124
124
|
throw new Error('scopeDir: group-topic.path must have 1 or 2 segments');
|
|
125
125
|
}
|
|
126
126
|
for (const s of segs) assertSafeSegment(s, 'group-topic.path');
|
|
127
|
-
return `group/${scope.
|
|
127
|
+
return `group/${scope.sessionId}/topic/${segs.join('/')}`;
|
|
128
128
|
}
|
|
129
129
|
case 'chat': {
|
|
130
130
|
if (!scope.id) throw new Error('scopeDir: chat scope requires id');
|
|
@@ -190,7 +190,7 @@ function assertSafeSegment(s, ctx) {
|
|
|
190
190
|
*/
|
|
191
191
|
export function isValidTopic(scope) {
|
|
192
192
|
if (!scope || scope.kind !== 'group-topic') return false;
|
|
193
|
-
if (!scope.
|
|
193
|
+
if (!scope.sessionId || typeof scope.sessionId !== 'string') return false;
|
|
194
194
|
if (!Array.isArray(scope.path)) return false;
|
|
195
195
|
if (scope.path.length < 1 || scope.path.length > 2) return false;
|
|
196
196
|
for (const s of scope.path) {
|
|
@@ -412,7 +412,7 @@ export function seedSummaryIfMissingSync(scope, body, opts = {}) {
|
|
|
412
412
|
|
|
413
413
|
/**
|
|
414
414
|
* Synchronously remove a scope's directory under the memory root. Used by
|
|
415
|
-
* `deleteVp` / `
|
|
415
|
+
* `deleteVp` / `deleteSession` to cascade memory cleanup so a recreate of the
|
|
416
416
|
* same id doesn't see stale `summary.md` / `memory.md` / `segments/` files.
|
|
417
417
|
*
|
|
418
418
|
* Idempotent — missing directory is a no-op.
|
|
@@ -460,10 +460,10 @@ export async function ensureScope(scope, opts = {}) {
|
|
|
460
460
|
* Walks shallowly:
|
|
461
461
|
* user/ → { kind: 'user' }
|
|
462
462
|
* group/<g>/ → { kind: 'group', id: g }
|
|
463
|
-
* group/<g>/user/ → { kind: 'group-user',
|
|
464
|
-
* group/<g>/vp/<v>/ → { kind: 'group-vp',
|
|
465
|
-
* group/<g>/feature/<f>/ → { kind: 'group-feature',
|
|
466
|
-
* group/<g>/topic/<l1>[/<l2>]/ → { kind: 'group-topic',
|
|
463
|
+
* group/<g>/user/ → { kind: 'group-user', sessionId: g }
|
|
464
|
+
* group/<g>/vp/<v>/ → { kind: 'group-vp', sessionId: g, id: v }
|
|
465
|
+
* group/<g>/feature/<f>/ → { kind: 'group-feature', sessionId: g, id: f }
|
|
466
|
+
* group/<g>/topic/<l1>[/<l2>]/ → { kind: 'group-topic', sessionId: g, path: [...] }
|
|
467
467
|
*
|
|
468
468
|
* Skips `.legacy/` and any dotfile / unsafe segment.
|
|
469
469
|
*
|
|
@@ -478,13 +478,13 @@ export async function listScopes(opts = {}) {
|
|
|
478
478
|
// user/
|
|
479
479
|
if (existsSync(join(root, 'user'))) out.push({ kind: 'user' });
|
|
480
480
|
|
|
481
|
-
// group/<g>/...
|
|
481
|
+
// group/<g>/... (LEGACY scope tree; kept for un-migrated data)
|
|
482
482
|
const groupRoot = join(root, 'group');
|
|
483
483
|
let groups;
|
|
484
484
|
try { groups = await fsp.readdir(groupRoot, { withFileTypes: true }); }
|
|
485
485
|
catch (err) {
|
|
486
|
-
if (err && err.code === 'ENOENT')
|
|
487
|
-
throw err;
|
|
486
|
+
if (err && err.code === 'ENOENT') groups = [];
|
|
487
|
+
else throw err;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
for (const gent of groups) {
|
|
@@ -497,7 +497,7 @@ export async function listScopes(opts = {}) {
|
|
|
497
497
|
|
|
498
498
|
// group/<g>/user/
|
|
499
499
|
if (existsSync(join(gAbs, 'user'))) {
|
|
500
|
-
out.push({ kind: 'group-user',
|
|
500
|
+
out.push({ kind: 'group-user', sessionId: g });
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
// group/<g>/vp/<v>/ and group/<g>/feature/<f>/
|
|
@@ -514,7 +514,7 @@ export async function listScopes(opts = {}) {
|
|
|
514
514
|
if (!isSafeId(ent.name)) continue;
|
|
515
515
|
out.push({
|
|
516
516
|
kind: kind === 'vp' ? 'group-vp' : 'group-feature',
|
|
517
|
-
|
|
517
|
+
sessionId: g,
|
|
518
518
|
id: ent.name,
|
|
519
519
|
});
|
|
520
520
|
}
|
|
@@ -540,14 +540,14 @@ export async function listScopes(opts = {}) {
|
|
|
540
540
|
for (const l2ent of l2s) {
|
|
541
541
|
if (!l2ent.isDirectory()) continue;
|
|
542
542
|
if (!isSafeId(l2ent.name)) continue;
|
|
543
|
-
out.push({ kind: 'group-topic',
|
|
543
|
+
out.push({ kind: 'group-topic', sessionId: g, path: [l1, l2ent.name] });
|
|
544
544
|
hasL2 = true;
|
|
545
545
|
}
|
|
546
546
|
if (!hasL2) {
|
|
547
547
|
const hasMemory = existsSync(join(l1abs, 'memory.md'));
|
|
548
548
|
const hasSummary = existsSync(join(l1abs, 'summary.md'));
|
|
549
549
|
if (hasMemory || hasSummary) {
|
|
550
|
-
out.push({ kind: 'group-topic',
|
|
550
|
+
out.push({ kind: 'group-topic', sessionId: g, path: [l1] });
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
}
|
package/yeaft/prompts.js
CHANGED
|
@@ -268,7 +268,7 @@ export function normalizePromptLanguage(language) {
|
|
|
268
268
|
*
|
|
269
269
|
* Active Scope params (DESIGN-PROMPT §3 ④):
|
|
270
270
|
* @param {object} [activeScope] — structured scope summary for this turn
|
|
271
|
-
* @param {string} [activeScope.
|
|
271
|
+
* @param {string} [activeScope.sessionId]
|
|
272
272
|
* @param {string} [activeScope.vpId]
|
|
273
273
|
* @param {object} [activeScope.envelope] inbound routing info (sender, intent)
|
|
274
274
|
*
|
|
@@ -280,7 +280,7 @@ export function normalizePromptLanguage(language) {
|
|
|
280
280
|
* skillContent?: string,
|
|
281
281
|
* activeScope?: object,
|
|
282
282
|
* vpPersona?: object,
|
|
283
|
-
*
|
|
283
|
+
* sessionAnnouncement?: string,
|
|
284
284
|
* projectDoc?: string,
|
|
285
285
|
* }} params
|
|
286
286
|
* @returns {string}
|
|
@@ -293,7 +293,7 @@ export function buildSystemPrompt({
|
|
|
293
293
|
skillContent,
|
|
294
294
|
activeScope,
|
|
295
295
|
vpPersona,
|
|
296
|
-
|
|
296
|
+
sessionAnnouncement = '',
|
|
297
297
|
projectDoc = '',
|
|
298
298
|
} = {}) {
|
|
299
299
|
// Normalize app locales like `zh-CN` to prompt dictionary/template keys.
|
|
@@ -341,7 +341,7 @@ export function buildSystemPrompt({
|
|
|
341
341
|
// When a group has set an announcement, every VP in the group sees it
|
|
342
342
|
// near the top of the system prompt — before tools, memory, mode-specific
|
|
343
343
|
// instructions. Empty/whitespace = no block emitted.
|
|
344
|
-
const annText = (typeof
|
|
344
|
+
const annText = (typeof sessionAnnouncement === 'string') ? sessionAnnouncement.trim() : '';
|
|
345
345
|
if (annText) {
|
|
346
346
|
parts.push(`${lang.groupAnnouncementHeader || '[Group Announcement]'}\n${annText}`);
|
|
347
347
|
}
|
|
@@ -482,7 +482,7 @@ function hasCjk(text) {
|
|
|
482
482
|
*
|
|
483
483
|
* Schema:
|
|
484
484
|
* ## active_scope
|
|
485
|
-
* group: <
|
|
485
|
+
* group: <sessionId> (omitted when missing)
|
|
486
486
|
* vp: <vpId> (omitted when missing)
|
|
487
487
|
* envelope: from=<sender> intent=<intent> (omitted when no envelope)
|
|
488
488
|
*
|
|
@@ -491,7 +491,7 @@ function hasCjk(text) {
|
|
|
491
491
|
* with the rest of the Feature system; the JSDoc once described them.)
|
|
492
492
|
*
|
|
493
493
|
* @param {object} [activeScope]
|
|
494
|
-
* @param {string} [activeScope.
|
|
494
|
+
* @param {string} [activeScope.sessionId]
|
|
495
495
|
* @param {string} [activeScope.vpId]
|
|
496
496
|
* @param {object} [activeScope.envelope] inbound routing summary
|
|
497
497
|
* @param {object} lang
|
|
@@ -501,8 +501,8 @@ function renderActiveScope(activeScope, lang) {
|
|
|
501
501
|
if (!activeScope || typeof activeScope !== 'object') return '';
|
|
502
502
|
|
|
503
503
|
const lines = [];
|
|
504
|
-
const group = typeof activeScope.
|
|
505
|
-
? activeScope.
|
|
504
|
+
const group = typeof activeScope.sessionId === 'string' && activeScope.sessionId.trim()
|
|
505
|
+
? activeScope.sessionId.trim()
|
|
506
506
|
: '';
|
|
507
507
|
if (group) lines.push(`group: ${group}`);
|
|
508
508
|
|
|
@@ -615,7 +615,7 @@ export function renderLayerASummaries(summaries, language = 'en') {
|
|
|
615
615
|
* Earlier task-322 / task-334e variants accepted `taskScope` and
|
|
616
616
|
* `turnScope` pass-through strings so callers could append their own
|
|
617
617
|
* scope blocks. DESIGN-PROMPT v1 retired that surface — Active Scope is
|
|
618
|
-
* now structured (`activeScope: {
|
|
618
|
+
* now structured (`activeScope: { sessionId, vpId, envelope }`) and
|
|
619
619
|
* rendered by `buildSystemPrompt` itself. Both pass-through params
|
|
620
620
|
* had zero remaining callers when v1 landed; removing them prevents the
|
|
621
621
|
* "two ways to describe scope" drift §1 set out to eliminate.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* rejects when the chain length would exceed MAX_CHAIN_DEPTH (10).
|
|
10
10
|
*
|
|
11
11
|
* 2. Rate throttle — within a sliding window (WINDOW_MS = 5000, default
|
|
12
|
-
* MAX_HITS_PER_WINDOW = 8), a single (
|
|
12
|
+
* MAX_HITS_PER_WINDOW = 8), a single (sessionId, vpId) target may be
|
|
13
13
|
* @-forwarded at most N times. On overflow, the forward returns a
|
|
14
14
|
* `throttled` error and does NOT dispatch. The counter uses a simple
|
|
15
15
|
* ring (timestamps array) so expired hits are collected on insert.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* threat model is one runaway turn storm within a single process tick.
|
|
20
20
|
*
|
|
21
21
|
* Long-running process hygiene (N1, task-334d-followup):
|
|
22
|
-
* The `hits` Map is keyed by "
|
|
22
|
+
* The `hits` Map is keyed by "sessionId::vpId" and would otherwise grow
|
|
23
23
|
* unboundedly over a long session. Two complementary bounds:
|
|
24
24
|
* - TTL sweep: on each NEW key insert, drop entries whose most
|
|
25
25
|
* recent hit is older than `ttlMultiplier × windowMs` (default 2×).
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
*
|
|
37
37
|
* Integration contract (routing/router.js):
|
|
38
38
|
* - router stamps envelope.meta.causedBy = [...prevChain, currentMsgId]
|
|
39
|
-
* - router calls `guard.check({
|
|
39
|
+
* - router calls `guard.check({ sessionId, targetVpId, chain })` BEFORE
|
|
40
40
|
* calling coordinator.deliver; on `{ ok: false, reason }` returns a
|
|
41
41
|
* tool-level error.
|
|
42
|
-
* - on ok=true, router calls `guard.record({
|
|
42
|
+
* - on ok=true, router calls `guard.record({ sessionId, targetVpId })` to
|
|
43
43
|
* advance the rate counter.
|
|
44
44
|
*/
|
|
45
45
|
|
|
@@ -69,13 +69,13 @@ export function createLoopGuard(options = {}) {
|
|
|
69
69
|
const ttlMultiplier = options.ttlMultiplier ?? DEFAULT_TTL_MULTIPLIER;
|
|
70
70
|
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
71
71
|
|
|
72
|
-
/** Map<"
|
|
72
|
+
/** Map<"sessionId::vpId", number[]> — sorted ascending timestamps.
|
|
73
73
|
* Map insertion order doubles as LRU recency: touching (delete+set) on
|
|
74
74
|
* every access keeps the oldest-used entry at the front for eviction. */
|
|
75
75
|
const hits = new Map();
|
|
76
76
|
let evictions = 0;
|
|
77
77
|
|
|
78
|
-
function key(
|
|
78
|
+
function key(sessionId, vpId) { return `${sessionId}::${vpId}`; }
|
|
79
79
|
|
|
80
80
|
function trim(arr, cutoff) {
|
|
81
81
|
let i = 0;
|
|
@@ -126,15 +126,15 @@ export function createLoopGuard(options = {}) {
|
|
|
126
126
|
|
|
127
127
|
return {
|
|
128
128
|
/**
|
|
129
|
-
* Check whether a forward to (
|
|
129
|
+
* Check whether a forward to (sessionId, vpId) with the supplied causedBy
|
|
130
130
|
* chain is permitted. Does NOT record — call record() after the caller
|
|
131
131
|
* decides to proceed (keeps dry-run / simulation honest).
|
|
132
132
|
*
|
|
133
|
-
* @param {{
|
|
133
|
+
* @param {{ sessionId:string, targetVpId:string, chain?:string[] }} args
|
|
134
134
|
* @returns {{ ok:true } | { ok:false, reason:'chain_depth_exceeded'|'throttled', detail?:any }}
|
|
135
135
|
*/
|
|
136
|
-
check({
|
|
137
|
-
if (!
|
|
136
|
+
check({ sessionId, targetVpId, chain = [] }) {
|
|
137
|
+
if (!sessionId || !targetVpId) {
|
|
138
138
|
return { ok: false, reason: 'chain_depth_exceeded', detail: { missing: true } };
|
|
139
139
|
}
|
|
140
140
|
if (Array.isArray(chain) && chain.length >= maxChainDepth) {
|
|
@@ -144,7 +144,7 @@ export function createLoopGuard(options = {}) {
|
|
|
144
144
|
detail: { depth: chain.length, limit: maxChainDepth },
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
|
-
const k = key(
|
|
147
|
+
const k = key(sessionId, targetVpId);
|
|
148
148
|
const arr = hits.get(k);
|
|
149
149
|
if (arr) {
|
|
150
150
|
const cutoff = now() - windowMs;
|
|
@@ -164,9 +164,9 @@ export function createLoopGuard(options = {}) {
|
|
|
164
164
|
},
|
|
165
165
|
|
|
166
166
|
/** Record a successful forward — advances the rate counter. */
|
|
167
|
-
record({
|
|
168
|
-
if (!
|
|
169
|
-
const k = key(
|
|
167
|
+
record({ sessionId, targetVpId }) {
|
|
168
|
+
if (!sessionId || !targetVpId) return;
|
|
169
|
+
const k = key(sessionId, targetVpId);
|
|
170
170
|
let arr = hits.get(k);
|
|
171
171
|
const creating = !arr;
|
|
172
172
|
if (!arr) {
|
package/yeaft/routing/router.js
CHANGED
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
* (d) Tool schema uses defineTool (agent/yeaft/tools/types.js).
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { resolveMemberId } from '../
|
|
28
|
+
import { resolveMemberId } from '../sessions/roster.js';
|
|
29
29
|
import { createLoopGuard, extendCausedBy } from './loop-guard.js';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Build a router bound to a single GroupCoordinator + loop guard.
|
|
33
33
|
*
|
|
34
34
|
* @param {{
|
|
35
|
-
* coordinator: import('../
|
|
35
|
+
* coordinator: import('../sessions/coordinator.js').GroupCoordinator,
|
|
36
36
|
* guard?: ReturnType<typeof createLoopGuard>,
|
|
37
37
|
* now?: () => number,
|
|
38
38
|
* }} deps
|
|
@@ -62,7 +62,7 @@ export function createRouter(deps = {}) {
|
|
|
62
62
|
* ok: boolean,
|
|
63
63
|
* error?: string,
|
|
64
64
|
* dispatched?: string[],
|
|
65
|
-
* report?: import('../
|
|
65
|
+
* report?: import('../sessions/coordinator.js').DispatchReport,
|
|
66
66
|
* }}
|
|
67
67
|
*/
|
|
68
68
|
function forward(args, opts = {}) {
|
|
@@ -113,7 +113,7 @@ export function createRouter(deps = {}) {
|
|
|
113
113
|
// member inboxes.
|
|
114
114
|
const guardKey = targetVpId;
|
|
115
115
|
const verdict = guard.check({
|
|
116
|
-
|
|
116
|
+
sessionId: meta.id,
|
|
117
117
|
targetVpId: guardKey,
|
|
118
118
|
chain,
|
|
119
119
|
});
|
|
@@ -157,7 +157,7 @@ export function createRouter(deps = {}) {
|
|
|
157
157
|
// dispatches (e.g. task.members gate) we still count it as a hit —
|
|
158
158
|
// the forwarder still tried, and the guard's job is to throttle the
|
|
159
159
|
// sender's ability to keep trying.
|
|
160
|
-
guard.record({
|
|
160
|
+
guard.record({ sessionId: meta.id, targetVpId: guardKey });
|
|
161
161
|
|
|
162
162
|
return {
|
|
163
163
|
ok: true,
|
package/yeaft/session.js
CHANGED
|
@@ -42,7 +42,7 @@ import { ToolUsageStats } from './stats/tool-usage.js';
|
|
|
42
42
|
// resumes with the same onDemand/recent membership it had on
|
|
43
43
|
// disconnect. Engine.#runQuery uses the registry to populate the
|
|
44
44
|
// AMS each turn and to run `memory/adjust.js` post-turn.
|
|
45
|
-
import {
|
|
45
|
+
import { ensureDefaultSessionIfEmpty } from './sessions/session-crud.js';
|
|
46
46
|
import { seedDefaultVps } from './vp/seed-defaults.js';
|
|
47
47
|
import { topUpDefaultVps } from './vp/seed-topup.js';
|
|
48
48
|
import { runSummaryBackfill, archiveLegacyScopes } from './memory/seed-backfill.js';
|
|
@@ -261,7 +261,7 @@ export async function loadSession(options = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
// ─── 5-ams. (GC.1 follow-up) Group-keyed AMS registry ────
|
|
264
|
-
// The registry caches one ActiveMemorySet per
|
|
264
|
+
// The registry caches one ActiveMemorySet per sessionId and
|
|
265
265
|
// persists their state to disk so a deactivated group can be
|
|
266
266
|
// reactivated with the same onDemand/recent membership it had
|
|
267
267
|
// on disconnect. Without memoryIndex we have nothing to
|
|
@@ -289,7 +289,7 @@ export async function loadSession(options = {}) {
|
|
|
289
289
|
if (!config._readOnly) {
|
|
290
290
|
// task-337: seed the default VPs (steve, linus, martin, kongzi, buffett, omni, …)
|
|
291
291
|
// on a fresh install so the library is never empty. Idempotent — a no-op
|
|
292
|
-
// once the user has any VP on disk. Must run BEFORE
|
|
292
|
+
// once the user has any VP on disk. Must run BEFORE ensureDefaultSessionIfEmpty
|
|
293
293
|
// so the default group's roster scan sees the seeded VPs. Must also run
|
|
294
294
|
// before any VpLoader.start() (VpLoader is lazy-started in vp-bridge.js
|
|
295
295
|
// on first subscribe, which happens strictly after loadSession returns).
|
|
@@ -324,9 +324,9 @@ export async function loadSession(options = {}) {
|
|
|
324
324
|
console.warn(`[Yeaft] topUpDefaultVps failed: ${err?.message || err}`);
|
|
325
325
|
}
|
|
326
326
|
try {
|
|
327
|
-
|
|
327
|
+
ensureDefaultSessionIfEmpty(yeaftDir, { memoryRoot: join(yeaftDir, 'memory') });
|
|
328
328
|
} catch (err) {
|
|
329
|
-
console.warn(`[Yeaft]
|
|
329
|
+
console.warn(`[Yeaft] ensureDefaultSessionIfEmpty failed: ${err?.message || err}`);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// task-fix-memory-load: backfill summary.md for VPs / groups created
|