@noodleseed/one 0.177.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 (71) 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.map +1 -1
  5. package/dist/commands/solutions-conversations.js +60 -6
  6. package/dist/commands/solutions-conversations.js.map +1 -1
  7. package/dist/commands/solutions-history.d.ts +9 -0
  8. package/dist/commands/solutions-history.d.ts.map +1 -0
  9. package/dist/commands/solutions-history.js +166 -0
  10. package/dist/commands/solutions-history.js.map +1 -0
  11. package/dist/commands/solutions-ops.d.ts.map +1 -1
  12. package/dist/commands/solutions-ops.js +3 -0
  13. package/dist/commands/solutions-ops.js.map +1 -1
  14. package/dist/commands/whatsapp-ops.d.ts.map +1 -1
  15. package/dist/commands/whatsapp-ops.js +0 -7
  16. package/dist/commands/whatsapp-ops.js.map +1 -1
  17. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  18. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +14 -0
  19. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  20. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +6 -0
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +5 -2
  22. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  23. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +18 -0
  24. package/node_modules/@noodle-borg/authoring/dist/assistant.js +14 -1
  25. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +0 -3
  26. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-conversations.js +50 -5
  27. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-history.js +82 -0
  28. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +3 -42
  29. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +8 -0
  30. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +7 -0
  31. package/node_modules/@noodle-borg/service/dist/application-activity.js +76 -21
  32. package/node_modules/@noodle-borg/service/dist/application-history-settings.js +108 -0
  33. package/node_modules/@noodle-borg/service/dist/assistant-composition.js +11 -2
  34. package/node_modules/@noodle-borg/service/dist/channels/composition.js +10 -1
  35. package/node_modules/@noodle-borg/service/dist/channels/provider.js +13 -13
  36. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +31 -5
  37. package/node_modules/@noodle-borg/service/dist/conversation-history/capture.js +63 -7
  38. package/node_modules/@noodle-borg/service/dist/conversation-history/contracts.js +13 -0
  39. package/node_modules/@noodle-borg/service/dist/conversation-history/customer.js +118 -0
  40. package/node_modules/@noodle-borg/service/dist/conversation-history/operator.js +133 -24
  41. package/node_modules/@noodle-borg/service/dist/routes/assistant-conversations.js +52 -0
  42. package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +6 -0
  43. package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +3 -0
  44. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +2 -1
  45. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +6 -11
  46. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +6 -0
  47. package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +16 -0
  48. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +5 -17
  49. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +6 -5
  50. package/node_modules/@noodle-borg/service/dist/routes/business-information-conversations.js +36 -11
  51. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +4 -2
  52. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +4 -2
  53. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +4 -0
  54. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +0 -10
  55. package/node_modules/@noodle-borg/service/dist/service-history-projections.js +32 -4
  56. package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +32 -0
  57. package/node_modules/@noodle-borg/service/dist/service.js +4 -3
  58. package/node_modules/@noodle-borg/service/package.json +1 -1
  59. package/node_modules/@noodle-borg/wire-contracts/dist/application-activity.d.ts +0 -28
  60. package/node_modules/@noodle-borg/wire-contracts/dist/application-activity.js +0 -18
  61. package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.d.ts +190 -0
  62. package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.js +92 -0
  63. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +3 -0
  64. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +6 -0
  65. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.d.ts +176 -0
  66. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.js +74 -2
  67. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  68. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  69. package/node_modules/@noodleseed/assistant/README.md +19 -0
  70. package/node_modules/@noodleseed/assistant/package.json +1 -1
  71. package/package.json +2 -2
@@ -1,6 +1,8 @@
1
1
  import { createHmac } from 'node:crypto';
2
2
  import { canonicalJson } from '@noodle-borg/compiler';
3
- import { ConversationExportResponseSchema, ConversationForgetRequestSchema, ConversationForgetResponseSchema, ConversationIdSchema, ConversationListResponseSchema, ConversationShowResponseSchema, } from '@noodle-borg/wire-contracts';
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';
4
6
  const runLocal = (operation) => operation();
5
7
  const MESSAGES = {
6
8
  conversation_invalid: 'Invalid conversation request.',
@@ -17,7 +19,8 @@ export class ConversationHistoryError extends Error {
17
19
  const PAGE = { list: { default: 50, maximum: 100 }, export: { default: 25, maximum: 25 } };
18
20
  /**
19
21
  * The staff projection of conversation history (ADR 0241). Callers authorize before and after, and every
20
- * read, export and forget returns identifier-only audit details; message text never reaches audit.
22
+ * read, review, note, export and forget returns identifier-only audit details; message and note text
23
+ * never reach audit.
21
24
  */
22
25
  export class ApplicationConversations {
23
26
  options;
@@ -26,7 +29,7 @@ export class ApplicationConversations {
26
29
  }
27
30
  async project(scope, action, input, local = runLocal) {
28
31
  const tenant = { org: scope.org, app: scope.app, env: scope.env };
29
- const allowed = action === 'list' || action === 'export' ? ['limit', 'cursor', 'channel'] : [];
32
+ const allowed = action === 'list' || action === 'export' ? ['limit', 'cursor', 'channel', 'status'] : [];
30
33
  const parameters = new Map();
31
34
  for (const [key, value] of input.parameters) {
32
35
  if (!allowed.includes(key) || parameters.has(key))
@@ -37,36 +40,94 @@ export class ApplicationConversations {
37
40
  eventType,
38
41
  details,
39
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
+ }
40
84
  if (action === 'forget') {
41
85
  const parsed = ConversationForgetRequestSchema.safeParse(input.body);
42
86
  if (!parsed.success)
43
87
  throw invalid();
44
88
  const request = parsed.data;
45
- const forgotten = await local(() => 'conversationId' in request
46
- ? this.options.store.forget(tenant, request.conversationId)
47
- : this.options.store.forgetSubject(tenant, request.subject));
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
+ });
48
104
  return {
49
105
  response: ConversationForgetResponseSchema.parse({ ok: true, data: { forgotten } }),
50
- audit: audit('conversation.forgotten', {
51
- conversations: forgotten.conversations,
52
- items: forgotten.items,
53
- kind: 'conversationId' in request ? 'conversation' : request.subject.kind,
54
- }),
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
+ ],
55
114
  };
56
115
  }
57
116
  if (action === 'show') {
58
117
  const id = ConversationIdSchema.safeParse(input.conversationId);
59
118
  if (!id.success)
60
119
  throw new ConversationHistoryError('conversation_not_found');
61
- const conversation = await local(() => this.options.store.read(tenant, id.data, this.#now()));
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));
62
123
  if (!conversation)
63
124
  throw new ConversationHistoryError('conversation_not_found');
64
125
  return {
65
126
  response: ConversationShowResponseSchema.parse({
66
127
  ok: true,
67
- data: { conversation: detail(conversation) },
128
+ data: { conversation: shown(conversation, now - notBefore) },
68
129
  }),
69
- audit: audit('conversation.read', { conversationId: id.data }),
130
+ audits: [audit('conversation.read', { conversationId: id.data })],
70
131
  };
71
132
  }
72
133
  const page = await this.#page(scope, tenant, action, parameters, local);
@@ -79,11 +140,13 @@ export class ApplicationConversations {
79
140
  ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
80
141
  },
81
142
  }),
143
+ audits: [],
82
144
  };
83
145
  const conversations = await local(async () => {
84
- const now = this.#now();
85
- const read = await Promise.all(page.summaries.map((row) => this.options.store.read(tenant, row.id, now)));
86
- return read.filter((row) => row !== undefined).map(detail);
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));
87
150
  });
88
151
  return {
89
152
  response: ConversationExportResponseSchema.parse({
@@ -93,18 +156,20 @@ export class ApplicationConversations {
93
156
  ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
94
157
  },
95
158
  }),
96
- audit: audit('conversation.exported', { count: conversations.length }),
159
+ audits: [audit('conversation.exported', { count: conversations.length })],
97
160
  };
98
161
  }
99
162
  async #page(scope, tenant, purpose, parameters, local) {
100
163
  const bounds = PAGE[purpose];
101
164
  const limit = parameters.has('limit') ? Number(parameters.get('limit')) : bounds.default;
102
165
  const channel = parameters.get('channel');
166
+ const status = parameters.get('status');
103
167
  const cursor = parameters.get('cursor');
104
168
  if (!Number.isInteger(limit) ||
105
169
  limit < 1 ||
106
170
  limit > bounds.maximum ||
107
171
  (channel !== undefined && channel !== 'website' && channel !== 'whatsapp') ||
172
+ (status !== undefined && !ConversationReviewStatusSchema.safeParse(status).success) ||
108
173
  (cursor !== undefined && (!cursor || cursor.length > 2048)))
109
174
  throw invalid();
110
175
  const binding = this.#hash({
@@ -112,26 +177,42 @@ export class ApplicationConversations {
112
177
  scope,
113
178
  limit,
114
179
  channel: channel ?? null,
180
+ status: status ?? null,
115
181
  });
116
182
  const after = decodeConversationCursor(cursor, binding);
183
+ const now = this.#now();
184
+ const notBefore = await this.#notBefore(tenant, now);
117
185
  const rows = await local(() => this.options.store.list(tenant, {
118
- now: this.#now(),
186
+ now,
187
+ notBefore,
119
188
  limit: limit + 1,
120
189
  ...(after ? { after } : {}),
121
190
  ...(channel ? { channel: channel } : {}),
191
+ ...(status ? { reviewStatus: status } : {}),
122
192
  }));
123
193
  const summaries = rows.slice(0, limit);
124
194
  const last = summaries.at(-1);
125
195
  return {
196
+ now,
197
+ notBefore,
126
198
  summaries,
127
- nextCursor: rows.length > limit && last
128
- ? Buffer.from(JSON.stringify({ binding, lastMessageAt: last.lastMessageAt, id: last.id })).toString('base64url')
129
- : undefined,
199
+ nextCursor: rows.length > limit && last ? encodeConversationCursor(binding, last) : undefined,
130
200
  };
131
201
  }
132
202
  #now() {
133
203
  return this.options.now?.() ?? Date.now();
134
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
+ }
135
216
  #hash(value) {
136
217
  return createHmac('sha256', this.options.identityKey)
137
218
  .update(canonicalJson(value))
@@ -149,11 +230,30 @@ function summary(row) {
149
230
  startedAt: new Date(row.startedAt).toISOString(),
150
231
  lastMessageAt: new Date(row.lastMessageAt).toISOString(),
151
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
+ })),
152
251
  };
153
252
  }
154
- function detail(conversation) {
253
+ function detail(conversation, windowMs) {
155
254
  return {
156
255
  ...summary({ ...conversation, itemCount: conversation.items.length }),
256
+ expiresAt: new Date(removalAt(conversation, windowMs)).toISOString(),
157
257
  items: conversation.items.map((item) => item.kind === 'message'
158
258
  ? { kind: item.kind, role: item.role, text: item.text, at: new Date(item.at).toISOString() }
159
259
  : {
@@ -165,7 +265,16 @@ function detail(conversation) {
165
265
  }),
166
266
  };
167
267
  }
168
- function decodeConversationCursor(raw, binding) {
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) {
169
278
  if (raw === undefined)
170
279
  return undefined;
171
280
  try {
@@ -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(', '));