openvibers/openvibe.chat Commits
Respect consent for personal chat AI profiles (#513)
Changes
@@@ -379,6 +379,14 @@ async function _refreshUser(uid, maxId) {if (!dayRows.length) dayRows = await db.getChatMessagesForAi({ userId: uid, order: 'desc', limit: 80 });if (!dayRows.length) return false;++ // A personal profile is still AI training use: respect the same Network consent as the shared++ // summaries, and erase the cached profile if consent was withdrawn (or cannot be verified).++ dayRows = await consent.filterRows(dayRows);++ if (!dayRows.length) {++ await db.deleteChatAiSubjectMemory(uid);++ return false;++ }++const uname = dayRows[dayRows.length - 1].username || `user#${uid}`;const priorMemory = prior ? (prior.memory_json || '') : '';const totalSeen = (prior ? (prior.message_count || 0) : 0);@@@ -643,7 +651,7 @@ async function getAnonInsight(anonId) {}module.exports = {-- start, stop, _tick, _msgData, _topics, _people, _refreshRoom, _refreshChannel, WORKFLOW_GLOBAL,++ start, stop, _tick, _msgData, _topics, _people, _refreshRoom, _refreshChannel, _refreshUser, WORKFLOW_GLOBAL,getGlobalInsight, getUserInsight, getRelayUserInsight, getAnonInsight,WORKFLOW_PROFILE,};@@@ -1279,6 +1279,15 @@ async function getChatAiSummaries(scope, subjectId) {return await all('SELECT * FROM chat_ai_summaries WHERE scope = ? AND subject_id = ? ORDER BY "window"',[scope, subjectId || 0]);}++async function deleteChatAiSubjectMemory(subjectId) {++ const id = Number(subjectId);++ if (!Number.isSafeInteger(id) || id <= 0) return false;++ return await transaction(async () => {++ await run("DELETE FROM chat_ai_summaries WHERE scope = 'user' AND subject_id = ?", [id]);++ await run("DELETE FROM chat_timeline_events WHERE scope = 'user' AND subject_id = ?", [id]);++ return true;++ });++}// Paginated + searchable timeline browse. `before` = epoch ms (exclusive upper bound); `q`// filters label/detail; `since` = epoch ms lower bound (for period jumps). Newest first.async function getChatTimelineEvents({ scope = 'global', subjectId = 0, before = null, since = null, q = null, limit = 25 } = {}) {@@@ -1569,6 +1578,7 @@ module.exports = {// chat AI (Live's server/ai/chat-ai.js, moved): the job's selection/read helpers and the routes' readsgetChatAiSummary,getChatAiSummaries,++ deleteChatAiSubjectMemory,getChatTimelineEvents,getMaxChatMessageId,countChatMessagesSince,@@@ -221,6 +221,24 @@ t('a consent check that fails leaves everyone out (fail closed)', async () => {consent._reset();});++t('a personal summary is erased when Network consent is absent', async () => {++ const consent = require('../server/net/consent');++ consent._reset();++ await msg({ user_id: alice.id, username: 'alice', message: 'a private topic' });++ await db.upsertChatAiSummary({++ scope: 'user', subject_id: alice.id, window: 'rolling', overview: 'cached',++ memory_json: 'cached memory', timeline_json: '[{"ts":"2026-01-01 00:00:00","label":"cached"}]',++ });++ await db.addChatTimelineEvents('user', alice.id, [{ ts: '2026-01-01 00:00:00', label: 'cached' }]);++ h.consent.allowed.delete(alice.subject_id);++++ await chatAi._refreshUser(alice.id, 1);++++ assert.strictEqual(await db.getChatAiSummary('user', alice.id, 'rolling'), null);++ const events = await db.getChatTimelineEvents({ scope: 'user', subjectId: alice.id, limit: 10 });++ assert.deepStrictEqual(events, []);++});++t('GET /api/v1/rooms/global/insight answers the room shape, publicly', async () => {await db.upsertChatAiSummary({scope: 'global', subject_id: 0, window: 'global', overview: 'arrivals see this', memory_json: 'm',
@@ -379,6 +379,14 @@ async function _refreshUser(uid, maxId) {
@@ -379,6 +379,14 @@ async function _refreshUser(uid, maxId) {
if (!dayRows.length) dayRows = await db.getChatMessagesForAi({ userId: uid, order: 'desc', limit: 80 });
if (!dayRows.length) dayRows = await db.getChatMessagesForAi({ userId: uid, order: 'desc', limit: 80 });
if (!dayRows.length) return false;
if (!dayRows.length) return false;
+ // A personal profile is still AI training use: respect the same Network consent as the shared
+ // summaries, and erase the cached profile if consent was withdrawn (or cannot be verified).
+ dayRows = await consent.filterRows(dayRows);
+ if (!dayRows.length) {
+ await db.deleteChatAiSubjectMemory(uid);
+ return false;
+ }
+
const uname = dayRows[dayRows.length - 1].username || `user#${uid}`;
const uname = dayRows[dayRows.length - 1].username || `user#${uid}`;
const priorMemory = prior ? (prior.memory_json || '') : '';
const priorMemory = prior ? (prior.memory_json || '') : '';
const totalSeen = (prior ? (prior.message_count || 0) : 0);
const totalSeen = (prior ? (prior.message_count || 0) : 0);
@@ -643,7 +651,7 @@ async function getAnonInsight(anonId) {
@@ -643,7 +651,7 @@ async function getAnonInsight(anonId) {
}
}
module.exports = {
module.exports = {
- start, stop, _tick, _msgData, _topics, _people, _refreshRoom, _refreshChannel, WORKFLOW_GLOBAL,
+ start, stop, _tick, _msgData, _topics, _people, _refreshRoom, _refreshChannel, _refreshUser, WORKFLOW_GLOBAL,
getGlobalInsight, getUserInsight, getRelayUserInsight, getAnonInsight,
getGlobalInsight, getUserInsight, getRelayUserInsight, getAnonInsight,
WORKFLOW_PROFILE,
WORKFLOW_PROFILE,
};
};
@@ -1279,6 +1279,15 @@ async function getChatAiSummaries(scope, subjectId) {
@@ -1279,6 +1279,15 @@ async function getChatAiSummaries(scope, subjectId) {
return await all('SELECT * FROM chat_ai_summaries WHERE scope = ? AND subject_id = ? ORDER BY "window"',
return await all('SELECT * FROM chat_ai_summaries WHERE scope = ? AND subject_id = ? ORDER BY "window"',
[scope, subjectId || 0]);
[scope, subjectId || 0]);
}
}
+async function deleteChatAiSubjectMemory(subjectId) {
+ const id = Number(subjectId);
+ if (!Number.isSafeInteger(id) || id <= 0) return false;
+ return await transaction(async () => {
+ await run("DELETE FROM chat_ai_summaries WHERE scope = 'user' AND subject_id = ?", [id]);
+ await run("DELETE FROM chat_timeline_events WHERE scope = 'user' AND subject_id = ?", [id]);
+ return true;
+ });
+}
// Paginated + searchable timeline browse. `before` = epoch ms (exclusive upper bound); `q`
// Paginated + searchable timeline browse. `before` = epoch ms (exclusive upper bound); `q`
// filters label/detail; `since` = epoch ms lower bound (for period jumps). Newest first.
// filters label/detail; `since` = epoch ms lower bound (for period jumps). Newest first.
async function getChatTimelineEvents({ scope = 'global', subjectId = 0, before = null, since = null, q = null, limit = 25 } = {}) {
async function getChatTimelineEvents({ scope = 'global', subjectId = 0, before = null, since = null, q = null, limit = 25 } = {}) {
@@ -1569,6 +1578,7 @@ module.exports = {
@@ -1569,6 +1578,7 @@ module.exports = {
// chat AI (Live's server/ai/chat-ai.js, moved): the job's selection/read helpers and the routes' reads
// chat AI (Live's server/ai/chat-ai.js, moved): the job's selection/read helpers and the routes' reads
getChatAiSummary,
getChatAiSummary,
getChatAiSummaries,
getChatAiSummaries,
+ deleteChatAiSubjectMemory,
getChatTimelineEvents,
getChatTimelineEvents,
getMaxChatMessageId,
getMaxChatMessageId,
countChatMessagesSince,
countChatMessagesSince,
@@ -221,6 +221,24 @@ t('a consent check that fails leaves everyone out (fail closed)', async () => {
@@ -221,6 +221,24 @@ t('a consent check that fails leaves everyone out (fail closed)', async () => {
consent._reset();
consent._reset();
});
});
+t('a personal summary is erased when Network consent is absent', async () => {
+ const consent = require('../server/net/consent');
+ consent._reset();
+ await msg({ user_id: alice.id, username: 'alice', message: 'a private topic' });
+ await db.upsertChatAiSummary({
+ scope: 'user', subject_id: alice.id, window: 'rolling', overview: 'cached',
+ memory_json: 'cached memory', timeline_json: '[{"ts":"2026-01-01 00:00:00","label":"cached"}]',
+ });
+ await db.addChatTimelineEvents('user', alice.id, [{ ts: '2026-01-01 00:00:00', label: 'cached' }]);
+ h.consent.allowed.delete(alice.subject_id);
+
+ await chatAi._refreshUser(alice.id, 1);
+
+ assert.strictEqual(await db.getChatAiSummary('user', alice.id, 'rolling'), null);
+ const events = await db.getChatTimelineEvents({ scope: 'user', subjectId: alice.id, limit: 10 });
+ assert.deepStrictEqual(events, []);
+});
+
t('GET /api/v1/rooms/global/insight answers the room shape, publicly', async () => {
t('GET /api/v1/rooms/global/insight answers the room shape, publicly', async () => {
await db.upsertChatAiSummary({
await db.upsertChatAiSummary({
scope: 'global', subject_id: 0, window: 'global', overview: 'arrivals see this', memory_json: 'm',
scope: 'global', subject_id: 0, window: 'global', overview: 'arrivals see this', memory_json: 'm',