@noodleseed/one 0.176.0 → 0.178.0

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 (73) hide show
  1. package/dist/commands/solutions-activity.d.ts.map +1 -1
  2. package/dist/commands/solutions-activity.js +9 -35
  3. package/dist/commands/solutions-activity.js.map +1 -1
  4. package/dist/commands/solutions-conversations.d.ts +6 -0
  5. package/dist/commands/solutions-conversations.d.ts.map +1 -0
  6. package/dist/commands/solutions-conversations.js +191 -0
  7. package/dist/commands/solutions-conversations.js.map +1 -0
  8. package/dist/commands/solutions-history.d.ts +9 -0
  9. package/dist/commands/solutions-history.d.ts.map +1 -0
  10. package/dist/commands/solutions-history.js +166 -0
  11. package/dist/commands/solutions-history.js.map +1 -0
  12. package/dist/commands/solutions-ops.d.ts.map +1 -1
  13. package/dist/commands/solutions-ops.js +6 -0
  14. package/dist/commands/solutions-ops.js.map +1 -1
  15. package/dist/commands/whatsapp-ops.d.ts.map +1 -1
  16. package/dist/commands/whatsapp-ops.js +0 -7
  17. package/dist/commands/whatsapp-ops.js.map +1 -1
  18. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  19. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +14 -0
  20. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +6 -0
  22. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +5 -2
  23. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  24. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +18 -0
  25. package/node_modules/@noodle-borg/authoring/dist/assistant.js +14 -1
  26. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +0 -3
  27. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-conversations.js +142 -0
  28. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-history.js +82 -0
  29. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +5 -42
  30. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +8 -0
  31. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +7 -0
  32. package/node_modules/@noodle-borg/service/dist/application-activity.js +76 -21
  33. package/node_modules/@noodle-borg/service/dist/application-history-settings.js +108 -0
  34. package/node_modules/@noodle-borg/service/dist/assistant-composition.js +11 -2
  35. package/node_modules/@noodle-borg/service/dist/channels/composition.js +10 -1
  36. package/node_modules/@noodle-borg/service/dist/channels/provider.js +40 -4
  37. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +31 -5
  38. package/node_modules/@noodle-borg/service/dist/conversation-history/capture.js +63 -7
  39. package/node_modules/@noodle-borg/service/dist/conversation-history/contracts.js +13 -0
  40. package/node_modules/@noodle-borg/service/dist/conversation-history/customer.js +118 -0
  41. package/node_modules/@noodle-borg/service/dist/conversation-history/operator.js +293 -0
  42. package/node_modules/@noodle-borg/service/dist/routes/assistant-conversations.js +52 -0
  43. package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +6 -0
  44. package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +3 -0
  45. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +2 -1
  46. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +6 -11
  47. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +6 -0
  48. package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +16 -0
  49. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +5 -17
  50. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +6 -5
  51. package/node_modules/@noodle-borg/service/dist/routes/business-information-conversations.js +90 -0
  52. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +7 -2
  53. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +20 -2
  54. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +4 -0
  55. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +0 -10
  56. package/node_modules/@noodle-borg/service/dist/service-history-projections.js +52 -0
  57. package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +32 -0
  58. package/node_modules/@noodle-borg/service/dist/service.js +6 -9
  59. package/node_modules/@noodle-borg/service/package.json +1 -1
  60. package/node_modules/@noodle-borg/wire-contracts/dist/application-activity.d.ts +0 -28
  61. package/node_modules/@noodle-borg/wire-contracts/dist/application-activity.js +0 -18
  62. package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.d.ts +190 -0
  63. package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.js +92 -0
  64. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +3 -0
  65. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +6 -0
  66. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.d.ts +523 -0
  67. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.js +192 -0
  68. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +2 -0
  69. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +2 -0
  70. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +2 -2
  71. package/node_modules/@noodleseed/assistant/README.md +19 -0
  72. package/node_modules/@noodleseed/assistant/package.json +1 -1
  73. package/package.json +2 -2
@@ -0,0 +1,293 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { canonicalJson } from '@noodle-borg/compiler';
3
+ import { ConversationExportResponseSchema, ConversationForgetRequestSchema, ConversationForgetResponseSchema, ConversationIdSchema, ConversationListResponseSchema, ConversationReviewRequestSchema, ConversationReviewStatusSchema, ConversationShowResponseSchema, } from '@noodle-borg/wire-contracts';
4
+ import { maskPaymentCards } from '../payment-card.js';
5
+ import { CONVERSATION_DAY_MS, CONVERSATION_NOTE_LIMIT, } from './contracts.js';
6
+ const runLocal = (operation) => operation();
7
+ const MESSAGES = {
8
+ conversation_invalid: 'Invalid conversation request.',
9
+ conversation_not_found: 'Conversation not found.',
10
+ conversation_unavailable: 'Conversation history is unavailable.',
11
+ };
12
+ export class ConversationHistoryError extends Error {
13
+ code;
14
+ constructor(code) {
15
+ super(MESSAGES[code]);
16
+ this.code = code;
17
+ }
18
+ }
19
+ const PAGE = { list: { default: 50, maximum: 100 }, export: { default: 25, maximum: 25 } };
20
+ /**
21
+ * The staff projection of conversation history (ADR 0241). Callers authorize before and after, and every
22
+ * read, review, note, export and forget returns identifier-only audit details; message and note text
23
+ * never reach audit.
24
+ */
25
+ export class ApplicationConversations {
26
+ options;
27
+ constructor(options) {
28
+ this.options = options;
29
+ }
30
+ async project(scope, action, input, local = runLocal) {
31
+ const tenant = { org: scope.org, app: scope.app, env: scope.env };
32
+ const allowed = action === 'list' || action === 'export' ? ['limit', 'cursor', 'channel', 'status'] : [];
33
+ const parameters = new Map();
34
+ for (const [key, value] of input.parameters) {
35
+ if (!allowed.includes(key) || parameters.has(key))
36
+ throw invalid();
37
+ parameters.set(key, value);
38
+ }
39
+ const audit = (eventType, details) => ({
40
+ eventType,
41
+ details,
42
+ });
43
+ if (action === 'review') {
44
+ const id = ConversationIdSchema.safeParse(input.conversationId);
45
+ if (!id.success)
46
+ throw new ConversationHistoryError('conversation_not_found');
47
+ const parsed = ConversationReviewRequestSchema.safeParse(input.body);
48
+ if (!parsed.success || (parsed.data.note !== undefined && !input.actor))
49
+ throw invalid();
50
+ const { reviewStatus, note } = parsed.data;
51
+ const now = this.#now();
52
+ const notBefore = await this.#notBefore(tenant, now);
53
+ const conversation = await local(async () => {
54
+ const current = await this.options.store.read(tenant, id.data, now, notBefore);
55
+ if (!current)
56
+ throw new ConversationHistoryError('conversation_not_found');
57
+ if (note !== undefined && current.notes.length >= CONVERSATION_NOTE_LIMIT)
58
+ throw invalid();
59
+ if (reviewStatus !== undefined)
60
+ await this.options.store.setReviewStatus(tenant, id.data, reviewStatus);
61
+ if (note !== undefined)
62
+ await this.options.store.addNote(tenant, id.data, {
63
+ author: String(input.actor),
64
+ text: maskPaymentCards(note),
65
+ at: now,
66
+ });
67
+ return this.options.store.read(tenant, id.data, now, notBefore);
68
+ });
69
+ if (!conversation)
70
+ throw new ConversationHistoryError('conversation_not_found');
71
+ return {
72
+ response: ConversationShowResponseSchema.parse({
73
+ ok: true,
74
+ data: { conversation: shown(conversation, now - notBefore) },
75
+ }),
76
+ audits: [
77
+ ...(reviewStatus === undefined
78
+ ? []
79
+ : [audit('conversation.reviewed', { conversationId: id.data, reviewStatus })]),
80
+ ...(note === undefined ? [] : [audit('conversation.noted', { conversationId: id.data })]),
81
+ ],
82
+ };
83
+ }
84
+ if (action === 'forget') {
85
+ const parsed = ConversationForgetRequestSchema.safeParse(input.body);
86
+ if (!parsed.success)
87
+ throw invalid();
88
+ const request = parsed.data;
89
+ // A WhatsApp participant's working memory goes with their history (ADR 0241); a web session
90
+ // expires on its own, so a customer has none to erase.
91
+ const { forgotten, workingMemory } = await local(async () => {
92
+ if ('conversationId' in request)
93
+ return {
94
+ forgotten: await this.options.store.forget(tenant, request.conversationId),
95
+ workingMemory: false,
96
+ };
97
+ const erased = await this.options.store.forgetSubject(tenant, request.subject);
98
+ return {
99
+ forgotten: erased,
100
+ workingMemory: request.subject.kind === 'participant' &&
101
+ (await input.forgetWorkingMemory?.(tenant, request.subject.ref, String(input.actor))) === true,
102
+ };
103
+ });
104
+ return {
105
+ response: ConversationForgetResponseSchema.parse({ ok: true, data: { forgotten } }),
106
+ audits: [
107
+ audit('conversation.forgotten', {
108
+ conversations: forgotten.conversations,
109
+ items: forgotten.items,
110
+ kind: 'conversationId' in request ? 'conversation' : request.subject.kind,
111
+ workingMemory,
112
+ }),
113
+ ],
114
+ };
115
+ }
116
+ if (action === 'show') {
117
+ const id = ConversationIdSchema.safeParse(input.conversationId);
118
+ if (!id.success)
119
+ throw new ConversationHistoryError('conversation_not_found');
120
+ const now = this.#now();
121
+ const notBefore = await this.#notBefore(tenant, now);
122
+ const conversation = await local(() => this.options.store.read(tenant, id.data, now, notBefore));
123
+ if (!conversation)
124
+ throw new ConversationHistoryError('conversation_not_found');
125
+ return {
126
+ response: ConversationShowResponseSchema.parse({
127
+ ok: true,
128
+ data: { conversation: shown(conversation, now - notBefore) },
129
+ }),
130
+ audits: [audit('conversation.read', { conversationId: id.data })],
131
+ };
132
+ }
133
+ const page = await this.#page(scope, tenant, action, parameters, local);
134
+ if (action === 'list')
135
+ return {
136
+ response: ConversationListResponseSchema.parse({
137
+ ok: true,
138
+ data: {
139
+ conversations: page.summaries.map(summary),
140
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
141
+ },
142
+ }),
143
+ audits: [],
144
+ };
145
+ const conversations = await local(async () => {
146
+ const read = await Promise.all(page.summaries.map((row) => this.options.store.read(tenant, row.id, page.now, page.notBefore)));
147
+ return read
148
+ .filter((row) => row !== undefined)
149
+ .map((row) => detail(row, page.now - page.notBefore));
150
+ });
151
+ return {
152
+ response: ConversationExportResponseSchema.parse({
153
+ ok: true,
154
+ data: {
155
+ conversations,
156
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
157
+ },
158
+ }),
159
+ audits: [audit('conversation.exported', { count: conversations.length })],
160
+ };
161
+ }
162
+ async #page(scope, tenant, purpose, parameters, local) {
163
+ const bounds = PAGE[purpose];
164
+ const limit = parameters.has('limit') ? Number(parameters.get('limit')) : bounds.default;
165
+ const channel = parameters.get('channel');
166
+ const status = parameters.get('status');
167
+ const cursor = parameters.get('cursor');
168
+ if (!Number.isInteger(limit) ||
169
+ limit < 1 ||
170
+ limit > bounds.maximum ||
171
+ (channel !== undefined && channel !== 'website' && channel !== 'whatsapp') ||
172
+ (status !== undefined && !ConversationReviewStatusSchema.safeParse(status).success) ||
173
+ (cursor !== undefined && (!cursor || cursor.length > 2048)))
174
+ throw invalid();
175
+ const binding = this.#hash({
176
+ purpose: `conversations-${purpose}-v1`,
177
+ scope,
178
+ limit,
179
+ channel: channel ?? null,
180
+ status: status ?? null,
181
+ });
182
+ const after = decodeConversationCursor(cursor, binding);
183
+ const now = this.#now();
184
+ const notBefore = await this.#notBefore(tenant, now);
185
+ const rows = await local(() => this.options.store.list(tenant, {
186
+ now,
187
+ notBefore,
188
+ limit: limit + 1,
189
+ ...(after ? { after } : {}),
190
+ ...(channel ? { channel: channel } : {}),
191
+ ...(status ? { reviewStatus: status } : {}),
192
+ }));
193
+ const summaries = rows.slice(0, limit);
194
+ const last = summaries.at(-1);
195
+ return {
196
+ now,
197
+ notBefore,
198
+ summaries,
199
+ nextCursor: rows.length > limit && last ? encodeConversationCursor(binding, last) : undefined,
200
+ };
201
+ }
202
+ #now() {
203
+ return this.options.now?.() ?? Date.now();
204
+ }
205
+ /**
206
+ * Oldest visible item time under the live window. Items the window hides are not deleted: they purge
207
+ * at their stamped expiry, so restoring a longer plan before then shows them again. That recovers
208
+ * nothing deleted, which is all the business history contract forbids.
209
+ */
210
+ async #notBefore(tenant, now) {
211
+ const policy = await this.options.policy(tenant).catch(() => undefined);
212
+ if (!policy || !Number.isInteger(policy.maximumDays))
213
+ throw new ConversationHistoryError('conversation_unavailable');
214
+ return conversationReadBound(policy, now);
215
+ }
216
+ #hash(value) {
217
+ return createHmac('sha256', this.options.identityKey)
218
+ .update(canonicalJson(value))
219
+ .digest('hex');
220
+ }
221
+ }
222
+ function invalid() {
223
+ return new ConversationHistoryError('conversation_invalid');
224
+ }
225
+ function summary(row) {
226
+ return {
227
+ id: row.id,
228
+ channel: row.channel,
229
+ subject: row.subject.kind === 'anonymous' ? { kind: 'anonymous' } : row.subject,
230
+ startedAt: new Date(row.startedAt).toISOString(),
231
+ lastMessageAt: new Date(row.lastMessageAt).toISOString(),
232
+ itemCount: row.itemCount,
233
+ reviewStatus: row.reviewStatus,
234
+ };
235
+ }
236
+ /**
237
+ * The last visible item's effective removal: its stored expiry, or sooner where the live window of
238
+ * `windowMs` hides it first.
239
+ */
240
+ function removalAt(conversation, windowMs) {
241
+ return Math.max(...conversation.items.map((item) => Math.min(item.expiresAt, item.at + windowMs)));
242
+ }
243
+ function shown(conversation, windowMs) {
244
+ return {
245
+ ...detail(conversation, windowMs),
246
+ notes: conversation.notes.map((note) => ({
247
+ author: note.author,
248
+ text: note.text,
249
+ at: new Date(note.at).toISOString(),
250
+ })),
251
+ };
252
+ }
253
+ function detail(conversation, windowMs) {
254
+ return {
255
+ ...summary({ ...conversation, itemCount: conversation.items.length }),
256
+ expiresAt: new Date(removalAt(conversation, windowMs)).toISOString(),
257
+ items: conversation.items.map((item) => item.kind === 'message'
258
+ ? { kind: item.kind, role: item.role, text: item.text, at: new Date(item.at).toISOString() }
259
+ : {
260
+ kind: item.kind,
261
+ interactionId: item.interactionId,
262
+ tool: item.tool,
263
+ status: item.status,
264
+ at: new Date(item.at).toISOString(),
265
+ }),
266
+ };
267
+ }
268
+ /** Oldest visible item time under a policy's live window; Off hides every item. */
269
+ export function conversationReadBound(policy, now) {
270
+ const days = Math.max(0, Math.min(policy.conversationDays ?? 0, policy.maximumDays));
271
+ return days === 0 ? now + 1 : now - days * CONVERSATION_DAY_MS;
272
+ }
273
+ /** An opaque keyset position bound to the purpose, scope and page shape that `binding` hashes. */
274
+ export function encodeConversationCursor(binding, last) {
275
+ return Buffer.from(JSON.stringify({ binding, lastMessageAt: last.lastMessageAt, id: last.id })).toString('base64url');
276
+ }
277
+ export function decodeConversationCursor(raw, binding) {
278
+ if (raw === undefined)
279
+ return undefined;
280
+ try {
281
+ const value = JSON.parse(Buffer.from(raw, 'base64url').toString());
282
+ if (value.binding !== binding ||
283
+ !Number.isSafeInteger(value.lastMessageAt) ||
284
+ value.lastMessageAt < 0 ||
285
+ !ConversationIdSchema.safeParse(value.id).success)
286
+ throw new Error();
287
+ return { lastMessageAt: value.lastMessageAt, id: value.id };
288
+ }
289
+ catch {
290
+ throw invalid();
291
+ }
292
+ }
293
+ //# sourceMappingURL=operator.js.map
@@ -0,0 +1,52 @@
1
+ import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
2
+ import { ConversationHistoryError } from '../conversation-history/operator.js';
3
+ import { sendUnauthorized } from '../http-util.js';
4
+ import { basicCredentials } from './assistant-route-http.js';
5
+ /**
6
+ * Backend-only conversation routes for an embed client (ADR 0241 decision 12). They authenticate
7
+ * exactly like the session exchange and never read a tenant from the body; there is no browser CORS
8
+ * surface, because the client secret must never reach a browser.
9
+ */
10
+ export async function handleAssistantConversations(req, res, action, deps) {
11
+ if (req.method !== 'POST') {
12
+ res.setHeader('allow', 'POST');
13
+ return sendJson(res, 405, { error: 'method not allowed' });
14
+ }
15
+ const credentials = basicCredentials(req);
16
+ if (!credentials)
17
+ return sendUnauthorized(res, 'invalid assistant client');
18
+ const client = await deps.store.authenticateClient(credentials.id, credentials.secret);
19
+ if (!client)
20
+ return sendUnauthorized(res, 'invalid assistant client');
21
+ res.setHeader('cache-control', 'private, no-store');
22
+ const body = await readJsonBody(req, deps.maxBody);
23
+ if (!body.ok)
24
+ return sendJson(res, body.status, { error: body.error });
25
+ try {
26
+ const conversations = deps.customerConversations;
27
+ if (!conversations)
28
+ throw new ConversationHistoryError('conversation_unavailable');
29
+ const owner = { id: client.id, tenant: client.tenant };
30
+ const result = action === 'list'
31
+ ? await conversations.list(owner, body.value)
32
+ : await conversations.forgetUser(owner, body.value);
33
+ await deps.audit.emit({
34
+ ...result.audit,
35
+ org: client.tenant.org,
36
+ app: client.tenant.app,
37
+ env: client.tenant.env,
38
+ decision: 'allow',
39
+ actorSubject: client.id,
40
+ });
41
+ sendJson(res, 200, result.response);
42
+ }
43
+ catch (error) {
44
+ if (!(error instanceof ConversationHistoryError))
45
+ throw error;
46
+ sendJson(res, error.code === 'conversation_unavailable' ? 503 : 400, {
47
+ code: error.code,
48
+ error: error.message,
49
+ });
50
+ }
51
+ }
52
+ //# sourceMappingURL=assistant-conversations.js.map
@@ -1,6 +1,7 @@
1
1
  import { handleAssistantAppRequest, handleAssistantPreflight, handleAssistantSession, handleAssistantTurn, handleConsoleApprovalNonce, } from './assistant.js';
2
2
  import { handleAssistantAppearance } from './assistant-appearance.js';
3
3
  import { handleAssistantClients } from './assistant-clients.js';
4
+ import { handleAssistantConversations } from './assistant-conversations.js';
4
5
  import { handleAssistantDoctor } from './assistant-doctor.js';
5
6
  import { handleAssistantEmbedScript } from './assistant-embed-script.js';
6
7
  import { handleAssistantEmbeds } from './assistant-embeds.js';
@@ -60,6 +61,11 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
60
61
  if (url.pathname === '/v1/assistant/sessions' && req.method === 'POST') {
61
62
  return run(req, res, deps, () => handleAssistantSession(req, res, deps));
62
63
  }
64
+ const conversationsMatch = /^\/v1\/assistant\/conversations\/(list|forget-user)$/.exec(url.pathname);
65
+ if (conversationsMatch?.[1] === 'list' || conversationsMatch?.[1] === 'forget-user') {
66
+ const action = conversationsMatch[1];
67
+ return run(req, res, deps, () => handleAssistantConversations(req, res, action, deps));
68
+ }
63
69
  if (url.pathname === '/v1/assistant/turns' && req.method === 'POST') {
64
70
  return run(req, res, deps, () => handleAssistantTurn(req, res, deps));
65
71
  }
@@ -3,6 +3,7 @@ import { anonymousBehavior } from '@noodle-borg/compiler';
3
3
  import { claimableStateHandleNames } from '@noodle-borg/runtime';
4
4
  import { sendJson } from '@noodle-borg/transport-http';
5
5
  import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
6
+ import { sessionHistoryNotice, } from '../conversation-history/capture.js';
6
7
  function isMixed(surface) {
7
8
  return surface?.mode === 'mixed';
8
9
  }
@@ -147,6 +148,8 @@ export async function elevateAssistantSession(res, deps, input) {
147
148
  // The armed hint: the widget answers with one { resume: true } turn on the turns endpoint.
148
149
  ...(elevated.resumeArmed ? { resume: { tool: elevated.tool } } : {}),
149
150
  continuedAfterAuthentication: true,
151
+ // The caller is now verified, so the notice states the signed-in customers' window.
152
+ ...(await sessionHistoryNotice(deps.conversations, input.client.tenant, 'signed_in_customers', input.surfaceHistory)),
150
153
  };
151
154
  assistantSessionResponseSchema.parse(body);
152
155
  return sendJson(res, 200, body);
@@ -1,4 +1,5 @@
1
1
  import { assistantConfirmationProposal } from '@noodle-borg/assistant-gateway/portable';
2
+ import { sessionSurfaceHistory } from '../conversation-history/capture.js';
2
3
  import { narrateInteractionResolution } from './assistant-agent.js';
3
4
  /** Stream the optional post-resolution narration without weakening the durable structured result. */
4
5
  export async function narrateResolvedInteraction(res, deps, target, session, tool, action, result, context, suggestions = false) {
@@ -10,7 +11,7 @@ export async function narrateResolvedInteraction(res, deps, target, session, too
10
11
  { role: 'assistant', content: generated.narration, kind: 'visible' },
11
12
  ];
12
13
  await deps.store.appendHistory(session.id, rows);
13
- await deps.conversations?.recordSessionTurn(session, rows);
14
+ await deps.conversations?.recordSessionTurn(session, rows, sessionSurfaceHistory(target.served.artifact.server.assistant, session));
14
15
  }
15
16
  if (generated.suggestions.length > 0) {
16
17
  await deps.store.replaceLatestSuggestions(session.id, {
@@ -1,6 +1,7 @@
1
1
  import { AssistantInteractionCapacityError, assistantConfirmationReview, assistantConfirmationTransition, assistantInputTransition, assistantInteractionTool, assistantPreparedArgumentReview, assistantPreparedToolContinuation, assistantSafeOutput, assistantToolContinuation, assistantViewAvailableData, auditAssistantInteraction, boundedAssistantExecution, recoverableAssistantView, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
2
2
  import { executePreparedTool, executeToolInteractive, hasEphemeralEvidence, resumeTool, resumeToolPreparation, } from '@noodle-borg/runtime';
3
3
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
4
+ import { sessionSurfaceHistory } from '../conversation-history/capture.js';
4
5
  import { applyBrowserCors, authenticateSession, now, } from './assistant.js';
5
6
  import { replayOrConflict, writeUnknownOutcome } from './assistant-interaction-replay.js';
6
7
  import { parseInteractionResponse, validateInputContent } from './assistant-interaction-request.js';
@@ -340,11 +341,7 @@ async function finishExecution(res, deps, session, interaction, context, result,
340
341
  status: 'failed',
341
342
  reasonCode: code,
342
343
  });
343
- await deps.conversations?.recordSessionOutcome(session, {
344
- interactionId: interaction.id,
345
- tool: toolName,
346
- status: 'failed',
347
- });
344
+ await deps.conversations?.recordSessionOutcome(session, { interactionId: interaction.id, tool: toolName, status: 'failed' }, sessionSurfaceHistory(target.served.artifact.server.assistant, session));
348
345
  writeInteractionSseHeaders(res, session.origin);
349
346
  writeInteractionResolved(res, interaction.id, 'accept');
350
347
  writeInteractionError(res, code, false);
@@ -375,11 +372,7 @@ async function finishExecution(res, deps, session, interaction, context, result,
375
372
  kind: 'narration',
376
373
  },
377
374
  ]);
378
- await deps.conversations?.recordSessionOutcome(session, {
379
- interactionId: interaction.id,
380
- tool: toolName,
381
- status: 'succeeded',
382
- });
375
+ await deps.conversations?.recordSessionOutcome(session, { interactionId: interaction.id, tool: toolName, status: 'succeeded' }, sessionSurfaceHistory(target.served.artifact.server.assistant, session));
383
376
  writeInteractionSseHeaders(res, session.origin);
384
377
  writeInteractionResolved(res, interaction.id, 'accept');
385
378
  writeToolCompleted(res, interaction.id, toolName, safeOutput);
@@ -463,7 +456,9 @@ async function resolveStop(req, res, deps, session, interaction, action, suggest
463
456
  interactionId: interaction.id,
464
457
  tool: toolName,
465
458
  status: action === 'decline' ? 'declined' : 'cancelled',
466
- });
459
+ },
460
+ // A vanished target leaves the surface unreadable, which records nothing (fail closed).
461
+ sessionSurfaceHistory(target?.served.artifact.server.assistant, session));
467
462
  writeInteractionSseHeaders(res, session.origin);
468
463
  writeInteractionResolved(res, interaction.id, action);
469
464
  if (target && context) {
@@ -4,6 +4,7 @@ import { effectiveAssistantBrowserConfiguration, mintPublicSession, publicEmbedT
4
4
  import { assistantSessionUsageRequestEvent, captureAssistantUsage, captureRefusedAssistantSession, } from '@noodle-borg/observability';
5
5
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
6
6
  import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
7
+ import { sessionHistoryNotice, websiteSurfaceHistory, } from '../conversation-history/capture.js';
7
8
  import { applyBrowserCors, assistantSessionEndpoints, now } from './assistant-route-http.js';
8
9
  import { activeAssistantTarget } from './assistant-session-target.js';
9
10
  /**
@@ -35,6 +36,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
35
36
  let configuration;
36
37
  let sponsoredBudget;
37
38
  let usageSession;
39
+ let surfaceHistory = { historyDisabled: true };
38
40
  const result = await mintPublicSession({ embedId, visitorId, origin, ...(addressBucket ? { addressBucket } : {}) }, deps.admissionEnvelope ?? ADMISSION_DEFAULTS, {
39
41
  embeds,
40
42
  counters,
@@ -59,6 +61,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
59
61
  !publicSurfaceOf(target.served.artifact.server.assistant)?.origins.includes(input.origin))
60
62
  throw new Error('assistant surface changed mid-mint');
61
63
  const current = now(deps);
64
+ surfaceHistory = websiteSurfaceHistory(target.served.artifact.server.assistant, 'public');
62
65
  configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(input.embed), deps.appearance, 'public', target.businessNotice)).effective;
63
66
  const created = await deps.store.createSession({
64
67
  // A public surface has no client credential, so the embed id stands in as the minting
@@ -99,6 +102,9 @@ export async function handlePublicAssistantSession(req, res, deps) {
99
102
  expiresAt: result.expiresAt,
100
103
  endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
101
104
  ...(configuration ? { configuration } : {}),
105
+ ...(usageSession
106
+ ? await sessionHistoryNotice(deps.conversations, usageSession.tenant, 'website_visitors', surfaceHistory)
107
+ : {}),
102
108
  };
103
109
  // Parse rather than trust: every published @noodleseed/assistant widget consumes this shape, and the
104
110
  // public mint must not be the path that quietly diverges from it (ADR 0151).
@@ -60,4 +60,20 @@ export function assistantSessionEndpoints(base) {
60
60
  suggestions: `${base}/v1/assistant/suggestions`,
61
61
  };
62
62
  }
63
+ /** HTTP Basic embed-client credentials for the backend-only assistant routes. */
64
+ export function basicCredentials(req) {
65
+ const match = /^Basic\s+(.+)$/i.exec(req.headers.authorization ?? '');
66
+ if (!match?.[1])
67
+ return undefined;
68
+ try {
69
+ const decoded = Buffer.from(match[1], 'base64').toString('utf8');
70
+ const index = decoded.indexOf(':');
71
+ return index > 0
72
+ ? { id: decoded.slice(0, index), secret: decoded.slice(index + 1) }
73
+ : undefined;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
63
79
  //# sourceMappingURL=assistant-route-http.js.map
@@ -8,11 +8,12 @@ import { mapExecutionError, mapResourceContents, mapResourcesList, mapToolOutput
8
8
  import { executeResource } from '@noodle-borg/runtime';
9
9
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
10
10
  import { assistantMessageTurnRequestSchema, assistantResumeTurnRequestSchema, assistantSessionResponseSchema, } from '@noodle-borg/wire-contracts';
11
+ import { sessionHistoryNotice, sessionSurfaceHistory, websiteSurfaceHistory, } from '../conversation-history/capture.js';
11
12
  import { sendForbidden, sendUnauthorized } from '../http-util.js';
12
13
  import { createAssistantTurnStats, runAgentTurn } from './assistant-agent.js';
13
14
  import { elevateAssistantSession } from './assistant-elevation.js';
14
15
  import { executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, resolveAssistantKnowledge, } from './assistant-knowledge.js';
15
- import { applyBrowserCors, assistantSessionEndpoints, authenticateSession, handleAssistantPreflight, now, } from './assistant-route-http.js';
16
+ import { applyBrowserCors, assistantSessionEndpoints, authenticateSession, basicCredentials, handleAssistantPreflight, now, } from './assistant-route-http.js';
16
17
  import { activeAssistantTarget, sessionScopedTarget } from './assistant-session-target.js';
17
18
  import { authorizeControlPlane } from './control-plane.js';
18
19
  export { applyBrowserCors, authenticateSession, handleAssistantPreflight, now };
@@ -141,6 +142,7 @@ export async function handleAssistantSession(req, res, deps) {
141
142
  caller,
142
143
  configuration,
143
144
  endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
145
+ surfaceHistory: websiteSurfaceHistory(assistant, surfaceBinding.kind),
144
146
  });
145
147
  }
146
148
  const session = await deps.store.createSession({
@@ -166,6 +168,7 @@ export async function handleAssistantSession(req, res, deps) {
166
168
  expiresAt: session.session.expiresAt,
167
169
  endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
168
170
  ...(configuration ? { configuration } : {}),
171
+ ...(await sessionHistoryNotice(deps.conversations, client.tenant, 'signed_in_customers', websiteSurfaceHistory(assistant, surfaceBinding.kind))),
169
172
  };
170
173
  // Every published @noodleseed/assistant widget consumes this shape; parse (don't just test)
171
174
  // so a contract break fails loudly at mint time instead of stranding deployed widgets (ADR 0151).
@@ -344,7 +347,7 @@ export async function handleAssistantTurn(req, res, deps) {
344
347
  ];
345
348
  try {
346
349
  await deps.store.appendHistory(session.id, rows);
347
- await deps.conversations?.recordSessionTurn(session, rows);
350
+ await deps.conversations?.recordSessionTurn(session, rows, sessionSurfaceHistory(target?.served.artifact.server.assistant, session));
348
351
  }
349
352
  catch {
350
353
  emit({ event: 'error', data: { code: 'conversation_state_failed', retryable: false } });
@@ -502,19 +505,4 @@ export async function handleAssistantAppRequest(req, res, deps) {
502
505
  function isRecord(value) {
503
506
  return typeof value === 'object' && value !== null && !Array.isArray(value);
504
507
  }
505
- function basicCredentials(req) {
506
- const match = /^Basic\s+(.+)$/i.exec(req.headers.authorization ?? '');
507
- if (!match?.[1])
508
- return undefined;
509
- try {
510
- const decoded = Buffer.from(match[1], 'base64').toString('utf8');
511
- const index = decoded.indexOf(':');
512
- return index > 0
513
- ? { id: decoded.slice(0, index), secret: decoded.slice(index + 1) }
514
- : undefined;
515
- }
516
- catch {
517
- return undefined;
518
- }
519
- }
520
508
  //# sourceMappingURL=assistant.js.map
@@ -1,16 +1,17 @@
1
1
  import { readJsonBody, sendJson } from '@noodle-borg/transport-http';
2
- import { ActivityPolicyError, } from '../application-activity.js';
2
+ import { ActivityPolicyError } from '../application-activity.js';
3
3
  import { businessGrantAllows } from '../business-information/model.js';
4
4
  import { requireIdentity, requireInstallationPermission, } from './business-information.js';
5
5
  export async function handleApplicationActivity(req, res, url, ref, deps) {
6
6
  const coordination = ref.action === 'coordination';
7
+ const history = ref.action === 'history';
7
8
  const suffix = url.pathname.split('/').at(-1);
8
9
  const action = coordination
9
10
  ? suffix === 'resolve'
10
11
  ? 'coordination-resolve'
11
12
  : 'coordination-list'
12
- : suffix === 'settings'
13
- ? req.method === 'PATCH'
13
+ : history
14
+ ? req.method === 'PUT'
14
15
  ? 'save-settings'
15
16
  : 'settings'
16
17
  : suffix === 'preview' || suffix === 'export'
@@ -20,8 +21,8 @@ export async function handleApplicationActivity(req, res, url, ref, deps) {
20
21
  ? suffix === 'resolve'
21
22
  ? ['POST']
22
23
  : ['GET']
23
- : suffix === 'settings'
24
- ? ['GET', 'PATCH']
24
+ : history
25
+ ? ['GET', 'PUT']
25
26
  : ['GET'];
26
27
  if (!allowed.includes(req.method ?? '')) {
27
28
  res.setHeader('allow', allowed.join(', '));