@vellumai/assistant 0.9.0-dev.202606180940.f141ea3 → 0.9.0-dev.202606181130.57d4082
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/openapi.yaml
CHANGED
|
@@ -12116,6 +12116,19 @@ paths:
|
|
|
12116
12116
|
propertyNames:
|
|
12117
12117
|
type: string
|
|
12118
12118
|
additionalProperties: {}
|
|
12119
|
+
sourceType:
|
|
12120
|
+
type: string
|
|
12121
|
+
enum:
|
|
12122
|
+
- heartbeat
|
|
12123
|
+
- memory_consolidation
|
|
12124
|
+
- schedule
|
|
12125
|
+
- auto_analysis
|
|
12126
|
+
- user
|
|
12127
|
+
- other
|
|
12128
|
+
sourceKey:
|
|
12129
|
+
type: string
|
|
12130
|
+
sourceLabel:
|
|
12131
|
+
type: string
|
|
12119
12132
|
createdAt:
|
|
12120
12133
|
type: string
|
|
12121
12134
|
required:
|
|
@@ -12296,6 +12309,19 @@ paths:
|
|
|
12296
12309
|
propertyNames:
|
|
12297
12310
|
type: string
|
|
12298
12311
|
additionalProperties: {}
|
|
12312
|
+
sourceType:
|
|
12313
|
+
type: string
|
|
12314
|
+
enum:
|
|
12315
|
+
- heartbeat
|
|
12316
|
+
- memory_consolidation
|
|
12317
|
+
- schedule
|
|
12318
|
+
- auto_analysis
|
|
12319
|
+
- user
|
|
12320
|
+
- other
|
|
12321
|
+
sourceKey:
|
|
12322
|
+
type: string
|
|
12323
|
+
sourceLabel:
|
|
12324
|
+
type: string
|
|
12299
12325
|
createdAt:
|
|
12300
12326
|
type: string
|
|
12301
12327
|
required:
|
|
@@ -12508,6 +12534,19 @@ paths:
|
|
|
12508
12534
|
propertyNames:
|
|
12509
12535
|
type: string
|
|
12510
12536
|
additionalProperties: {}
|
|
12537
|
+
sourceType:
|
|
12538
|
+
type: string
|
|
12539
|
+
enum:
|
|
12540
|
+
- heartbeat
|
|
12541
|
+
- memory_consolidation
|
|
12542
|
+
- schedule
|
|
12543
|
+
- auto_analysis
|
|
12544
|
+
- user
|
|
12545
|
+
- other
|
|
12546
|
+
sourceKey:
|
|
12547
|
+
type: string
|
|
12548
|
+
sourceLabel:
|
|
12549
|
+
type: string
|
|
12511
12550
|
createdAt:
|
|
12512
12551
|
type: string
|
|
12513
12552
|
required:
|
|
@@ -17936,6 +17975,19 @@ paths:
|
|
|
17936
17975
|
propertyNames:
|
|
17937
17976
|
type: string
|
|
17938
17977
|
additionalProperties: {}
|
|
17978
|
+
sourceType:
|
|
17979
|
+
type: string
|
|
17980
|
+
enum:
|
|
17981
|
+
- heartbeat
|
|
17982
|
+
- memory_consolidation
|
|
17983
|
+
- schedule
|
|
17984
|
+
- auto_analysis
|
|
17985
|
+
- user
|
|
17986
|
+
- other
|
|
17987
|
+
sourceKey:
|
|
17988
|
+
type: string
|
|
17989
|
+
sourceLabel:
|
|
17990
|
+
type: string
|
|
17939
17991
|
createdAt:
|
|
17940
17992
|
type: string
|
|
17941
17993
|
required:
|
package/package.json
CHANGED
package/src/api/index.ts
CHANGED
|
@@ -55,6 +55,25 @@ export const FeedItemCategorySchema = z.enum([
|
|
|
55
55
|
]);
|
|
56
56
|
export type FeedItemCategory = z.infer<typeof FeedItemCategorySchema>;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Producer of a feed item's source conversation — lets clients filter the
|
|
60
|
+
* activity feed by what generated each notification: the periodic
|
|
61
|
+
* heartbeat, a memory-consolidation pass, a recurring schedule, an
|
|
62
|
+
* auto-analysis run, etc. Derived at read time from the source
|
|
63
|
+
* conversation's `source` column (see `home/feed-source-enrichment.ts`).
|
|
64
|
+
* Individual schedules are distinguished by `sourceKey`/`sourceLabel`, not
|
|
65
|
+
* this coarse type.
|
|
66
|
+
*/
|
|
67
|
+
export const FeedItemSourceTypeSchema = z.enum([
|
|
68
|
+
"heartbeat",
|
|
69
|
+
"memory_consolidation",
|
|
70
|
+
"schedule",
|
|
71
|
+
"auto_analysis",
|
|
72
|
+
"user",
|
|
73
|
+
"other",
|
|
74
|
+
]);
|
|
75
|
+
export type FeedItemSourceType = z.infer<typeof FeedItemSourceTypeSchema>;
|
|
76
|
+
|
|
58
77
|
/**
|
|
59
78
|
* A single action button attached to a feed item.
|
|
60
79
|
*
|
|
@@ -120,6 +139,13 @@ export const FeedItemSchema = z.object({
|
|
|
120
139
|
noteworthy: z.boolean().optional(),
|
|
121
140
|
fromAssistant: z.boolean().optional(),
|
|
122
141
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
142
|
+
// Source-conversation classification, enriched at read time. `sourceKey`
|
|
143
|
+
// is the stable filter id — `schedule:<id>` for schedules so each filters
|
|
144
|
+
// separately, otherwise the `sourceType`. `sourceLabel` is the display
|
|
145
|
+
// string (a schedule's name, or a static label like "Heartbeat").
|
|
146
|
+
sourceType: FeedItemSourceTypeSchema.optional(),
|
|
147
|
+
sourceKey: z.string().optional(),
|
|
148
|
+
sourceLabel: z.string().optional(),
|
|
123
149
|
createdAt: z.string(),
|
|
124
150
|
});
|
|
125
151
|
export type FeedItem = z.infer<typeof FeedItemSchema>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { type FeedItem } from "../api/responses/home.js";
|
|
4
|
+
import {
|
|
5
|
+
classifyConversationSource,
|
|
6
|
+
enrichFeedItemsWithSource,
|
|
7
|
+
type FeedSourceEnrichmentDeps,
|
|
8
|
+
} from "./feed-source-enrichment.js";
|
|
9
|
+
|
|
10
|
+
function makeItem(partial: Partial<FeedItem> & Pick<FeedItem, "id">): FeedItem {
|
|
11
|
+
return {
|
|
12
|
+
type: "notification",
|
|
13
|
+
priority: 50,
|
|
14
|
+
summary: "summary",
|
|
15
|
+
timestamp: "2026-06-18T00:00:00.000Z",
|
|
16
|
+
createdAt: "2026-06-18T00:00:00.000Z",
|
|
17
|
+
status: "new",
|
|
18
|
+
...partial,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("classifyConversationSource", () => {
|
|
23
|
+
it("maps each known producer source to its coarse type", () => {
|
|
24
|
+
expect(classifyConversationSource("heartbeat")).toBe("heartbeat");
|
|
25
|
+
expect(classifyConversationSource("memory-retrospective")).toBe(
|
|
26
|
+
"memory_consolidation",
|
|
27
|
+
);
|
|
28
|
+
expect(classifyConversationSource("memory-retrospective-fork")).toBe(
|
|
29
|
+
"memory_consolidation",
|
|
30
|
+
);
|
|
31
|
+
expect(classifyConversationSource("schedule")).toBe("schedule");
|
|
32
|
+
expect(classifyConversationSource("auto-analysis")).toBe("auto_analysis");
|
|
33
|
+
expect(classifyConversationSource("user")).toBe("user");
|
|
34
|
+
expect(classifyConversationSource("home-feed")).toBe("user");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("falls through to 'other' for unknown / paired-delivery / empty sources", () => {
|
|
38
|
+
expect(classifyConversationSource("notification")).toBe("other");
|
|
39
|
+
expect(classifyConversationSource("something-else")).toBe("other");
|
|
40
|
+
expect(classifyConversationSource(null)).toBe("other");
|
|
41
|
+
expect(classifyConversationSource(undefined)).toBe("other");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("enrichFeedItemsWithSource", () => {
|
|
46
|
+
const deps = (
|
|
47
|
+
conv: Record<string, { source: string; scheduleJobId: string | null }>,
|
|
48
|
+
schedules: Record<string, string> = {},
|
|
49
|
+
): FeedSourceEnrichmentDeps => ({
|
|
50
|
+
getConversationRow: (id) => conv[id] ?? null,
|
|
51
|
+
getScheduleName: (id) => schedules[id] ?? null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("classifies a heartbeat item with a static label", () => {
|
|
55
|
+
const [item] = enrichFeedItemsWithSource(
|
|
56
|
+
[makeItem({ id: "n1", conversationId: "c1" })],
|
|
57
|
+
deps({ c1: { source: "heartbeat", scheduleJobId: null } }),
|
|
58
|
+
);
|
|
59
|
+
expect(item.sourceType).toBe("heartbeat");
|
|
60
|
+
expect(item.sourceKey).toBe("heartbeat");
|
|
61
|
+
expect(item.sourceLabel).toBe("Heartbeat");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("classifies a memory-consolidation item", () => {
|
|
65
|
+
const [item] = enrichFeedItemsWithSource(
|
|
66
|
+
[makeItem({ id: "n2", conversationId: "c2" })],
|
|
67
|
+
deps({ c2: { source: "memory-retrospective", scheduleJobId: null } }),
|
|
68
|
+
);
|
|
69
|
+
expect(item.sourceType).toBe("memory_consolidation");
|
|
70
|
+
expect(item.sourceKey).toBe("memory_consolidation");
|
|
71
|
+
expect(item.sourceLabel).toBe("Memory consolidation");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("gives distinct keys and names to two different schedules", () => {
|
|
75
|
+
const [a, b] = enrichFeedItemsWithSource(
|
|
76
|
+
[
|
|
77
|
+
makeItem({ id: "n3", conversationId: "cA" }),
|
|
78
|
+
makeItem({ id: "n4", conversationId: "cB" }),
|
|
79
|
+
],
|
|
80
|
+
deps(
|
|
81
|
+
{
|
|
82
|
+
cA: { source: "schedule", scheduleJobId: "sched-A" },
|
|
83
|
+
cB: { source: "schedule", scheduleJobId: "sched-B" },
|
|
84
|
+
},
|
|
85
|
+
{ "sched-A": "Morning digest", "sched-B": "Evening recap" },
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
expect(a.sourceType).toBe("schedule");
|
|
89
|
+
expect(a.sourceKey).toBe("schedule:sched-A");
|
|
90
|
+
expect(a.sourceLabel).toBe("Morning digest");
|
|
91
|
+
// scheduleId recovered from the conversation row is surfaced in metadata.
|
|
92
|
+
expect(a.metadata?.scheduleId).toBe("sched-A");
|
|
93
|
+
|
|
94
|
+
expect(b.sourceKey).toBe("schedule:sched-B");
|
|
95
|
+
expect(b.sourceLabel).toBe("Evening recap");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("prefers an explicit metadata.scheduleId over the conversation row", () => {
|
|
99
|
+
const [item] = enrichFeedItemsWithSource(
|
|
100
|
+
[
|
|
101
|
+
makeItem({
|
|
102
|
+
id: "n5",
|
|
103
|
+
conversationId: "cX",
|
|
104
|
+
metadata: { scheduleId: "from-payload" },
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
deps(
|
|
108
|
+
{ cX: { source: "schedule", scheduleJobId: "from-row" } },
|
|
109
|
+
{ "from-payload": "Payload schedule" },
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
expect(item.sourceKey).toBe("schedule:from-payload");
|
|
113
|
+
expect(item.sourceLabel).toBe("Payload schedule");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("labels a schedule with no resolvable name as 'Scheduled'", () => {
|
|
117
|
+
const [item] = enrichFeedItemsWithSource(
|
|
118
|
+
[makeItem({ id: "n6", conversationId: "cY" })],
|
|
119
|
+
deps({ cY: { source: "schedule", scheduleJobId: "gone" } }),
|
|
120
|
+
);
|
|
121
|
+
expect(item.sourceKey).toBe("schedule:gone");
|
|
122
|
+
expect(item.sourceLabel).toBe("Scheduled");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("classifies items with no source conversation as 'other'", () => {
|
|
126
|
+
const [item] = enrichFeedItemsWithSource(
|
|
127
|
+
[makeItem({ id: "n7" })],
|
|
128
|
+
deps({}),
|
|
129
|
+
);
|
|
130
|
+
expect(item.sourceType).toBe("other");
|
|
131
|
+
expect(item.sourceKey).toBe("other");
|
|
132
|
+
expect(item.sourceLabel).toBe("Other");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("resolves each distinct conversation at most once", () => {
|
|
136
|
+
const calls: string[] = [];
|
|
137
|
+
enrichFeedItemsWithSource(
|
|
138
|
+
[
|
|
139
|
+
makeItem({ id: "n8", conversationId: "dup" }),
|
|
140
|
+
makeItem({ id: "n9", conversationId: "dup" }),
|
|
141
|
+
],
|
|
142
|
+
{
|
|
143
|
+
getConversationRow: (id) => {
|
|
144
|
+
calls.push(id);
|
|
145
|
+
return { source: "heartbeat", scheduleJobId: null };
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
expect(calls).toEqual(["dup"]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-time enrichment of home-feed items with their source-conversation
|
|
3
|
+
* classification.
|
|
4
|
+
*
|
|
5
|
+
* The activity feed surfaces notifications produced by many background
|
|
6
|
+
* flows — the periodic heartbeat, memory-consolidation passes, each
|
|
7
|
+
* recurring schedule, auto-analysis runs, etc. Clients let the user filter
|
|
8
|
+
* the feed by that producer, so every item is tagged with:
|
|
9
|
+
*
|
|
10
|
+
* - `sourceType` — coarse producer category.
|
|
11
|
+
* - `sourceKey` — stable filter id (`schedule:<id>` per schedule,
|
|
12
|
+
* otherwise the `sourceType`).
|
|
13
|
+
* - `sourceLabel` — human-readable display (a schedule's name, or a
|
|
14
|
+
* static label such as "Heartbeat").
|
|
15
|
+
*
|
|
16
|
+
* The classification is derived from the source conversation's `source`
|
|
17
|
+
* column (and, for schedules, its name) rather than persisted onto the
|
|
18
|
+
* feed item. Resolving at read time keeps labels fresh across schedule
|
|
19
|
+
* renames and retroactively classifies items written before this feature
|
|
20
|
+
* existed, with no migration.
|
|
21
|
+
*
|
|
22
|
+
* The lookups are injectable so the enrichment can be unit-tested without
|
|
23
|
+
* a live database; production callers use the defaults.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
type FeedItem,
|
|
28
|
+
type FeedItemSourceType,
|
|
29
|
+
} from "../api/responses/home.js";
|
|
30
|
+
import { AUTO_ANALYSIS_SOURCE } from "../memory/auto-analysis-constants.js";
|
|
31
|
+
import { getConversation } from "../memory/conversation-crud.js";
|
|
32
|
+
import {
|
|
33
|
+
MEMORY_RETROSPECTIVE_FORK_SOURCE,
|
|
34
|
+
MEMORY_RETROSPECTIVE_SOURCE,
|
|
35
|
+
} from "../memory/memory-retrospective-constants.js";
|
|
36
|
+
import { getSchedule } from "../schedule/schedule-store.js";
|
|
37
|
+
|
|
38
|
+
/** Minimal source-conversation shape the enrichment needs. */
|
|
39
|
+
interface ConversationSourceRow {
|
|
40
|
+
source: string;
|
|
41
|
+
scheduleJobId: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FeedSourceEnrichmentDeps {
|
|
45
|
+
/** Resolve the source columns for a conversation, or null when missing. */
|
|
46
|
+
getConversationRow?: (id: string) => ConversationSourceRow | null;
|
|
47
|
+
/** Resolve a schedule's display name, or null when missing. */
|
|
48
|
+
getScheduleName?: (id: string) => string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Map a conversation's `source` column to a coarse feed source type.
|
|
53
|
+
* Mechanical mapping over the known producer sources; anything else
|
|
54
|
+
* (including the broadcaster's paired `"notification"` delivery
|
|
55
|
+
* conversations) falls through to `"other"`.
|
|
56
|
+
*/
|
|
57
|
+
export function classifyConversationSource(
|
|
58
|
+
source: string | null | undefined,
|
|
59
|
+
): FeedItemSourceType {
|
|
60
|
+
switch (source) {
|
|
61
|
+
case "heartbeat":
|
|
62
|
+
return "heartbeat";
|
|
63
|
+
case MEMORY_RETROSPECTIVE_SOURCE:
|
|
64
|
+
case MEMORY_RETROSPECTIVE_FORK_SOURCE:
|
|
65
|
+
return "memory_consolidation";
|
|
66
|
+
case "schedule":
|
|
67
|
+
return "schedule";
|
|
68
|
+
case AUTO_ANALYSIS_SOURCE:
|
|
69
|
+
return "auto_analysis";
|
|
70
|
+
case "user":
|
|
71
|
+
case "home-feed":
|
|
72
|
+
return "user";
|
|
73
|
+
default:
|
|
74
|
+
return "other";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Static display labels for non-schedule source types. */
|
|
79
|
+
const STATIC_SOURCE_LABELS: Record<
|
|
80
|
+
Exclude<FeedItemSourceType, "schedule">,
|
|
81
|
+
string
|
|
82
|
+
> = {
|
|
83
|
+
heartbeat: "Heartbeat",
|
|
84
|
+
memory_consolidation: "Memory consolidation",
|
|
85
|
+
auto_analysis: "Auto-analysis",
|
|
86
|
+
user: "Conversation",
|
|
87
|
+
other: "Other",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function defaultGetConversationRow(id: string): ConversationSourceRow | null {
|
|
91
|
+
try {
|
|
92
|
+
const row = getConversation(id);
|
|
93
|
+
return row
|
|
94
|
+
? { source: row.source, scheduleJobId: row.scheduleJobId }
|
|
95
|
+
: null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defaultGetScheduleName(id: string): string | null {
|
|
102
|
+
try {
|
|
103
|
+
return getSchedule(id)?.name ?? null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Return a copy of each feed item enriched with `sourceType`, `sourceKey`,
|
|
111
|
+
* and `sourceLabel`. Conversation and schedule lookups are memoized across
|
|
112
|
+
* the batch so each distinct source is resolved at most once.
|
|
113
|
+
*/
|
|
114
|
+
export function enrichFeedItemsWithSource(
|
|
115
|
+
items: FeedItem[],
|
|
116
|
+
deps: FeedSourceEnrichmentDeps = {},
|
|
117
|
+
): FeedItem[] {
|
|
118
|
+
if (items.length === 0) return items;
|
|
119
|
+
|
|
120
|
+
const getConversationRow =
|
|
121
|
+
deps.getConversationRow ?? defaultGetConversationRow;
|
|
122
|
+
const getScheduleName = deps.getScheduleName ?? defaultGetScheduleName;
|
|
123
|
+
|
|
124
|
+
const convCache = new Map<string, ConversationSourceRow | null>();
|
|
125
|
+
const resolveConv = (id: string): ConversationSourceRow | null => {
|
|
126
|
+
if (!convCache.has(id)) convCache.set(id, getConversationRow(id));
|
|
127
|
+
return convCache.get(id) ?? null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const scheduleNameCache = new Map<string, string | null>();
|
|
131
|
+
const resolveScheduleName = (id: string): string | null => {
|
|
132
|
+
if (!scheduleNameCache.has(id)) {
|
|
133
|
+
scheduleNameCache.set(id, getScheduleName(id));
|
|
134
|
+
}
|
|
135
|
+
return scheduleNameCache.get(id) ?? null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return items.map((item) => {
|
|
139
|
+
const row = item.conversationId ? resolveConv(item.conversationId) : null;
|
|
140
|
+
const sourceType = classifyConversationSource(row?.source);
|
|
141
|
+
|
|
142
|
+
const metadataScheduleId =
|
|
143
|
+
typeof item.metadata?.scheduleId === "string"
|
|
144
|
+
? item.metadata.scheduleId
|
|
145
|
+
: undefined;
|
|
146
|
+
const scheduleId = metadataScheduleId ?? row?.scheduleJobId ?? undefined;
|
|
147
|
+
|
|
148
|
+
let sourceKey: string;
|
|
149
|
+
let sourceLabel: string;
|
|
150
|
+
if (sourceType === "schedule" && scheduleId) {
|
|
151
|
+
sourceKey = `schedule:${scheduleId}`;
|
|
152
|
+
sourceLabel = resolveScheduleName(scheduleId) ?? "Scheduled";
|
|
153
|
+
} else if (sourceType === "schedule") {
|
|
154
|
+
sourceKey = "schedule";
|
|
155
|
+
sourceLabel = "Scheduled";
|
|
156
|
+
} else {
|
|
157
|
+
sourceKey = sourceType;
|
|
158
|
+
sourceLabel = STATIC_SOURCE_LABELS[sourceType];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Surface the scheduleId in metadata when it was recovered from the
|
|
162
|
+
// source conversation so clients have a single place to read it.
|
|
163
|
+
const metadata =
|
|
164
|
+
scheduleId !== undefined && metadataScheduleId === undefined
|
|
165
|
+
? { ...(item.metadata ?? {}), scheduleId }
|
|
166
|
+
: item.metadata;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
...item,
|
|
170
|
+
sourceType,
|
|
171
|
+
sourceKey,
|
|
172
|
+
sourceLabel,
|
|
173
|
+
...(metadata ? { metadata } : {}),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type FeedItemStatus,
|
|
29
29
|
HomeFeedResponseSchema,
|
|
30
30
|
} from "../../api/responses/home.js";
|
|
31
|
+
import { enrichFeedItemsWithSource } from "../../home/feed-source-enrichment.js";
|
|
31
32
|
import { patchFeedItemStatus, readHomeFeed } from "../../home/feed-writer.js";
|
|
32
33
|
import { revalidateHomeContentInBackground } from "../../home/home-content-refresh.js";
|
|
33
34
|
import { getPersonalizedGreeting } from "../../home/home-greeting.js";
|
|
@@ -135,8 +136,10 @@ export async function handleGetHomeFeed({
|
|
|
135
136
|
// v2 schema dropped per-item `minTimeAway` gating; surface every item
|
|
136
137
|
// and let the client decide what to render based on its own
|
|
137
138
|
// session state. `timeAwaySeconds` survives only to feed the
|
|
138
|
-
// context-banner relative-time label.
|
|
139
|
-
|
|
139
|
+
// context-banner relative-time label. Each item is enriched with its
|
|
140
|
+
// source-conversation classification (`sourceType`/`sourceKey`/
|
|
141
|
+
// `sourceLabel`) so clients can filter the feed by what produced it.
|
|
142
|
+
const filtered = enrichFeedItemsWithSource(feed.items);
|
|
140
143
|
|
|
141
144
|
const now = new Date();
|
|
142
145
|
|
|
@@ -287,7 +290,9 @@ export function handleListHomeFeed({
|
|
|
287
290
|
const total = filtered.length;
|
|
288
291
|
const offset = params.offset ?? 0;
|
|
289
292
|
const limit = params.limit ?? 20;
|
|
290
|
-
const items =
|
|
293
|
+
const items = enrichFeedItemsWithSource(
|
|
294
|
+
filtered.slice(offset, offset + limit),
|
|
295
|
+
);
|
|
291
296
|
|
|
292
297
|
return {
|
|
293
298
|
items,
|