@parall/parel-channel 1.56.2 → 1.57.1

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.
@@ -70,6 +70,7 @@ export interface ChannelPromptArgs {
70
70
  /** Conversation shape as recorded on the ChannelConversation (dm/group). */
71
71
  conversationType?: string;
72
72
  externalConversationId?: string;
73
+ externalConversationName?: string;
73
74
  externalMessageId?: string;
74
75
  senderName: string;
75
76
  /** The inbound message text (raw, un-framed). */
@@ -1 +1 @@
1
- {"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA+BH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAqB5D;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAkBlE"}
1
+ {"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA+BH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAqB5D;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CA2BlE"}
@@ -98,9 +98,18 @@ export function buildChatPrompt(args) {
98
98
  }
99
99
  export function buildChannelPrompt(args) {
100
100
  const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
101
- const convLabel = args.externalConversationId
102
- ? `${sanitizeMeta(args.externalConversationId)} (${sanitizeMeta(args.conversationType ?? 'conversation')})`
103
- : sanitizeMeta(args.conversationType ?? 'conversation');
101
+ const convType = sanitizeMeta(args.conversationType ?? 'conversation');
102
+ const convId = args.externalConversationId
103
+ ? sanitizeMeta(args.externalConversationId)
104
+ : undefined;
105
+ const convName = args.externalConversationName
106
+ ? sanitizeMeta(args.externalConversationName)
107
+ : undefined;
108
+ const convLabel = convName
109
+ ? `${convName} (${convId ? `${convId}, ` : ''}${convType})`
110
+ : convId
111
+ ? `${convId} (${convType})`
112
+ : convType;
104
113
  const lines = [
105
114
  '[Event: channel.message]',
106
115
  `[Channel: ${providerLabel} | conversation: ${convLabel}]`,
@@ -114,14 +123,23 @@ export function buildChannelPrompt(args) {
114
123
  return lines.join('\n') + buildChannelReplyHint(args);
115
124
  }
116
125
  function buildChannelReplyHint(args) {
117
- // Single-path routing (multi-channel-architecture-design §6/§8.3): with
118
- // the feishu-cli grant, the minted LARKSUITE_CLI_* env rides this turn's
119
- // invocation context, so the official lark-cli works ambiently in the
120
- // sandbox and is THE reply path. Without the grant there is no outbound
121
- // path — say so instead of pointing at the retired provider clip.
122
- // cliCapable alone decides: the mint is feishu-specific, so a successful
123
- // grant implies Feishu even when the cosmetic provider-label lookup failed.
124
- if (args.cliCapable) {
126
+ // The outbound path is a property of the PROVIDER, not of the agent's
127
+ // grants. Feishu replies go through the vendor CLI with a platform-brokered
128
+ // credential; Slack and WeChat replies are platform verbs the `parall` CLI
129
+ // already carries under the agent's own identity nothing to mint.
130
+ //
131
+ // Deciding on the Feishu grant alone told a Slack or WeChat inbound that
132
+ // outbound was disabled while its own system prompt declared the matching
133
+ // `parall <provider> send` verb, and could hand a dual-bound agent a
134
+ // `lark-cli` instruction addressed to a Slack conversation.
135
+ //
136
+ // Whether the command is ALLOWED stays a runtime question: every verb and
137
+ // the Feishu broker re-check the grant at call time. This only names the
138
+ // right door.
139
+ // Both halves are required: the mint is Feishu-only and says nothing about
140
+ // where the message arrived, so a dual-bound agent's Slack inbound must not
141
+ // fall into this branch just because its Feishu grant is live.
142
+ if (args.cliCapable && args.provider === 'feishu') {
125
143
  const convRef = args.externalConversationId
126
144
  ? `chat_id "${args.externalConversationId}"`
127
145
  : 'the conversation id named in this event';
@@ -130,6 +148,24 @@ function buildChannelReplyHint(args) {
130
148
  : '';
131
149
  return `\n<system-reminder>To reply, use the official Feishu CLI in your sandbox: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned in your environment — if lark-cli is missing, install it once with \`npm i -g @larksuite/cli\`).${threadAlt} lark-cli is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
132
150
  }
151
+ if (args.provider === 'slack') {
152
+ // The flag and its value together, as agent-core renders it: writing the
153
+ // option separately from a labelled reference produced
154
+ // `--channel channel "C123"`.
155
+ const channelArg = args.externalConversationId
156
+ ? ` --channel "${args.externalConversationId}"`
157
+ : ' --channel <conversation id from this event>';
158
+ // Channel sends REQUIRE a reply anchor; DMs take one too so a question
159
+ // asked in a thread is answered in that thread.
160
+ const replyTo = args.externalMessageId ? ` --reply-to "${args.externalMessageId}"` : '';
161
+ return `\n<system-reminder>To reply, use \`parall slack send${channelArg}${replyTo} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
162
+ }
163
+ if (args.provider === 'wechat') {
164
+ const toArg = args.externalConversationId
165
+ ? `"${args.externalConversationId}"`
166
+ : '<wxid from this event>';
167
+ return `\n<system-reminder>To reply, use \`parall wechat send --to ${toArg} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). WeChat has no threads: a group reply lands in the whole room. That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
168
+ }
133
169
  const platform = args.provider ?? 'the external platform';
134
170
  return `\n<system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
135
171
  }
@@ -1 +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;AAkB3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAwL5B"}
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;AAkB3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAmM5B"}
@@ -1,7 +1,7 @@
1
1
  import { channelCapabilityEnv } from './channel-capability.js';
2
2
  import { buildChannelPrompt } from './channel-prompt.js';
3
3
  import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
4
- import { claimDispatchSource, completeSourceEffect, createChannelInputStep, ensureSession, invalidateSession, nextEnvelopeId, patchSession, readActiveEmit, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
4
+ import { claimDispatchSource, completeSourceEffect, createChannelInputStep, ensureSession, invalidateSession, nextEnvelopeId, patchSession, REQUEST_TIMEOUT_MS, readActiveEmit, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
5
5
  /**
6
6
  * External IM inbound (channel_message dispatches — Feishu/Slack via the
7
7
  * platform-mediated channel, external-im-channel-design.md). Split from
@@ -70,13 +70,18 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
70
70
  }
71
71
  const conv = (await c.json().catch(() => null));
72
72
  // Provider (= clip alias) for prompt labeling + the reply hint.
73
- // Best-effort: an unresolved provider degrades the wording, never blocks.
73
+ // A gone connection degrades the wording; an unreachable platform throws,
74
+ // because the provider now decides the turn's outbound path (see the
75
+ // resolver).
74
76
  const connectionId = conv?.connection_id ? String(conv.connection_id) : undefined;
75
77
  const provider = connectionId ? await resolveChannelProvider(ctx, connectionId, get) : undefined;
76
78
  const senderName = msg?.external_user_name || msg?.external_user_id || 'external user';
77
79
  const externalConversationId = conv?.external_conversation_id
78
80
  ? String(conv.external_conversation_id)
79
81
  : undefined;
82
+ const externalConversationName = conv?.external_conversation_name
83
+ ? String(conv.external_conversation_name)
84
+ : undefined;
80
85
  const externalMessageId = msg?.external_message_id ? String(msg.external_message_id) : undefined;
81
86
  // Progress reporting — identical ordering contract to the message path
82
87
  // (see the comments there): received settles before this function returns;
@@ -141,7 +146,10 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
141
146
  await writeActiveEmit(ctx, 'channel_message', sourceId, envelopeId, sessionId ?? undefined);
142
147
  // Live capability grant → this turn's lark-cli auth env (mint gate = the
143
148
  // no-drift revocation point) + the hint teaching the single reply path.
144
- const capabilityEnv = await channelCapabilityEnv(ctx);
149
+ // The mint is Feishu-only. Asking for it on a Slack or WeChat inbound buys
150
+ // nothing (those replies are platform verbs under the agent's own identity)
151
+ // and would put a Feishu credential in a turn that has no use for one.
152
+ const capabilityEnv = provider === 'feishu' ? await channelCapabilityEnv(ctx) : null;
145
153
  return [
146
154
  {
147
155
  type: 'emitEvent',
@@ -163,6 +171,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
163
171
  provider,
164
172
  conversationType: conv?.conversation_type ? String(conv.conversation_type) : undefined,
165
173
  externalConversationId,
174
+ externalConversationName,
166
175
  externalMessageId,
167
176
  senderName,
168
177
  text,
@@ -172,6 +181,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
172
181
  channelMessageId: sourceId,
173
182
  provider,
174
183
  externalConversationId,
184
+ externalConversationName,
175
185
  externalMessageId,
176
186
  },
177
187
  // No chatId on purpose: the reply is the agent's own lark-cli send,
@@ -225,17 +235,20 @@ async function resolveChannelProvider(ctx, connectionId, get) {
225
235
  const cached = await ctx.store?.get(cacheKey).catch(() => null);
226
236
  if (typeof cached === 'string' && cached)
227
237
  return cached;
228
- try {
229
- const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
230
- if (!res.ok)
231
- return undefined;
232
- const body = (await res.json().catch(() => null));
233
- const provider = body?.provider ? String(body.provider) : undefined;
234
- if (provider)
235
- await ctx.store?.set(cacheKey, provider).catch(() => { });
236
- return provider;
237
- }
238
- catch {
238
+ const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
239
+ // A gone connection is an ANSWER (no provider); anything else is the
240
+ // platform failing to answer. That distinction used to be moot — the
241
+ // provider was prompt wording — but it now decides which outbound path the
242
+ // turn is taught, and a turn taught nothing still completes its source and
243
+ // is never revisited. Throwing leaves the dispatch for the catch-up pass.
244
+ if (res.status === 404 || res.status === 410)
239
245
  return undefined;
246
+ if (!res.ok) {
247
+ throw new Error(`channel connection lookup failed (${res.status})`);
240
248
  }
249
+ const body = (await res.json().catch(() => null));
250
+ const provider = body?.provider ? String(body.provider) : undefined;
251
+ if (provider)
252
+ await ctx.store?.set(cacheKey, provider).catch(() => { });
253
+ return provider;
241
254
  }
package/dist/inbound.js CHANGED
@@ -1,10 +1,10 @@
1
+ import { armCatchUpEffect, beginCatchUp, CATCHUP_TIMER_KEY, deferToCatchUp, runCatchUpRound, } from './catchup.js';
1
2
  import { channelCapabilityEnv } from './channel-capability.js';
2
3
  import { buildChatPrompt } from './channel-prompt.js';
3
- import { armCatchUpEffect, beginCatchUp, CATCHUP_TIMER_KEY, deferToCatchUp, runCatchUpRound, } from './catchup.js';
4
4
  import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
5
- import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
6
5
  import { buildForkScopePrefix, bumpForkRetryAttempts, childConfirmed, childRefKnown, clearForkState, clearForkSubject, FORK_RETRY_DELAY_MS, FORK_RETRY_PREFIX, handleChildSpawnFailed, MAX_SPAWN_VERIFY_ATTEMPTS, noteMainTurnEvent, planForkDecision, recordSpawn, settleChildEvent, trackFollowUpAck, } from './fork.js';
7
- import { ackByIdEffect, claimDispatchSource, completeFencedEnvelopes, completeSourceEffect, conversationKey, createErrorStep, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, nextEnvelopeId, noteTurnEventsObserved, patchSession, readActiveEmit, reportingEnabled, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
6
+ import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
7
+ import { ackByIdEffect, claimDispatchSource, completeFencedEnvelopes, completeSourceEffect, conversationKey, createErrorStep, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, nextEnvelopeId, noteTurnEventsObserved, patchSession, REQUEST_TIMEOUT_MS, readActiveEmit, reportingEnabled, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
8
8
  import { isTypedEventType, planTypedDispatch } from './typed-dispatch.js';
9
9
  /**
10
10
  * Connection-keyed hook serialization — a v1-host compatibility shim. The v1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parel-channel",
3
- "version": "1.56.2",
3
+ "version": "1.57.1",
4
4
  "description": "Parall channel plugin for the parel runtime — lets a parel agent treat Parall as a managed_ws channel (receive dispatches, reply via the Parall API)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -141,6 +141,7 @@ export interface ChannelPromptArgs {
141
141
  /** Conversation shape as recorded on the ChannelConversation (dm/group). */
142
142
  conversationType?: string;
143
143
  externalConversationId?: string;
144
+ externalConversationName?: string;
144
145
  externalMessageId?: string;
145
146
  senderName: string;
146
147
  /** The inbound message text (raw, un-framed). */
@@ -156,9 +157,18 @@ export interface ChannelPromptArgs {
156
157
 
157
158
  export function buildChannelPrompt(args: ChannelPromptArgs): string {
158
159
  const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
159
- const convLabel = args.externalConversationId
160
- ? `${sanitizeMeta(args.externalConversationId)} (${sanitizeMeta(args.conversationType ?? 'conversation')})`
161
- : sanitizeMeta(args.conversationType ?? 'conversation');
160
+ const convType = sanitizeMeta(args.conversationType ?? 'conversation');
161
+ const convId = args.externalConversationId
162
+ ? sanitizeMeta(args.externalConversationId)
163
+ : undefined;
164
+ const convName = args.externalConversationName
165
+ ? sanitizeMeta(args.externalConversationName)
166
+ : undefined;
167
+ const convLabel = convName
168
+ ? `${convName} (${convId ? `${convId}, ` : ''}${convType})`
169
+ : convId
170
+ ? `${convId} (${convType})`
171
+ : convType;
162
172
  const lines = [
163
173
  '[Event: channel.message]',
164
174
  `[Channel: ${providerLabel} | conversation: ${convLabel}]`,
@@ -175,14 +185,23 @@ export function buildChannelPrompt(args: ChannelPromptArgs): string {
175
185
  }
176
186
 
177
187
  function buildChannelReplyHint(args: ChannelPromptArgs): string {
178
- // Single-path routing (multi-channel-architecture-design §6/§8.3): with
179
- // the feishu-cli grant, the minted LARKSUITE_CLI_* env rides this turn's
180
- // invocation context, so the official lark-cli works ambiently in the
181
- // sandbox and is THE reply path. Without the grant there is no outbound
182
- // path — say so instead of pointing at the retired provider clip.
183
- // cliCapable alone decides: the mint is feishu-specific, so a successful
184
- // grant implies Feishu even when the cosmetic provider-label lookup failed.
185
- if (args.cliCapable) {
188
+ // The outbound path is a property of the PROVIDER, not of the agent's
189
+ // grants. Feishu replies go through the vendor CLI with a platform-brokered
190
+ // credential; Slack and WeChat replies are platform verbs the `parall` CLI
191
+ // already carries under the agent's own identity nothing to mint.
192
+ //
193
+ // Deciding on the Feishu grant alone told a Slack or WeChat inbound that
194
+ // outbound was disabled while its own system prompt declared the matching
195
+ // `parall <provider> send` verb, and could hand a dual-bound agent a
196
+ // `lark-cli` instruction addressed to a Slack conversation.
197
+ //
198
+ // Whether the command is ALLOWED stays a runtime question: every verb and
199
+ // the Feishu broker re-check the grant at call time. This only names the
200
+ // right door.
201
+ // Both halves are required: the mint is Feishu-only and says nothing about
202
+ // where the message arrived, so a dual-bound agent's Slack inbound must not
203
+ // fall into this branch just because its Feishu grant is live.
204
+ if (args.cliCapable && args.provider === 'feishu') {
186
205
  const convRef = args.externalConversationId
187
206
  ? `chat_id "${args.externalConversationId}"`
188
207
  : 'the conversation id named in this event';
@@ -191,6 +210,24 @@ function buildChannelReplyHint(args: ChannelPromptArgs): string {
191
210
  : '';
192
211
  return `\n<system-reminder>To reply, use the official Feishu CLI in your sandbox: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned in your environment — if lark-cli is missing, install it once with \`npm i -g @larksuite/cli\`).${threadAlt} lark-cli is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
193
212
  }
213
+ if (args.provider === 'slack') {
214
+ // The flag and its value together, as agent-core renders it: writing the
215
+ // option separately from a labelled reference produced
216
+ // `--channel channel "C123"`.
217
+ const channelArg = args.externalConversationId
218
+ ? ` --channel "${args.externalConversationId}"`
219
+ : ' --channel <conversation id from this event>';
220
+ // Channel sends REQUIRE a reply anchor; DMs take one too so a question
221
+ // asked in a thread is answered in that thread.
222
+ const replyTo = args.externalMessageId ? ` --reply-to "${args.externalMessageId}"` : '';
223
+ return `\n<system-reminder>To reply, use \`parall slack send${channelArg}${replyTo} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
224
+ }
225
+ if (args.provider === 'wechat') {
226
+ const toArg = args.externalConversationId
227
+ ? `"${args.externalConversationId}"`
228
+ : '<wxid from this event>';
229
+ return `\n<system-reminder>To reply, use \`parall wechat send --to ${toArg} --text <text>\` from your sandbox (use \`--text-file -\` with a quoted heredoc for anything containing \`$\`, backticks or quotes). WeChat has no threads: a group reply lands in the whole room. That verb is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
230
+ }
194
231
  const platform = args.provider ?? 'the external platform';
195
232
  return `\n<system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
196
233
  }
@@ -10,8 +10,8 @@ import {
10
10
  invalidateSession,
11
11
  nextEnvelopeId,
12
12
  patchSession,
13
- readActiveEmit,
14
13
  REQUEST_TIMEOUT_MS,
14
+ readActiveEmit,
15
15
  TURN_ENVELOPE_KEY,
16
16
  writeActiveEmit,
17
17
  } from './session.js';
@@ -100,11 +100,14 @@ export async function effectsForChannelDispatch(
100
100
  const conv = (await c.json().catch(() => null)) as {
101
101
  connection_id?: string;
102
102
  external_conversation_id?: string;
103
+ external_conversation_name?: string;
103
104
  conversation_type?: string;
104
105
  } | null;
105
106
 
106
107
  // Provider (= clip alias) for prompt labeling + the reply hint.
107
- // Best-effort: an unresolved provider degrades the wording, never blocks.
108
+ // A gone connection degrades the wording; an unreachable platform throws,
109
+ // because the provider now decides the turn's outbound path (see the
110
+ // resolver).
108
111
  const connectionId = conv?.connection_id ? String(conv.connection_id) : undefined;
109
112
  const provider = connectionId ? await resolveChannelProvider(ctx, connectionId, get) : undefined;
110
113
 
@@ -112,6 +115,9 @@ export async function effectsForChannelDispatch(
112
115
  const externalConversationId = conv?.external_conversation_id
113
116
  ? String(conv.external_conversation_id)
114
117
  : undefined;
118
+ const externalConversationName = conv?.external_conversation_name
119
+ ? String(conv.external_conversation_name)
120
+ : undefined;
115
121
  const externalMessageId = msg?.external_message_id ? String(msg.external_message_id) : undefined;
116
122
 
117
123
  // Progress reporting — identical ordering contract to the message path
@@ -178,7 +184,10 @@ export async function effectsForChannelDispatch(
178
184
 
179
185
  // Live capability grant → this turn's lark-cli auth env (mint gate = the
180
186
  // no-drift revocation point) + the hint teaching the single reply path.
181
- const capabilityEnv = await channelCapabilityEnv(ctx);
187
+ // The mint is Feishu-only. Asking for it on a Slack or WeChat inbound buys
188
+ // nothing (those replies are platform verbs under the agent's own identity)
189
+ // and would put a Feishu credential in a turn that has no use for one.
190
+ const capabilityEnv = provider === 'feishu' ? await channelCapabilityEnv(ctx) : null;
182
191
 
183
192
  return [
184
193
  {
@@ -201,6 +210,7 @@ export async function effectsForChannelDispatch(
201
210
  provider,
202
211
  conversationType: conv?.conversation_type ? String(conv.conversation_type) : undefined,
203
212
  externalConversationId,
213
+ externalConversationName,
204
214
  externalMessageId,
205
215
  senderName,
206
216
  text,
@@ -210,6 +220,7 @@ export async function effectsForChannelDispatch(
210
220
  channelMessageId: sourceId,
211
221
  provider,
212
222
  externalConversationId,
223
+ externalConversationName,
213
224
  externalMessageId,
214
225
  },
215
226
  // No chatId on purpose: the reply is the agent's own lark-cli send,
@@ -272,14 +283,18 @@ async function resolveChannelProvider(
272
283
  const cacheKey = `channelProvider:${connectionId}`;
273
284
  const cached = await ctx.store?.get(cacheKey).catch(() => null);
274
285
  if (typeof cached === 'string' && cached) return cached;
275
- try {
276
- const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
277
- if (!res.ok) return undefined;
278
- const body = (await res.json().catch(() => null)) as { provider?: string } | null;
279
- const provider = body?.provider ? String(body.provider) : undefined;
280
- if (provider) await ctx.store?.set(cacheKey, provider).catch(() => {});
281
- return provider;
282
- } catch {
283
- return undefined;
286
+ const res = await get(`/channel-connections/${connectionId}`, PROVIDER_LOOKUP_TIMEOUT_MS);
287
+ // A gone connection is an ANSWER (no provider); anything else is the
288
+ // platform failing to answer. That distinction used to be moot — the
289
+ // provider was prompt wording but it now decides which outbound path the
290
+ // turn is taught, and a turn taught nothing still completes its source and
291
+ // is never revisited. Throwing leaves the dispatch for the catch-up pass.
292
+ if (res.status === 404 || res.status === 410) return undefined;
293
+ if (!res.ok) {
294
+ throw new Error(`channel connection lookup failed (${res.status})`);
284
295
  }
296
+ const body = (await res.json().catch(() => null)) as { provider?: string } | null;
297
+ const provider = body?.provider ? String(body.provider) : undefined;
298
+ if (provider) await ctx.store?.set(cacheKey, provider).catch(() => {});
299
+ return provider;
285
300
  }
package/src/inbound.ts CHANGED
@@ -5,8 +5,6 @@ import type {
5
5
  ConnectorEffect,
6
6
  WebSocketFrame,
7
7
  } from '@parel/plugin-sdk';
8
- import { channelCapabilityEnv } from './channel-capability.js';
9
- import { buildChatPrompt, type PromptAttachment } from './channel-prompt.js';
10
8
  import {
11
9
  armCatchUpEffect,
12
10
  beginCatchUp,
@@ -14,8 +12,9 @@ import {
14
12
  deferToCatchUp,
15
13
  runCatchUpRound,
16
14
  } from './catchup.js';
15
+ import { channelCapabilityEnv } from './channel-capability.js';
16
+ import { buildChatPrompt, type PromptAttachment } from './channel-prompt.js';
17
17
  import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
18
- import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
19
18
  import {
20
19
  buildForkScopePrefix,
21
20
  bumpForkRetryAttempts,
@@ -33,6 +32,7 @@ import {
33
32
  settleChildEvent,
34
33
  trackFollowUpAck,
35
34
  } from './fork.js';
35
+ import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
36
36
  import {
37
37
  ackByIdEffect,
38
38
  claimDispatchSource,
@@ -51,9 +51,9 @@ import {
51
51
  nextEnvelopeId,
52
52
  noteTurnEventsObserved,
53
53
  patchSession,
54
+ REQUEST_TIMEOUT_MS,
54
55
  readActiveEmit,
55
56
  reportingEnabled,
56
- REQUEST_TIMEOUT_MS,
57
57
  TURN_ENVELOPE_KEY,
58
58
  writeActiveEmit,
59
59
  } from './session.js';