@dsh-enhanced/assistant-work-items 0.0.0-stage → 0.1.66

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +685 -2
  3. package/cordis.patch.yml +11 -0
  4. package/lib/acceptance-binding.d.ts +86 -0
  5. package/lib/acceptance-binding.d.ts.map +1 -0
  6. package/lib/acceptance-binding.js +250 -0
  7. package/lib/acceptance-binding.js.map +1 -0
  8. package/lib/admission.d.ts +95 -0
  9. package/lib/admission.d.ts.map +1 -0
  10. package/lib/admission.js +642 -0
  11. package/lib/admission.js.map +1 -0
  12. package/lib/authority.d.ts +92 -0
  13. package/lib/authority.d.ts.map +1 -0
  14. package/lib/authority.js +205 -0
  15. package/lib/authority.js.map +1 -0
  16. package/lib/config.d.ts +365 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +672 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/continuation-contract.d.ts +126 -0
  21. package/lib/continuation-contract.d.ts.map +1 -0
  22. package/lib/continuation-contract.js +109 -0
  23. package/lib/continuation-contract.js.map +1 -0
  24. package/lib/continuation-record.d.ts +32 -0
  25. package/lib/continuation-record.d.ts.map +1 -0
  26. package/lib/continuation-record.js +57 -0
  27. package/lib/continuation-record.js.map +1 -0
  28. package/lib/continuation.d.ts +89 -0
  29. package/lib/continuation.d.ts.map +1 -0
  30. package/lib/continuation.js +607 -0
  31. package/lib/continuation.js.map +1 -0
  32. package/lib/diagnosis-contract.d.ts +95 -0
  33. package/lib/diagnosis-contract.d.ts.map +1 -0
  34. package/lib/diagnosis-contract.js +167 -0
  35. package/lib/diagnosis-contract.js.map +1 -0
  36. package/lib/diagnosis.d.ts +70 -0
  37. package/lib/diagnosis.d.ts.map +1 -0
  38. package/lib/diagnosis.js +466 -0
  39. package/lib/diagnosis.js.map +1 -0
  40. package/lib/dispatch.d.ts +100 -0
  41. package/lib/dispatch.d.ts.map +1 -0
  42. package/lib/dispatch.js +317 -0
  43. package/lib/dispatch.js.map +1 -0
  44. package/lib/engineering-context.d.ts +86 -0
  45. package/lib/engineering-context.d.ts.map +1 -0
  46. package/lib/engineering-context.js +263 -0
  47. package/lib/engineering-context.js.map +1 -0
  48. package/lib/evaluation.d.ts +125 -0
  49. package/lib/evaluation.d.ts.map +1 -0
  50. package/lib/evaluation.js +295 -0
  51. package/lib/evaluation.js.map +1 -0
  52. package/lib/event-follow.d.ts +93 -0
  53. package/lib/event-follow.d.ts.map +1 -0
  54. package/lib/event-follow.js +150 -0
  55. package/lib/event-follow.js.map +1 -0
  56. package/lib/feedback-targets.d.ts +91 -0
  57. package/lib/feedback-targets.d.ts.map +1 -0
  58. package/lib/feedback-targets.js +362 -0
  59. package/lib/feedback-targets.js.map +1 -0
  60. package/lib/index.d.ts +27 -0
  61. package/lib/index.d.ts.map +1 -0
  62. package/lib/index.js +25 -0
  63. package/lib/index.js.map +1 -0
  64. package/lib/intake-admission.d.ts +49 -0
  65. package/lib/intake-admission.d.ts.map +1 -0
  66. package/lib/intake-admission.js +100 -0
  67. package/lib/intake-admission.js.map +1 -0
  68. package/lib/intake-evaluation-runtime.d.ts +74 -0
  69. package/lib/intake-evaluation-runtime.d.ts.map +1 -0
  70. package/lib/intake-evaluation-runtime.js +343 -0
  71. package/lib/intake-evaluation-runtime.js.map +1 -0
  72. package/lib/intake-evaluation.d.ts +180 -0
  73. package/lib/intake-evaluation.d.ts.map +1 -0
  74. package/lib/intake-evaluation.js +440 -0
  75. package/lib/intake-evaluation.js.map +1 -0
  76. package/lib/intake.d.ts +118 -0
  77. package/lib/intake.d.ts.map +1 -0
  78. package/lib/intake.js +137 -0
  79. package/lib/intake.js.map +1 -0
  80. package/lib/ledger.d.ts +86 -0
  81. package/lib/ledger.d.ts.map +1 -0
  82. package/lib/ledger.js +145 -0
  83. package/lib/ledger.js.map +1 -0
  84. package/lib/mandate.d.ts +45 -0
  85. package/lib/mandate.d.ts.map +1 -0
  86. package/lib/mandate.js +168 -0
  87. package/lib/mandate.js.map +1 -0
  88. package/lib/outcome.d.ts +137 -0
  89. package/lib/outcome.d.ts.map +1 -0
  90. package/lib/outcome.js +303 -0
  91. package/lib/outcome.js.map +1 -0
  92. package/lib/owner-access.d.ts +35 -0
  93. package/lib/owner-access.d.ts.map +1 -0
  94. package/lib/owner-access.js +152 -0
  95. package/lib/owner-access.js.map +1 -0
  96. package/lib/owner-continuation.d.ts +27 -0
  97. package/lib/owner-continuation.d.ts.map +1 -0
  98. package/lib/owner-continuation.js +131 -0
  99. package/lib/owner-continuation.js.map +1 -0
  100. package/lib/owner-recall.d.ts +85 -0
  101. package/lib/owner-recall.d.ts.map +1 -0
  102. package/lib/owner-recall.js +134 -0
  103. package/lib/owner-recall.js.map +1 -0
  104. package/lib/owner-replies.d.ts +44 -0
  105. package/lib/owner-replies.d.ts.map +1 -0
  106. package/lib/owner-replies.js +232 -0
  107. package/lib/owner-replies.js.map +1 -0
  108. package/lib/peers.d.ts +622 -0
  109. package/lib/peers.d.ts.map +1 -0
  110. package/lib/peers.js +10 -0
  111. package/lib/peers.js.map +1 -0
  112. package/lib/policy-notice.d.ts +31 -0
  113. package/lib/policy-notice.d.ts.map +1 -0
  114. package/lib/policy-notice.js +137 -0
  115. package/lib/policy-notice.js.map +1 -0
  116. package/lib/reply-contract.d.ts +67 -0
  117. package/lib/reply-contract.d.ts.map +1 -0
  118. package/lib/reply-contract.js +58 -0
  119. package/lib/reply-contract.js.map +1 -0
  120. package/lib/service.d.ts +625 -0
  121. package/lib/service.d.ts.map +1 -0
  122. package/lib/service.js +2388 -0
  123. package/lib/service.js.map +1 -0
  124. package/lib/settlement.d.ts +74 -0
  125. package/lib/settlement.d.ts.map +1 -0
  126. package/lib/settlement.js +233 -0
  127. package/lib/settlement.js.map +1 -0
  128. package/lib/signals.d.ts +176 -0
  129. package/lib/signals.d.ts.map +1 -0
  130. package/lib/signals.js +497 -0
  131. package/lib/signals.js.map +1 -0
  132. package/lib/store.d.ts +789 -0
  133. package/lib/store.d.ts.map +1 -0
  134. package/lib/store.js +3266 -0
  135. package/lib/store.js.map +1 -0
  136. package/lib/tools.d.ts +7 -0
  137. package/lib/tools.d.ts.map +1 -0
  138. package/lib/tools.js +443 -0
  139. package/lib/tools.js.map +1 -0
  140. package/lib/types.d.ts +831 -0
  141. package/lib/types.d.ts.map +1 -0
  142. package/lib/types.js +29 -0
  143. package/lib/types.js.map +1 -0
  144. package/lib/version.d.ts +2 -0
  145. package/lib/version.d.ts.map +1 -0
  146. package/lib/version.js +2 -0
  147. package/lib/version.js.map +1 -0
  148. package/lib/workspace.d.ts +43 -0
  149. package/lib/workspace.d.ts.map +1 -0
  150. package/lib/workspace.js +137 -0
  151. package/lib/workspace.js.map +1 -0
  152. package/package.json +101 -3
package/lib/config.js ADDED
@@ -0,0 +1,672 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import Schema from '@deepseek-ai/schemastery';
3
+ import { WorkItemError } from "./types.js";
4
+ import { INTAKE_SOURCE_ID } from "./intake.js";
5
+ const KIND = /^[a-z][a-z0-9-]{0,63}$/;
6
+ const PRESET = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
+ const AUTOMATION_ID = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,127}$/;
8
+ const SOURCE_ID = /^[a-z0-9][a-z0-9._-]{0,63}\/v[0-9]{1,3}$/;
9
+ const SCOPE_KEYS = ['workspace', 'preset', 'principalId', 'ownerRouteId', 'expiresAt'];
10
+ const ADMISSION_KEYS = ['allowedKinds', 'maxOpenItems', 'maxAdmitsPerHour', 'dedupWindowMs', 'cooldownMs', 'maxBriefBytes'];
11
+ const LIMITS_KEYS = ['leaseMs', 'maxAttempts', 'maxReportsPerItem', 'maxReportBytes', 'waitingResumeMs'];
12
+ const DELIVERY_KEYS = ['notifyOn'];
13
+ const SETTLEMENT_KEYS = ['enabled', 'windowMs', 'sweepIntervalMs', 'maxSourcesPerSweep', 'maxPendingPerSweep', 'maxSettlementsPerSweep'];
14
+ const SOURCES_KEYS = ['foregroundFailures'];
15
+ const FOREGROUND_FAILURE_KEYS = ['enabled', 'kind', 'includeTimedOut', 'maxCandidatesPerSweep', 'maxAdmitsPerSweep', 'sweepIntervalMs'];
16
+ const TOP_LEVEL_KEYS = ['enabled', 'mode', 'databasePath', 'workRoot', 'intakeSourceId', 'workerAutomationId', 'scope', 'admission', 'limits', 'delivery', 'sources', 'settlement', 'diagnosis', 'ownerRecall', 'ownerReportRouting', 'ownerContinuation', 'ownerReplyContinuation', 'ownerReportFeedback', 'intakeLedger', 'follows', 'acceptance'];
17
+ const FOLLOWS_KEYS = ['eventTriggers'];
18
+ const FOLLOW_SOURCE_KEYS = ['enabled', 'acceptHeadRef'];
19
+ const INTAKE_LEDGER_KEYS = ['enabled', 'sources', 'evaluation'];
20
+ const INTAKE_EVALUATION_KEYS = ['enabled', 'timeoutMs', 'maxConsecutiveFailures', 'maxPerRun', 'maxCallsPerHour', 'admit'];
21
+ const INTAKE_ADMIT_KEYS = ['enabled', 'mode', 'kind', 'originBindingId', 'maxPerHour'];
22
+ const BINDING_ID = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,255}$/;
23
+ const INTAKE_SOURCE_KEYS = ['unaddressedGroupMessages', 'unsubscribedEvents'];
24
+ const MAX_INTAKE_SOURCES = 32;
25
+ const REPORT_STATUSES = ['progress', 'waiting', 'ready', 'blocked', 'failed'];
26
+ const NOTIFY_STATUSES = ['ready', 'blocked', 'failed'];
27
+ const scopeSchema = Schema.object({
28
+ workspace: Schema.string().required(),
29
+ preset: Schema.string().required(),
30
+ principalId: Schema.string().required(),
31
+ ownerRouteId: Schema.string().required(),
32
+ expiresAt: Schema.number().step(1).required(),
33
+ });
34
+ const admissionSchema = Schema.object({
35
+ allowedKinds: Schema.array(Schema.string()).default([]),
36
+ maxOpenItems: Schema.number().step(1).min(1).max(100).default(8),
37
+ maxAdmitsPerHour: Schema.number().step(1).min(1).max(100).default(4),
38
+ dedupWindowMs: Schema.number().step(1).min(3_600_000).max(2_592_000_000).default(604_800_000),
39
+ cooldownMs: Schema.number().step(1).min(0).max(86_400_000).default(600_000),
40
+ maxBriefBytes: Schema.number().step(1).min(256).max(65_536).default(8_192),
41
+ });
42
+ const limitsSchema = Schema.object({
43
+ leaseMs: Schema.number().step(1).min(30_000).max(86_400_000).default(900_000),
44
+ maxAttempts: Schema.number().step(1).min(1).max(10).default(3),
45
+ maxReportsPerItem: Schema.number().step(1).min(1).max(100).default(20),
46
+ maxReportBytes: Schema.number().step(1).min(256).max(65_536).default(16_384),
47
+ waitingResumeMs: Schema.number().step(1).min(60_000).max(3_600_000).default(300_000),
48
+ });
49
+ const deliverySchema = Schema.object({
50
+ notifyOn: Schema.array(Schema.union(NOTIFY_STATUSES)).default([...NOTIFY_STATUSES]),
51
+ });
52
+ const settlementSchema = Schema.object({
53
+ enabled: Schema.boolean().default(true),
54
+ windowMs: Schema.number().step(1).min(3_600_000).max(777_600_000).default(604_800_000),
55
+ sweepIntervalMs: Schema.number().step(1).min(60_000).max(86_400_000).default(600_000),
56
+ maxSourcesPerSweep: Schema.number().step(1).min(1).max(100).default(25),
57
+ maxPendingPerSweep: Schema.number().step(1).min(1).max(50).default(10),
58
+ maxSettlementsPerSweep: Schema.number().step(1).min(1).max(50).default(10),
59
+ });
60
+ const foregroundFailureSchema = Schema.object({
61
+ enabled: Schema.boolean().default(false),
62
+ kind: Schema.string().default('foreground-repair'),
63
+ includeTimedOut: Schema.boolean().default(true),
64
+ maxCandidatesPerSweep: Schema.number().step(1).min(1).max(100).default(10),
65
+ maxAdmitsPerSweep: Schema.number().step(1).min(1).max(10).default(2),
66
+ sweepIntervalMs: Schema.number().step(1).min(60_000).max(86_400_000).default(3_600_000),
67
+ });
68
+ const sourcesSchema = Schema.object({
69
+ foregroundFailures: foregroundFailureSchema,
70
+ });
71
+ /** Synchronous Standard-Schema validator used by DSH before construction. */
72
+ export const ConfigSchema = Schema.object({
73
+ enabled: Schema.boolean().default(false),
74
+ mode: Schema.union(['observe', 'active']).default('observe'),
75
+ databasePath: Schema.string().required(),
76
+ workRoot: Schema.string().required(),
77
+ intakeSourceId: Schema.string().default('work-items/v1'),
78
+ workerAutomationId: Schema.string(),
79
+ // Object schemas default to {}; the union preserves an absent optional scope.
80
+ scope: Schema.union([Schema.const(undefined), scopeSchema]),
81
+ admission: admissionSchema,
82
+ limits: limitsSchema,
83
+ delivery: deliverySchema,
84
+ sources: sourcesSchema,
85
+ settlement: settlementSchema,
86
+ ownerReplyContinuation: Schema.boolean().default(false),
87
+ ownerReportFeedback: Schema.boolean().default(false),
88
+ intakeLedger: Schema.object({
89
+ enabled: Schema.boolean().default(false),
90
+ sources: Schema.dict(Schema.object({
91
+ unaddressedGroupMessages: Schema.boolean().default(false),
92
+ unsubscribedEvents: Schema.boolean().default(false),
93
+ })),
94
+ evaluation: Schema.object({
95
+ enabled: Schema.boolean().default(false),
96
+ timeoutMs: Schema.number().step(1), maxConsecutiveFailures: Schema.number().step(1),
97
+ maxPerRun: Schema.number().step(1), maxCallsPerHour: Schema.number().step(1),
98
+ admit: Schema.object({
99
+ enabled: Schema.boolean().default(false),
100
+ mode: Schema.union(['observe', 'auto']).default('observe'),
101
+ kind: Schema.string(), originBindingId: Schema.string(), maxPerHour: Schema.number().step(1),
102
+ }),
103
+ }),
104
+ }),
105
+ follows: Schema.object({ eventTriggers: Schema.object({ enabled: Schema.boolean().default(false), acceptHeadRef: Schema.boolean().default(false) }) }),
106
+ acceptance: Schema.object({
107
+ autoSettle: Schema.object({ enabled: Schema.boolean().default(false), maxPerTick: Schema.number().step(1) }),
108
+ bindWorkerRun: Schema.object({ enabled: Schema.boolean().default(false), maxPerTick: Schema.number().step(1) }),
109
+ verdictGate: Schema.object({ enabled: Schema.boolean().default(false) }),
110
+ holdReport: Schema.object({ enabled: Schema.boolean().default(false), holdTimeoutMs: Schema.number().step(1) }),
111
+ }),
112
+ ownerRecall: Schema.object({ enabled: Schema.boolean().default(false), linkedAliases: Schema.array(Schema.string()) }),
113
+ ownerReportRouting: Schema.object({ linkedFallback: Schema.array(Schema.string()), prefer: Schema.union(['origin', 'recent']) }),
114
+ ownerContinuation: Schema.object({
115
+ enabled: Schema.boolean().default(false), automationId: Schema.string(), allowedTools: Schema.array(Schema.string()),
116
+ budgetId: Schema.string(), budgetAmount: Schema.number().step(1),
117
+ maxModelCalls: Schema.number().step(1), maxToolCalls: Schema.number().step(1),
118
+ maxOutputTokens: Schema.number().step(1), timeoutMs: Schema.number().step(1), expiresAt: Schema.number().step(1),
119
+ }),
120
+ diagnosis: Schema.object({
121
+ enabled: Schema.boolean().default(false),
122
+ budgetId: Schema.string(), budgetAmount: Schema.number().step(1),
123
+ maxModelCalls: Schema.number().step(1), maxToolCalls: Schema.number().step(1),
124
+ maxOutputTokens: Schema.number().step(1), timeoutMs: Schema.number().step(1), expiresAt: Schema.number().step(1),
125
+ }),
126
+ });
127
+ function fail(code, message) {
128
+ throw new WorkItemError(code, message);
129
+ }
130
+ function plainObject(value, field) {
131
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
132
+ fail('invalid-input', `${field} must be an object`);
133
+ }
134
+ return value;
135
+ }
136
+ function rejectUnknown(value, allowed, field) {
137
+ for (const key of Object.keys(value)) {
138
+ if (!allowed.includes(key))
139
+ fail('invalid-input', `${field} contains unknown field ${key}`);
140
+ }
141
+ }
142
+ function assertAbsolute(value, field) {
143
+ if (!isAbsolute(value))
144
+ fail('invalid-input', `${field} must be an absolute path`);
145
+ const normalized = resolve(value);
146
+ if (normalized === resolve('/'))
147
+ fail('invalid-input', `${field} must not be the filesystem root`);
148
+ return normalized;
149
+ }
150
+ /** Byte length under UTF-8 without allocating a Buffer for short values. */
151
+ export function byteLength(value) {
152
+ return Buffer.byteLength(value, 'utf8');
153
+ }
154
+ /** Normalize NFC, trim, reject empties and over-long text at a byte boundary. */
155
+ export function text(value, field, maxBytes) {
156
+ if (typeof value !== 'string')
157
+ fail('invalid-input', `${field} must be a string`);
158
+ const normalized = value.normalize('NFC').trim();
159
+ if (normalized.length === 0)
160
+ fail('invalid-input', `${field} must not be empty`);
161
+ if (byteLength(normalized) > maxBytes)
162
+ fail('invalid-input', `${field} exceeds ${maxBytes} bytes`);
163
+ return normalized;
164
+ }
165
+ /** Truncate to at most `maxBytes` UTF-8 bytes without splitting a code point. */
166
+ export function truncateUtf8(value, maxBytes) {
167
+ if (byteLength(value) <= maxBytes)
168
+ return { value, truncated: false };
169
+ let low = 0;
170
+ let high = value.length;
171
+ while (low < high) {
172
+ const middle = Math.ceil((low + high) / 2);
173
+ if (byteLength(value.slice(0, middle)) <= maxBytes)
174
+ low = middle;
175
+ else
176
+ high = middle - 1;
177
+ }
178
+ let end = low;
179
+ // Never leave a dangling high surrogate at the cut point.
180
+ const code = value.charCodeAt(end - 1);
181
+ if (end > 0 && code >= 0xd800 && code <= 0xdbff)
182
+ end -= 1;
183
+ return { value: value.slice(0, end), truncated: true };
184
+ }
185
+ function integer(value, field, minimum, maximum) {
186
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
187
+ fail('invalid-input', `${field} must be an integer between ${minimum} and ${maximum}`);
188
+ }
189
+ return value;
190
+ }
191
+ function normalizeScope(raw, now, historicalFeedback = false) {
192
+ if (raw === undefined)
193
+ return null;
194
+ const scope = plainObject(raw, 'scope');
195
+ rejectUnknown(scope, SCOPE_KEYS, 'scope');
196
+ const normalized = {
197
+ workspace: assertAbsolute(text(scope['workspace'], 'scope.workspace', 4_096), 'scope.workspace'),
198
+ preset: text(scope['preset'], 'scope.preset', 128),
199
+ principalId: text(scope['principalId'], 'scope.principalId', 256),
200
+ ownerRouteId: text(scope['ownerRouteId'], 'scope.ownerRouteId', 256),
201
+ expiresAt: integer(scope['expiresAt'], 'scope.expiresAt', 1, Number.MAX_SAFE_INTEGER),
202
+ };
203
+ if (!PRESET.test(normalized.preset))
204
+ fail('invalid-input', 'scope.preset has an invalid shape');
205
+ if (!historicalFeedback && normalized.expiresAt <= now)
206
+ fail('invalid-input', 'scope.expiresAt must be in the future');
207
+ return Object.freeze(normalized);
208
+ }
209
+ function normalizeAdmission(raw, mode) {
210
+ const value = raw === undefined ? {} : plainObject(raw, 'admission');
211
+ rejectUnknown(value, ADMISSION_KEYS, 'admission');
212
+ const allowedKinds = (value['allowedKinds'] ?? []);
213
+ if (!Array.isArray(allowedKinds))
214
+ fail('invalid-input', 'admission.allowedKinds must be an array');
215
+ const kinds = allowedKinds.map(entry => text(entry, 'admission.allowedKinds[]', 64));
216
+ for (const kind of kinds)
217
+ if (!KIND.test(kind))
218
+ fail('invalid-input', `admission.allowedKinds entry ${kind} has an invalid shape`);
219
+ if (new Set(kinds).size !== kinds.length)
220
+ fail('invalid-input', 'admission.allowedKinds must not repeat an entry');
221
+ const normalized = Object.freeze({
222
+ allowedKinds: Object.freeze(kinds),
223
+ maxOpenItems: integer(value['maxOpenItems'] ?? 8, 'admission.maxOpenItems', 1, 100),
224
+ maxAdmitsPerHour: integer(value['maxAdmitsPerHour'] ?? 4, 'admission.maxAdmitsPerHour', 1, 100),
225
+ dedupWindowMs: integer(value['dedupWindowMs'] ?? 604_800_000, 'admission.dedupWindowMs', 3_600_000, 2_592_000_000),
226
+ cooldownMs: integer(value['cooldownMs'] ?? 600_000, 'admission.cooldownMs', 0, 86_400_000),
227
+ maxBriefBytes: integer(value['maxBriefBytes'] ?? 8_192, 'admission.maxBriefBytes', 256, 65_536),
228
+ });
229
+ if (mode === 'active' && kinds.length === 0) {
230
+ fail('kind-rejected', 'an active work-item intake requires at least one admission.allowedKinds entry');
231
+ }
232
+ return normalized;
233
+ }
234
+ function normalizeLimits(raw) {
235
+ const value = raw === undefined ? {} : plainObject(raw, 'limits');
236
+ rejectUnknown(value, LIMITS_KEYS, 'limits');
237
+ return Object.freeze({
238
+ leaseMs: integer(value['leaseMs'] ?? 900_000, 'limits.leaseMs', 30_000, 86_400_000),
239
+ maxAttempts: integer(value['maxAttempts'] ?? 3, 'limits.maxAttempts', 1, 10),
240
+ maxReportsPerItem: integer(value['maxReportsPerItem'] ?? 20, 'limits.maxReportsPerItem', 1, 100),
241
+ maxReportBytes: integer(value['maxReportBytes'] ?? 16_384, 'limits.maxReportBytes', 256, 65_536),
242
+ waitingResumeMs: integer(value['waitingResumeMs'] ?? 300_000, 'limits.waitingResumeMs', 60_000, 3_600_000),
243
+ });
244
+ }
245
+ function normalizeDelivery(raw) {
246
+ const value = raw === undefined ? {} : plainObject(raw, 'delivery');
247
+ rejectUnknown(value, DELIVERY_KEYS, 'delivery');
248
+ const notifyOn = (value['notifyOn'] ?? [...NOTIFY_STATUSES]);
249
+ if (!Array.isArray(notifyOn) || notifyOn.length === 0)
250
+ fail('invalid-input', 'delivery.notifyOn must be a non-empty array');
251
+ const statuses = notifyOn.map(entry => {
252
+ if (typeof entry !== 'string' || !NOTIFY_STATUSES.includes(entry)) {
253
+ fail('invalid-input', `delivery.notifyOn entry ${String(entry)} is not a notifiable status`);
254
+ }
255
+ return entry;
256
+ });
257
+ if (new Set(statuses).size !== statuses.length)
258
+ fail('invalid-input', 'delivery.notifyOn must not repeat an entry');
259
+ return Object.freeze(statuses);
260
+ }
261
+ function normalizeSources(raw) {
262
+ const value = raw === undefined ? {} : plainObject(raw, 'sources');
263
+ rejectUnknown(value, SOURCES_KEYS, 'sources');
264
+ const foreground = value['foregroundFailures'] === undefined
265
+ ? {}
266
+ : plainObject(value['foregroundFailures'], 'sources.foregroundFailures');
267
+ rejectUnknown(foreground, FOREGROUND_FAILURE_KEYS, 'sources.foregroundFailures');
268
+ const enabled = foreground['enabled'] ?? false;
269
+ if (typeof enabled !== 'boolean')
270
+ fail('invalid-input', 'sources.foregroundFailures.enabled must be a boolean');
271
+ const kind = text(foreground['kind'] ?? 'foreground-repair', 'sources.foregroundFailures.kind', 64);
272
+ if (!KIND.test(kind))
273
+ fail('invalid-input', 'sources.foregroundFailures.kind has an invalid shape');
274
+ const includeTimedOut = foreground['includeTimedOut'] ?? true;
275
+ if (typeof includeTimedOut !== 'boolean')
276
+ fail('invalid-input', 'sources.foregroundFailures.includeTimedOut must be a boolean');
277
+ return Object.freeze({
278
+ foregroundFailures: Object.freeze({
279
+ enabled,
280
+ kind,
281
+ includeTimedOut,
282
+ maxCandidatesPerSweep: integer(foreground['maxCandidatesPerSweep'] ?? 10, 'sources.foregroundFailures.maxCandidatesPerSweep', 1, 100),
283
+ maxAdmitsPerSweep: integer(foreground['maxAdmitsPerSweep'] ?? 2, 'sources.foregroundFailures.maxAdmitsPerSweep', 1, 10),
284
+ sweepIntervalMs: integer(foreground['sweepIntervalMs'] ?? 3_600_000, 'sources.foregroundFailures.sweepIntervalMs', 60_000, 86_400_000),
285
+ }),
286
+ });
287
+ }
288
+ function normalizeSettlement(raw) {
289
+ const value = raw === undefined ? {} : plainObject(raw, 'settlement');
290
+ rejectUnknown(value, SETTLEMENT_KEYS, 'settlement');
291
+ const enabled = value['enabled'] ?? true;
292
+ if (typeof enabled !== 'boolean')
293
+ fail('invalid-input', 'settlement.enabled must be a boolean');
294
+ return Object.freeze({
295
+ enabled,
296
+ windowMs: integer(value['windowMs'] ?? 604_800_000, 'settlement.windowMs', 3_600_000, 777_600_000),
297
+ sweepIntervalMs: integer(value['sweepIntervalMs'] ?? 600_000, 'settlement.sweepIntervalMs', 60_000, 86_400_000),
298
+ maxSourcesPerSweep: integer(value['maxSourcesPerSweep'] ?? 25, 'settlement.maxSourcesPerSweep', 1, 100),
299
+ maxPendingPerSweep: integer(value['maxPendingPerSweep'] ?? 10, 'settlement.maxPendingPerSweep', 1, 50),
300
+ maxSettlementsPerSweep: integer(value['maxSettlementsPerSweep'] ?? 10, 'settlement.maxSettlementsPerSweep', 1, 50),
301
+ });
302
+ }
303
+ function normalizeDiagnosis(raw, scope, now, historicalFeedback = false) {
304
+ if (raw === undefined)
305
+ return undefined;
306
+ const value = plainObject(raw, 'diagnosis');
307
+ rejectUnknown(value, ['enabled', 'budgetId', 'budgetAmount', 'maxModelCalls', 'maxToolCalls', 'maxOutputTokens', 'timeoutMs', 'expiresAt'], 'diagnosis');
308
+ if (value['enabled'] === undefined || value['enabled'] === false)
309
+ return undefined;
310
+ if (value['enabled'] !== true)
311
+ fail('invalid-input', 'diagnosis.enabled must be a boolean');
312
+ if (scope === null)
313
+ fail('invalid-input', 'diagnosis requires an explicit owner scope');
314
+ const expiresAt = integer(value['expiresAt'], 'diagnosis.expiresAt', 1, Number.MAX_SAFE_INTEGER);
315
+ if ((!historicalFeedback && expiresAt <= now) || expiresAt > scope.expiresAt)
316
+ fail('invalid-input', 'diagnosis expiry must be future and no later than the owner scope');
317
+ return Object.freeze({ enabled: true, budgetId: text(value['budgetId'], 'diagnosis.budgetId', 200),
318
+ budgetAmount: integer(value['budgetAmount'], 'diagnosis.budgetAmount', 1, 10_000_000),
319
+ maxModelCalls: integer(value['maxModelCalls'], 'diagnosis.maxModelCalls', 1, 100),
320
+ maxToolCalls: integer(value['maxToolCalls'], 'diagnosis.maxToolCalls', 1, 1000),
321
+ maxOutputTokens: integer(value['maxOutputTokens'], 'diagnosis.maxOutputTokens', 1, 131072),
322
+ timeoutMs: integer(value['timeoutMs'], 'diagnosis.timeoutMs', 1000, 300000), expiresAt });
323
+ }
324
+ /** True when at least one automatic signal source is armed. */
325
+ export function anySourceEnabled(config) {
326
+ return config.sources.foregroundFailures.enabled;
327
+ }
328
+ /**
329
+ * Validate and default operator configuration synchronously.
330
+ *
331
+ * Unknown fields anywhere fail closed, paths must be absolute, and an
332
+ * `enabled: true` + `mode: 'active'` deployment must name its worker
333
+ * automation, owner scope and at least one admitted kind. The default
334
+ * deployment is `enabled: false`, `mode: 'observe'`: it writes only local
335
+ * observation rows and never calls automations or delivery.
336
+ */
337
+ export function normalizeConfig(raw, now = Date.now()) {
338
+ const value = plainObject(raw, 'config');
339
+ rejectUnknown(value, TOP_LEVEL_KEYS, 'config');
340
+ const ownerReportFeedback = value['ownerReportFeedback'];
341
+ if (ownerReportFeedback !== undefined && typeof ownerReportFeedback !== 'boolean')
342
+ fail('invalid-input', 'ownerReportFeedback must be a boolean');
343
+ const enabled = value['enabled'] ?? false;
344
+ if (typeof enabled !== 'boolean')
345
+ fail('invalid-input', 'enabled must be a boolean');
346
+ const mode = value['mode'] ?? 'observe';
347
+ if (mode !== 'observe' && mode !== 'active')
348
+ fail('invalid-input', "mode must be 'observe' or 'active'");
349
+ const databasePath = assertAbsolute(text(value['databasePath'], 'databasePath', 4_096), 'databasePath');
350
+ const workRoot = assertAbsolute(text(value['workRoot'], 'workRoot', 4_096), 'workRoot');
351
+ if (databasePath === workRoot)
352
+ fail('invalid-input', 'databasePath and workRoot must differ');
353
+ if (workRoot.startsWith(`${databasePath}/`))
354
+ fail('invalid-input', 'databasePath must not contain workRoot');
355
+ const intakeSourceId = text(value['intakeSourceId'] ?? 'work-items/v1', 'intakeSourceId', 128);
356
+ if (!SOURCE_ID.test(intakeSourceId))
357
+ fail('invalid-input', 'intakeSourceId must look like <source>/v<number>');
358
+ const rawAutomationId = value['workerAutomationId'];
359
+ const workerAutomationId = rawAutomationId === undefined ? null : text(rawAutomationId, 'workerAutomationId', 128);
360
+ if (workerAutomationId !== null && !AUTOMATION_ID.test(workerAutomationId)) {
361
+ fail('invalid-input', 'workerAutomationId has an invalid shape');
362
+ }
363
+ const scope = normalizeScope(value['scope'], now, ownerReportFeedback === true);
364
+ const admission = normalizeAdmission(value['admission'], mode);
365
+ const limits = normalizeLimits(value['limits']);
366
+ const notifyOn = normalizeDelivery(value['delivery']);
367
+ const sources = normalizeSources(value['sources']);
368
+ const settlement = normalizeSettlement(value['settlement']);
369
+ const diagnosis = normalizeDiagnosis(value['diagnosis'], scope, now, ownerReportFeedback === true);
370
+ if (diagnosis && (!enabled || mode !== 'active' || !sources.foregroundFailures.enabled))
371
+ fail('invalid-input', 'diagnosis requires active enabled intake and foreground failure source');
372
+ if (enabled && mode === 'active' && (workerAutomationId === null || scope === null)) {
373
+ fail('invalid-input', "an active work-item deployment requires workerAutomationId and scope");
374
+ }
375
+ if (enabled && mode === 'active' && sources.foregroundFailures.enabled && !admission.allowedKinds.includes(sources.foregroundFailures.kind)) {
376
+ fail('invalid-input', 'sources.foregroundFailures.kind must be one of admission.allowedKinds');
377
+ }
378
+ const ownerRecall = value['ownerRecall'] === undefined ? {} : plainObject(value['ownerRecall'], 'ownerRecall');
379
+ rejectUnknown(ownerRecall, ['enabled', 'linkedAliases'], 'ownerRecall');
380
+ if (ownerRecall['enabled'] !== undefined && typeof ownerRecall['enabled'] !== 'boolean')
381
+ fail('invalid-input', 'ownerRecall.enabled must be a boolean');
382
+ if (ownerRecall['enabled'] === true && (!enabled || mode !== 'active' || scope === null || workerAutomationId === null)) {
383
+ fail('invalid-input', 'ownerRecall requires an enabled active worker and complete scope');
384
+ }
385
+ const linkedAliases = normalizeLinkedAliases(ownerRecall['linkedAliases'], ownerRecall['enabled'] === true, scope);
386
+ const ownerReportRouting = value['ownerReportRouting'] === undefined ? {} : plainObject(value['ownerReportRouting'], 'ownerReportRouting');
387
+ rejectUnknown(ownerReportRouting, ['linkedFallback', 'prefer'], 'ownerReportRouting');
388
+ const linkedFallback = normalizeLinkedAliases(ownerReportRouting['linkedFallback'], enabled && mode === 'active' && scope !== null && workerAutomationId !== null, scope, 'ownerReportRouting.linkedFallback', 'an enabled active worker and complete scope');
389
+ const reportPrefer = ownerReportRouting['prefer'];
390
+ if (reportPrefer !== undefined && reportPrefer !== 'origin' && reportPrefer !== 'recent')
391
+ fail('invalid-input', "ownerReportRouting.prefer must be 'origin' or 'recent'");
392
+ if (reportPrefer === 'recent' && linkedFallback === undefined)
393
+ fail('invalid-input', "ownerReportRouting.prefer 'recent' requires a non-empty linkedFallback");
394
+ const ownerContinuation = normalizeOwnerContinuation(value['ownerContinuation'], scope, workerAutomationId, now, ownerReportFeedback === true);
395
+ if (ownerContinuation && !admission.allowedKinds.includes('owner-continuation'))
396
+ fail('invalid-input', 'ownerContinuation requires owner-continuation in admission.allowedKinds');
397
+ if (ownerContinuation && (!enabled || mode !== 'active' || ownerRecall['enabled'] !== true)) {
398
+ fail('invalid-input', 'ownerContinuation requires active intake and ownerRecall');
399
+ }
400
+ if (ownerReportFeedback === true && !ownerContinuation)
401
+ fail('invalid-input', 'ownerReportFeedback requires configured ownerContinuation');
402
+ const ownerReplyContinuation = value['ownerReplyContinuation'];
403
+ if (ownerReplyContinuation !== undefined && typeof ownerReplyContinuation !== 'boolean')
404
+ fail('invalid-input', 'ownerReplyContinuation must be a boolean');
405
+ if (ownerReplyContinuation === true && !ownerContinuation)
406
+ fail('invalid-input', 'ownerReplyContinuation requires active ownerContinuation authorization');
407
+ const intakeLedger = normalizeIntakeLedger(value['intakeLedger']);
408
+ const intakeAdmit = intakeLedger?.evaluation?.admit;
409
+ if (intakeAdmit?.mode === 'auto') {
410
+ if (!enabled || mode !== 'active' || diagnosis)
411
+ fail('invalid-input', 'intakeLedger.evaluation.admit auto mode requires an enabled active intake without diagnosis');
412
+ if (!admission.allowedKinds.includes(intakeAdmit.kind))
413
+ fail('invalid-input', 'intakeLedger.evaluation.admit.kind must be one of admission.allowedKinds');
414
+ }
415
+ const follows = normalizeFollows(value['follows']);
416
+ if (follows?.eventTriggers === true && (!enabled || scope === null))
417
+ fail('invalid-input', 'follows.eventTriggers requires an enabled intake with an owner scope');
418
+ const acceptance = normalizeAcceptance(value['acceptance']);
419
+ const acceptanceAutoSettle = acceptance.autoSettle;
420
+ if (acceptanceAutoSettle && (!enabled || mode !== 'active'))
421
+ fail('invalid-input', 'acceptance.autoSettle requires an enabled active intake');
422
+ const acceptanceBindWorkerRun = acceptance.bindWorkerRun;
423
+ if (acceptanceBindWorkerRun && (!enabled || mode !== 'active'))
424
+ fail('invalid-input', 'acceptance.bindWorkerRun requires an enabled active intake');
425
+ const acceptanceVerdictGate = acceptance.verdictGate;
426
+ if (acceptanceVerdictGate && (!enabled || mode !== 'active'))
427
+ fail('invalid-input', 'acceptance.verdictGate requires an enabled active intake');
428
+ const acceptanceHoldReport = acceptance.holdReport;
429
+ if (acceptanceHoldReport && (!enabled || mode !== 'active' || diagnosis))
430
+ fail('invalid-input', 'acceptance.holdReport requires an enabled active intake without diagnosis');
431
+ if (acceptanceHoldReport && (!acceptanceBindWorkerRun || !acceptanceAutoSettle))
432
+ fail('invalid-input', 'acceptance.holdReport requires acceptance.bindWorkerRun and acceptance.autoSettle');
433
+ return Object.freeze({
434
+ enabled,
435
+ mode,
436
+ databasePath,
437
+ workRoot,
438
+ intakeSourceId,
439
+ workerAutomationId,
440
+ scope,
441
+ admission,
442
+ limits,
443
+ notifyOn,
444
+ sources,
445
+ settlement,
446
+ ...(diagnosis === undefined ? {} : { diagnosis }),
447
+ ...(ownerRecall['enabled'] === true ? { ownerRecall: Object.freeze({ enabled: true, ...(linkedAliases ? { linkedAliases } : {}) }) } : {}),
448
+ ...(linkedFallback === undefined ? {} : { ownerReportRouting: Object.freeze({ linkedFallback, ...(reportPrefer === 'recent' ? { prefer: 'recent' } : {}) }) }),
449
+ ...(ownerContinuation === undefined ? {} : { ownerContinuation }),
450
+ ...(ownerReplyContinuation === true ? { ownerReplyContinuation: true } : {}),
451
+ ...(ownerReportFeedback === true ? { ownerReportFeedback: true } : {}),
452
+ ...(intakeLedger === undefined ? {} : { intakeLedger }),
453
+ ...(follows === undefined ? {} : { follows }),
454
+ ...(acceptanceAutoSettle === undefined ? {} : { acceptanceAutoSettle }),
455
+ ...(acceptanceBindWorkerRun === undefined ? {} : { acceptanceBindWorkerRun }),
456
+ ...(acceptanceVerdictGate === true ? { acceptanceVerdictGate: true } : {}),
457
+ ...(acceptanceHoldReport === undefined ? {} : { acceptanceHoldReport }),
458
+ });
459
+ }
460
+ /** Absent or disabled acceptance passes normalize to `undefined`. */
461
+ function normalizeAcceptance(raw) {
462
+ if (raw === undefined)
463
+ return {};
464
+ const value = plainObject(raw, 'acceptance');
465
+ rejectUnknown(value, ['autoSettle', 'bindWorkerRun', 'verdictGate', 'holdReport'], 'acceptance');
466
+ const pass = (key) => {
467
+ if (value[key] === undefined)
468
+ return undefined;
469
+ const section = plainObject(value[key], `acceptance.${key}`);
470
+ rejectUnknown(section, ['enabled', 'maxPerTick'], `acceptance.${key}`);
471
+ const enabled = section['enabled'] ?? false;
472
+ if (typeof enabled !== 'boolean')
473
+ fail('invalid-input', `acceptance.${key}.enabled must be a boolean`);
474
+ if (!enabled)
475
+ return undefined;
476
+ return Object.freeze({ maxPerTick: integer(section['maxPerTick'] ?? 5, `acceptance.${key}.maxPerTick`, 1, 10) });
477
+ };
478
+ const autoSettle = pass('autoSettle'), bindWorkerRun = pass('bindWorkerRun');
479
+ let verdictGate = false;
480
+ if (value['verdictGate'] !== undefined) {
481
+ const section = plainObject(value['verdictGate'], 'acceptance.verdictGate');
482
+ rejectUnknown(section, ['enabled'], 'acceptance.verdictGate');
483
+ const gate = section['enabled'] ?? false;
484
+ if (typeof gate !== 'boolean')
485
+ fail('invalid-input', 'acceptance.verdictGate.enabled must be a boolean');
486
+ verdictGate = gate;
487
+ }
488
+ let holdReport;
489
+ if (value['holdReport'] !== undefined) {
490
+ const section = plainObject(value['holdReport'], 'acceptance.holdReport');
491
+ rejectUnknown(section, ['enabled', 'holdTimeoutMs'], 'acceptance.holdReport');
492
+ const hold = section['enabled'] ?? false;
493
+ if (typeof hold !== 'boolean')
494
+ fail('invalid-input', 'acceptance.holdReport.enabled must be a boolean');
495
+ // One minute to one day: a verifier outage releases the report unverified, never holds it forever.
496
+ if (hold)
497
+ holdReport = Object.freeze({ holdTimeoutMs: integer(section['holdTimeoutMs'] ?? 1_800_000, 'acceptance.holdReport.holdTimeoutMs', 60_000, 86_400_000) });
498
+ }
499
+ return { ...(autoSettle === undefined ? {} : { autoSettle }), ...(bindWorkerRun === undefined ? {} : { bindWorkerRun }),
500
+ ...(verdictGate ? { verdictGate: true } : {}), ...(holdReport === undefined ? {} : { holdReport }) };
501
+ }
502
+ /** Absent or all-disabled follow sources normalize to `undefined`. */
503
+ function normalizeFollows(raw) {
504
+ if (raw === undefined)
505
+ return undefined;
506
+ const value = plainObject(raw, 'follows');
507
+ rejectUnknown(value, FOLLOWS_KEYS, 'follows');
508
+ const source = value['eventTriggers'] === undefined ? {} : plainObject(value['eventTriggers'], 'follows.eventTriggers');
509
+ rejectUnknown(source, FOLLOW_SOURCE_KEYS, 'follows.eventTriggers');
510
+ const eventTriggers = source['enabled'] ?? false;
511
+ if (typeof eventTriggers !== 'boolean')
512
+ fail('invalid-input', 'follows.eventTriggers.enabled must be a boolean');
513
+ const acceptHeadRef = source['acceptHeadRef'] ?? false;
514
+ if (typeof acceptHeadRef !== 'boolean')
515
+ fail('invalid-input', 'follows.eventTriggers.acceptHeadRef must be a boolean');
516
+ if (!eventTriggers)
517
+ return undefined;
518
+ return Object.freeze(acceptHeadRef ? { eventTriggers, eventTriggersAcceptHeadRef: true } : { eventTriggers });
519
+ }
520
+ /** Absent or disabled ledgers normalize to `undefined`; opt-ins default off. */
521
+ function normalizeIntakeLedger(raw) {
522
+ if (raw === undefined)
523
+ return undefined;
524
+ const value = plainObject(raw, 'intakeLedger');
525
+ rejectUnknown(value, INTAKE_LEDGER_KEYS, 'intakeLedger');
526
+ const enabled = value['enabled'] ?? false;
527
+ if (typeof enabled !== 'boolean')
528
+ fail('invalid-input', 'intakeLedger.enabled must be a boolean');
529
+ const rawSources = value['sources'] === undefined ? {} : plainObject(value['sources'], 'intakeLedger.sources');
530
+ const ids = Object.keys(rawSources);
531
+ if (ids.length > MAX_INTAKE_SOURCES)
532
+ fail('invalid-input', `intakeLedger.sources accepts at most ${MAX_INTAKE_SOURCES} sources`);
533
+ const sources = Object.create(null);
534
+ for (const id of ids.sort()) {
535
+ if (!INTAKE_SOURCE_ID.test(id))
536
+ fail('invalid-input', 'intakeLedger.sources keys must be lowercase source ids');
537
+ const entry = plainObject(rawSources[id], `intakeLedger.sources.${id}`);
538
+ rejectUnknown(entry, INTAKE_SOURCE_KEYS, `intakeLedger.sources.${id}`);
539
+ const policy = {};
540
+ for (const key of INTAKE_SOURCE_KEYS) {
541
+ const flag = entry[key] ?? false;
542
+ if (typeof flag !== 'boolean')
543
+ fail('invalid-input', `intakeLedger.sources.${id}.${key} must be a boolean`);
544
+ policy[key] = flag;
545
+ }
546
+ sources[id] = Object.freeze(policy);
547
+ }
548
+ const evaluation = normalizeIntakeEvaluation(value['evaluation']);
549
+ if (!enabled)
550
+ return undefined;
551
+ return Object.freeze({ enabled: true, sources: Object.freeze(sources), ...(evaluation === undefined ? {} : { evaluation }) });
552
+ }
553
+ /** Absent or disabled evaluation normalizes to `undefined`; limits default small. */
554
+ function normalizeIntakeEvaluation(raw) {
555
+ if (raw === undefined)
556
+ return undefined;
557
+ const value = plainObject(raw, 'intakeLedger.evaluation');
558
+ rejectUnknown(value, INTAKE_EVALUATION_KEYS, 'intakeLedger.evaluation');
559
+ const enabled = value['enabled'] ?? false;
560
+ if (typeof enabled !== 'boolean')
561
+ fail('invalid-input', 'intakeLedger.evaluation.enabled must be a boolean');
562
+ const normalized = Object.freeze({
563
+ enabled: true,
564
+ timeoutMs: integer(value['timeoutMs'] ?? 30_000, 'intakeLedger.evaluation.timeoutMs', 100, 600_000),
565
+ maxConsecutiveFailures: integer(value['maxConsecutiveFailures'] ?? 3, 'intakeLedger.evaluation.maxConsecutiveFailures', 1, 10),
566
+ maxPerRun: integer(value['maxPerRun'] ?? 5, 'intakeLedger.evaluation.maxPerRun', 1, 50),
567
+ maxCallsPerHour: integer(value['maxCallsPerHour'] ?? 20, 'intakeLedger.evaluation.maxCallsPerHour', 1, 1_000),
568
+ });
569
+ const admit = normalizeIntakeAdmit(value['admit']);
570
+ if (!enabled)
571
+ return undefined;
572
+ return admit === undefined ? normalized : Object.freeze({ ...normalized, admit });
573
+ }
574
+ /** Absent or disabled admission normalizes to `undefined`; enabled needs a kind and binding. */
575
+ function normalizeIntakeAdmit(raw) {
576
+ if (raw === undefined)
577
+ return undefined;
578
+ const value = plainObject(raw, 'intakeLedger.evaluation.admit');
579
+ rejectUnknown(value, INTAKE_ADMIT_KEYS, 'intakeLedger.evaluation.admit');
580
+ const enabled = value['enabled'] ?? false;
581
+ if (typeof enabled !== 'boolean')
582
+ fail('invalid-input', 'intakeLedger.evaluation.admit.enabled must be a boolean');
583
+ if (!enabled)
584
+ return undefined;
585
+ const mode = value['mode'] ?? 'observe';
586
+ if (mode !== 'observe' && mode !== 'auto')
587
+ fail('invalid-input', "intakeLedger.evaluation.admit.mode must be 'observe' or 'auto'");
588
+ const kind = text(value['kind'] ?? 'intake-inferred', 'intakeLedger.evaluation.admit.kind', 64);
589
+ if (!KIND.test(kind))
590
+ fail('invalid-input', 'intakeLedger.evaluation.admit.kind has an invalid shape');
591
+ const originBindingId = text(value['originBindingId'], 'intakeLedger.evaluation.admit.originBindingId', 256);
592
+ if (!BINDING_ID.test(originBindingId))
593
+ fail('invalid-input', 'intakeLedger.evaluation.admit.originBindingId has an invalid shape');
594
+ return Object.freeze({
595
+ enabled: true, mode, kind, originBindingId,
596
+ maxPerHour: integer(value['maxPerHour'] ?? 5, 'intakeLedger.evaluation.admit.maxPerHour', 1, 100),
597
+ });
598
+ }
599
+ /** Maximum explicitly allowlisted linked aliases per owner scope. */
600
+ export const MAX_LINKED_ALIASES = 8;
601
+ /**
602
+ * Canonical Delivery external principal id: four `encodeURIComponent`-encoded
603
+ * components (`channel/account/tenant/user`) of Delivery's key alphabet
604
+ * `[A-Za-z0-9][A-Za-z0-9._@/-]*`, so only `%40` and `%2F` escapes occur.
605
+ * Mirrors assistant-delivery `externalPrincipalId`; anything else (Unicode,
606
+ * NFD/compatibility variants, whitespace, lower-case escapes) can never equal
607
+ * a Delivery receipt and is rejected rather than silently never matching.
608
+ */
609
+ const DELIVERY_PRINCIPAL_COMPONENT = String.raw `[A-Za-z0-9](?:[A-Za-z0-9._-]|%40|%2F)*`;
610
+ const DELIVERY_PRINCIPAL_ID = new RegExp(`^${DELIVERY_PRINCIPAL_COMPONENT}(?:/${DELIVERY_PRINCIPAL_COMPONENT}){3}$`, 'u');
611
+ /**
612
+ * Normalize the owner's explicit linked-alias allowlist.
613
+ *
614
+ * An alias read is a cross-channel identity decision, so it must be named
615
+ * here rather than inherited from a Delivery link plus a Policy rule that
616
+ * happens not to match on `principal`. Entries are exact Delivery principal
617
+ * ids, unique, bounded, and never the root owner itself.
618
+ */
619
+ function normalizeLinkedAliases(raw, recallEnabled, scope, field = 'ownerRecall.linkedAliases', requirement = 'ownerRecall.enabled') {
620
+ // Schema defaults materialize an empty array; only a non-empty list is a choice.
621
+ if (raw === undefined || Array.isArray(raw) && raw.length === 0)
622
+ return undefined;
623
+ if (!recallEnabled)
624
+ fail('invalid-input', `${field} requires ${requirement}`);
625
+ if (!Array.isArray(raw) || raw.length > MAX_LINKED_ALIASES)
626
+ fail('invalid-input', `${field} must be an array of at most ${MAX_LINKED_ALIASES} principal ids`);
627
+ const seen = new Set();
628
+ for (const entry of raw) {
629
+ if (typeof entry !== 'string' || entry.length > 256 || !DELIVERY_PRINCIPAL_ID.test(entry)) {
630
+ fail('invalid-input', `${field} entries must be canonical Delivery principal ids (channel/account/tenant/user)`);
631
+ }
632
+ if (entry === scope?.principalId)
633
+ fail('invalid-input', `${field} must not contain the owner principal`);
634
+ if (seen.has(entry))
635
+ fail('invalid-input', `${field} must not contain duplicates`);
636
+ seen.add(entry);
637
+ }
638
+ return seen.size === 0 ? undefined : Object.freeze([...seen]);
639
+ }
640
+ function normalizeOwnerContinuation(raw, scope, workerId, now, historicalFeedback = false) {
641
+ if (raw === undefined)
642
+ return undefined;
643
+ const value = plainObject(raw, 'ownerContinuation');
644
+ const { automationId, allowedTools, ...limits } = value;
645
+ const budget = normalizeDiagnosis(limits, scope, now, historicalFeedback);
646
+ if (!budget) {
647
+ if (automationId !== undefined || allowedTools !== undefined && (!Array.isArray(allowedTools) || allowedTools.length !== 0))
648
+ fail('invalid-input', 'disabled ownerContinuation cannot carry execution settings');
649
+ return undefined;
650
+ }
651
+ const id = text(automationId, 'ownerContinuation.automationId', 128);
652
+ if (!AUTOMATION_ID.test(id) || id === workerId)
653
+ fail('invalid-input', 'ownerContinuation requires a distinct automationId');
654
+ if (!Array.isArray(allowedTools) || allowedTools.length < 3 || allowedTools.length > 64)
655
+ fail('invalid-input', 'ownerContinuation requires a bounded explicit tool list');
656
+ const tools = allowedTools.map(value => text(value, 'ownerContinuation.allowedTools', 128));
657
+ if (new Set(tools).size !== tools.length || tools.some(name => !/^[A-Za-z][A-Za-z0-9_-]*$/u.test(name)
658
+ || ['work_item_continue', 'work_item_list', 'work_item_recall', 'work_item_read_source'].includes(name))
659
+ || ['work_item_claim', 'work_item_report', 'work_item_read_context'].some(name => !tools.includes(name))) {
660
+ fail('invalid-input', 'continuation tools require claim/report/context and cannot include foreground or diagnosis capabilities');
661
+ }
662
+ return Object.freeze({ ...budget, automationId: id, allowedTools: Object.freeze(tools.sort()) });
663
+ }
664
+ /** True only when external admission and dispatch may happen. */
665
+ export function isActive(config) {
666
+ return config.enabled && config.mode === 'active';
667
+ }
668
+ /** Prove the config exposes only known report statuses (used by tests). */
669
+ export function knownReportStatuses() {
670
+ return REPORT_STATUSES;
671
+ }
672
+ //# sourceMappingURL=config.js.map