@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
@@ -5,7 +5,7 @@ export function publicMessaging(input) {
5
5
  throw new Error('publicMessaging requires an explicit capability allowlist');
6
6
  }
7
7
  for (const key of Object.keys(input)) {
8
- if (!['channel', 'capabilities', 'instructions'].includes(key)) {
8
+ if (!['channel', 'capabilities', 'instructions', 'history'].includes(key)) {
9
9
  throw new Error(`publicMessaging does not accept ${key}`);
10
10
  }
11
11
  }
@@ -17,6 +17,7 @@ export function publicMessaging(input) {
17
17
  channel: input.channel,
18
18
  capabilities: normalizeCapabilities(input.capabilities),
19
19
  ...(input.instructions === undefined ? {} : { instructions: input.instructions }),
20
+ ...historyOptOut(input.history),
20
21
  };
21
22
  }
22
23
  export function authenticatedWebsite(input) {
@@ -27,6 +28,7 @@ export function authenticatedWebsite(input) {
27
28
  ...(input.sessionClaims ? { sessionClaims: structuredClone(input.sessionClaims) } : {}),
28
29
  ...(input.capabilities ? { capabilities: [...input.capabilities] } : {}),
29
30
  ...(input.webmcp === undefined ? {} : { webmcp: { ...input.webmcp } }),
31
+ ...historyOptOut(input.history),
30
32
  };
31
33
  }
32
34
  export function publicWebsite(input) {
@@ -37,8 +39,18 @@ export function publicWebsite(input) {
37
39
  ...(input.instructions === undefined ? {} : { instructions: input.instructions }),
38
40
  ...(input.webmcp === undefined ? {} : { webmcp: { ...input.webmcp } }),
39
41
  ...(input.continuity === undefined ? {} : { continuity: { ...input.continuity } }),
42
+ ...historyOptOut(input.history),
40
43
  };
41
44
  }
45
+ /** Refuses anything but the literal `false`, because days are operator state (ADR 0212). */
46
+ function historyOptOut(history) {
47
+ if (history === undefined)
48
+ return {};
49
+ if (history !== false) {
50
+ throw new Error('history accepts only false (this surface never keeps chats); how many days a recorded surface keeps is the business setting in noodle solutions history settings');
51
+ }
52
+ return { history: false };
53
+ }
42
54
  function serializeOrigins(origins, path) {
43
55
  return origins.map((origin, index) => typeof origin === 'string' ? origin : serializeVariableRef(origin, `${path}.${index}`));
44
56
  }
@@ -88,6 +100,7 @@ export function embeddedAssistant(input) {
88
100
  ...(surface.mode !== 'authenticated' && surface.continuity !== undefined
89
101
  ? { continuity: { ...surface.continuity } }
90
102
  : {}),
103
+ ...(surface.history === false ? { history: false } : {}),
91
104
  }),
92
105
  allowedOrigins: surfaces.flatMap((surface) => surface.kind === 'messaging' ? [] : [...surface.origins]),
93
106
  ...structuredClone(ui),
@@ -114,9 +114,6 @@ export const CHANNELS_COMMAND = {
114
114
  ...mutation,
115
115
  ]),
116
116
  ]),
117
- group('conversation', 'Erase conversation content.', [
118
- leaf('forget', 'Erase participant context and message content while preserving safeguards.', [participant, ...mutation]),
119
- ]),
120
117
  ]),
121
118
  ],
122
119
  };
@@ -1,5 +1,14 @@
1
1
  /** Staff conversation history operations (ADR 0241); imported by the solutions family. Data only. */
2
2
  import { INSTALLATION_ARGUMENT, OPTIONAL_FLAG, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
3
+ const STATUS_CHOICES = ['new', 'needs-attention', 'reviewed'];
4
+ const CONVERSATION_FLAG = {
5
+ ...OPTIONAL_FLAG,
6
+ required: true,
7
+ name: 'conversation',
8
+ type: 'string',
9
+ value: '<id>',
10
+ summary: 'Conversation id from conversations list.',
11
+ };
3
12
  const paging = (maximum) => [
4
13
  {
5
14
  ...OPTIONAL_FLAG,
@@ -24,6 +33,14 @@ const paging = (maximum) => [
24
33
  summary: 'Only conversations from this channel.',
25
34
  constraints: { choices: ['website', 'whatsapp'] },
26
35
  },
36
+ {
37
+ ...OPTIONAL_FLAG,
38
+ name: 'status',
39
+ type: 'string',
40
+ value: '<status>',
41
+ summary: 'Only conversations with this review status.',
42
+ constraints: { choices: STATUS_CHOICES },
43
+ },
27
44
  ];
28
45
  const SELECTORS = ['conversation', 'customer', 'participant'];
29
46
  const SELECTOR_SUMMARIES = {
@@ -33,7 +50,7 @@ const SELECTOR_SUMMARIES = {
33
50
  };
34
51
  export const SOLUTION_CONVERSATIONS = {
35
52
  name: 'conversations',
36
- summary: 'Read, export and forget customer conversation history.',
53
+ summary: 'Read, review, note, export and forget customer conversation history.',
37
54
  arguments: [],
38
55
  flags: [],
39
56
  subcommands: [
@@ -46,17 +63,45 @@ export const SOLUTION_CONVERSATIONS = {
46
63
  },
47
64
  {
48
65
  name: 'show',
49
- summary: 'Show one conversation with its messages; the read is audited.',
66
+ summary: 'Show one conversation with its messages and private notes; the read is audited.',
67
+ arguments: [INSTALLATION_ARGUMENT],
68
+ flags: [...SOLUTION_COMMON_FLAGS, CONVERSATION_FLAG],
69
+ jsonOutput: { mode: 'single' },
70
+ },
71
+ {
72
+ name: 'review',
73
+ summary: 'Set the review status of one conversation (Operator or above); audited.',
74
+ arguments: [INSTALLATION_ARGUMENT],
75
+ flags: [
76
+ ...SOLUTION_COMMON_FLAGS,
77
+ CONVERSATION_FLAG,
78
+ {
79
+ ...OPTIONAL_FLAG,
80
+ required: true,
81
+ name: 'status',
82
+ type: 'string',
83
+ value: '<status>',
84
+ summary: 'New review status.',
85
+ constraints: { choices: STATUS_CHOICES },
86
+ },
87
+ ],
88
+ jsonOutput: { mode: 'single' },
89
+ },
90
+ {
91
+ name: 'note',
92
+ summary: 'Add a private staff note the customer never sees (Operator or above); audited.',
50
93
  arguments: [INSTALLATION_ARGUMENT],
51
94
  flags: [
52
95
  ...SOLUTION_COMMON_FLAGS,
96
+ CONVERSATION_FLAG,
53
97
  {
54
98
  ...OPTIONAL_FLAG,
55
99
  required: true,
56
- name: 'conversation',
100
+ name: 'text',
57
101
  type: 'string',
58
- value: '<id>',
59
- summary: 'Conversation id from conversations list.',
102
+ value: '<text>',
103
+ summary: 'Note text, up to 2000 characters.',
104
+ constraints: { maxLength: 2000 },
60
105
  },
61
106
  ],
62
107
  jsonOutput: { mode: 'single' },
@@ -0,0 +1,82 @@
1
+ /** The installation history setting (ADR 0241 decision 6); imported by the solutions family. Data only. */
2
+ import { INSTALLATION_ARGUMENT, OPTIONAL_FLAG, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
3
+ const days = (name, summary, conflictsWith = []) => ({
4
+ ...OPTIONAL_FLAG,
5
+ name,
6
+ type: 'integer',
7
+ value: '<days>',
8
+ summary,
9
+ constraints: { minimum: 1, maximum: 365 },
10
+ conflictsWith,
11
+ });
12
+ const channel = (name, summary) => ({
13
+ ...OPTIONAL_FLAG,
14
+ name,
15
+ type: 'string',
16
+ value: '<on|off>',
17
+ summary,
18
+ constraints: { choices: ['on', 'off'] },
19
+ });
20
+ export const SOLUTION_HISTORY = {
21
+ name: 'history',
22
+ summary: 'Inspect or change how long Activity and conversations are kept.',
23
+ arguments: [],
24
+ flags: [],
25
+ subcommands: [
26
+ {
27
+ name: 'settings',
28
+ summary: 'Read or change the installation history setting within plan limits.',
29
+ arguments: [],
30
+ flags: [],
31
+ subcommands: [
32
+ {
33
+ name: 'get',
34
+ summary: 'Read Activity and conversation durations, recording switches, edit permission and revision.',
35
+ arguments: [INSTALLATION_ARGUMENT],
36
+ flags: SOLUTION_COMMON_FLAGS,
37
+ jsonOutput: { mode: 'single' },
38
+ },
39
+ {
40
+ name: 'set',
41
+ summary: 'Change durations or recording switches with a revision check; shortening or turning off conversations needs --confirm.',
42
+ arguments: [INSTALLATION_ARGUMENT],
43
+ flags: [
44
+ ...SOLUTION_COMMON_FLAGS,
45
+ days('activity-days', 'Days to keep Activity, within the plan maximum.'),
46
+ days('conversation-days', 'Days to keep conversations; turns recording on.', [
47
+ 'conversations',
48
+ ]),
49
+ {
50
+ ...OPTIONAL_FLAG,
51
+ name: 'conversations',
52
+ type: 'string',
53
+ value: 'off',
54
+ summary: 'Turn conversation recording off.',
55
+ constraints: { choices: ['off'] },
56
+ conflictsWith: ['conversation-days'],
57
+ },
58
+ channel('website-visitors', 'Record anonymous website visitors.'),
59
+ channel('signed-in-customers', 'Record signed-in customers.'),
60
+ channel('whatsapp', 'Record WhatsApp conversations.'),
61
+ {
62
+ ...OPTIONAL_FLAG,
63
+ required: true,
64
+ name: 'expected-revision',
65
+ type: 'string',
66
+ value: '<hash>',
67
+ summary: 'Revision returned by history settings get.',
68
+ },
69
+ {
70
+ ...OPTIONAL_FLAG,
71
+ name: 'confirm',
72
+ type: 'boolean',
73
+ summary: 'Confirm a change that shortens or turns off conversation history; without it the effect is shown and nothing is saved.',
74
+ },
75
+ ],
76
+ jsonOutput: { mode: 'single' },
77
+ },
78
+ ],
79
+ },
80
+ ],
81
+ };
82
+ //# sourceMappingURL=catalog-data-solution-history.js.map
@@ -2,6 +2,7 @@
2
2
  import { SOLUTION_CONVERSATIONS } from './catalog-data-solution-conversations.js';
3
3
  import { SOLUTION_DRAFTS } from './catalog-data-solution-drafts.js';
4
4
  import { AUTH_TOKEN, EXPECTED_REVISION, IDEMPOTENCY_KEY, INSTALLATION_ARGUMENT, JSON_FLAG, OPTIONAL_FLAG, PAGING_FLAGS, REQUIRED_ARGUMENT, SERVICE, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
5
+ import { SOLUTION_HISTORY } from './catalog-data-solution-history.js';
5
6
  import { SOLUTION_PAGE } from './catalog-data-solution-page.js';
6
7
  import { COLLECTION_ARGUMENT, SOLUTION_RECORDS } from './catalog-data-solution-records.js';
7
8
  import { SOLUTION_WORKSPACE } from './catalog-data-solution-workspace.js';
@@ -173,9 +174,10 @@ export const CATALOG_SOLUTIONS = {
173
174
  ],
174
175
  },
175
176
  SOLUTION_CONVERSATIONS,
177
+ SOLUTION_HISTORY,
176
178
  {
177
179
  name: 'activity',
178
- summary: 'Inspect payload-free operation evidence and its retention policy.',
180
+ summary: 'Inspect payload-free operation evidence and preview plan history impact.',
179
181
  arguments: [],
180
182
  flags: [],
181
183
  subcommands: [
@@ -212,47 +214,6 @@ export const CATALOG_SOLUTIONS = {
212
214
  flags: SOLUTION_COMMON_FLAGS,
213
215
  jsonOutput: { mode: 'single' },
214
216
  },
215
- {
216
- name: 'settings',
217
- summary: 'Inspect or change Activity retention within plan limits.',
218
- arguments: [],
219
- flags: [],
220
- subcommands: [
221
- {
222
- name: 'get',
223
- summary: 'Read retention, plan maximum, edit permission and current revision.',
224
- arguments: [INSTALLATION_ARGUMENT],
225
- flags: SOLUTION_COMMON_FLAGS,
226
- jsonOutput: { mode: 'single' },
227
- },
228
- {
229
- name: 'set',
230
- summary: 'Change retention with an explicit revision check.',
231
- arguments: [INSTALLATION_ARGUMENT],
232
- flags: [
233
- ...SOLUTION_COMMON_FLAGS,
234
- {
235
- ...OPTIONAL_FLAG,
236
- required: true,
237
- name: 'retention-days',
238
- type: 'integer',
239
- value: '<days>',
240
- summary: 'Desired retention within the current plan maximum.',
241
- constraints: { minimum: 1, maximum: 365 },
242
- },
243
- {
244
- ...OPTIONAL_FLAG,
245
- required: true,
246
- name: 'expected-revision',
247
- type: 'string',
248
- value: '<hash>',
249
- summary: 'Revision returned by activity settings get.',
250
- },
251
- ],
252
- jsonOutput: { mode: 'single' },
253
- },
254
- ],
255
- },
256
217
  ],
257
218
  },
258
219
  {
@@ -372,6 +372,7 @@ export declare const manifestV1Schema: z.ZodObject<{
372
372
  name: z.ZodString;
373
373
  }, z.core.$strict>>;
374
374
  instructions: z.ZodOptional<z.ZodString>;
375
+ history: z.ZodOptional<z.ZodLiteral<false>>;
375
376
  }, z.core.$strict>, z.ZodObject<{
376
377
  kind: z.ZodOptional<z.ZodLiteral<"website">>;
377
378
  mode: z.ZodEnum<{
@@ -401,6 +402,7 @@ export declare const manifestV1Schema: z.ZodObject<{
401
402
  windowSeconds: z.ZodOptional<z.ZodNumber>;
402
403
  maxRestores: z.ZodOptional<z.ZodNumber>;
403
404
  }, z.core.$strict>>;
405
+ history: z.ZodOptional<z.ZodLiteral<false>>;
404
406
  }, z.core.$strict>]>>>;
405
407
  sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
406
408
  exposeToModel: z.ZodOptional<z.ZodBoolean>;
@@ -876,6 +878,7 @@ export declare const manifestV2Schema: z.ZodObject<{
876
878
  name: z.ZodString;
877
879
  }, z.core.$strict>>;
878
880
  instructions: z.ZodOptional<z.ZodString>;
881
+ history: z.ZodOptional<z.ZodLiteral<false>>;
879
882
  }, z.core.$strict>, z.ZodObject<{
880
883
  kind: z.ZodOptional<z.ZodLiteral<"website">>;
881
884
  mode: z.ZodEnum<{
@@ -905,6 +908,7 @@ export declare const manifestV2Schema: z.ZodObject<{
905
908
  windowSeconds: z.ZodOptional<z.ZodNumber>;
906
909
  maxRestores: z.ZodOptional<z.ZodNumber>;
907
910
  }, z.core.$strict>>;
911
+ history: z.ZodOptional<z.ZodLiteral<false>>;
908
912
  }, z.core.$strict>]>>>;
909
913
  sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
910
914
  exposeToModel: z.ZodOptional<z.ZodBoolean>;
@@ -1674,6 +1678,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1674
1678
  name: z.ZodString;
1675
1679
  }, z.core.$strict>>;
1676
1680
  instructions: z.ZodOptional<z.ZodString>;
1681
+ history: z.ZodOptional<z.ZodLiteral<false>>;
1677
1682
  }, z.core.$strict>, z.ZodObject<{
1678
1683
  kind: z.ZodOptional<z.ZodLiteral<"website">>;
1679
1684
  mode: z.ZodEnum<{
@@ -1703,6 +1708,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1703
1708
  windowSeconds: z.ZodOptional<z.ZodNumber>;
1704
1709
  maxRestores: z.ZodOptional<z.ZodNumber>;
1705
1710
  }, z.core.$strict>>;
1711
+ history: z.ZodOptional<z.ZodLiteral<false>>;
1706
1712
  }, z.core.$strict>]>>>;
1707
1713
  sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
1708
1714
  exposeToModel: z.ZodOptional<z.ZodBoolean>;
@@ -2177,6 +2183,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
2177
2183
  name: z.ZodString;
2178
2184
  }, z.core.$strict>>;
2179
2185
  instructions: z.ZodOptional<z.ZodString>;
2186
+ history: z.ZodOptional<z.ZodLiteral<false>>;
2180
2187
  }, z.core.$strict>, z.ZodObject<{
2181
2188
  kind: z.ZodOptional<z.ZodLiteral<"website">>;
2182
2189
  mode: z.ZodEnum<{
@@ -2206,6 +2213,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
2206
2213
  windowSeconds: z.ZodOptional<z.ZodNumber>;
2207
2214
  maxRestores: z.ZodOptional<z.ZodNumber>;
2208
2215
  }, z.core.$strict>>;
2216
+ history: z.ZodOptional<z.ZodLiteral<false>>;
2209
2217
  }, z.core.$strict>]>>>;
2210
2218
  sessionClaims: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
2211
2219
  exposeToModel: z.ZodOptional<z.ZodBoolean>;
@@ -269,6 +269,11 @@ const assistantCapabilitySchema = z
269
269
  const MAX_SERVER_INSTRUCTIONS = 4000;
270
270
  /** Verified session claims an embedding backend may pass at exchange; undeclared keys are dropped. */
271
271
  const sessionClaimsSchema = z.record(z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/, 'claim keys are identifier-style'), z.object({ exposeToModel: z.boolean().optional() }).strict());
272
+ /**
273
+ * `false` means this surface's chats are never kept as history (ADR 0241 decision 11). Only the literal
274
+ * is accepted: a number of days is operator state (ADR 0212), so a count is refused, never read.
275
+ */
276
+ const surfaceHistorySchema = z.literal(false).optional();
272
277
  const embeddedAssistantSchema = z
273
278
  .object({
274
279
  model: z.union([
@@ -297,6 +302,7 @@ const embeddedAssistantSchema = z
297
302
  mode: z.literal('public'),
298
303
  capabilities: z.array(assistantCapabilitySchema),
299
304
  instructions: z.string().trim().min(1).max(MAX_SERVER_INSTRUCTIONS).optional(),
305
+ history: surfaceHistorySchema,
300
306
  })
301
307
  .strict(),
302
308
  z
@@ -324,6 +330,7 @@ const embeddedAssistantSchema = z
324
330
  windowSeconds: z.number().int().min(0).max(600).optional(),
325
331
  maxRestores: z.number().int().min(0).max(10).optional(),
326
332
  }),
333
+ history: surfaceHistorySchema,
327
334
  })
328
335
  .strict(),
329
336
  ]))
@@ -1,6 +1,7 @@
1
1
  import { createHmac } from 'node:crypto';
2
2
  import { canonicalJson, sha256Canonical } from '@noodle-borg/compiler';
3
- import { ApplicationActivityListResponseSchema, ApplicationActivityPreviewResponseSchema, ApplicationActivitySettingsResponseSchema, ApplicationActivitySettingsSaveRequestSchema, OperationCoordinationListRequestSchema, OperationCoordinationListResponseSchema, OperationCoordinationResolveRequestSchema, OperationCoordinationResolveResponseSchema, } from '@noodle-borg/wire-contracts';
3
+ import { ApplicationActivityListResponseSchema, ApplicationActivityPreviewResponseSchema, ApplicationHistorySettingsResponseSchema, ApplicationHistorySettingsSaveRequestSchema, ApplicationHistorySettingsSaveResponseSchema, OperationCoordinationListRequestSchema, OperationCoordinationListResponseSchema, OperationCoordinationResolveRequestSchema, OperationCoordinationResolveResponseSchema, } from '@noodle-borg/wire-contracts';
4
+ import { conversationPolicyFromSetting, historySettingsProjection, lazyHistorySetting, newInstallationHistorySetting, planHistorySettingsChange, } from './application-history-settings.js';
4
5
  import { resolveApplicationRuntimeTarget } from './application-runtime-target.js';
5
6
  import { createOperationCoordinationPort, } from './operation-coordination.js';
6
7
  import { createOperationEvidencePort, } from './operation-evidence.js';
@@ -111,11 +112,13 @@ export class ApplicationActivity {
111
112
  });
112
113
  }
113
114
  if (action === 'save-settings') {
114
- const parsed = ApplicationActivitySettingsSaveRequestSchema.safeParse(input.body);
115
+ const parsed = ApplicationHistorySettingsSaveRequestSchema.safeParse(input.body);
115
116
  if (!parsed.success)
116
117
  throw invalid();
117
- const data = await this.save(scope, parsed.data, local);
118
- return mutation(ApplicationActivitySettingsResponseSchema.parse({ ok: true, data }), 'config.activity.retention_changed', { retentionDays: data.retentionDays });
118
+ const { audit, ...saved } = await this.save(scope, parsed.data, local);
119
+ const data = { ...saved, settings: await this.withApplicationView(scope, saved.settings) };
120
+ const response = ApplicationHistorySettingsSaveResponseSchema.parse({ ok: true, data });
121
+ return audit ? mutation(response, 'config.history.settings_changed', audit) : { response };
119
122
  }
120
123
  if (action === 'preview')
121
124
  return {
@@ -126,9 +129,9 @@ export class ApplicationActivity {
126
129
  };
127
130
  if (action === 'settings')
128
131
  return {
129
- response: ApplicationActivitySettingsResponseSchema.parse({
132
+ response: ApplicationHistorySettingsResponseSchema.parse({
130
133
  ok: true,
131
- data: (await this.settings(scope, input.canEdit, local)).projection,
134
+ data: await this.withApplicationView(scope, (await this.settings(scope, input.canEdit, local)).projection),
132
135
  }),
133
136
  };
134
137
  const limit = parameters.has('limit') ? Number(parameters.get('limit')) : 50;
@@ -149,7 +152,18 @@ export class ApplicationActivity {
149
152
  }),
150
153
  };
151
154
  }
152
- async settings(scope, canEdit, local = runLocal) {
155
+ /** Adds what the application itself declared; the stored setting and its revision are unchanged. */
156
+ async withApplicationView(scope, projection) {
157
+ const disabled = (await this.options.historyDisabledSurfaces?.(scope)) ?? [];
158
+ if (disabled.length === 0)
159
+ return projection;
160
+ return {
161
+ ...projection,
162
+ conversations: { ...projection.conversations, disabledByApplication: [...disabled] },
163
+ };
164
+ }
165
+ /** The verified plan allowance, or unavailable; never a default invented locally. */
166
+ async allowance(scope) {
153
167
  const allowance = await this.options.allowance(scope.org);
154
168
  if (!allowance ||
155
169
  !validDays(allowance.maximumDays) ||
@@ -157,36 +171,77 @@ export class ApplicationActivity {
157
171
  allowance.defaultDays > allowance.maximumDays ||
158
172
  !allowance.revision)
159
173
  throw new ActivityPolicyError('activity_unavailable');
174
+ return allowance;
175
+ }
176
+ async settings(scope, canEdit, local = runLocal) {
177
+ const allowance = await this.allowance(scope);
160
178
  const setting = await local(async () => {
161
179
  let value = await this.options.store.readRetention(scope);
162
180
  if (!value) {
163
- await this.options.store.setRetention(scope, allowance.defaultDays, undefined);
181
+ await this.options.store.setRetention(scope, lazyHistorySetting(allowance), undefined);
164
182
  value = await this.options.store.readRetention(scope);
165
183
  }
166
184
  return value;
167
185
  });
168
- if (!setting || !validDays(setting.days))
186
+ if (!setting ||
187
+ !validDays(setting.days) ||
188
+ (setting.conversationDays !== null &&
189
+ setting.conversationDays !== 0 &&
190
+ !validDays(setting.conversationDays)))
169
191
  throw new ActivityPolicyError('activity_unavailable');
170
192
  return {
171
193
  setting,
172
194
  allowance,
173
- projection: {
174
- revision: this.revision(scope, setting, allowance),
175
- retentionDays: Math.min(setting.days, allowance.maximumDays),
176
- maximumDays: allowance.maximumDays,
177
- canEdit,
178
- },
195
+ projection: historySettingsProjection(setting, allowance, this.revision(scope, setting, allowance), canEdit),
179
196
  };
180
197
  }
198
+ /**
199
+ * One change under one revision. Shortening or turning off conversations caps stored expiry right
200
+ * after the setting is written, so no longer transcript survives behind the shorter window; a dry
201
+ * run counts exactly what that cap would hide, saves nothing and is not audited.
202
+ */
181
203
  async save(scope, input, local = runLocal) {
182
204
  const current = await this.settings(scope, true, local);
183
205
  if (current.projection.revision !== input.expectedRevision)
184
206
  throw new ActivityPolicyError('activity_conflict');
185
- if (!validDays(input.retentionDays) || input.retentionDays > current.allowance.maximumDays)
207
+ const plan = planHistorySettingsChange(current.setting, input, current.allowance.maximumDays);
208
+ if (!plan)
186
209
  throw new ActivityPolicyError('activity_invalid');
187
- if (!(await local(() => this.options.store.setRetention(scope, input.retentionDays, current.setting.revision))))
210
+ const store = this.options.conversationHistory;
211
+ const window = (method) => plan.shortened === undefined || !store
212
+ ? { conversations: 0, items: 0 }
213
+ : local(() => store[method]({ org: scope.org, app: scope.app, env: scope.env }, { now: this.options.now?.() ?? Date.now(), days: plan.shortened ?? 0 }));
214
+ const shortensConversations = plan.shortened !== undefined;
215
+ if (input.dryRun)
216
+ return {
217
+ shortensConversations,
218
+ impact: await window('countOutsideWindow'),
219
+ dryRun: true,
220
+ settings: current.projection,
221
+ audit: undefined,
222
+ };
223
+ if (!(await local(() => this.options.store.setRetention(scope, plan.next, current.setting.revision))))
188
224
  throw new ActivityPolicyError('activity_conflict');
189
- return (await this.settings(scope, true, local)).projection;
225
+ const impact = await window('capExpiry');
226
+ return {
227
+ shortensConversations,
228
+ impact,
229
+ dryRun: false,
230
+ settings: (await this.settings(scope, true, local)).projection,
231
+ audit: plan.audit,
232
+ };
233
+ }
234
+ /**
235
+ * Writes a new installation's first setting so it records conversations from day one. Runs only for a
236
+ * created installation, before anything reads (and lazily creates) a not-enabled setting.
237
+ */
238
+ async initializeHistory(scope) {
239
+ return this.options.store.setRetention(scope, newInstallationHistorySetting(await this.allowance(scope)), undefined);
240
+ }
241
+ /** The capture policy: side-effect free, so the capture path never creates a setting. */
242
+ async conversationPolicy(scope) {
243
+ const allowance = await this.allowance(scope);
244
+ return conversationPolicyFromSetting(await this.options.store.readRetention(scope), allowance);
190
245
  }
191
246
  async page(scope, input, local = runLocal) {
192
247
  if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100)
@@ -198,7 +253,7 @@ export class ApplicationActivity {
198
253
  revision: policy.revision,
199
254
  limit: input.limit,
200
255
  });
201
- const records = await local(() => this.options.store.list(scope, this.options.now?.() ?? Date.now(), policy.retentionDays, input.limit + 1, decodeActivityCursor(input.cursor, binding)));
256
+ const records = await local(() => this.options.store.list(scope, this.options.now?.() ?? Date.now(), policy.activity.retentionDays, input.limit + 1, decodeActivityCursor(input.cursor, binding)));
202
257
  if ((await this.settings(scope, false, local)).projection.revision !== policy.revision)
203
258
  throw new ActivityPolicyError('activity_conflict');
204
259
  const page = records.slice(0, input.limit);
@@ -217,7 +272,7 @@ export class ApplicationActivity {
217
272
  : { completedAt: new Date(completedAt).toISOString() }),
218
273
  ...(reference === undefined ? {} : { reference }),
219
274
  })),
220
- historyDays: policy.retentionDays,
275
+ historyDays: policy.activity.retentionDays,
221
276
  ...(records.length > page.length && last
222
277
  ? {
223
278
  nextCursor: Buffer.from(JSON.stringify({ binding, startedAt: last.startedAt, id: last.id })).toString('base64url'),
@@ -337,7 +392,7 @@ export class ApplicationActivity {
337
392
  ...this.options,
338
393
  scope,
339
394
  deploymentId: target.deploymentId,
340
- historyDays: async () => (await this.settings(scope, false)).projection.retentionDays,
395
+ historyDays: async () => (await this.settings(scope, false)).projection.activity.retentionDays,
341
396
  executionBoundMs: (intent) => intent.executionBoundMs,
342
397
  connectionGeneration: (id) => target.served.deps.executionBinding?.connections[id] ?? revision,
343
398
  authorize,