@yeaft/webchat-agent 0.1.874 → 0.1.876

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.
Files changed (46) hide show
  1. package/connection/message-router.js +20 -13
  2. package/package.json +1 -1
  3. package/yeaft/attachments.js +2 -2
  4. package/yeaft/cli.js +13 -13
  5. package/yeaft/compact/compactor.js +20 -20
  6. package/yeaft/conversation/persist.js +95 -95
  7. package/yeaft/debug-trace.js +12 -12
  8. package/yeaft/dream-v2/apply.js +15 -15
  9. package/yeaft/dream-v2/merge.js +12 -12
  10. package/yeaft/dream-v2/prompts/{extract-group.md → extract-session.md} +12 -12
  11. package/yeaft/dream-v2/prompts/index.js +3 -3
  12. package/yeaft/dream-v2/prompts/triage-pass1.md +1 -1
  13. package/yeaft/dream-v2/runner.js +37 -37
  14. package/yeaft/dream-v2/segment.js +3 -3
  15. package/yeaft/dream-v2/session-wiring.js +22 -22
  16. package/yeaft/dream-v2/state.js +7 -7
  17. package/yeaft/dream-v2/triage.js +22 -22
  18. package/yeaft/engine.js +65 -65
  19. package/yeaft/memory/ams-registry.js +22 -22
  20. package/yeaft/memory/seed-backfill.js +9 -9
  21. package/yeaft/memory/store-v2.js +27 -27
  22. package/yeaft/prompts.js +9 -9
  23. package/yeaft/routing/loop-guard.js +14 -14
  24. package/yeaft/routing/router.js +5 -5
  25. package/yeaft/session.js +5 -5
  26. package/yeaft/sessions/coordinator.js +96 -20
  27. package/yeaft/{groups → sessions}/ids.js +3 -3
  28. package/yeaft/{groups → sessions}/index.js +28 -28
  29. package/yeaft/sessions/pre-flow.js +178 -42
  30. package/yeaft/{groups → sessions}/seed-default.js +19 -19
  31. package/yeaft/{groups/group-config.js → sessions/session-config.js} +29 -29
  32. package/yeaft/{groups/group-crud.js → sessions/session-crud.js} +113 -113
  33. package/yeaft/sessions/session-store.js +85 -154
  34. package/yeaft/stop-hooks.js +4 -4
  35. package/yeaft/tools/todo-write.js +1 -1
  36. package/yeaft/tools/types.js +2 -2
  37. package/yeaft/vp/registry.js +1 -1
  38. package/yeaft/vp/vp-crud.js +1 -1
  39. package/yeaft/vp-status-broker.js +28 -28
  40. package/yeaft/web-bridge.js +411 -398
  41. package/yeaft/groups/coordinator.js +0 -221
  42. package/yeaft/groups/group-store.js +0 -212
  43. package/yeaft/groups/pre-flow.js +0 -329
  44. /package/yeaft/{groups → sessions}/feature-flag.js +0 -0
  45. /package/yeaft/{groups → sessions}/project-doc.js +0 -0
  46. /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/<groupId>/ams.json
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(groupId, {ownVpId}) — returns the cached AMS or loads
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(groupId) — writes the current cached AMS to disk.
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
- * `groupId` is trusted: `nextGroupId()` (groups/ids.js) emits ids matching
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} groupId
85
+ * @param {string} sessionId
86
86
  * @returns {string}
87
87
  */
88
- amsPath(groupId) {
89
- const key = String(groupId || DEFAULT_GROUP_KEY);
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} groupId
110
+ * @param {string|null|undefined} sessionId
111
111
  * @param {{ ownVpId?: string|null }} [opts]
112
112
  * @returns {ActiveMemorySet}
113
113
  */
114
- getOrCreate(groupId, opts = {}) {
115
- const key = groupId || DEFAULT_GROUP_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} groupId
134
+ * @param {string|null|undefined} sessionId
135
135
  * @returns {boolean}
136
136
  */
137
- adjustRanThisSession(groupId) {
138
- const key = groupId || DEFAULT_GROUP_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} groupId
147
+ * @param {string|null|undefined} sessionId
148
148
  * @param {boolean} value
149
149
  */
150
- setAdjustRanThisSession(groupId, value) {
151
- const key = groupId || DEFAULT_GROUP_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} groupId
160
+ * @param {string|null|undefined} sessionId
161
161
  */
162
- markDirty(groupId) {
163
- this._dirty.add(groupId || DEFAULT_GROUP_KEY);
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} groupId
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(groupId, opts = {}) {
179
- const key = groupId || DEFAULT_GROUP_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} groupDir
153
+ * @param {string} sessionDir
154
154
  * @returns {string|null}
155
155
  */
156
- function readGroupSummaryBody(groupDir) {
157
- const metaPath = join(groupDir, 'group.json');
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 groupsRoot = join(yeaftDir, 'groups');
240
- if (!existsSync(groupsRoot)) return { scanned, seeded };
239
+ const sessionsRoot = join(yeaftDir, 'sessions');
240
+ if (!existsSync(sessionsRoot)) return { scanned, seeded };
241
241
  let entries;
242
- try { entries = readdirSync(groupsRoot); } catch { return { scanned, seeded }; }
242
+ try { entries = readdirSync(sessionsRoot); } catch { return { scanned, seeded }; }
243
243
  for (const name of entries) {
244
244
  if (name.startsWith('.')) continue;
245
- const groupDir = join(groupsRoot, name);
245
+ const sessionDir = join(sessionsRoot, name);
246
246
  let isDir = false;
247
- try { isDir = statSync(groupDir).isDirectory(); } catch { /* skip */ }
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(groupDir);
252
+ const body = readGroupSummaryBody(sessionDir);
253
253
  if (!body) continue;
254
254
  try {
255
255
  writeAtomicSync(summaryPath, body);
@@ -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/<groupId>/ memory.md summary.md
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} [groupId] — required for every group-* kind
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.groupId) throw new Error('scopeDir: group-user scope requires groupId');
102
- assertSafeSegment(scope.groupId, 'group-user.groupId');
103
- return `group/${scope.groupId}/user`;
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.groupId) throw new Error('scopeDir: group-vp scope requires groupId');
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.groupId, 'group-vp.groupId');
108
+ assertSafeSegment(scope.sessionId, 'group-vp.sessionId');
109
109
  assertSafeSegment(scope.id, 'group-vp.id');
110
- return `group/${scope.groupId}/vp/${scope.id}`;
110
+ return `group/${scope.sessionId}/vp/${scope.id}`;
111
111
  }
112
112
  case 'group-feature': {
113
- if (!scope.groupId) throw new Error('scopeDir: group-feature scope requires groupId');
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.groupId, 'group-feature.groupId');
115
+ assertSafeSegment(scope.sessionId, 'group-feature.sessionId');
116
116
  assertSafeSegment(scope.id, 'group-feature.id');
117
- return `group/${scope.groupId}/feature/${scope.id}`;
117
+ return `group/${scope.sessionId}/feature/${scope.id}`;
118
118
  }
119
119
  case 'group-topic': {
120
- if (!scope.groupId) throw new Error('scopeDir: group-topic scope requires groupId');
121
- assertSafeSegment(scope.groupId, 'group-topic.groupId');
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.groupId}/topic/${segs.join('/')}`;
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.groupId || typeof scope.groupId !== 'string') return false;
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` / `deleteGroup` to cascade memory cleanup so a recreate of the
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', groupId: g }
464
- * group/<g>/vp/<v>/ → { kind: 'group-vp', groupId: g, id: v }
465
- * group/<g>/feature/<f>/ → { kind: 'group-feature', groupId: g, id: f }
466
- * group/<g>/topic/<l1>[/<l2>]/ → { kind: 'group-topic', groupId: g, path: [...] }
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') return out;
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', groupId: g });
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
- groupId: g,
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', groupId: g, path: [l1, l2ent.name] });
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', groupId: g, path: [l1] });
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.groupId]
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
- * groupAnnouncement?: string,
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
- groupAnnouncement = '',
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 groupAnnouncement === 'string') ? groupAnnouncement.trim() : '';
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: <groupId> (omitted when missing)
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.groupId]
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.groupId === 'string' && activeScope.groupId.trim()
505
- ? activeScope.groupId.trim()
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: { groupId, vpId, envelope }`) and
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 (groupId, vpId) target may be
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 "groupId::vpId" and would otherwise grow
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({ groupId, targetVpId, chain })` BEFORE
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({ groupId, targetVpId })` to
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<"groupId::vpId", number[]> — sorted ascending timestamps.
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(groupId, vpId) { return `${groupId}::${vpId}`; }
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 (groupId, vpId) with the supplied causedBy
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 {{ groupId:string, targetVpId:string, chain?:string[] }} args
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({ groupId, targetVpId, chain = [] }) {
137
- if (!groupId || !targetVpId) {
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(groupId, targetVpId);
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({ groupId, targetVpId }) {
168
- if (!groupId || !targetVpId) return;
169
- const k = key(groupId, targetVpId);
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) {
@@ -25,14 +25,14 @@
25
25
  * (d) Tool schema uses defineTool (agent/yeaft/tools/types.js).
26
26
  */
27
27
 
28
- import { resolveMemberId } from '../groups/roster.js';
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('../groups/coordinator.js').GroupCoordinator,
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('../groups/coordinator.js').DispatchReport,
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
- groupId: meta.id,
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({ groupId: meta.id, targetVpId: guardKey });
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 { ensureDefaultGroupIfEmpty } from './groups/group-crud.js';
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 groupId and
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 ensureDefaultGroupIfEmpty
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
- ensureDefaultGroupIfEmpty(yeaftDir, { memoryRoot: join(yeaftDir, 'memory') });
327
+ ensureDefaultSessionIfEmpty(yeaftDir, { memoryRoot: join(yeaftDir, 'memory') });
328
328
  } catch (err) {
329
- console.warn(`[Yeaft] ensureDefaultGroupIfEmpty failed: ${err?.message || err}`);
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