@parall/parel-channel 1.40.0 → 1.41.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.
@@ -0,0 +1,29 @@
1
+ import type { ConnectorContext, ConnectorEffect } from '@parel/plugin-sdk';
2
+ /**
3
+ * External IM inbound (channel_message dispatches — Feishu/Slack via the
4
+ * platform-mediated channel, external-im-channel-design.md). Split from
5
+ * inbound.ts as a pure move; the pipeline contract lives there.
6
+ */
7
+ /**
8
+ * Miss-path lookups here are cosmetic (prompt labeling only), so they get a
9
+ * tight timeout — same 3s budget as deliver's front-of-reply session lookup —
10
+ * rather than holding the message's emit/ack behind the full
11
+ * REQUEST_TIMEOUT_MS. Also reused by inbound.ts's chat-name prefetch.
12
+ */
13
+ export declare const PROVIDER_LOOKUP_TIMEOUT_MS = 3000;
14
+ /**
15
+ * channel_message dispatch → agent (external IM channel,
16
+ * external-im-channel-design.md). Same pipeline shape as the message path,
17
+ * with the ChannelConversation (chv_) standing in for the chat as the
18
+ * routing subject: fetch the durable ChannelMessage + its conversation,
19
+ * resolve the provider for prompt labeling (cached — the connection→provider
20
+ * mapping is stable), frame the prompt for the EXTERNAL audience, report
21
+ * progress against the conversation-keyed session, and ack by source.
22
+ *
23
+ * Error contract mirrors the message path (and agent-core's
24
+ * fetchAndHandleChannelMessage): 404/403 on the source entities is
25
+ * permanently unusable → ack-and-drop; a transient failure keeps the
26
+ * dispatch pending for the catch-up sweep.
27
+ */
28
+ export declare function effectsForChannelDispatch(sourceId: string, dispatchId: string, ctx: ConnectorContext): Promise<ConnectorEffect[]>;
29
+ //# sourceMappingURL=inbound-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-channel.d.ts","sourceRoot":"","sources":["../src/inbound-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAc3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAuJ5B"}
@@ -0,0 +1,213 @@
1
+ import { buildChannelPrompt } from './channel-prompt.js';
2
+ import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
3
+ import { ackEffect, createChannelInputStep, ensureSession, invalidateSession, markDispatchReceived, patchSession, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, } from './session.js';
4
+ /**
5
+ * External IM inbound (channel_message dispatches — Feishu/Slack via the
6
+ * platform-mediated channel, external-im-channel-design.md). Split from
7
+ * inbound.ts as a pure move; the pipeline contract lives there.
8
+ */
9
+ /**
10
+ * Miss-path lookups here are cosmetic (prompt labeling only), so they get a
11
+ * tight timeout — same 3s budget as deliver's front-of-reply session lookup —
12
+ * rather than holding the message's emit/ack behind the full
13
+ * REQUEST_TIMEOUT_MS. Also reused by inbound.ts's chat-name prefetch.
14
+ */
15
+ export const PROVIDER_LOOKUP_TIMEOUT_MS = 3_000;
16
+ /**
17
+ * channel_message dispatch → agent (external IM channel,
18
+ * external-im-channel-design.md). Same pipeline shape as the message path,
19
+ * with the ChannelConversation (chv_) standing in for the chat as the
20
+ * routing subject: fetch the durable ChannelMessage + its conversation,
21
+ * resolve the provider for prompt labeling (cached — the connection→provider
22
+ * mapping is stable), frame the prompt for the EXTERNAL audience, report
23
+ * progress against the conversation-keyed session, and ack by source.
24
+ *
25
+ * Error contract mirrors the message path (and agent-core's
26
+ * fetchAndHandleChannelMessage): 404/403 on the source entities is
27
+ * permanently unusable → ack-and-drop; a transient failure keeps the
28
+ * dispatch pending for the catch-up sweep.
29
+ */
30
+ export async function effectsForChannelDispatch(sourceId, dispatchId, ctx) {
31
+ const apiUrl = parallApiUrl(ctx);
32
+ const orgId = parallOrgId(ctx);
33
+ const authHeaders = { Authorization: `Bearer ${requireAgk(ctx)}` };
34
+ const get = (path, timeoutMs = REQUEST_TIMEOUT_MS) => fetch(`${apiUrl}/api/v1/orgs/${orgId}${path}`, {
35
+ headers: authHeaders,
36
+ signal: AbortSignal.timeout(timeoutMs),
37
+ });
38
+ const m = await get(`/channel-messages/${sourceId}`);
39
+ if (!m.ok) {
40
+ if (m.status === 404 || m.status === 403) {
41
+ // Deleted / inaccessible (also the shape a disabled org feature flag
42
+ // takes) — permanently unusable, drop it.
43
+ return [ackEffect(ctx, 'channel_message', sourceId)];
44
+ }
45
+ return []; // transient: keep pending, the catch-up sweep retries
46
+ }
47
+ const msg = (await m.json().catch(() => null));
48
+ const conversationId = msg?.conversation_id ? String(msg.conversation_id) : undefined;
49
+ const text = msg?.text ?? '';
50
+ if (!conversationId) {
51
+ // No conversation → no session key and no reply target: unusable.
52
+ return [ackEffect(ctx, 'channel_message', sourceId)];
53
+ }
54
+ if (!text) {
55
+ // The ingress adapter only persists mechanically parsed text; a row with
56
+ // none will never grow any (same reasoning as attachment-only chat
57
+ // messages) — drop rather than let it pile up in the pending queue.
58
+ return [ackEffect(ctx, 'channel_message', sourceId)];
59
+ }
60
+ const c = await get(`/channel-conversations/${conversationId}`);
61
+ if (!c.ok) {
62
+ if (c.status === 404 || c.status === 403) {
63
+ return [ackEffect(ctx, 'channel_message', sourceId)];
64
+ }
65
+ return [];
66
+ }
67
+ const conv = (await c.json().catch(() => null));
68
+ // Provider (= clip alias) for prompt labeling + the reply hint.
69
+ // Best-effort: an unresolved provider degrades the wording, never blocks.
70
+ const connectionId = conv?.connection_id ? String(conv.connection_id) : undefined;
71
+ const provider = connectionId ? await resolveChannelProvider(ctx, connectionId, get) : undefined;
72
+ const senderName = msg?.external_user_name || msg?.external_user_id || 'external user';
73
+ const externalConversationId = conv?.external_conversation_id
74
+ ? String(conv.external_conversation_id)
75
+ : undefined;
76
+ const externalMessageId = msg?.external_message_id ? String(msg.external_message_id) : undefined;
77
+ // Progress reporting — identical ordering contract to the message path
78
+ // (see the comments there): received settles before the ack effect is
79
+ // returned; the turnEnvelope marker goes down before any session write.
80
+ const receivedSettled = markDispatchReceived(ctx, 'channel_message', sourceId);
81
+ const envelopeId = dispatchId || `channel_message:${sourceId}`;
82
+ let sessionId = await ensureSession(ctx);
83
+ if (sessionId) {
84
+ await ctx.store?.set(TURN_ENVELOPE_KEY, envelopeId).catch(() => { });
85
+ const report = (sid) => Promise.all([
86
+ patchSession(ctx, sid, { status: 'active', trigger_message_id: sourceId }),
87
+ createChannelInputStep(ctx, sid, {
88
+ conversationId,
89
+ channelMessageId: sourceId,
90
+ provider,
91
+ externalConversationId,
92
+ senderId: msg?.external_user_id ? String(msg.external_user_id) : 'external',
93
+ senderName,
94
+ text,
95
+ sentAt: msg?.received_at ? String(msg.received_at) : undefined,
96
+ }),
97
+ ]);
98
+ const results = await report(sessionId);
99
+ if (results.includes('stale')) {
100
+ await invalidateSession(ctx);
101
+ const fresh = await ensureSession(ctx);
102
+ if (fresh && fresh !== sessionId) {
103
+ await report(fresh);
104
+ sessionId = fresh;
105
+ }
106
+ }
107
+ // Record the durable chv_ ↔ ase_ mapping (ops drill-down from a
108
+ // conversation into its session; last-write-wins on the server). Only on
109
+ // change — the store remembers what was last pushed — and fully
110
+ // fire-and-forget: pure bookkeeping must never add latency to the
111
+ // emit/ack path (a lost attempt is retried on the next message because
112
+ // the store memo only updates on success).
113
+ void recordConversationSession(ctx, conversationId, sessionId).catch(() => { });
114
+ }
115
+ await receivedSettled;
116
+ return [
117
+ {
118
+ type: 'emitEvent',
119
+ event: {
120
+ id: envelopeId,
121
+ orgId,
122
+ connectionId: ctx.connectionId,
123
+ source: 'parall',
124
+ // The host's message intake path turns data.text into the user
125
+ // message — the external-audience framing rides inside it (parel has
126
+ // no separate prompt channel), so 'message' is the right type here.
127
+ type: 'message',
128
+ // main routing sends this to the agent's one session too; subject
129
+ // still carries the external conversation (chv_) for parel's turn
130
+ // input grouping (F2) and per-conversation event correlation.
131
+ subject: conversationId,
132
+ data: {
133
+ text: buildChannelPrompt({
134
+ provider,
135
+ conversationType: conv?.conversation_type ? String(conv.conversation_type) : undefined,
136
+ externalConversationId,
137
+ externalMessageId,
138
+ senderName,
139
+ text,
140
+ }),
141
+ conversationId,
142
+ channelMessageId: sourceId,
143
+ provider,
144
+ externalConversationId,
145
+ externalMessageId,
146
+ },
147
+ // No chatId on purpose: the reply goes out through the provider clip
148
+ // (agent-invoked), NOT deliver() — deliver only runs the redundant
149
+ // session idle for channel routes (see delivery.ts).
150
+ replyRoute: {
151
+ kind: 'provider_http',
152
+ connectionId: ctx.connectionId,
153
+ data: { conversationId, envelopeId },
154
+ },
155
+ // No invocation context: PRLL_CHAT_ID must not point CLI defaults at
156
+ // a chat that doesn't exist, and the reply targets (external ids)
157
+ // already ride the prompt verbatim.
158
+ },
159
+ },
160
+ ackEffect(ctx, 'channel_message', sourceId),
161
+ ];
162
+ }
163
+ /**
164
+ * connection id → provider alias, cached in the durable store (stable
165
+ * mapping; avoids one connection fetch per inbound message). Cache hits are
166
+ * a store read; any failure returns undefined — prompt wording degrades to
167
+ * neutral rather than inventing a clip alias.
168
+ */
169
+ async function resolveChannelProvider(ctx, connectionId, get) {
170
+ const cacheKey = `channelProvider:${connectionId}`;
171
+ const cached = await ctx.store?.get(cacheKey).catch(() => null);
172
+ if (typeof cached === 'string' && cached)
173
+ return cached;
174
+ try {
175
+ const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
176
+ if (!res.ok)
177
+ return undefined;
178
+ const body = (await res.json().catch(() => null));
179
+ const provider = body?.provider ? String(body.provider) : undefined;
180
+ if (provider)
181
+ await ctx.store?.set(cacheKey, provider).catch(() => { });
182
+ return provider;
183
+ }
184
+ catch {
185
+ return undefined;
186
+ }
187
+ }
188
+ /** Push the chv_ → ase_ pointer when it changed; warn-only on failure. */
189
+ async function recordConversationSession(ctx, conversationId, sessionId) {
190
+ const mappedKey = `convSession:${conversationId}`;
191
+ const mapped = await ctx.store?.get(mappedKey).catch(() => null);
192
+ if (mapped === sessionId)
193
+ return;
194
+ try {
195
+ const res = await fetch(`${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/channel-conversations/${conversationId}/session`, {
196
+ method: 'PATCH',
197
+ headers: {
198
+ Authorization: `Bearer ${requireAgk(ctx)}`,
199
+ 'Content-Type': 'application/json',
200
+ },
201
+ body: JSON.stringify({ agent_session_id: sessionId }),
202
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
203
+ });
204
+ if (!res.ok) {
205
+ console.warn(`[parel-channel] conversation session mapping failed (${res.status})`);
206
+ return;
207
+ }
208
+ await ctx.store?.set(mappedKey, sessionId).catch(() => { });
209
+ }
210
+ catch (err) {
211
+ console.warn('[parel-channel] conversation session mapping failed', err);
212
+ }
213
+ }
package/dist/inbound.d.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  import type { AgentEvent, AgentEventEffect, ConnectorContext, ConnectorEffect, WebSocketFrame } from '@parel/plugin-sdk';
2
+ /** Surface a violated isolation assumption as a log line, not silent
3
+ * mis-serialization (see the chain doc above). */
4
+ export declare function assertChainConnection(ctx: ConnectorContext): void;
5
+ export declare function serialized<T>(fn: () => Promise<T>): Promise<T>;
2
6
  export declare function handleParallFrame(frame: WebSocketFrame, ctx: ConnectorContext): Promise<ConnectorEffect[]>;
7
+ /**
8
+ * Fork retry timers (armed by child_spawn_failed): re-plan the stranded
9
+ * message through the normal dispatch pipeline. onTimer returns the FULL
10
+ * effect union (unlike onAgentEvent), so the re-plan can emit/spawn/ack.
11
+ * The parked row is cleared first — a re-plan that fails transiently leaves
12
+ * the WorkItem in `received` for the reconnect sweep, same as any dispatch.
13
+ */
14
+ export declare function handleParallTimer(timer: {
15
+ key: string;
16
+ }, ctx: ConnectorContext): Promise<ConnectorEffect[]>;
3
17
  /**
4
18
  * Sweep pending dispatches on (re)connect — the live WS push has no replay.
5
19
  * Paginates with the server cursor so a long outage's backlog is not silently
@@ -1 +1 @@
1
- {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AAiD3B,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAuDrF;AAkdD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyD7B"}
1
+ {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AAwG3B;kDACkD;AAClD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CASjE;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO9D;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAG5B;AAwCD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACtB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAE5B;AA8BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAE/E;AAgmBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}