@yeaft/webchat-agent 0.1.874 → 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.
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
@@ -1,221 +0,0 @@
1
- /**
2
- * coordinator.js — Group Coordinator (task-334b).
3
- *
4
- * Consumes user/VP messages, persists them to the group's 334o jsonl-log,
5
- * and dispatches user-text turns to target RoleInstances'
6
- * `inputQueue`.
7
- *
8
- * As of GC.1 Commit B, VP-selection (parseMentions + dispatch matrix:
9
- * mention / @all / fallback / vp-author no-op) lives in
10
- * `groups/pre-flow.js` so the same logic can be invoked directly by
11
- * the parallel fan-out path in web-bridge.js. Coordinator's job is now
12
- * narrower: persist the message and translate the selection result
13
- * into deliver() calls.
14
- *
15
- * This module DOES NOT run the engine. It only:
16
- * 1. Persists the message (via GroupHandle.appendMessage)
17
- * 2. Asks pre-flow for the list of target vpIds
18
- * 3. Calls a user-supplied deliver(vpId, envelope) per target
19
- */
20
-
21
- import { parseMentions, selectRespondingVps } from './pre-flow.js';
22
-
23
- // Re-export so existing importers (`createCoordinator(...).parseMentions`,
24
- // or modules importing `parseMentions` from coordinator) keep working
25
- // without churn. New code should import from `./pre-flow.js` directly.
26
- export { parseMentions };
27
-
28
- /**
29
- * Build a Group Coordinator bound to a single GroupHandle.
30
- *
31
- * @param {import('./group-store.js').GroupHandle} group
32
- * @param {Object} [options]
33
- * @param {(vpId:string, envelope:any)=>void} [options.deliver] called per target
34
- * @param {number} [options.perGroupFanOut=16] @all cap (arch §5.3)
35
- * @returns {GroupCoordinator}
36
- */
37
- export function createCoordinator(group, options = {}) {
38
- const deliver = options.deliver || (() => {});
39
- const fanOutCap = options.perGroupFanOut ?? 16;
40
-
41
- /**
42
- * Ingest one message. Returns a dispatch report describing what would/did
43
- * go out to RoleInstances.
44
- *
45
- * @param {{
46
- * from: string, // 'user' | vpId
47
- * role?: 'user'|'assistant',
48
- * text: string,
49
- * taskId?: string|null,
50
- * meta?: any,
51
- * id?: string, ts?: string,
52
- * }} input
53
- * @param {{ taskMembers?: string[] }} [opts]
54
- * When taskId is set, restricts dispatch to vps in taskMembers (334n owns
55
- * the list). If omitted, coordinator will not filter.
56
- */
57
- function ingest(input, opts = {}) {
58
- if (!input || typeof input !== 'object') {
59
- throw new Error('ingest: input required');
60
- }
61
- if (typeof input.text !== 'string') {
62
- throw new Error('ingest: input.text required (string)');
63
- }
64
- const meta = group.getMeta();
65
- if (!meta) throw new Error('group not initialised (call createGroup first)');
66
-
67
- // `fromUser` drives `selectRespondingVps` — when true, the @-mention
68
- // matrix runs (mention/broadcast/fallback). When false, VPs cannot
69
- // text-@-route (VP-authored free text is surface noise per arch §6).
70
- //
71
- // route_forward injection is a special case: the message is VP-authored
72
- // (role='assistant') but it MUST trigger dispatch (target VP needs to
73
- // run). We detect it via `meta.injectedBy === 'route_forward'` and
74
- // treat it as "user-like" for dispatch purposes only. Persistence still
75
- // honours the caller's `role` field so the on-disk record correctly
76
- // attributes the turn to the sending VP, not to the user.
77
- const isRouteForwardInjection = input?.meta?.injectedBy === 'route_forward';
78
- const fromUser = input.from === 'user'
79
- || input.role === 'user'
80
- || isRouteForwardInjection;
81
- const mentions = parseMentions(input.text);
82
-
83
- // Persist first — audit log / replay works even if dispatch has bugs.
84
- //
85
- // Convention: any field on `input` that starts with `_` is treated
86
- // as ephemeral and is forwarded to the envelope (so per-turn driver
87
- // payloads — image base64 blocks, prompt suffixes — reach the LLM
88
- // call) but is NEVER passed to appendMessage. The jsonl-log must
89
- // stay lean: base64 in audit history would blow up replay.
90
- //
91
- // The split is enforced structurally — see the assertion below the
92
- // partition loop. Don't loosen it. If a new ephemeral key is added,
93
- // it gets the `_` prefix at its source and inherits the protection
94
- // for free; no allowlist to maintain.
95
- const persistInput = {};
96
- const ephemeral = {};
97
- for (const [k, v] of Object.entries(input)) {
98
- if (typeof k === 'string' && k.startsWith('_')) {
99
- ephemeral[k] = v;
100
- } else {
101
- persistInput[k] = v;
102
- }
103
- }
104
- // Structural guarantee: nothing with a `_` prefix may reach the
105
- // jsonl-log via `persistInput`. If this ever throws, the `_` rule
106
- // got bypassed — fix the caller, not this assertion.
107
- {
108
- const leaked = Object.keys(persistInput).filter((k) => typeof k === 'string' && k.startsWith('_'));
109
- if (leaked.length > 0) {
110
- throw new Error(`coordinator.ingest: ephemeral fields leaked into persisted record: ${leaked.join(', ')}`);
111
- }
112
- }
113
- const stored = group.appendMessage({
114
- ...persistInput,
115
- mentions,
116
- role: input.role || (fromUser ? 'user' : 'assistant'),
117
- });
118
-
119
- // Ask pre-flow which VPs (if any) should respond.
120
- const selection = selectRespondingVps({
121
- meta,
122
- fromUser,
123
- mentions,
124
- sender: input.from,
125
- fanOutCap,
126
- taskMembers: opts.taskMembers,
127
- });
128
-
129
- // VP-authored: persist but no dispatch.
130
- if (selection.reason === 'vp-author-no-text-routing') {
131
- return {
132
- message: stored,
133
- dispatched: [],
134
- fallback: null,
135
- errors: [],
136
- skipped: 'vp-author-no-text-routing',
137
- };
138
- }
139
-
140
- if (selection.reason === 'broadcast') {
141
- const envelope = makeEnvelope(stored, meta, 'broadcast', ephemeral);
142
- for (const vpId of selection.dispatched) deliver(vpId, envelope);
143
- return {
144
- message: stored,
145
- dispatched: selection.dispatched,
146
- fallback: null,
147
- errors: selection.errors,
148
- broadcast: true,
149
- truncatedAtFanOutCap: !!selection.truncatedAtFanOutCap,
150
- };
151
- }
152
-
153
- if (selection.reason === 'mention') {
154
- for (const vpId of selection.dispatched) {
155
- deliver(vpId, makeEnvelope(stored, meta, 'mention', ephemeral));
156
- }
157
- return {
158
- message: stored,
159
- dispatched: selection.dispatched,
160
- fallback: null,
161
- errors: selection.errors,
162
- };
163
- }
164
-
165
- if (selection.reason === 'fallback' && selection.fallback) {
166
- deliver(selection.fallback, makeEnvelope(stored, meta, 'fallback', ephemeral));
167
- return {
168
- message: stored,
169
- dispatched: selection.dispatched,
170
- fallback: selection.fallback,
171
- errors: selection.errors,
172
- };
173
- }
174
-
175
- // no-default / nothing to dispatch
176
- return {
177
- message: stored,
178
- dispatched: [],
179
- fallback: null,
180
- errors: selection.errors,
181
- };
182
- }
183
-
184
- return {
185
- group,
186
- ingest,
187
- parseMentions,
188
- };
189
- }
190
-
191
- function makeEnvelope(msg, meta, trigger, ephemeral = {}) {
192
- return {
193
- groupId: meta.id,
194
- taskId: msg.taskId || null,
195
- msg,
196
- trigger, // 'broadcast' | 'mention' | 'fallback'
197
- // Ephemeral fields (any `_`-prefixed key on coord.ingest input).
198
- // Used to ferry per-turn payloads (e.g. image base64 blocks) that
199
- // must reach the driver but must NOT be persisted to the group log.
200
- ...ephemeral,
201
- };
202
- }
203
-
204
- /**
205
- * @typedef {Object} GroupCoordinator
206
- * @property {import('./group-store.js').GroupHandle} group
207
- * @property {(input:any, opts?:any)=>DispatchReport} ingest
208
- * @property {(text:string)=>string[]} parseMentions
209
- */
210
-
211
- /**
212
- * @typedef {Object} DispatchReport
213
- * @property {any} message
214
- * @property {string[]} dispatched
215
- * @property {string|null} fallback
216
- * @property {Array<{vpId?:string, error:string}>} errors
217
- * @property {boolean=} broadcast
218
- * @property {boolean=} truncatedAtFanOutCap
219
- * @property {string=} skipped
220
- */
221
-
@@ -1,212 +0,0 @@
1
- /**
2
- * group-store.js — Per-group persistent store for task-334b.
3
- *
4
- * Layout (see architecture §2):
5
- * ~/.yeaft/groups/<group-id>/
6
- * group.json # { id, name, roster: [vpId...], defaultVpId, createdAt }
7
- * messages/ # JSONL size-rotation log (334o openLog)
8
- * 000001.jsonl
9
- * index.json
10
- * tasks/ # populated by 334n — reserved here
11
- * vps/ # populated by 334c RoleInstance runtime — reserved here
12
- *
13
- * This module owns only the group.json + messages/ log. Roster mutation
14
- * logic lives in roster.js so coordinator and group-store both compose it.
15
- *
16
- * Hard constraint: the store does not parse @-mentions, does not dispatch,
17
- * and has no knowledge of VP/RoleInstance. It is pure persistence over 334o.
18
- */
19
-
20
- import {
21
- existsSync,
22
- mkdirSync,
23
- readFileSync,
24
- readdirSync,
25
- statSync,
26
- } from 'fs';
27
- import { join } from 'path';
28
- import { writeAtomic, openLog } from '../storage/index.js';
29
- import { nextMsgId, isReservedVpId, ReservedVpIdError, validateVpId, InvalidVpIdError } from './ids.js';
30
-
31
- const GROUP_FILE = 'group.json';
32
- const MESSAGES_DIR = 'messages';
33
-
34
- /**
35
- * Load (or create) the directory for a single group.
36
- *
37
- * @param {string} groupsRoot e.g. `${yeaftDir}/groups`
38
- * @param {string} groupId
39
- * @returns {GroupHandle}
40
- */
41
- export function openGroup(groupsRoot, groupId) {
42
- if (!groupId || typeof groupId !== 'string') {
43
- throw new Error('openGroup: groupId required (string)');
44
- }
45
- const dir = join(groupsRoot, groupId);
46
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
47
-
48
- let meta = loadGroupMeta(dir);
49
- if (!meta) {
50
- // Fresh group — caller must call initGroup() next; we return a handle
51
- // with meta=null so createGroup() can write the initial file.
52
- }
53
-
54
- const messagesDir = join(dir, MESSAGES_DIR);
55
- if (!existsSync(messagesDir)) mkdirSync(messagesDir, { recursive: true });
56
- const log = openLog(messagesDir);
57
-
58
- return {
59
- dir,
60
- id: groupId,
61
- /** Return current meta (reads fresh from memory after last save). */
62
- getMeta() { return meta ? structuredClone(meta) : null; },
63
- /** Overwrite group.json atomically. */
64
- saveMeta(next) {
65
- validateMeta(next);
66
- meta = next;
67
- writeAtomic(join(dir, GROUP_FILE), JSON.stringify(meta, null, 2));
68
- },
69
- /**
70
- * Append a message to the group log. Assigns an id if absent.
71
- * Returns the stored record (with id + ts).
72
- *
73
- * Structural invariant: NO field on `record` may start with `_`.
74
- * The `_` prefix is reserved for ephemeral per-turn payloads (image
75
- * base64, prompt suffixes) that must reach the driver but must
76
- * never hit the persisted jsonl-log. If this throws, the caller
77
- * forgot to partition ephemeral fields off — fix the caller.
78
- */
79
- appendMessage(record) {
80
- if (!record || typeof record !== 'object') {
81
- throw new Error('appendMessage: record required');
82
- }
83
- {
84
- const leaked = Object.keys(record).filter((k) => typeof k === 'string' && k.startsWith('_'));
85
- if (leaked.length > 0) {
86
- throw new Error(`appendMessage: ephemeral fields leaked into log: ${leaked.join(', ')}`);
87
- }
88
- }
89
- const stored = {
90
- id: record.id || nextMsgId(),
91
- ts: record.ts || new Date().toISOString(),
92
- from: record.from, // vpId | 'user'
93
- role: record.role || (record.from === 'user' ? 'user' : 'assistant'),
94
- text: record.text ?? '',
95
- taskId: record.taskId || null,
96
- mentions: Array.isArray(record.mentions) ? record.mentions.slice() : [],
97
- meta: record.meta || {},
98
- };
99
- log.append(stored);
100
- return stored;
101
- },
102
- /** Iterate all messages oldest→newest. */
103
- *streamMessages() {
104
- yield* log.streamAll();
105
- },
106
- /** Iterate a message id range inclusive. */
107
- *readMessageRange(firstId, lastId) {
108
- yield* log.readRange(firstId, lastId);
109
- },
110
- /** Flush + close underlying log (on shutdown). */
111
- close() { log.close(); },
112
- };
113
- }
114
-
115
- /**
116
- * Create a fresh group on disk. Fails if group.json already exists.
117
- * @returns {GroupHandle}
118
- */
119
- export function createGroup(groupsRoot, spec) {
120
- if (!spec || !spec.id) throw new Error('createGroup: spec.id required');
121
- const h = openGroup(groupsRoot, spec.id);
122
- if (h.getMeta()) {
123
- throw new Error(`group ${spec.id} already exists`);
124
- }
125
- const roster = Array.isArray(spec.roster) ? spec.roster.slice() : [];
126
- for (const v of roster) {
127
- if (isReservedVpId(v)) throw new ReservedVpIdError(v);
128
- const verdict = validateVpId(v);
129
- if (!verdict.ok) throw new InvalidVpIdError(v, verdict.reason);
130
- }
131
- if (spec.defaultVpId) {
132
- if (isReservedVpId(spec.defaultVpId)) throw new ReservedVpIdError(spec.defaultVpId);
133
- const dverdict = validateVpId(spec.defaultVpId);
134
- if (!dverdict.ok) throw new InvalidVpIdError(spec.defaultVpId, dverdict.reason);
135
- }
136
- const meta = {
137
- id: spec.id,
138
- name: spec.name || spec.id,
139
- roster,
140
- defaultVpId: spec.defaultVpId || null,
141
- announcement: typeof spec.announcement === 'string' ? spec.announcement : '',
142
- workDir: typeof spec.workDir === 'string' ? spec.workDir.trim() : '',
143
- createdAt: spec.createdAt || new Date().toISOString(),
144
- };
145
- h.saveMeta(meta);
146
- return h;
147
- }
148
-
149
- /** Non-destructive load — returns null if group.json is missing/corrupt. */
150
- export function loadGroupMeta(dir) {
151
- const path = join(dir, GROUP_FILE);
152
- if (!existsSync(path)) return null;
153
- try {
154
- const raw = readFileSync(path, 'utf8');
155
- const parsed = JSON.parse(raw);
156
- validateMeta(parsed);
157
- // Legacy groups created before optional fields were added are
158
- // forward-compat: missing fields read back as safe empty strings.
159
- if (typeof parsed.announcement !== 'string') parsed.announcement = '';
160
- if (typeof parsed.workDir !== 'string') parsed.workDir = '';
161
- return parsed;
162
- } catch {
163
- return null;
164
- }
165
- }
166
-
167
- /** List every group directory under `groupsRoot`. */
168
- export function listGroups(groupsRoot) {
169
- if (!existsSync(groupsRoot)) return [];
170
- const out = [];
171
- for (const name of readdirSync(groupsRoot)) {
172
- // Skip dotfiles and legacy soft-archive dirs (`.archived-*`).
173
- if (name.startsWith('.')) continue;
174
- const p = join(groupsRoot, name);
175
- try {
176
- if (!statSync(p).isDirectory()) continue;
177
- } catch { continue; }
178
- const meta = loadGroupMeta(p);
179
- if (meta) out.push(meta);
180
- }
181
- return out;
182
- }
183
-
184
- function validateMeta(meta) {
185
- if (!meta || typeof meta !== 'object') throw new Error('group.json must be object');
186
- if (!meta.id || typeof meta.id !== 'string') throw new Error('group.id required');
187
- if (!Array.isArray(meta.roster)) throw new Error('group.roster must be array');
188
- for (const v of meta.roster) {
189
- if (typeof v !== 'string') throw new Error('group.roster must be string[]');
190
- }
191
- if (meta.defaultVpId != null && typeof meta.defaultVpId !== 'string') {
192
- throw new Error('group.defaultVpId must be string|null');
193
- }
194
- if (meta.announcement != null && typeof meta.announcement !== 'string') {
195
- throw new Error('group.announcement must be string');
196
- }
197
- if (meta.workDir != null && typeof meta.workDir !== 'string') {
198
- throw new Error('group.workDir must be string');
199
- }
200
- }
201
-
202
- /**
203
- * @typedef {Object} GroupHandle
204
- * @property {string} dir
205
- * @property {string} id
206
- * @property {() => any} getMeta
207
- * @property {(next:any)=>void} saveMeta
208
- * @property {(record:any)=>any} appendMessage
209
- * @property {() => Generator<any>} streamMessages
210
- * @property {(first:string,last:string)=>Generator<any>} readMessageRange
211
- * @property {() => void} close
212
- */