@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +2 -1
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/whatsapp-ops.d.ts +13 -0
- package/dist/commands/whatsapp-ops.d.ts.map +1 -0
- package/dist/commands/whatsapp-ops.js +244 -0
- package/dist/commands/whatsapp-ops.js.map +1 -0
- package/dist/commands/whatsapp-output.d.ts +3 -0
- package/dist/commands/whatsapp-output.d.ts.map +1 -0
- package/dist/commands/whatsapp-output.js +70 -0
- package/dist/commands/whatsapp-output.js.map +1 -0
- package/dist/dev.js +2 -0
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.d.ts +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +3 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +6 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/app-audit/dist/audit-embedded-assistant.js +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +2 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +9 -4
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.d.ts +16 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.js +19 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +145 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +378 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +160 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.d.ts +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.js +69 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-spend.js +82 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +42 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.js +76 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +134 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +72 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +20 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +221 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +15 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +34 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +19 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +22 -6
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-appearance-store.js +83 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-elevation.js +151 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interaction-writes.js +45 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interactions.js +340 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant.js +439 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-channel-store.js +102 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-continuity-store.js +159 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-elevation-store.js +110 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-embed-store.js +134 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres.js +11 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +8 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +23 -0
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +17 -2
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +48 -19
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
- package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +121 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/index.js +1 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/config.js +1 -1
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +72 -12
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +51 -27
- package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +19 -0
- package/node_modules/@noodle-borg/module-audit/dist/index.js +37 -0
- package/node_modules/@noodle-borg/module-audit/dist/postgres-audit.js +126 -0
- package/node_modules/@noodle-borg/module-audit/dist/route.js +66 -0
- package/node_modules/@noodle-borg/runtime/dist/managed-origins.js +8 -4
- package/node_modules/@noodle-borg/runtime/dist/postgres-state-handles.js +311 -0
- package/node_modules/@noodle-borg/runtime/dist/postgres.js +4 -0
- package/node_modules/@noodle-borg/service/dist/assistant-composition.js +18 -0
- package/node_modules/@noodle-borg/service/dist/build-info.js +5 -2
- package/node_modules/@noodle-borg/service/dist/business-information/source-ingestion-coordinator.js +7 -0
- package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +233 -0
- package/node_modules/@noodle-borg/service/dist/channels/cipher.js +37 -0
- package/node_modules/@noodle-borg/service/dist/channels/composition.js +37 -0
- package/node_modules/@noodle-borg/service/dist/channels/inference-guard.js +53 -0
- package/node_modules/@noodle-borg/service/dist/channels/runtime.js +337 -0
- package/node_modules/@noodle-borg/service/dist/channels/worker-loop.js +50 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +62 -34
- package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +6 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +14 -1
- package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +238 -0
- package/node_modules/@noodle-borg/service/dist/service.js +30 -32
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +33 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +9 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +2 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +10 -2
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +426 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +213 -0
- package/package.json +2 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ChannelStore, ChannelTransaction } from './channel-store.js';
|
|
2
|
+
import { type ChannelBinding, type ChannelConfigure, type ChannelEvent, type ChannelInbound, type ChannelLimitsInput } from './channel-types.js';
|
|
3
|
+
import type { TenantRef } from './tenant-ref.js';
|
|
4
|
+
export interface ChannelBlock {
|
|
5
|
+
readonly participantId: string;
|
|
6
|
+
readonly actor: string;
|
|
7
|
+
readonly reason: 'operator';
|
|
8
|
+
readonly until: number | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function requireChannel(tx: ChannelTransaction, id: string): Promise<ChannelBinding>;
|
|
11
|
+
export declare function channelBlocked(tx: ChannelTransaction, id: string, participant: string, now: number): Promise<boolean>;
|
|
12
|
+
export declare function requireChannelLive(tx: ChannelTransaction, id: string): Promise<ChannelBinding>;
|
|
13
|
+
/** Sole authority for channel state. HTTP adapters authenticate tenants before obtaining a binding id. */
|
|
14
|
+
export declare class ChannelCoordinator {
|
|
15
|
+
readonly store: ChannelStore;
|
|
16
|
+
readonly now: () => number;
|
|
17
|
+
constructor(store: ChannelStore, now?: () => number);
|
|
18
|
+
get(tenant: TenantRef): Promise<{
|
|
19
|
+
id: string;
|
|
20
|
+
tenant: TenantRef;
|
|
21
|
+
provider: "360dialog";
|
|
22
|
+
phoneNumberId: string;
|
|
23
|
+
apiKeySecret: string;
|
|
24
|
+
webhookSecret: string;
|
|
25
|
+
deploymentId: string;
|
|
26
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
27
|
+
supportEmail: string;
|
|
28
|
+
revision: number;
|
|
29
|
+
generation: number;
|
|
30
|
+
state: "paused" | "enabled" | "disconnected";
|
|
31
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
actor: string;
|
|
35
|
+
readyRevision?: number | undefined;
|
|
36
|
+
readyAt?: number | undefined;
|
|
37
|
+
} | undefined>;
|
|
38
|
+
internal(id: string): Promise<ChannelBinding>;
|
|
39
|
+
bindingPage(after?: string): Promise<{
|
|
40
|
+
ids: string[];
|
|
41
|
+
after?: string;
|
|
42
|
+
}>;
|
|
43
|
+
configure(input: ChannelConfigure, actor: string, key: string, revision: number): Promise<{
|
|
44
|
+
id: string;
|
|
45
|
+
tenant: TenantRef;
|
|
46
|
+
provider: "360dialog";
|
|
47
|
+
phoneNumberId: string;
|
|
48
|
+
apiKeySecret: string;
|
|
49
|
+
webhookSecret: string;
|
|
50
|
+
deploymentId: string;
|
|
51
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
52
|
+
supportEmail: string;
|
|
53
|
+
revision: number;
|
|
54
|
+
generation: number;
|
|
55
|
+
state: "paused" | "enabled" | "disconnected";
|
|
56
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
57
|
+
createdAt: number;
|
|
58
|
+
updatedAt: number;
|
|
59
|
+
actor: string;
|
|
60
|
+
readyRevision?: number | undefined;
|
|
61
|
+
readyAt?: number | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
checkCredentials(id: string, digest: string): Promise<void>;
|
|
64
|
+
markReady(id: string, revision: number): Promise<void>;
|
|
65
|
+
setState(id: string, state: ChannelBinding['state'], actor: string, key: string, revision: number): Promise<{
|
|
66
|
+
id: string;
|
|
67
|
+
tenant: TenantRef;
|
|
68
|
+
provider: "360dialog";
|
|
69
|
+
phoneNumberId: string;
|
|
70
|
+
apiKeySecret: string;
|
|
71
|
+
webhookSecret: string;
|
|
72
|
+
deploymentId: string;
|
|
73
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
74
|
+
supportEmail: string;
|
|
75
|
+
revision: number;
|
|
76
|
+
generation: number;
|
|
77
|
+
state: "paused" | "enabled" | "disconnected";
|
|
78
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
79
|
+
createdAt: number;
|
|
80
|
+
updatedAt: number;
|
|
81
|
+
actor: string;
|
|
82
|
+
readyRevision?: number | undefined;
|
|
83
|
+
readyAt?: number | undefined;
|
|
84
|
+
}>;
|
|
85
|
+
block(id: string, participantId: string, actor: string, key: string, until: number | null): Promise<void>;
|
|
86
|
+
unblock(id: string, participantId: string, actor: string, key: string): Promise<void>;
|
|
87
|
+
blocks(id: string, after?: string): Promise<ChannelBlock[]>;
|
|
88
|
+
events(id: string, after?: string): Promise<ChannelEvent[]>;
|
|
89
|
+
forget(id: string, participantId: string, actor: string, key: string): Promise<void>;
|
|
90
|
+
updateLimits(id: string, input: ChannelLimitsInput, actor: string, key: string, revision: number): Promise<{
|
|
91
|
+
id: string;
|
|
92
|
+
tenant: TenantRef;
|
|
93
|
+
provider: "360dialog";
|
|
94
|
+
phoneNumberId: string;
|
|
95
|
+
apiKeySecret: string;
|
|
96
|
+
webhookSecret: string;
|
|
97
|
+
deploymentId: string;
|
|
98
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
99
|
+
supportEmail: string;
|
|
100
|
+
revision: number;
|
|
101
|
+
generation: number;
|
|
102
|
+
state: "paused" | "enabled" | "disconnected";
|
|
103
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
104
|
+
createdAt: number;
|
|
105
|
+
updatedAt: number;
|
|
106
|
+
actor: string;
|
|
107
|
+
readyRevision?: number | undefined;
|
|
108
|
+
readyAt?: number | undefined;
|
|
109
|
+
}>;
|
|
110
|
+
cooldown(id: string, participantId: string): Promise<{
|
|
111
|
+
participantId: string;
|
|
112
|
+
until: number | null;
|
|
113
|
+
}>;
|
|
114
|
+
clearCooldown(id: string, participantId: string, actor: string, key: string): Promise<void>;
|
|
115
|
+
expireHistory(id: string, retentionMs: number): Promise<void>;
|
|
116
|
+
claimTool(id: string, participantId: string, name: string): Promise<boolean>;
|
|
117
|
+
event(id: string, eventId: string): Promise<ChannelEvent | undefined>;
|
|
118
|
+
fail(id: string, eventId: string, lease: string, code: string): Promise<void>;
|
|
119
|
+
receive(id: string, messages: readonly ChannelInbound[], retentionMs?: number): Promise<Pick<ChannelEvent, "state" | "id" | "code" | "participantId">[]>;
|
|
120
|
+
claim(id: string, retentionMs?: number): Promise<import("./channel-work.js").ChannelWork | undefined>;
|
|
121
|
+
renew(id: string, eventId: string, lease: string): Promise<void>;
|
|
122
|
+
complete(id: string, eventId: string, lease: string, reply: string, retentionMs?: number, code?: string): Promise<void>;
|
|
123
|
+
assertSend(id: string, eventId: string, lease: string): Promise<void>;
|
|
124
|
+
prepareSend(id: string): Promise<import("./channel-work.js").ChannelWork | undefined>;
|
|
125
|
+
sent(id: string, eventId: string, lease: string, result: {
|
|
126
|
+
state: 'accepted' | 'unknown' | 'failed';
|
|
127
|
+
providerMessageId?: string;
|
|
128
|
+
code?: string;
|
|
129
|
+
}): Promise<void>;
|
|
130
|
+
reserveSpend(id: string, attempt: string, maximumMicroUsd: number): Promise<void>;
|
|
131
|
+
settleSpend(id: string, attempt: string, actualMicroUsd: number): Promise<void>;
|
|
132
|
+
usage(id: string): Promise<{
|
|
133
|
+
resetAt: number;
|
|
134
|
+
admittedToday: number;
|
|
135
|
+
newParticipantsToday: number;
|
|
136
|
+
pending: number;
|
|
137
|
+
oldestPendingAt: number | null;
|
|
138
|
+
unknownSends: number;
|
|
139
|
+
day: string;
|
|
140
|
+
dailyMicroUsd: number;
|
|
141
|
+
reservedMicroUsd: number;
|
|
142
|
+
spentMicroUsd: number;
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=channel-coordinator.d.ts.map
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { receiveChannelMessages } from './channel-inbox.js';
|
|
3
|
+
import { channelUsage, reserveChannelSpend, settleChannelSpend } from './channel-spend.js';
|
|
4
|
+
import { CHANNEL_REGISTRY, CHANNEL_RETENTION_MS, ChannelError, channelDigest, channelLimits, channelRow, channelTenantKey, channelValue, publicChannelBinding, writeChannelEvent, } from './channel-types.js';
|
|
5
|
+
import { assertChannelSend, claimChannelTurn, completeChannelTurn, prepareChannelSend, renewChannelTurn, updateChannelSend, } from './channel-work.js';
|
|
6
|
+
export async function requireChannel(tx, id) {
|
|
7
|
+
const binding = await channelValue(tx, id, 'binding');
|
|
8
|
+
if (!binding)
|
|
9
|
+
throw new ChannelError('channel_not_found');
|
|
10
|
+
return binding;
|
|
11
|
+
}
|
|
12
|
+
export async function channelBlocked(tx, id, participant, now) {
|
|
13
|
+
const block = await channelValue(tx, id, `block:${participant}`);
|
|
14
|
+
return !!block && (block.until === null || block.until > now);
|
|
15
|
+
}
|
|
16
|
+
export async function requireChannelLive(tx, id) {
|
|
17
|
+
const binding = await requireChannel(tx, id);
|
|
18
|
+
if (binding.state !== 'enabled')
|
|
19
|
+
throw new ChannelError('channel_paused');
|
|
20
|
+
return binding;
|
|
21
|
+
}
|
|
22
|
+
async function mutation(tx, id, key, input, now, work) {
|
|
23
|
+
if (!key || key.length > 200)
|
|
24
|
+
throw new ChannelError('idempotency_key_invalid');
|
|
25
|
+
const recordId = `mutation:${channelDigest(key)}`;
|
|
26
|
+
const fingerprint = channelDigest(JSON.stringify(input));
|
|
27
|
+
const old = await channelValue(tx, id, recordId);
|
|
28
|
+
if (old) {
|
|
29
|
+
if (old.fingerprint !== fingerprint)
|
|
30
|
+
throw new ChannelError('idempotency_conflict');
|
|
31
|
+
return old.result;
|
|
32
|
+
}
|
|
33
|
+
const result = await work();
|
|
34
|
+
await tx.put(id, channelRow(recordId, 'mutation', { fingerprint, result }, now, {
|
|
35
|
+
expiresAt: now + CHANNEL_RETENTION_MS,
|
|
36
|
+
}));
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/** Sole authority for channel state. HTTP adapters authenticate tenants before obtaining a binding id. */
|
|
40
|
+
export class ChannelCoordinator {
|
|
41
|
+
store;
|
|
42
|
+
now;
|
|
43
|
+
constructor(store, now = Date.now) {
|
|
44
|
+
this.store = store;
|
|
45
|
+
this.now = now;
|
|
46
|
+
}
|
|
47
|
+
async get(tenant) {
|
|
48
|
+
const id = await this.store.transaction([CHANNEL_REGISTRY], (tx) => channelValue(tx, CHANNEL_REGISTRY, channelTenantKey(tenant)));
|
|
49
|
+
return id ? publicChannelBinding(await this.internal(id)) : undefined;
|
|
50
|
+
}
|
|
51
|
+
async internal(id) {
|
|
52
|
+
return this.store.transaction([id], (tx) => requireChannel(tx, id));
|
|
53
|
+
}
|
|
54
|
+
async bindingPage(after) {
|
|
55
|
+
return this.store.transaction([CHANNEL_REGISTRY], async (tx) => {
|
|
56
|
+
const rows = await tx.list(CHANNEL_REGISTRY, {
|
|
57
|
+
kind: 'binding',
|
|
58
|
+
limit: 100,
|
|
59
|
+
...(after ? { after } : {}),
|
|
60
|
+
});
|
|
61
|
+
const last = rows.at(-1)?.id;
|
|
62
|
+
return {
|
|
63
|
+
ids: rows.map((row) => row.value),
|
|
64
|
+
...(rows.length === 100 && last ? { after: last } : {}),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async configure(input, actor, key, revision) {
|
|
69
|
+
const tenantKey = channelTenantKey(input.tenant);
|
|
70
|
+
const priorId = await this.store.transaction([CHANNEL_REGISTRY], (tx) => channelValue(tx, CHANNEL_REGISTRY, tenantKey));
|
|
71
|
+
const prior = priorId ? await this.internal(priorId) : undefined;
|
|
72
|
+
const sameAsset = prior?.phoneNumberId === input.phoneNumberId;
|
|
73
|
+
const id = sameAsset ? prior.id : randomUUID();
|
|
74
|
+
return this.store.transaction([CHANNEL_REGISTRY, id, ...(priorId ? [priorId] : [])], (tx) => mutation(tx, id, key, { input, revision, actor }, this.now(), async () => {
|
|
75
|
+
if ((await channelValue(tx, CHANNEL_REGISTRY, tenantKey)) !== priorId)
|
|
76
|
+
throw new ChannelError('revision_conflict');
|
|
77
|
+
const existing = priorId
|
|
78
|
+
? await channelValue(tx, priorId, 'binding')
|
|
79
|
+
: undefined;
|
|
80
|
+
if ((existing?.revision ?? 0) !== revision)
|
|
81
|
+
throw new ChannelError('revision_conflict');
|
|
82
|
+
if (existing && !sameAsset && existing.state !== 'disconnected')
|
|
83
|
+
throw new ChannelError('asset_change_requires_disconnect');
|
|
84
|
+
const assetKey = `asset:${channelDigest(input.phoneNumberId)}`;
|
|
85
|
+
const owner = await channelValue(tx, CHANNEL_REGISTRY, assetKey);
|
|
86
|
+
if (owner && owner !== id)
|
|
87
|
+
throw new ChannelError('asset_in_use');
|
|
88
|
+
const now = this.now();
|
|
89
|
+
const binding = {
|
|
90
|
+
...input,
|
|
91
|
+
id,
|
|
92
|
+
provider: '360dialog',
|
|
93
|
+
revision: revision + 1,
|
|
94
|
+
generation: (existing?.generation ?? 0) + 1,
|
|
95
|
+
state: 'paused',
|
|
96
|
+
indexKey: sameAsset ? prior.indexKey : randomBytes(32).toString('base64'),
|
|
97
|
+
limits: channelLimits(input.limits ?? (sameAsset ? prior.limits : undefined)),
|
|
98
|
+
actor,
|
|
99
|
+
createdAt: existing?.createdAt ?? now,
|
|
100
|
+
updatedAt: now,
|
|
101
|
+
};
|
|
102
|
+
await tx.put(CHANNEL_REGISTRY, channelRow(tenantKey, 'binding', id, now));
|
|
103
|
+
await tx.put(CHANNEL_REGISTRY, channelRow(assetKey, 'asset', id, now));
|
|
104
|
+
await tx.put(id, channelRow('binding', 'binding', binding, now));
|
|
105
|
+
return publicChannelBinding(binding);
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
async checkCredentials(id, digest) {
|
|
109
|
+
const changed = await this.store.transaction([id], async (tx) => {
|
|
110
|
+
const binding = await requireChannel(tx, id);
|
|
111
|
+
if (binding.credentialDigest === digest)
|
|
112
|
+
return false;
|
|
113
|
+
const changed = binding.credentialDigest !== undefined;
|
|
114
|
+
await tx.put(id, channelRow('binding', 'binding', {
|
|
115
|
+
...binding,
|
|
116
|
+
credentialDigest: digest,
|
|
117
|
+
...(changed
|
|
118
|
+
? {
|
|
119
|
+
state: 'paused',
|
|
120
|
+
generation: binding.generation + 1,
|
|
121
|
+
revision: binding.revision + 1,
|
|
122
|
+
readyAt: undefined,
|
|
123
|
+
readyRevision: undefined,
|
|
124
|
+
}
|
|
125
|
+
: {}),
|
|
126
|
+
}, this.now()));
|
|
127
|
+
return changed;
|
|
128
|
+
});
|
|
129
|
+
if (changed)
|
|
130
|
+
throw new ChannelError('credentials_changed');
|
|
131
|
+
}
|
|
132
|
+
async markReady(id, revision) {
|
|
133
|
+
await this.store.transaction([id], async (tx) => {
|
|
134
|
+
const binding = await requireChannel(tx, id);
|
|
135
|
+
if (binding.revision !== revision)
|
|
136
|
+
throw new ChannelError('revision_conflict');
|
|
137
|
+
await tx.put(id, channelRow('binding', 'binding', { ...binding, readyAt: this.now(), readyRevision: revision }, this.now()));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async setState(id, state, actor, key, revision) {
|
|
141
|
+
return this.store.transaction([CHANNEL_REGISTRY, id], (tx) => mutation(tx, id, key, { state, actor, revision }, this.now(), async () => {
|
|
142
|
+
const binding = await requireChannel(tx, id);
|
|
143
|
+
if (binding.revision !== revision)
|
|
144
|
+
throw new ChannelError('revision_conflict');
|
|
145
|
+
if (state === 'enabled' &&
|
|
146
|
+
(binding.readyRevision !== revision || this.now() - (binding.readyAt ?? 0) > 300_000))
|
|
147
|
+
throw new ChannelError('not_ready');
|
|
148
|
+
if (state === 'enabled' &&
|
|
149
|
+
(await channelValue(tx, CHANNEL_REGISTRY, `asset:${channelDigest(binding.phoneNumberId)}`)) !== id)
|
|
150
|
+
throw new ChannelError('asset_not_owned');
|
|
151
|
+
if (state === 'disconnected') {
|
|
152
|
+
const assetKey = `asset:${channelDigest(binding.phoneNumberId)}`;
|
|
153
|
+
if ((await channelValue(tx, CHANNEL_REGISTRY, assetKey)) === id)
|
|
154
|
+
await tx.remove(CHANNEL_REGISTRY, assetKey);
|
|
155
|
+
}
|
|
156
|
+
const updated = {
|
|
157
|
+
...binding,
|
|
158
|
+
state,
|
|
159
|
+
revision: revision + 1,
|
|
160
|
+
generation: binding.generation + 1,
|
|
161
|
+
actor,
|
|
162
|
+
updatedAt: this.now(),
|
|
163
|
+
readyAt: undefined,
|
|
164
|
+
readyRevision: undefined,
|
|
165
|
+
};
|
|
166
|
+
await tx.put(id, channelRow('binding', 'binding', updated, this.now()));
|
|
167
|
+
return publicChannelBinding(updated);
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
async block(id, participantId, actor, key, until) {
|
|
171
|
+
if (!/^p_[a-f0-9]{64}$/.test(participantId) ||
|
|
172
|
+
(until !== null && (!Number.isSafeInteger(until) || until <= this.now())))
|
|
173
|
+
throw new ChannelError('block_invalid');
|
|
174
|
+
await this.store.transaction([id], (tx) => mutation(tx, id, key, { participantId, actor, until }, this.now(), async () => {
|
|
175
|
+
await requireChannel(tx, id);
|
|
176
|
+
const block = { participantId, actor, reason: 'operator', until };
|
|
177
|
+
await tx.put(id, channelRow(`block:${participantId}`, 'block', block, this.now(), until === null ? {} : { expiresAt: until + CHANNEL_RETENTION_MS }));
|
|
178
|
+
return null;
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
async unblock(id, participantId, actor, key) {
|
|
182
|
+
await this.store.transaction([id], (tx) => mutation(tx, id, key, { participantId, actor, action: 'unblock' }, this.now(), async () => {
|
|
183
|
+
await requireChannel(tx, id);
|
|
184
|
+
await tx.remove(id, `block:${participantId}`);
|
|
185
|
+
return null;
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
async blocks(id, after) {
|
|
189
|
+
return this.store.transaction([id], async (tx) => {
|
|
190
|
+
await requireChannel(tx, id);
|
|
191
|
+
return (await tx.list(id, {
|
|
192
|
+
kind: 'block',
|
|
193
|
+
limit: 100,
|
|
194
|
+
...(after ? { after: `block:${after}` } : {}),
|
|
195
|
+
})).map((row) => row.value);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async events(id, after) {
|
|
199
|
+
return this.store.transaction([id], async (tx) => {
|
|
200
|
+
await requireChannel(tx, id);
|
|
201
|
+
return (await tx.list(id, { kind: 'event', limit: 100, ...(after ? { after } : {}) })).map((row) => row.value);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async forget(id, participantId, actor, key) {
|
|
205
|
+
await this.store.transaction([id], (tx) => mutation(tx, id, key, { participantId, actor, action: 'forget' }, this.now(), async () => {
|
|
206
|
+
await requireChannel(tx, id);
|
|
207
|
+
await tx.remove(id, participantId);
|
|
208
|
+
// Bounded pages retain minimal replay evidence but erase every message body and reply.
|
|
209
|
+
let after;
|
|
210
|
+
for (;;) {
|
|
211
|
+
const rows = await tx.list(id, {
|
|
212
|
+
kind: 'event',
|
|
213
|
+
limit: 1000,
|
|
214
|
+
...(after ? { after } : {}),
|
|
215
|
+
});
|
|
216
|
+
for (const row of rows) {
|
|
217
|
+
const event = row.value;
|
|
218
|
+
if (event.participantId === participantId) {
|
|
219
|
+
const { text: _text, reply: _reply, ...minimal } = event;
|
|
220
|
+
await tx.put(id, {
|
|
221
|
+
...row,
|
|
222
|
+
state: 'cancelled',
|
|
223
|
+
value: { ...minimal, state: 'cancelled', code: 'forgotten' },
|
|
224
|
+
updatedAt: this.now(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (rows.length < 1000)
|
|
229
|
+
break;
|
|
230
|
+
after = rows.at(-1).id;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
async updateLimits(id, input, actor, key, revision) {
|
|
236
|
+
return this.store.transaction([id], (tx) => mutation(tx, id, key, { input, actor, revision }, this.now(), async () => {
|
|
237
|
+
const binding = await requireChannel(tx, id);
|
|
238
|
+
if (binding.revision !== revision)
|
|
239
|
+
throw new ChannelError('revision_conflict');
|
|
240
|
+
const next = {
|
|
241
|
+
...binding,
|
|
242
|
+
limits: channelLimits({ ...binding.limits, ...input }),
|
|
243
|
+
revision: revision + 1,
|
|
244
|
+
actor,
|
|
245
|
+
updatedAt: this.now(),
|
|
246
|
+
};
|
|
247
|
+
await tx.put(id, channelRow('binding', 'binding', next, this.now()));
|
|
248
|
+
return publicChannelBinding(next);
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
async cooldown(id, participantId) {
|
|
252
|
+
return this.store.transaction([id], async (tx) => {
|
|
253
|
+
await requireChannel(tx, id);
|
|
254
|
+
const participant = await channelValue(tx, id, participantId);
|
|
255
|
+
return {
|
|
256
|
+
participantId,
|
|
257
|
+
until: participant?.cooldownUntil && participant.cooldownUntil > this.now()
|
|
258
|
+
? participant.cooldownUntil
|
|
259
|
+
: null,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async clearCooldown(id, participantId, actor, key) {
|
|
264
|
+
await this.store.transaction([id], (tx) => mutation(tx, id, key, { participantId, actor, action: 'clear-cooldown' }, this.now(), async () => {
|
|
265
|
+
await requireChannel(tx, id);
|
|
266
|
+
const row = await tx.get(id, participantId);
|
|
267
|
+
if (row) {
|
|
268
|
+
const { cooldownUntil: _until, ...participant } = row.value;
|
|
269
|
+
await tx.put(id, {
|
|
270
|
+
...row,
|
|
271
|
+
value: { ...participant, strikes: [] },
|
|
272
|
+
updatedAt: this.now(),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
async expireHistory(id, retentionMs) {
|
|
279
|
+
await this.store.transaction([id], async (tx) => {
|
|
280
|
+
await requireChannel(tx, id);
|
|
281
|
+
const now = this.now(), cutoff = now - Math.min(retentionMs, CHANNEL_RETENTION_MS);
|
|
282
|
+
for (const kind of ['event', 'participant']) {
|
|
283
|
+
const key = `purge:${kind}`;
|
|
284
|
+
const cursor = await channelValue(tx, id, key);
|
|
285
|
+
const rows = await tx.list(id, { kind, limit: 100, ...(cursor ? { after: cursor } : {}) });
|
|
286
|
+
for (const row of rows) {
|
|
287
|
+
if (kind === 'event') {
|
|
288
|
+
const event = row.value;
|
|
289
|
+
if (event.receivedAt > cutoff || (!event.text && !event.reply))
|
|
290
|
+
continue;
|
|
291
|
+
const { text: _text, reply: _reply, ...minimal } = event;
|
|
292
|
+
const state = ['queued', 'running', 'reply', 'sending'].includes(event.state)
|
|
293
|
+
? 'expired'
|
|
294
|
+
: event.state;
|
|
295
|
+
await tx.put(id, { ...row, state, value: { ...minimal, state }, updatedAt: now });
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const person = row.value;
|
|
299
|
+
if (person.lastInboundAt <= cutoff)
|
|
300
|
+
await tx.remove(id, row.id);
|
|
301
|
+
else
|
|
302
|
+
await tx.put(id, {
|
|
303
|
+
...row,
|
|
304
|
+
value: { ...person, history: person.history.filter((item) => item.at > cutoff) },
|
|
305
|
+
expiresAt: Math.min(row.expiresAt ?? Infinity, person.lastInboundAt + retentionMs),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const last = rows.at(-1)?.id;
|
|
310
|
+
if (rows.length === 100 && last)
|
|
311
|
+
await tx.put(id, channelRow(key, 'mutation', last, now));
|
|
312
|
+
else
|
|
313
|
+
await tx.remove(id, key);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async claimTool(id, participantId, name) {
|
|
318
|
+
return this.store.transaction([id], async (tx) => {
|
|
319
|
+
await requireChannelLive(tx, id);
|
|
320
|
+
const row = await tx.get(id, participantId);
|
|
321
|
+
if (!row)
|
|
322
|
+
throw new ChannelError('context_expired');
|
|
323
|
+
const participant = row.value;
|
|
324
|
+
if (participant.modelToolUses?.includes(name))
|
|
325
|
+
return false;
|
|
326
|
+
await tx.put(id, {
|
|
327
|
+
...row,
|
|
328
|
+
value: { ...participant, modelToolUses: [...(participant.modelToolUses ?? []), name] },
|
|
329
|
+
});
|
|
330
|
+
return true;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async event(id, eventId) {
|
|
334
|
+
return this.store.transaction([id], async (tx) => {
|
|
335
|
+
await requireChannel(tx, id);
|
|
336
|
+
return channelValue(tx, id, eventId);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async fail(id, eventId, lease, code) {
|
|
340
|
+
await this.store.transaction([id], async (tx) => {
|
|
341
|
+
const event = await channelValue(tx, id, eventId);
|
|
342
|
+
if (!event || event.lease !== lease || event.state !== 'running')
|
|
343
|
+
return;
|
|
344
|
+
await writeChannelEvent(tx, id, { ...event, state: 'failed', code }, this.now());
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
receive(id, messages, retentionMs = CHANNEL_RETENTION_MS) {
|
|
348
|
+
return receiveChannelMessages(this.store, id, messages, this.now(), retentionMs);
|
|
349
|
+
}
|
|
350
|
+
claim(id, retentionMs = CHANNEL_RETENTION_MS) {
|
|
351
|
+
return claimChannelTurn(this.store, id, this.now(), retentionMs);
|
|
352
|
+
}
|
|
353
|
+
renew(id, eventId, lease) {
|
|
354
|
+
return renewChannelTurn(this.store, id, eventId, lease, this.now());
|
|
355
|
+
}
|
|
356
|
+
complete(id, eventId, lease, reply, retentionMs = CHANNEL_RETENTION_MS, code) {
|
|
357
|
+
return completeChannelTurn(this.store, id, eventId, lease, reply, this.now(), retentionMs, code);
|
|
358
|
+
}
|
|
359
|
+
assertSend(id, eventId, lease) {
|
|
360
|
+
return assertChannelSend(this.store, id, eventId, lease, this.now());
|
|
361
|
+
}
|
|
362
|
+
prepareSend(id) {
|
|
363
|
+
return prepareChannelSend(this.store, id, this.now());
|
|
364
|
+
}
|
|
365
|
+
sent(id, eventId, lease, result) {
|
|
366
|
+
return updateChannelSend(this.store, id, eventId, lease, result, this.now());
|
|
367
|
+
}
|
|
368
|
+
reserveSpend(id, attempt, maximumMicroUsd) {
|
|
369
|
+
return reserveChannelSpend(this.store, id, attempt, maximumMicroUsd, this.now());
|
|
370
|
+
}
|
|
371
|
+
settleSpend(id, attempt, actualMicroUsd) {
|
|
372
|
+
return settleChannelSpend(this.store, id, attempt, actualMicroUsd, this.now());
|
|
373
|
+
}
|
|
374
|
+
usage(id) {
|
|
375
|
+
return channelUsage(this.store, id, this.now());
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=channel-coordinator.js.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { channelBlocked, requireChannel } from './channel-coordinator.js';
|
|
2
|
+
import { CHANNEL_DAY_MS, CHANNEL_RETENTION_MS, ChannelError, channelDigest, channelParticipantId, channelRow, channelValue, writeChannelEvent, } from './channel-types.js';
|
|
3
|
+
export async function channelCounter(tx, scope, key, now, period, add = 0) {
|
|
4
|
+
const bucket = Math.floor(now / period);
|
|
5
|
+
const id = `counter:${key}:${period}:${bucket}`;
|
|
6
|
+
const value = ((await channelValue(tx, scope, id)) ?? 0) + add;
|
|
7
|
+
if (add)
|
|
8
|
+
await tx.put(scope, channelRow(id, 'counter', value, now, { expiresAt: (bucket + 2) * period }));
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
async function refusal(tx, binding, message, participant, pending, now) {
|
|
12
|
+
if (binding.state !== 'enabled')
|
|
13
|
+
return 'channel_paused';
|
|
14
|
+
if (await channelBlocked(tx, binding.id, participant, now))
|
|
15
|
+
return 'blocked';
|
|
16
|
+
if (message.eventAt < now - CHANNEL_RETENTION_MS || message.eventAt > now + 60_000)
|
|
17
|
+
return 'timestamp_invalid';
|
|
18
|
+
const record = await channelValue(tx, binding.id, participant);
|
|
19
|
+
if (record?.cooldownUntil && record.cooldownUntil > now)
|
|
20
|
+
return 'cooldown';
|
|
21
|
+
if (pending.length >= binding.limits.pending ||
|
|
22
|
+
pending.filter((event) => event.participantId === participant).length >=
|
|
23
|
+
binding.limits.pendingPerParticipant)
|
|
24
|
+
return 'queue_full';
|
|
25
|
+
for (const [key, period, maximum] of [
|
|
26
|
+
[participant, 60_000, binding.limits.perMinute],
|
|
27
|
+
[participant, 3_600_000, binding.limits.perHour],
|
|
28
|
+
[participant, CHANNEL_DAY_MS, binding.limits.perDay],
|
|
29
|
+
['all', CHANNEL_DAY_MS, binding.limits.channelPerDay],
|
|
30
|
+
...(!record ? [['new', CHANNEL_DAY_MS, binding.limits.newParticipantsPerDay]] : []),
|
|
31
|
+
])
|
|
32
|
+
if ((await channelCounter(tx, binding.id, key, now, period)) >= maximum)
|
|
33
|
+
return period === 60_000 ? 'minute_limited' : 'rate_limited';
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Returns content-free admission receipts; callback retries never recover or expose message bodies. */
|
|
37
|
+
export async function receiveChannelMessages(store, id, messages, now, retentionMs = CHANNEL_RETENTION_MS) {
|
|
38
|
+
if (messages.length > 100)
|
|
39
|
+
throw new ChannelError('batch_too_large');
|
|
40
|
+
return store.transaction([id], async (tx) => {
|
|
41
|
+
const binding = await requireChannel(tx, id);
|
|
42
|
+
const pending = [];
|
|
43
|
+
for (const state of ['queued', 'running', 'reply', 'sending'])
|
|
44
|
+
pending.push(...(await tx.list(id, { kind: 'event', state, limit: 1000 })).map((row) => row.value));
|
|
45
|
+
const receipts = [];
|
|
46
|
+
for (const message of messages) {
|
|
47
|
+
if (!message.providerId ||
|
|
48
|
+
message.providerId.length > 512 ||
|
|
49
|
+
!message.address.value ||
|
|
50
|
+
message.address.value.length > 128 ||
|
|
51
|
+
!Number.isSafeInteger(message.eventAt))
|
|
52
|
+
throw new ChannelError('message_invalid');
|
|
53
|
+
const seenId = `seen:${channelDigest(message.providerId)}`;
|
|
54
|
+
const fingerprint = channelDigest(JSON.stringify(message));
|
|
55
|
+
const seen = await channelValue(tx, id, seenId);
|
|
56
|
+
if (seen) {
|
|
57
|
+
if (seen.fingerprint !== fingerprint)
|
|
58
|
+
throw new ChannelError('event_conflict');
|
|
59
|
+
receipts.push(seen.receipt);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Bound even rejected anonymous traffic. Retryable overload acknowledges no uncommitted event.
|
|
63
|
+
if ((await channelCounter(tx, id, 'ingress', now, CHANNEL_DAY_MS)) >= 10_000)
|
|
64
|
+
throw new ChannelError('ingress_capacity');
|
|
65
|
+
const participantId = channelParticipantId(binding, message.address);
|
|
66
|
+
const code = await refusal(tx, binding, message, participantId, pending, now);
|
|
67
|
+
const sequence = ((await channelValue(tx, id, 'sequence')) ?? 0) + 1;
|
|
68
|
+
await tx.put(id, channelRow('sequence', 'counter', sequence, now));
|
|
69
|
+
const eventId = `e_${String(sequence).padStart(20, '0')}`;
|
|
70
|
+
const unsupported = message.text === undefined || message.text.length > binding.limits.textCharacters;
|
|
71
|
+
let event = {
|
|
72
|
+
id: eventId,
|
|
73
|
+
participantId,
|
|
74
|
+
providerId: message.providerId,
|
|
75
|
+
eventAt: Math.min(now, message.eventAt),
|
|
76
|
+
receivedAt: now,
|
|
77
|
+
generation: binding.generation,
|
|
78
|
+
deploymentId: binding.deploymentId,
|
|
79
|
+
attempts: 0,
|
|
80
|
+
state: code ? 'refused' : unsupported ? 'reply' : 'queued',
|
|
81
|
+
...(code ? { code } : {}),
|
|
82
|
+
...(!code && !unsupported ? { text: message.text } : {}),
|
|
83
|
+
...(!code && unsupported
|
|
84
|
+
? {
|
|
85
|
+
reply: `Please send a text message of up to ${binding.limits.textCharacters} characters. For help, contact ${binding.supportEmail}.`,
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
};
|
|
89
|
+
if (code === 'minute_limited') {
|
|
90
|
+
const row = await tx.get(id, participantId);
|
|
91
|
+
if (row) {
|
|
92
|
+
const participant = row.value;
|
|
93
|
+
const strikes = [...participant.strikes.filter((at) => at > now - 600_000), now].slice(-3);
|
|
94
|
+
await tx.put(id, {
|
|
95
|
+
...row,
|
|
96
|
+
value: {
|
|
97
|
+
...participant,
|
|
98
|
+
strikes,
|
|
99
|
+
...(strikes.length >= 3 ? { cooldownUntil: now + 600_000 } : {}),
|
|
100
|
+
},
|
|
101
|
+
updatedAt: now,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!code) {
|
|
106
|
+
const stored = await channelValue(tx, id, participantId);
|
|
107
|
+
const old = stored && stored.lastInboundAt > now - retentionMs ? stored : undefined;
|
|
108
|
+
const sameDeployment = old?.deploymentId === binding.deploymentId;
|
|
109
|
+
const participant = {
|
|
110
|
+
...old,
|
|
111
|
+
deploymentId: binding.deploymentId,
|
|
112
|
+
modelToolUses: sameDeployment ? (old?.modelToolUses ?? []) : [],
|
|
113
|
+
id: participantId,
|
|
114
|
+
address: message.address,
|
|
115
|
+
lastInboundAt: Math.max(old?.lastInboundAt ?? 0, event.eventAt),
|
|
116
|
+
history: (sameDeployment ? (old?.history ?? []) : []).filter((entry) => entry.at > now - retentionMs),
|
|
117
|
+
strikes: old?.strikes ?? [],
|
|
118
|
+
};
|
|
119
|
+
await tx.put(id, channelRow(participantId, 'participant', participant, now, {
|
|
120
|
+
expiresAt: now + CHANNEL_RETENTION_MS,
|
|
121
|
+
}));
|
|
122
|
+
for (const period of [60_000, 3_600_000, CHANNEL_DAY_MS])
|
|
123
|
+
await channelCounter(tx, id, participantId, now, period, 1);
|
|
124
|
+
await channelCounter(tx, id, 'all', now, CHANNEL_DAY_MS, 1);
|
|
125
|
+
if (!old)
|
|
126
|
+
await channelCounter(tx, id, 'new', now, CHANNEL_DAY_MS, 1);
|
|
127
|
+
pending.push(event);
|
|
128
|
+
}
|
|
129
|
+
if (code &&
|
|
130
|
+
['minute_limited', 'rate_limited', 'cooldown'].includes(code) &&
|
|
131
|
+
pending.length < binding.limits.pending &&
|
|
132
|
+
pending.filter((item) => item.participantId === participantId).length <
|
|
133
|
+
binding.limits.pendingPerParticipant) {
|
|
134
|
+
const person = await channelValue(tx, id, participantId);
|
|
135
|
+
if (person &&
|
|
136
|
+
person.lastInboundAt + CHANNEL_DAY_MS > now &&
|
|
137
|
+
(await channelCounter(tx, id, `notice:${participantId}`, now, 3_600_000)) < 1 &&
|
|
138
|
+
(await channelCounter(tx, id, 'notices', now, CHANNEL_DAY_MS)) < 100) {
|
|
139
|
+
event = {
|
|
140
|
+
...event,
|
|
141
|
+
state: 'reply',
|
|
142
|
+
reply: 'Please wait before sending another question. This assistant has reached a temporary limit.',
|
|
143
|
+
};
|
|
144
|
+
await channelCounter(tx, id, `notice:${participantId}`, now, 3_600_000, 1);
|
|
145
|
+
await channelCounter(tx, id, 'notices', now, CHANNEL_DAY_MS, 1);
|
|
146
|
+
pending.push(event);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await writeChannelEvent(tx, id, event, now);
|
|
150
|
+
const receipt = { id: eventId, participantId, state: event.state, ...(code ? { code } : {}) };
|
|
151
|
+
await tx.put(id, channelRow(seenId, 'mutation', { fingerprint, receipt }, now, {
|
|
152
|
+
expiresAt: now + CHANNEL_RETENTION_MS,
|
|
153
|
+
}));
|
|
154
|
+
await channelCounter(tx, id, 'ingress', now, CHANNEL_DAY_MS, 1);
|
|
155
|
+
receipts.push(receipt);
|
|
156
|
+
}
|
|
157
|
+
return receipts;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=channel-inbox.js.map
|