@yeaft/webchat-agent 0.1.536 → 0.1.538
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/unify/tasks/store.js +8 -2
- package/unify/tasks/summary.js +37 -11
- package/unify/web-bridge.js +16 -1
package/package.json
CHANGED
package/unify/tasks/store.js
CHANGED
|
@@ -301,9 +301,13 @@ export class TaskStore {
|
|
|
301
301
|
*
|
|
302
302
|
* If an `onEvent` callback was passed at construction time, emits a
|
|
303
303
|
* `task_member_added` event synchronously after the write:
|
|
304
|
-
* { type: 'task_member_added', taskId, vpId, members, ts }
|
|
304
|
+
* { type: 'task_member_added', taskId, vpId, addedBy, members, ts }
|
|
305
|
+
*
|
|
306
|
+
* @param {string} id — task id
|
|
307
|
+
* @param {string} vpId — VP being added
|
|
308
|
+
* @param {{ addedBy?: string }} [opts] — provenance: who triggered the add
|
|
305
309
|
*/
|
|
306
|
-
addMember(id, vpId) {
|
|
310
|
+
addMember(id, vpId, opts) {
|
|
307
311
|
const task = this.#tasks.get(id);
|
|
308
312
|
if (!task) return { task: null, added: false };
|
|
309
313
|
if (!vpId || typeof vpId !== 'string') {
|
|
@@ -315,10 +319,12 @@ export class TaskStore {
|
|
|
315
319
|
}
|
|
316
320
|
members.push(vpId);
|
|
317
321
|
this.update(id, { members });
|
|
322
|
+
const addedBy = opts?.addedBy || null;
|
|
318
323
|
this.#emit({
|
|
319
324
|
type: 'task_member_added',
|
|
320
325
|
taskId: id,
|
|
321
326
|
vpId,
|
|
327
|
+
addedBy,
|
|
322
328
|
members: members.slice(),
|
|
323
329
|
ts: Date.now(),
|
|
324
330
|
});
|
package/unify/tasks/summary.js
CHANGED
|
@@ -36,6 +36,11 @@ export const SUMMARY_REMINDER_MIN_AGE_MS = 20 * 60 * 1000;
|
|
|
36
36
|
export const EXTRACT_MIN_ENTRIES = 2;
|
|
37
37
|
export const EXTRACT_MAX_ENTRIES = 5;
|
|
38
38
|
|
|
39
|
+
/** Semantic progress anchors — string labels accepted alongside 0-100 number. */
|
|
40
|
+
export const PROGRESS_ANCHORS = Object.freeze([
|
|
41
|
+
'blocked', 'planning', 'in-progress', 'review', 'milestone', 'shipped', 'done',
|
|
42
|
+
]);
|
|
43
|
+
|
|
39
44
|
/** Whitelist of R6 kinds emitted by the summary-extractor. */
|
|
40
45
|
const EXTRACT_KINDS = Object.freeze(['progress', 'decision']);
|
|
41
46
|
|
|
@@ -56,7 +61,7 @@ const KIND_TO_SHARD = Object.freeze({
|
|
|
56
61
|
* taskId: string,
|
|
57
62
|
* fromVpId: string,
|
|
58
63
|
* body: string,
|
|
59
|
-
* progress?: number,
|
|
64
|
+
* progress?: number|string, // 0..100 or PROGRESS_ANCHORS string
|
|
60
65
|
* supersedes?: string[], // prior summary msgIds being superseded
|
|
61
66
|
* memoryDir: string, // groups/<g>/tasks/<t>/memory/
|
|
62
67
|
* now?: () => number, // test clock
|
|
@@ -87,9 +92,15 @@ export function postSummary(opts) {
|
|
|
87
92
|
throw new Error('postSummary: body required (non-empty string)');
|
|
88
93
|
}
|
|
89
94
|
if (progress != null) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
if (typeof progress === 'string') {
|
|
96
|
+
if (!PROGRESS_ANCHORS.includes(progress)) {
|
|
97
|
+
throw new Error(`postSummary: progress string must be one of: ${PROGRESS_ANCHORS.join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const p = Number(progress);
|
|
101
|
+
if (!Number.isFinite(p) || p < 0 || p > 100) {
|
|
102
|
+
throw new Error('postSummary: progress must be number in [0,100] or a semantic anchor string');
|
|
103
|
+
}
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
const supersedesArr = Array.isArray(supersedes)
|
|
@@ -104,7 +115,7 @@ export function postSummary(opts) {
|
|
|
104
115
|
taskId,
|
|
105
116
|
meta: {
|
|
106
117
|
type: 'summary',
|
|
107
|
-
progress: progress == null ? null : Number(progress),
|
|
118
|
+
progress: progress == null ? null : (typeof progress === 'string' ? progress : Number(progress)),
|
|
108
119
|
supersedes: supersedesArr,
|
|
109
120
|
},
|
|
110
121
|
});
|
|
@@ -186,8 +197,12 @@ export function defaultExtractor(body) {
|
|
|
186
197
|
// If we ended up with fewer than MIN and there was a body, collapse to
|
|
187
198
|
// one "progress" entry carrying the trimmed full body so we never emit 0
|
|
188
199
|
// when the caller gave us real content and asked for 2-5.
|
|
189
|
-
if (out.length < EXTRACT_MIN_ENTRIES &&
|
|
190
|
-
|
|
200
|
+
if (out.length < EXTRACT_MIN_ENTRIES && body.trim()) {
|
|
201
|
+
// Pad up to EXTRACT_MIN_ENTRIES with the full trimmed body when
|
|
202
|
+
// the line-by-line pass yielded fewer entries than the minimum.
|
|
203
|
+
while (out.length < EXTRACT_MIN_ENTRIES) {
|
|
204
|
+
out.push({ kind: 'progress', body: body.trim() });
|
|
205
|
+
}
|
|
191
206
|
}
|
|
192
207
|
return out;
|
|
193
208
|
}
|
|
@@ -255,11 +270,12 @@ export function buildSummaryReminder(input) {
|
|
|
255
270
|
* @param {{ tags?: string[], top?: number }} [opts]
|
|
256
271
|
* tags : optional tag hints to boost relevance
|
|
257
272
|
* top : default 5
|
|
258
|
-
* @returns {Array<{body:string, shard:string}>}
|
|
273
|
+
* @returns {Array<{body:string, shard:string, authoredBy?:string}>}
|
|
259
274
|
*/
|
|
260
275
|
export function buildTaskCtxMemories(memoryDir, opts = {}) {
|
|
261
276
|
const top = Number.isFinite(opts.top) ? Number(opts.top) : 5;
|
|
262
277
|
const tagHints = Array.isArray(opts.tags) ? opts.tags : [];
|
|
278
|
+
const nowMs = typeof opts.now === 'number' ? opts.now : Date.now();
|
|
263
279
|
let results = [];
|
|
264
280
|
try {
|
|
265
281
|
const store = openMemoryShardStore(memoryDir, 'task');
|
|
@@ -277,6 +293,7 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
|
|
|
277
293
|
tags: Array.isArray(r.tags) ? r.tags : [],
|
|
278
294
|
pinned: !!r.pinned,
|
|
279
295
|
createdAt: full?.createdAt || null,
|
|
296
|
+
authoredBy: full?.authoredBy || null,
|
|
280
297
|
};
|
|
281
298
|
})
|
|
282
299
|
.filter((r) => r.body && r.body.trim());
|
|
@@ -284,8 +301,13 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
|
|
|
284
301
|
const score = (r) => {
|
|
285
302
|
let s = 0;
|
|
286
303
|
if (r.pinned) s += 1000;
|
|
287
|
-
//
|
|
288
|
-
|
|
304
|
+
// Recency decay: half-life of 24h. Recent entries score up to +100,
|
|
305
|
+
// decaying toward 0 as they age.
|
|
306
|
+
if (r.createdAt) {
|
|
307
|
+
const ageMs = nowMs - new Date(r.createdAt).getTime();
|
|
308
|
+
const halfLifeMs = 24 * 60 * 60 * 1000; // 24 hours
|
|
309
|
+
s += Math.max(0, 100 * Math.pow(0.5, Math.max(0, ageMs) / halfLifeMs));
|
|
310
|
+
}
|
|
289
311
|
// tag relevance
|
|
290
312
|
for (const t of tagHints) if (r.tags.includes(t)) s += 5;
|
|
291
313
|
return s;
|
|
@@ -296,7 +318,11 @@ export function buildTaskCtxMemories(memoryDir, opts = {}) {
|
|
|
296
318
|
// stable recency tie-break
|
|
297
319
|
return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
|
|
298
320
|
});
|
|
299
|
-
results = hits.slice(0, top).map((r) => ({
|
|
321
|
+
results = hits.slice(0, top).map((r) => ({
|
|
322
|
+
body: r.body,
|
|
323
|
+
shard: r.shard,
|
|
324
|
+
...(r.authoredBy ? { authoredBy: r.authoredBy } : {}),
|
|
325
|
+
}));
|
|
300
326
|
} catch {
|
|
301
327
|
results = [];
|
|
302
328
|
}
|
package/unify/web-bridge.js
CHANGED
|
@@ -65,6 +65,10 @@ const QUERY_TIMEOUT_MS = 120_000;
|
|
|
65
65
|
/** Virtual conversationId for the Unify session */
|
|
66
66
|
let unifyConversationId = null;
|
|
67
67
|
|
|
68
|
+
/** task-334-followup-batch-b: stored unsubscribe fn from VP subscribe,
|
|
69
|
+
* called on session reset to prevent stale subscriber leaks. */
|
|
70
|
+
let _vpUnsubscribe = null;
|
|
71
|
+
|
|
68
72
|
/**
|
|
69
73
|
* task-320: per-thread accumulated conversation messages for context
|
|
70
74
|
* continuity. Previously a single flat array — which cross-contaminated
|
|
@@ -127,7 +131,13 @@ function sendUnifyEvent(event) {
|
|
|
127
131
|
* `vp_updated` / `vp_removed` events to every active subscriber.
|
|
128
132
|
*/
|
|
129
133
|
export function handleUnifyVpSubscribe(_msg) {
|
|
130
|
-
|
|
134
|
+
// Unsub any prior subscription before re-subscribing to prevent duplicate
|
|
135
|
+
// handler registration on reconnect / re-subscribe.
|
|
136
|
+
if (_vpUnsubscribe) {
|
|
137
|
+
try { _vpUnsubscribe(); } catch { /* ignore */ }
|
|
138
|
+
_vpUnsubscribe = null;
|
|
139
|
+
}
|
|
140
|
+
_vpUnsubscribe = handleVpSubscribe(sendUnifyEvent);
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
/**
|
|
@@ -1520,6 +1530,11 @@ export async function resetUnifySession() {
|
|
|
1520
1530
|
try { ctrl.abort(); } catch { /* ignore */ }
|
|
1521
1531
|
}
|
|
1522
1532
|
abortByThread.clear();
|
|
1533
|
+
// Clean up VP subscriber to prevent stale sends after reset.
|
|
1534
|
+
if (_vpUnsubscribe) {
|
|
1535
|
+
try { _vpUnsubscribe(); } catch { /* ignore */ }
|
|
1536
|
+
_vpUnsubscribe = null;
|
|
1537
|
+
}
|
|
1523
1538
|
if (session) {
|
|
1524
1539
|
await session.shutdown();
|
|
1525
1540
|
session = null;
|