@noodleseed/one 0.170.0 → 0.171.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 (105) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +3 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/catalog.d.ts.map +1 -1
  5. package/dist/commands/catalog.js +2 -1
  6. package/dist/commands/catalog.js.map +1 -1
  7. package/dist/commands/whatsapp-ops.d.ts +13 -0
  8. package/dist/commands/whatsapp-ops.d.ts.map +1 -0
  9. package/dist/commands/whatsapp-ops.js +244 -0
  10. package/dist/commands/whatsapp-ops.js.map +1 -0
  11. package/dist/commands/whatsapp-output.d.ts +3 -0
  12. package/dist/commands/whatsapp-output.d.ts.map +1 -0
  13. package/dist/commands/whatsapp-output.js +70 -0
  14. package/dist/commands/whatsapp-output.js.map +1 -0
  15. package/dist/dev.js +2 -0
  16. package/dist/dev.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/plugin-mode/plugin-operation-contract.d.ts +1 -1
  22. package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
  23. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +1 -0
  24. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +3 -0
  26. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +6 -0
  27. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  28. package/node_modules/@noodle-borg/app-audit/dist/audit-embedded-assistant.js +2 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +2 -1
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +9 -4
  31. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.d.ts +16 -0
  32. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.js +19 -0
  33. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +145 -0
  34. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +378 -0
  35. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +160 -0
  36. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.d.ts +18 -0
  37. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.js +69 -0
  38. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-spend.js +82 -0
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +42 -0
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.js +76 -0
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +134 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +72 -0
  43. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +20 -0
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +221 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +15 -0
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +34 -0
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +19 -1
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +22 -6
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +7 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +7 -0
  51. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-appearance-store.js +83 -0
  52. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-elevation.js +151 -0
  53. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interaction-writes.js +45 -0
  54. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interactions.js +340 -0
  55. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant.js +439 -0
  56. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-channel-store.js +102 -0
  57. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-continuity-store.js +159 -0
  58. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-elevation-store.js +110 -0
  59. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-embed-store.js +134 -0
  60. package/node_modules/@noodle-borg/assistant-gateway/dist/postgres.js +11 -0
  61. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +8 -0
  62. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +23 -0
  63. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +17 -2
  64. package/node_modules/@noodle-borg/authoring/dist/assistant.js +48 -19
  65. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  66. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
  67. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  68. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +121 -0
  69. package/node_modules/@noodle-borg/cli-catalog/dist/index.js +1 -0
  70. package/node_modules/@noodle-borg/compiler/dist/artifact/config.js +1 -1
  71. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  72. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +72 -12
  73. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +51 -27
  74. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +1 -0
  75. package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +19 -0
  76. package/node_modules/@noodle-borg/module-audit/dist/index.js +37 -0
  77. package/node_modules/@noodle-borg/module-audit/dist/postgres-audit.js +126 -0
  78. package/node_modules/@noodle-borg/module-audit/dist/route.js +66 -0
  79. package/node_modules/@noodle-borg/runtime/dist/managed-origins.js +8 -4
  80. package/node_modules/@noodle-borg/runtime/dist/postgres-state-handles.js +311 -0
  81. package/node_modules/@noodle-borg/runtime/dist/postgres.js +4 -0
  82. package/node_modules/@noodle-borg/service/dist/assistant-composition.js +18 -0
  83. package/node_modules/@noodle-borg/service/dist/build-info.js +5 -2
  84. package/node_modules/@noodle-borg/service/dist/business-information/source-ingestion-coordinator.js +7 -0
  85. package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +233 -0
  86. package/node_modules/@noodle-borg/service/dist/channels/cipher.js +37 -0
  87. package/node_modules/@noodle-borg/service/dist/channels/composition.js +37 -0
  88. package/node_modules/@noodle-borg/service/dist/channels/inference-guard.js +53 -0
  89. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +337 -0
  90. package/node_modules/@noodle-borg/service/dist/channels/worker-loop.js +50 -0
  91. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +62 -34
  92. package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +6 -2
  93. package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +14 -1
  94. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +238 -0
  95. package/node_modules/@noodle-borg/service/dist/service.js +30 -32
  96. package/node_modules/@noodle-borg/service/package.json +1 -1
  97. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +33 -0
  98. package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +9 -0
  99. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  100. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  101. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +2 -0
  102. package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +10 -2
  103. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +426 -0
  104. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +213 -0
  105. package/package.json +2 -2
@@ -0,0 +1,221 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { channelBlocked, requireChannel } from './channel-coordinator.js';
3
+ import { channelCounter } from './channel-inbox.js';
4
+ import { CHANNEL_DAY_MS, CHANNEL_RETENTION_MS, ChannelError, channelDigest, channelRow, channelValue, writeChannelEvent, } from './channel-types.js';
5
+ const LEASE_MS = 30_000;
6
+ async function pendingEvents(tx, id) {
7
+ const events = [];
8
+ for (const state of ['queued', 'running', 'reply', 'sending'])
9
+ events.push(...(await tx.list(id, { kind: 'event', state, limit: 1000 })).map((row) => row.value));
10
+ return events.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
11
+ }
12
+ async function disallowed(tx, binding, event, now) {
13
+ if (binding.state !== 'enabled' ||
14
+ binding.generation !== event.generation ||
15
+ binding.deploymentId !== event.deploymentId)
16
+ return 'authority_changed';
17
+ if (await channelBlocked(tx, binding.id, event.participantId, now))
18
+ return 'blocked';
19
+ return undefined;
20
+ }
21
+ async function reconcile(tx, binding, now) {
22
+ const pending = await pendingEvents(tx, binding.id);
23
+ const live = [];
24
+ for (const event of pending) {
25
+ const invalid = await disallowed(tx, binding, event, now);
26
+ let updated = event;
27
+ if (event.state === 'sending') {
28
+ if ((event.leaseUntil ?? 0) <= now)
29
+ updated = { ...event, state: 'unknown', code: 'send_lease_expired' };
30
+ }
31
+ else if (invalid)
32
+ updated = { ...event, state: 'cancelled', code: invalid };
33
+ else if (event.state === 'running' && (event.leaseUntil ?? 0) <= now)
34
+ updated = {
35
+ ...event,
36
+ state: event.attempts >= 3 || now - event.receivedAt > 300_000 ? 'failed' : 'queued',
37
+ code: 'worker_lease_expired',
38
+ };
39
+ else if ((event.state === 'queued' || event.state === 'reply') &&
40
+ now - event.receivedAt > 300_000)
41
+ updated = { ...event, state: 'expired', code: 'work_deadline' };
42
+ if (updated !== event)
43
+ await writeChannelEvent(tx, binding.id, updated, now);
44
+ if (['queued', 'running', 'reply', 'sending'].includes(updated.state))
45
+ live.push(updated);
46
+ }
47
+ return live;
48
+ }
49
+ export async function claimChannelTurn(store, id, now, retentionMs = CHANNEL_RETENTION_MS) {
50
+ return store.transaction([id], async (tx) => {
51
+ const binding = await requireChannel(tx, id);
52
+ const events = await reconcile(tx, binding, now);
53
+ if (binding.state !== 'enabled' ||
54
+ events.filter((event) => event.state === 'running').length >= binding.limits.concurrent)
55
+ return undefined;
56
+ const candidate = events.find((event, index) => event.state === 'queued' &&
57
+ !events.slice(0, index).some((older) => older.participantId === event.participantId));
58
+ if (!candidate)
59
+ return undefined;
60
+ const participant = await channelValue(tx, id, candidate.participantId);
61
+ if (!participant || participant.lastInboundAt < now - retentionMs) {
62
+ await writeChannelEvent(tx, id, { ...candidate, state: 'cancelled', code: 'context_expired' }, now);
63
+ return undefined;
64
+ }
65
+ const event = {
66
+ ...candidate,
67
+ state: 'running',
68
+ attempts: candidate.attempts + 1,
69
+ lease: randomUUID(),
70
+ leaseUntil: now + LEASE_MS,
71
+ turnDeadline: now + 60_000,
72
+ };
73
+ await writeChannelEvent(tx, id, event, now);
74
+ return {
75
+ binding,
76
+ event,
77
+ participant: {
78
+ ...participant,
79
+ history: participant.history.filter((entry) => entry.at > now - retentionMs).slice(-20),
80
+ },
81
+ };
82
+ });
83
+ }
84
+ async function leased(tx, id, eventId, lease, now) {
85
+ const event = await channelValue(tx, id, eventId);
86
+ if (!event ||
87
+ event.state !== 'running' ||
88
+ event.lease !== lease ||
89
+ (event.leaseUntil ?? 0) <= now ||
90
+ (event.turnDeadline ?? 0) <= now)
91
+ throw new ChannelError('lease_lost');
92
+ if (await disallowed(tx, await requireChannel(tx, id), event, now))
93
+ throw new ChannelError('authority_changed');
94
+ return event;
95
+ }
96
+ export async function renewChannelTurn(store, id, eventId, lease, now) {
97
+ await store.transaction([id], async (tx) => {
98
+ const event = await leased(tx, id, eventId, lease, now);
99
+ await writeChannelEvent(tx, id, { ...event, leaseUntil: Math.min(now + LEASE_MS, event.turnDeadline) }, now);
100
+ });
101
+ }
102
+ export async function completeChannelTurn(store, id, eventId, lease, reply, now, retentionMs = CHANNEL_RETENTION_MS, code) {
103
+ if (!reply.trim() || reply.length > 4096)
104
+ throw new ChannelError('reply_invalid');
105
+ await store.transaction([id], async (tx) => {
106
+ const event = await leased(tx, id, eventId, lease, now);
107
+ const participant = await channelValue(tx, id, event.participantId);
108
+ if (!participant)
109
+ throw new ChannelError('context_expired');
110
+ const history = [
111
+ ...participant.history.filter((entry) => entry.at > now - retentionMs),
112
+ { role: 'user', content: event.text ?? '', at: event.receivedAt },
113
+ { role: 'assistant', content: reply, at: now },
114
+ ].slice(-20);
115
+ await tx.put(id, channelRow(participant.id, 'participant', { ...participant, history }, now, {
116
+ expiresAt: participant.lastInboundAt + retentionMs,
117
+ }));
118
+ await writeChannelEvent(tx, id, {
119
+ ...event,
120
+ reply,
121
+ state: 'reply',
122
+ lease: undefined,
123
+ leaseUntil: undefined,
124
+ ...(code ? { code } : {}),
125
+ }, now);
126
+ });
127
+ }
128
+ export async function prepareChannelSend(store, id, now) {
129
+ return store.transaction([id], async (tx) => {
130
+ const binding = await requireChannel(tx, id);
131
+ const events = await reconcile(tx, binding, now);
132
+ if (binding.state !== 'enabled')
133
+ return undefined;
134
+ const candidate = events.find((event, index) => event.state === 'reply' &&
135
+ !events.slice(0, index).some((older) => older.participantId === event.participantId));
136
+ if (!candidate)
137
+ return undefined;
138
+ const participant = await channelValue(tx, id, candidate.participantId);
139
+ if (!participant || participant.lastInboundAt + CHANNEL_DAY_MS <= now) {
140
+ await writeChannelEvent(tx, id, { ...candidate, state: 'expired', code: 'reply_window_closed' }, now);
141
+ return undefined;
142
+ }
143
+ const event = {
144
+ ...candidate,
145
+ state: 'sending',
146
+ lease: randomUUID(),
147
+ leaseUntil: now + LEASE_MS,
148
+ };
149
+ await writeChannelEvent(tx, id, event, now);
150
+ return { binding, event, participant };
151
+ });
152
+ }
153
+ export async function updateChannelSend(store, id, eventId, lease, result, now) {
154
+ await store.transaction([id], async (tx) => {
155
+ const event = await channelValue(tx, id, eventId);
156
+ if (!event || event.lease !== lease || !['sending', 'unknown'].includes(event.state))
157
+ throw new ChannelError('lease_lost');
158
+ if (result.state === 'accepted' && !result.providerMessageId)
159
+ throw new ChannelError('provider_receipt_missing');
160
+ const early = result.providerMessageId
161
+ ? await channelValue(tx, id, `delivery:${channelDigest(result.providerMessageId)}`)
162
+ : undefined;
163
+ await writeChannelEvent(tx, id, {
164
+ ...event,
165
+ ...result,
166
+ ...(early && early !== 'sent' ? { state: early } : {}),
167
+ leaseUntil: undefined,
168
+ }, now);
169
+ if (result.providerMessageId)
170
+ await tx.put(id, channelRow(`receipt:${channelDigest(result.providerMessageId)}`, 'mutation', eventId, now, {
171
+ expiresAt: event.receivedAt + CHANNEL_RETENTION_MS,
172
+ }));
173
+ });
174
+ }
175
+ export async function recordChannelDelivery(store, id, providerMessageId, state, now) {
176
+ await store.transaction([id], async (tx) => {
177
+ await requireChannel(tx, id);
178
+ const eventId = await channelValue(tx, id, `receipt:${channelDigest(providerMessageId)}`);
179
+ if (!eventId) {
180
+ // A delivery callback can beat the response that supplies its message id. Bound this
181
+ // authenticated evidence separately, then join it when the acceptance receipt commits.
182
+ const key = `delivery:${channelDigest(providerMessageId)}`;
183
+ const prior = await channelValue(tx, id, key);
184
+ if (!prior) {
185
+ if ((await channelCounter(tx, id, 'delivery-ingress', now, CHANNEL_DAY_MS)) >= 10_000)
186
+ throw new ChannelError('ingress_capacity');
187
+ await channelCounter(tx, id, 'delivery-ingress', now, CHANNEL_DAY_MS, 1);
188
+ }
189
+ const order = { sent: 0, failed: 1, delivered: 2, read: 3 };
190
+ if (!prior || order[state] > order[prior])
191
+ await tx.put(id, channelRow(key, 'mutation', state, now, { expiresAt: now + CHANNEL_RETENTION_MS }));
192
+ return;
193
+ }
194
+ const event = await channelValue(tx, id, eventId);
195
+ if (!event)
196
+ return;
197
+ const next = state === 'sent' ? 'accepted' : state;
198
+ const rank = { sending: 0, unknown: 0, accepted: 1, failed: 2, delivered: 3, read: 4 };
199
+ if (!(event.state in rank) || rank[next] <= rank[event.state])
200
+ return;
201
+ await writeChannelEvent(tx, id, { ...event, state: next }, now);
202
+ });
203
+ }
204
+ /** Last local fence before the adapter starts external IO; already dispatched work cannot be recalled. */
205
+ export async function assertChannelSend(store, id, eventId, lease, now) {
206
+ await store.transaction([id], async (tx) => {
207
+ const event = await channelValue(tx, id, eventId);
208
+ const binding = await requireChannel(tx, id);
209
+ if (!event ||
210
+ event.state !== 'sending' ||
211
+ event.lease !== lease ||
212
+ (event.leaseUntil ?? 0) <= now)
213
+ throw new ChannelError('lease_lost');
214
+ if (await disallowed(tx, binding, event, now))
215
+ throw new ChannelError('authority_changed');
216
+ const participant = await channelValue(tx, id, event.participantId);
217
+ if (!participant || participant.lastInboundAt + CHANNEL_DAY_MS <= now)
218
+ throw new ChannelError('reply_window_closed');
219
+ });
220
+ }
221
+ //# sourceMappingURL=channel-work.js.map
@@ -0,0 +1,15 @@
1
+ import { type RuntimeArtifact } from '@noodle-borg/compiler';
2
+ import { type ExecuteDeps, type InvocationContext } from '@noodle-borg/runtime';
3
+ import type { AssistantToolDispatch } from './assistant-interactive.js';
4
+ import { type MessagingTurnContext } from './assistant-turn-context.js';
5
+ /** Read-only transport adapter over the same authorized runtime; native interactions are a later slice. */
6
+ export declare function dispatchMessagingReadTool(input: {
7
+ readonly artifact: RuntimeArtifact;
8
+ readonly tool: RuntimeArtifact['tools'][number];
9
+ readonly arguments: unknown;
10
+ readonly executeDeps: ExecuteDeps;
11
+ readonly context: InvocationContext;
12
+ readonly session: MessagingTurnContext;
13
+ readonly claimTool: (name: string) => Promise<boolean>;
14
+ }): Promise<AssistantToolDispatch>;
15
+ //# sourceMappingURL=messaging-tool-dispatch.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { requiresToolConfirmation } from '@noodle-borg/compiler';
2
+ import { evaluateToolAuthorization } from '@noodle-borg/protocol';
3
+ import { executeToolInteractive, } from '@noodle-borg/runtime';
4
+ import { assistantModelToolOncePerSession } from './assistant-guide.js';
5
+ import { assistantSafeOutput } from './assistant-presentation.js';
6
+ import { withAssistantTurnExecutionAuthority, } from './assistant-turn-context.js';
7
+ /** Read-only transport adapter over the same authorized runtime; native interactions are a later slice. */
8
+ export async function dispatchMessagingReadTool(input) {
9
+ const denied = (code) => ({
10
+ kind: 'event',
11
+ event: 'error',
12
+ data: { code, retryable: false },
13
+ });
14
+ if (input.tool.annotations?.readOnlyHint !== true ||
15
+ requiresToolConfirmation(input.tool.annotations) ||
16
+ input.tool._meta?.ui)
17
+ return denied('messaging_action_unsupported');
18
+ if (!evaluateToolAuthorization(input.tool.authorization, input.session.caller).allow)
19
+ return denied('tool_forbidden');
20
+ if (assistantModelToolOncePerSession(input.tool) && !(await input.claimTool(input.tool.name)))
21
+ return denied('invalid_model_tool_call');
22
+ const result = await executeToolInteractive(input.artifact, input.tool.name, input.arguments, {
23
+ ...withAssistantTurnExecutionAuthority(input.executeDeps, input.artifact, input.session),
24
+ caller: input.session.caller,
25
+ context: input.context,
26
+ });
27
+ if (result.status !== 'completed')
28
+ return denied(result.status === 'failed' ? result.error.code : 'messaging_action_unsupported');
29
+ return {
30
+ kind: 'tool_result',
31
+ output: assistantSafeOutput(input.tool.outputSchema, result.output),
32
+ };
33
+ }
34
+ //# sourceMappingURL=messaging-tool-dispatch.js.map
@@ -1,4 +1,4 @@
1
- import { type ModelCompletion, type ModelToolCall } from './model-stream.js';
1
+ import { type ModelCompletion, type ModelToolCall, type ModelUsage } from './model-stream.js';
2
2
  export type AssistantModelSource = 'operator' | 'noodle-managed';
3
3
  export type AssistantModelTransport = 'chat-completions' | 'responses';
4
4
  export interface AssistantModelRequestPolicy {
@@ -16,7 +16,24 @@ export interface AssistantModelRequestPolicy {
16
16
  /** Trusted operator-selected OpenAI-compatible request extensions. */
17
17
  readonly extraBody?: Readonly<Record<string, unknown>>;
18
18
  }
19
+ export interface AssistantInferenceCostBound {
20
+ readonly version: string;
21
+ readonly validUntil: string;
22
+ readonly maxInputTokens: number;
23
+ /** Includes every billed output token, including hidden reasoning. */
24
+ readonly maxBilledOutputTokens: number;
25
+ readonly inputMicroUsdPerMillionTokens: number;
26
+ readonly outputMicroUsdPerMillionTokens: number;
27
+ }
19
28
  interface ResolvedAssistantModelBase {
29
+ /** Deployment/provider-owned verified bound for this exact resolved model, never tenant-supplied. */
30
+ readonly inferenceCost?: AssistantInferenceCostBound;
31
+ readonly inferenceGuard?: {
32
+ reserve(): Promise<{
33
+ settle(usage: ModelUsage): Promise<void>;
34
+ cancelBeforeDispatch?(): Promise<void>;
35
+ }>;
36
+ };
20
37
  readonly source: AssistantModelSource;
21
38
  readonly transport?: AssistantModelTransport;
22
39
  readonly baseUrl: string;
@@ -64,6 +81,7 @@ export type ResolvedAssistantModel = ResolvedAssistantModelBase & ({
64
81
  });
65
82
  export interface ManagedAssistantModelResolver {
66
83
  resolve(input: {
84
+ readonly channel?: 'whatsapp';
67
85
  readonly tenant: {
68
86
  readonly org: string;
69
87
  readonly app: string;
@@ -1,6 +1,6 @@
1
1
  import { AssistantModelError, managedSponsorshipExhausted, managedSponsorshipUnavailable, modelFetchError, modelHttpError, modelRequestError, modelResponseError, } from './model-error.js';
2
2
  import { readResponsesCompletion, responsesInput, responsesTools } from './model-responses.js';
3
- import { readModelCompletion } from './model-stream.js';
3
+ import { readModelCompletion, } from './model-stream.js';
4
4
  export async function requestModelCompletion(input) {
5
5
  const { binding } = input;
6
6
  const transport = binding.transport ?? 'chat-completions';
@@ -36,10 +36,21 @@ export async function requestModelCompletion(input) {
36
36
  if (maxRequestBytes !== undefined && requestBytes > maxRequestBytes) {
37
37
  throw modelRequestError('model request too large');
38
38
  }
39
- await admitSponsorship(binding);
40
- const bearerToken = binding.apiKey ?? (await binding.bearerToken());
41
- if (!/^[^\s]{1,8192}$/.test(bearerToken)) {
42
- throw modelRequestError('invalid model bearer token');
39
+ input.signal?.throwIfAborted();
40
+ const reservation = await binding.inferenceGuard?.reserve();
41
+ let bearerToken;
42
+ try {
43
+ // Channel work can recover as a new paid attempt; each request consumes funding authority.
44
+ await admitSponsorship(binding.inferenceGuard ? { ...binding } : binding);
45
+ input.signal?.throwIfAborted();
46
+ bearerToken = binding.apiKey ?? (await binding.bearerToken());
47
+ if (!/^[^\s]{1,8192}$/.test(bearerToken))
48
+ throw modelRequestError('invalid model bearer token');
49
+ input.signal?.throwIfAborted();
50
+ }
51
+ catch (error) {
52
+ await reservation?.cancelBeforeDispatch?.();
53
+ throw error;
43
54
  }
44
55
  const timeoutSignal = AbortSignal.timeout(binding.requestPolicy?.timeoutMs ?? 30_000);
45
56
  let response;
@@ -58,7 +69,12 @@ export async function requestModelCompletion(input) {
58
69
  if (!response.ok)
59
70
  throw modelHttpError(response.status);
60
71
  try {
61
- return await (transport === 'responses' ? readResponsesCompletion : readModelCompletion)(response, input.onContent ?? (() => undefined), input.maxResponseBytes);
72
+ const completion = await (transport === 'responses'
73
+ ? readResponsesCompletion
74
+ : readModelCompletion)(response, input.onContent ?? (() => undefined), input.maxResponseBytes);
75
+ if (completion.usage)
76
+ await reservation?.settle(completion.usage);
77
+ return completion;
62
78
  }
63
79
  catch (error) {
64
80
  if (error instanceof AssistantModelError)
@@ -17,7 +17,13 @@ export * from './assistant-sensitive-values.js';
17
17
  export * from './assistant-store.js';
18
18
  export * from './assistant-transcript.js';
19
19
  export * from './assistant-transcript-events.js';
20
+ export * from './assistant-turn-context.js';
20
21
  export * from './assistant-view-availability.js';
22
+ export * from './channel-coordinator.js';
23
+ export * from './channel-provider-blocks.js';
24
+ export * from './channel-store.js';
25
+ export * from './channel-types.js';
26
+ export * from './channel-work.js';
21
27
  export * from './continuity-bounds.js';
22
28
  export * from './continuity-store.js';
23
29
  export * from './elevation.js';
@@ -30,6 +36,7 @@ export * from './in-memory-continuity-store.js';
30
36
  export * from './in-memory-elevation-store.js';
31
37
  export * from './in-memory-embed-store.js';
32
38
  export * from './managed-spend.js';
39
+ export * from './messaging-tool-dispatch.js';
33
40
  export * from './public-configuration.js';
34
41
  export * from './public-session.js';
35
42
  export * from './public-surface.js';
@@ -17,7 +17,13 @@ export * from './assistant-sensitive-values.js';
17
17
  export * from './assistant-store.js';
18
18
  export * from './assistant-transcript.js';
19
19
  export * from './assistant-transcript-events.js';
20
+ export * from './assistant-turn-context.js';
20
21
  export * from './assistant-view-availability.js';
22
+ export * from './channel-coordinator.js';
23
+ export * from './channel-provider-blocks.js';
24
+ export * from './channel-store.js';
25
+ export * from './channel-types.js';
26
+ export * from './channel-work.js';
21
27
  export * from './continuity-bounds.js';
22
28
  export * from './continuity-store.js';
23
29
  export * from './elevation.js';
@@ -30,6 +36,7 @@ export * from './in-memory-continuity-store.js';
30
36
  export * from './in-memory-elevation-store.js';
31
37
  export * from './in-memory-embed-store.js';
32
38
  export * from './managed-spend.js';
39
+ export * from './messaging-tool-dispatch.js';
33
40
  export * from './public-configuration.js';
34
41
  export * from './public-session.js';
35
42
  export * from './public-surface.js';
@@ -0,0 +1,83 @@
1
+ import { assistantAppearanceOverrideSchema } from './assistant-appearance.js';
2
+ function toRecord(row) {
3
+ const parsed = row.override_json === null
4
+ ? undefined
5
+ : assistantAppearanceOverrideSchema.parse(row.override_json);
6
+ return {
7
+ tenant: { org: row.org_slug, app: row.app_slug, env: row.environment },
8
+ ...(parsed === undefined ? {} : { override: parsed }),
9
+ revision: Number(row.revision),
10
+ updatedAt: row.updated_at,
11
+ updatedBy: row.updated_by,
12
+ };
13
+ }
14
+ /** Durable tenant/environment appearance overrides with atomic optimistic revisions. */
15
+ export class PostgresAssistantAppearanceSettingsStore {
16
+ #pool;
17
+ constructor(pool) {
18
+ this.#pool = pool;
19
+ }
20
+ async ensureSchema() {
21
+ await this.#pool.query(`
22
+ CREATE TABLE IF NOT EXISTS assistant_appearance_settings (
23
+ org_slug text NOT NULL,
24
+ app_slug text NOT NULL,
25
+ environment text NOT NULL,
26
+ override_json jsonb,
27
+ revision bigint NOT NULL,
28
+ updated_at timestamptz NOT NULL,
29
+ updated_by text NOT NULL,
30
+ PRIMARY KEY (org_slug, app_slug, environment),
31
+ CONSTRAINT assistant_appearance_revision_positive CHECK (revision > 0)
32
+ )
33
+ `);
34
+ }
35
+ async get(tenant) {
36
+ const result = await this.#pool.query(`SELECT * FROM assistant_appearance_settings
37
+ WHERE org_slug = $1 AND app_slug = $2 AND environment = $3`, [tenant.org, tenant.app, tenant.env]);
38
+ const row = result.rows[0];
39
+ return row === undefined ? undefined : toRecord(row);
40
+ }
41
+ async replace(input) {
42
+ if (input.expectedRevision === 0) {
43
+ const inserted = await this.#pool.query(`INSERT INTO assistant_appearance_settings
44
+ (org_slug, app_slug, environment, override_json, revision, updated_at, updated_by)
45
+ VALUES ($1, $2, $3, $4::jsonb, 1, $5, $6)
46
+ ON CONFLICT DO NOTHING
47
+ RETURNING *`, [
48
+ input.tenant.org,
49
+ input.tenant.app,
50
+ input.tenant.env,
51
+ input.override === undefined ? null : JSON.stringify(input.override),
52
+ input.updatedAt,
53
+ input.updatedBy,
54
+ ]);
55
+ const row = inserted.rows[0];
56
+ if (row !== undefined)
57
+ return { ok: true, record: toRecord(row) };
58
+ }
59
+ else {
60
+ const updated = await this.#pool.query(`UPDATE assistant_appearance_settings
61
+ SET override_json = $4::jsonb,
62
+ revision = revision + 1,
63
+ updated_at = $5,
64
+ updated_by = $6
65
+ WHERE org_slug = $1 AND app_slug = $2 AND environment = $3 AND revision = $7
66
+ RETURNING *`, [
67
+ input.tenant.org,
68
+ input.tenant.app,
69
+ input.tenant.env,
70
+ input.override === undefined ? null : JSON.stringify(input.override),
71
+ input.updatedAt,
72
+ input.updatedBy,
73
+ input.expectedRevision,
74
+ ]);
75
+ const row = updated.rows[0];
76
+ if (row !== undefined)
77
+ return { ok: true, record: toRecord(row) };
78
+ }
79
+ const current = await this.get(input.tenant);
80
+ return { ok: false, currentRevision: current?.revision ?? 0 };
81
+ }
82
+ }
83
+ //# sourceMappingURL=postgres-assistant-appearance-store.js.map
@@ -0,0 +1,151 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { insertAuditEvent } from '@noodle-borg/module-audit';
3
+ import { adoptCallerState } from '@noodle-borg/runtime/postgres';
4
+ import { elevationDigest } from './elevation-store.js';
5
+ import { sessionFromRow } from './postgres-assistant.js';
6
+ /** Hosted atomic implementation. In-memory stores remain local/test-only and use the portable fallback. */
7
+ export class PostgresAssistantElevationCoordinator {
8
+ #pool;
9
+ #now;
10
+ constructor(pool, options = {}) {
11
+ this.#pool = pool;
12
+ this.#now = options.now ?? (() => new Date());
13
+ }
14
+ async complete(request) {
15
+ const now = this.#now();
16
+ const client = await this.#pool.connect();
17
+ try {
18
+ await client.query('BEGIN');
19
+ const ticket = await this.#ticket(client, request.continuation);
20
+ if (!ticket || ticket.claimed_at !== null) {
21
+ return rollback(client, { ok: false, status: 403, code: 'elevation_ticket_invalid' });
22
+ }
23
+ if (ticket.org_slug !== request.tenant.org ||
24
+ ticket.app_slug !== request.tenant.app ||
25
+ ticket.environment !== request.tenant.env) {
26
+ return rollback(client, { ok: false, status: 403, code: 'elevation_tenant_mismatch' });
27
+ }
28
+ if (ticket.expires_at.getTime() <= now.getTime()) {
29
+ return rollback(client, { ok: false, status: 403, code: 'elevation_ticket_expired' });
30
+ }
31
+ const current = await client.query(`SELECT * FROM assistant_sessions WHERE id = $1 FOR UPDATE`, [ticket.session_id]);
32
+ const session = current.rows[0];
33
+ if (!session ||
34
+ session.expires_at.getTime() <= now.getTime() ||
35
+ session.absolute_expires_at.getTime() <= now.getTime()) {
36
+ return consumeAndCommitRefusal(client, ticket.id, now, {
37
+ ok: false,
38
+ status: 409,
39
+ code: 'elevation_session_unavailable',
40
+ });
41
+ }
42
+ if (session.caller.identityKind !== 'anonymous') {
43
+ return consumeAndCommitRefusal(client, ticket.id, now, {
44
+ ok: false,
45
+ status: 409,
46
+ code: 'elevation_already_signed_in',
47
+ });
48
+ }
49
+ const handles = claimableHandles(ticket.claimable_state_handles);
50
+ const adopted = await adoptCallerState(client, {
51
+ deploymentId: session.deployment_id,
52
+ handles,
53
+ sourceCallerSubject: session.caller.subject,
54
+ targetCallerSubject: request.caller.subject,
55
+ redirectExpiresAt: session.absolute_expires_at,
56
+ now,
57
+ });
58
+ if (!adopted.ok) {
59
+ return consumeAndCommitRefusal(client, ticket.id, now, {
60
+ ok: false,
61
+ status: 409,
62
+ code: 'elevation_state_conflict',
63
+ });
64
+ }
65
+ const pending = await client.query(`SELECT 1 FROM assistant_pending_tool_calls
66
+ WHERE session_id = $1 AND deployment_id = $2 AND status = 'pending' AND expires_at > $3
67
+ LIMIT 1`, [session.id, session.deployment_id, now]);
68
+ const resumeArmed = request.resume === true && (pending.rowCount ?? 0) === 0;
69
+ const token = `nss_${randomBytes(24).toString('base64url')}`;
70
+ const updated = await client.query(`UPDATE assistant_sessions
71
+ SET token_hash = $2, caller = $3::jsonb, client_id = $4, origin = $5,
72
+ customer_routing = COALESCE($6::jsonb, customer_routing),
73
+ pending_resume = $7::jsonb, latest_suggestions = NULL,
74
+ bound_surface = COALESCE($8, bound_surface)
75
+ WHERE id = $1 AND caller->>'identityKind' = 'anonymous'
76
+ RETURNING *`, [
77
+ session.id,
78
+ createHash('sha256').update(token).digest('hex'),
79
+ JSON.stringify(request.caller),
80
+ request.clientId,
81
+ request.origin,
82
+ request.customerRouting ? JSON.stringify(request.customerRouting) : null,
83
+ resumeArmed
84
+ ? JSON.stringify({ tool: ticket.tool, requestedAt: now.toISOString() })
85
+ : null,
86
+ request.boundSurface ?? null,
87
+ ]);
88
+ const elevated = updated.rows[0];
89
+ if (!elevated) {
90
+ return rollback(client, {
91
+ ok: false,
92
+ status: 409,
93
+ code: 'elevation_already_signed_in',
94
+ });
95
+ }
96
+ await client.query(`UPDATE assistant_elevations SET claimed_at = $2 WHERE id = $1 AND claimed_at IS NULL`, [ticket.id, now]);
97
+ if (handles.length > 0) {
98
+ await insertAuditEvent(client, {
99
+ eventType: 'assistant.state.claimed',
100
+ org: request.tenant.org,
101
+ app: request.tenant.app,
102
+ env: request.tenant.env,
103
+ deploymentId: session.deployment_id,
104
+ actorSubject: request.caller.subject,
105
+ decision: 'allow',
106
+ details: {
107
+ sessionId: session.id,
108
+ ticketId: ticket.id,
109
+ handleCount: adopted.adoptedHandles.length,
110
+ recordCount: adopted.adoptedRecords,
111
+ },
112
+ }, { now: () => now });
113
+ }
114
+ await client.query('COMMIT');
115
+ return {
116
+ ok: true,
117
+ session: sessionFromRow(elevated),
118
+ token,
119
+ tool: ticket.tool,
120
+ resumeArmed,
121
+ };
122
+ }
123
+ catch (error) {
124
+ await client.query('ROLLBACK');
125
+ throw error;
126
+ }
127
+ finally {
128
+ client.release();
129
+ }
130
+ }
131
+ async #ticket(client, continuation) {
132
+ const result = await client.query(`SELECT * FROM assistant_elevations WHERE continuation_hash = $1 FOR UPDATE`, [elevationDigest(continuation)]);
133
+ return result.rows[0];
134
+ }
135
+ }
136
+ function claimableHandles(value) {
137
+ if (!Array.isArray(value))
138
+ return [];
139
+ return [...new Set(value.filter((entry) => typeof entry === 'string'))].sort();
140
+ }
141
+ async function rollback(client, result) {
142
+ await client.query('ROLLBACK');
143
+ return result;
144
+ }
145
+ /** A valid ticket is single-use even when the authenticated transition is refused deterministically. */
146
+ async function consumeAndCommitRefusal(client, ticketId, now, result) {
147
+ await client.query(`UPDATE assistant_elevations SET claimed_at = $2 WHERE id = $1 AND claimed_at IS NULL`, [ticketId, now]);
148
+ await client.query('COMMIT');
149
+ return result;
150
+ }
151
+ //# sourceMappingURL=postgres-assistant-elevation.js.map