@parall/parel-channel 1.42.1 → 1.43.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/channel-capability.d.ts +29 -0
- package/dist/channel-capability.d.ts.map +1 -0
- package/dist/channel-capability.js +81 -0
- package/dist/channel-prompt.d.ts +16 -9
- package/dist/channel-prompt.d.ts.map +1 -1
- package/dist/channel-prompt.js +25 -19
- package/dist/fork.d.ts +8 -4
- package/dist/fork.d.ts.map +1 -1
- package/dist/inbound-channel.d.ts.map +1 -1
- package/dist/inbound-channel.js +13 -8
- package/dist/inbound.d.ts.map +1 -1
- package/dist/inbound.js +41 -30
- package/package.json +2 -2
- package/src/channel-capability.ts +121 -0
- package/src/channel-prompt.ts +33 -20
- package/src/fork.ts +6 -7
- package/src/inbound-channel.ts +14 -8
- package/src/inbound.ts +44 -37
- package/dist/parel-sdk-compat.d.ts +0 -63
- package/dist/parel-sdk-compat.d.ts.map +0 -1
- package/dist/parel-sdk-compat.js +0 -16
- package/src/parel-sdk-compat.ts +0 -77
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-capability credential injection — the parel-native form of the
|
|
3
|
+
* feishu-cli broker (multi-channel-architecture-design §8.3).
|
|
4
|
+
*
|
|
5
|
+
* Bridges deliver the capability as a PATH pointer shim that mints a fresh
|
|
6
|
+
* short-lived tenant token per lark-cli invocation. In parel we control
|
|
7
|
+
* neither the sandbox PATH nor a bridge process — but we DO control the
|
|
8
|
+
* per-turn invocation context, which sandbox-e2b flattens into every exec's
|
|
9
|
+
* env. So the connector mints at emit time and injects the official
|
|
10
|
+
* LARKSUITE_CLI_* env set: bare `lark-cli` works ambiently for the whole
|
|
11
|
+
* turn (turn length ≪ token TTL ~2h), the app secret never leaves the
|
|
12
|
+
* platform, and revocation bites on the next emit's mint (403 → no env).
|
|
13
|
+
*
|
|
14
|
+
* The mint endpoint IS the gate (the platform re-evaluates connection state
|
|
15
|
+
* + flag on every request — the no-drift invariant), so the connector needs
|
|
16
|
+
* no declaration-plane fetch: a 403 is the revocation signal and is
|
|
17
|
+
* negative-cached briefly to keep a revoked grant from adding one 403 per
|
|
18
|
+
* turn.
|
|
19
|
+
*/
|
|
20
|
+
import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
21
|
+
/** Test hook: the cache is module-level state, so suites reset it between cases. */
|
|
22
|
+
export declare function resetChannelCapabilityCache(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Returns the feishu-cli env set to spread into an envelope's invocation
|
|
25
|
+
* context, or null when the capability is not granted (or the mint degraded
|
|
26
|
+
* — a transient platform error must never block a dispatch emit).
|
|
27
|
+
*/
|
|
28
|
+
export declare function channelCapabilityEnv(ctx: ConnectorContext): Promise<Record<string, string> | null>;
|
|
29
|
+
//# sourceMappingURL=channel-capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-capability.d.ts","sourceRoot":"","sources":["../src/channel-capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAkC1D,oFAAoF;AACpF,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAcD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CA2CxC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
2
|
+
const MINT_TIMEOUT_MS = 5_000;
|
|
3
|
+
// Re-mint this long before the token expires so an env set injected at turn
|
|
4
|
+
// start stays valid through a long turn's tail.
|
|
5
|
+
const EXPIRY_SKEW_MS = 10 * 60 * 1000;
|
|
6
|
+
// After a 403 (no grant / revoked / flag off), skip minting for this long —
|
|
7
|
+
// revocation latency stays bounded while per-turn 403 noise does not.
|
|
8
|
+
const DENIED_TTL_MS = 60 * 1000;
|
|
9
|
+
// A GRANTED result is re-validated on the same cadence: the mint endpoint IS
|
|
10
|
+
// the revocation gate, so a positive cache lasting the token TTL (~2h) would
|
|
11
|
+
// extend revocation latency to hours. Re-minting is cheap (the platform
|
|
12
|
+
// Redis-caches the exchanged token and re-evaluates the gate per request);
|
|
13
|
+
// this cache only absorbs bursty emits within a conversation.
|
|
14
|
+
const GRANTED_TTL_MS = 60 * 1000;
|
|
15
|
+
// Per-connection cache. The plugin isolate is per-connection, so a plain
|
|
16
|
+
// module map keyed by connectionId is effectively single-entry — the key
|
|
17
|
+
// guards against isolate reuse across connections.
|
|
18
|
+
const cache = new Map();
|
|
19
|
+
/** Test hook: the cache is module-level state, so suites reset it between cases. */
|
|
20
|
+
export function resetChannelCapabilityCache() {
|
|
21
|
+
cache.clear();
|
|
22
|
+
}
|
|
23
|
+
function envFromMint(minted) {
|
|
24
|
+
return {
|
|
25
|
+
LARKSUITE_CLI_APP_ID: minted.app_id,
|
|
26
|
+
LARKSUITE_CLI_BRAND: minted.brand || 'feishu',
|
|
27
|
+
LARKSUITE_CLI_TENANT_ACCESS_TOKEN: minted.token,
|
|
28
|
+
// Bot lock: the credential is the app's, not a human's — mirror the
|
|
29
|
+
// bridge shim's injection exactly (agent-core channel-exec).
|
|
30
|
+
LARKSUITE_CLI_DEFAULT_AS: 'bot',
|
|
31
|
+
LARKSUITE_CLI_STRICT_MODE: 'bot',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the feishu-cli env set to spread into an envelope's invocation
|
|
36
|
+
* context, or null when the capability is not granted (or the mint degraded
|
|
37
|
+
* — a transient platform error must never block a dispatch emit).
|
|
38
|
+
*/
|
|
39
|
+
export async function channelCapabilityEnv(ctx) {
|
|
40
|
+
const key = ctx.connectionId;
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const hit = cache.get(key);
|
|
43
|
+
if (hit && hit.validUntil > now)
|
|
44
|
+
return hit.env;
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`${parallApiUrl(ctx)}/api/v1/orgs/${encodeURIComponent(parallOrgId(ctx))}/agents/me/channel-token`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: `Bearer ${requireAgk(ctx)}`,
|
|
50
|
+
'content-type': 'application/json',
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({ channel_type: 'feishu' }),
|
|
53
|
+
signal: AbortSignal.timeout(MINT_TIMEOUT_MS),
|
|
54
|
+
});
|
|
55
|
+
if (res.status === 403 || res.status === 404) {
|
|
56
|
+
// No grant (never had it / revoked / flag off) — or an old server
|
|
57
|
+
// without the endpoint. Same agent-facing outcome: no capability.
|
|
58
|
+
cache.set(key, { env: null, validUntil: now + DENIED_TTL_MS });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
// Transient (5xx/502-mint-upstream): don't cache, don't block the emit.
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const minted = (await res.json());
|
|
66
|
+
if (!minted?.token || !minted.app_id)
|
|
67
|
+
return null;
|
|
68
|
+
const env = envFromMint(minted);
|
|
69
|
+
const expiresAt = Date.parse(minted.expires_at);
|
|
70
|
+
// Bounded by BOTH the revalidation cadence (revocation latency) and the
|
|
71
|
+
// token's own remaining life (an injected env must outlive the turn).
|
|
72
|
+
const tokenBound = Number.isFinite(expiresAt) ? expiresAt - EXPIRY_SKEW_MS : now;
|
|
73
|
+
const validUntil = Math.max(now + 5_000, Math.min(now + GRANTED_TTL_MS, tokenBound));
|
|
74
|
+
cache.set(key, { env, validUntil });
|
|
75
|
+
return env;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Network failure — degrade to "no env this turn"; the next emit retries.
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/channel-prompt.d.ts
CHANGED
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
*
|
|
17
17
|
* - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
|
|
18
18
|
* attachment links in the reply (dead links there).
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* - Single-path reply (multi-channel-architecture-design §6): with the
|
|
20
|
+
* feishu-cli capability granted, the reply goes out through the official
|
|
21
|
+
* lark-cli — auth rides the turn's invocation context env
|
|
22
|
+
* (channel-capability.ts), so bare `lark-cli` works in the sandbox.
|
|
23
|
+
* Without the grant there is no outbound path and the hint says so (the
|
|
24
|
+
* provider clip path is retired).
|
|
25
25
|
* - When the inbound external message id is known, offer the in-thread reply
|
|
26
|
-
* form
|
|
27
|
-
*
|
|
26
|
+
* form so threaded conversations aren't nudged toward new top-level
|
|
27
|
+
* messages.
|
|
28
28
|
*/
|
|
29
29
|
export interface PromptAttachment {
|
|
30
30
|
id: string;
|
|
@@ -52,7 +52,7 @@ export interface ChatPromptArgs {
|
|
|
52
52
|
*/
|
|
53
53
|
export declare function buildChatPrompt(args: ChatPromptArgs): string;
|
|
54
54
|
export interface ChannelPromptArgs {
|
|
55
|
-
/** Provider name (
|
|
55
|
+
/** Provider name (e.g. "feishu"); undefined when unresolved. */
|
|
56
56
|
provider?: string;
|
|
57
57
|
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
58
58
|
conversationType?: string;
|
|
@@ -61,6 +61,13 @@ export interface ChannelPromptArgs {
|
|
|
61
61
|
senderName: string;
|
|
62
62
|
/** The inbound message text (raw, un-framed). */
|
|
63
63
|
text: string;
|
|
64
|
+
/**
|
|
65
|
+
* Live capability grant: the turn's invocation context carries the minted
|
|
66
|
+
* LARKSUITE_CLI_* env, so bare `lark-cli` works in the sandbox and is THE
|
|
67
|
+
* reply path. False → outbound is disabled for this org (no grant); the
|
|
68
|
+
* hint says so instead of pointing at the retired provider clip.
|
|
69
|
+
*/
|
|
70
|
+
cliCapable?: boolean;
|
|
64
71
|
}
|
|
65
72
|
export declare function buildChannelPrompt(args: ChannelPromptArgs): string;
|
|
66
73
|
//# sourceMappingURL=channel-prompt.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH,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,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAU5D;AAED,MAAM,WAAW,iBAAiB;IAChC,
|
|
1
|
+
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH,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,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAU5D;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"}
|
package/dist/channel-prompt.js
CHANGED
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
*
|
|
17
17
|
* - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
|
|
18
18
|
* attachment links in the reply (dead links there).
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* - Single-path reply (multi-channel-architecture-design §6): with the
|
|
20
|
+
* feishu-cli capability granted, the reply goes out through the official
|
|
21
|
+
* lark-cli — auth rides the turn's invocation context env
|
|
22
|
+
* (channel-capability.ts), so bare `lark-cli` works in the sandbox.
|
|
23
|
+
* Without the grant there is no outbound path and the hint says so (the
|
|
24
|
+
* provider clip path is retired).
|
|
25
25
|
* - When the inbound external message id is known, offer the in-thread reply
|
|
26
|
-
* form
|
|
27
|
-
*
|
|
26
|
+
* form so threaded conversations aren't nudged toward new top-level
|
|
27
|
+
* messages.
|
|
28
28
|
*/
|
|
29
29
|
function sanitizeMeta(value) {
|
|
30
30
|
return value
|
|
@@ -85,16 +85,22 @@ export function buildChannelPrompt(args) {
|
|
|
85
85
|
return lines.join('\n') + buildChannelReplyHint(args);
|
|
86
86
|
}
|
|
87
87
|
function buildChannelReplyHint(args) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (args.
|
|
96
|
-
const
|
|
97
|
-
|
|
88
|
+
// Single-path routing (multi-channel-architecture-design §6/§8.3): with
|
|
89
|
+
// the feishu-cli grant, the minted LARKSUITE_CLI_* env rides this turn's
|
|
90
|
+
// invocation context, so the official lark-cli works ambiently in the
|
|
91
|
+
// sandbox and is THE reply path. Without the grant there is no outbound
|
|
92
|
+
// path — say so instead of pointing at the retired provider clip.
|
|
93
|
+
// cliCapable alone decides: the mint is feishu-specific, so a successful
|
|
94
|
+
// grant implies Feishu even when the cosmetic provider-label lookup failed.
|
|
95
|
+
if (args.cliCapable) {
|
|
96
|
+
const convRef = args.externalConversationId
|
|
97
|
+
? `chat_id "${args.externalConversationId}"`
|
|
98
|
+
: 'the conversation id named in this event';
|
|
99
|
+
const threadAlt = args.externalMessageId
|
|
100
|
+
? ` To reply threaded to this specific message, reference message_id "${args.externalMessageId}".`
|
|
101
|
+
: '';
|
|
102
|
+
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>`;
|
|
98
103
|
}
|
|
99
|
-
|
|
104
|
+
const platform = args.provider ?? 'the external platform';
|
|
105
|
+
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>`;
|
|
100
106
|
}
|
package/dist/fork.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
2
|
-
|
|
1
|
+
import type { AgentEvent, ConnectorContext } from '@parel/plugin-sdk';
|
|
2
|
+
/** The child-spawn failure variant of the sdk's AgentEvent union. */
|
|
3
|
+
type ChildSpawnFailedEvent = Extract<AgentEvent, {
|
|
4
|
+
type: 'child_spawn_failed';
|
|
5
|
+
}>;
|
|
3
6
|
/** Delay before a failed spawn's messages are re-driven through onTimer. */
|
|
4
7
|
export declare const FORK_RETRY_DELAY_MS = 15000;
|
|
5
8
|
export declare const FORK_RETRY_PREFIX = "forkRetry:";
|
|
@@ -99,12 +102,12 @@ export declare function clearForkState(ctx: ConnectorContext): Promise<string[]>
|
|
|
99
102
|
* Track main-session busyness from the E1 turn stream. Child events (they
|
|
100
103
|
* carry childRef) must NOT feed this — only the main session's turns do.
|
|
101
104
|
*/
|
|
102
|
-
export declare function noteMainTurnEvent(ctx: ConnectorContext, event:
|
|
105
|
+
export declare function noteMainTurnEvent(ctx: ConnectorContext, event: AgentEvent): Promise<void>;
|
|
103
106
|
/**
|
|
104
107
|
* Resolve a child event's bookkeeping row (by its subject mirror) and fire
|
|
105
108
|
* the pending spawn ack exactly once. Returns the ack request when due.
|
|
106
109
|
*/
|
|
107
|
-
export declare function settleChildEvent(ctx: ConnectorContext, event:
|
|
110
|
+
export declare function settleChildEvent(ctx: ConnectorContext, event: AgentEvent): Promise<{
|
|
108
111
|
subject: string;
|
|
109
112
|
pendingAcks: PendingAck[];
|
|
110
113
|
} | null>;
|
|
@@ -145,4 +148,5 @@ export declare function handleChildSpawnFailed(ctx: ConnectorContext, event: Chi
|
|
|
145
148
|
* covers them.
|
|
146
149
|
*/
|
|
147
150
|
export declare function buildForkScopePrefix(subject: string, threadRootId?: string): string;
|
|
151
|
+
export {};
|
|
148
152
|
//# sourceMappingURL=fork.d.ts.map
|
package/dist/fork.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEtE,qEAAqE;AACrE,KAAK,qBAAqB,GAAG,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE,CAAC,CAAC;AA+CjF,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAS1C,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAW9C,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;2CAEuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;AAClB;oEACoE;GAClE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAMxC;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAwCvB;AAED;;sEAEsE;AACtE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,OAAO,CAAC,CASlB;AAED;;;+EAG+E;AAC/E,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO7F;AAED,0EAA0E;AAC1E,wBAAsB,cAAc,CAClC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,8EAA8E;AAC9E,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED,iEAAiE;AACjE,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoB7E;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/F;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,CA+BhE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CA2B3E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAYnF"}
|
|
@@ -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;
|
|
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,CAkK5B"}
|
package/dist/inbound-channel.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { channelCapabilityEnv } from './channel-capability.js';
|
|
1
2
|
import { buildChannelPrompt } from './channel-prompt.js';
|
|
2
3
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
3
4
|
import { ackEffect, createChannelInputStep, ensureSession, invalidateSession, markDispatchReceived, nextEnvelopeId, patchSession, readActiveEmit, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
|
|
@@ -116,6 +117,9 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
116
117
|
}
|
|
117
118
|
await receivedSettled;
|
|
118
119
|
await writeActiveEmit(ctx, 'channel_message', sourceId, envelopeId);
|
|
120
|
+
// Live capability grant → this turn's lark-cli auth env (mint gate = the
|
|
121
|
+
// no-drift revocation point) + the hint teaching the single reply path.
|
|
122
|
+
const capabilityEnv = await channelCapabilityEnv(ctx);
|
|
119
123
|
return [
|
|
120
124
|
{
|
|
121
125
|
type: 'emitEvent',
|
|
@@ -140,6 +144,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
140
144
|
externalMessageId,
|
|
141
145
|
senderName,
|
|
142
146
|
text,
|
|
147
|
+
cliCapable: capabilityEnv !== null,
|
|
143
148
|
}),
|
|
144
149
|
conversationId,
|
|
145
150
|
channelMessageId: sourceId,
|
|
@@ -147,19 +152,19 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
147
152
|
externalConversationId,
|
|
148
153
|
externalMessageId,
|
|
149
154
|
},
|
|
150
|
-
// No chatId on purpose: the reply
|
|
151
|
-
//
|
|
152
|
-
//
|
|
155
|
+
// No chatId on purpose: the reply is the agent's own lark-cli send,
|
|
156
|
+
// NOT deliver() — deliver only runs the redundant session idle for
|
|
157
|
+
// channel routes (see delivery.ts).
|
|
153
158
|
replyRoute: {
|
|
154
159
|
kind: 'provider_http',
|
|
155
160
|
connectionId: ctx.connectionId,
|
|
156
161
|
data: { conversationId, envelopeId },
|
|
157
162
|
},
|
|
158
|
-
//
|
|
159
|
-
// a chat that doesn't exist
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
+
// Invocation context carries ONLY the capability env: no PRLL_CHAT_ID
|
|
164
|
+
// (it must not point CLI defaults at a chat that doesn't exist) and
|
|
165
|
+
// no PRLL_DISPATCH_SOURCE_* (the external reply is a vendor-CLI send,
|
|
166
|
+
// and this row's ledger close is the turn event's complete-sources).
|
|
167
|
+
...(capabilityEnv ? { context: capabilityEnv } : {}),
|
|
163
168
|
},
|
|
164
169
|
},
|
|
165
170
|
// No ack effect — see the message path (inbound.ts): the row stays
|
package/dist/inbound.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAkH3B;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;AAsqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}
|
package/dist/inbound.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { channelCapabilityEnv } from './channel-capability.js';
|
|
1
2
|
import { buildChatPrompt } from './channel-prompt.js';
|
|
2
3
|
import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
|
|
3
4
|
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
4
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';
|
|
5
|
-
import { asChildSpawnFailed, asConnectorEffects, } from './parel-sdk-compat.js';
|
|
6
6
|
import { ackByIdEffect, ackEffect, completeFencedEnvelopes, conversationKey, createErrorStep, fetchMainSessionFresh, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, markDispatchReceived, nextEnvelopeId, patchSession, readActiveEmit, reportingEnabled, SESSION_CACHE_KEY, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
|
|
7
7
|
import { isTypedEventType, planTypedDispatch } from './typed-dispatch.js';
|
|
8
8
|
/**
|
|
@@ -356,12 +356,18 @@ async function effectsForDispatch(data, ctx) {
|
|
|
356
356
|
// so the server resolves this WorkItem in the reply's own transaction.
|
|
357
357
|
// (Fork turns get it too — their rows are legacy-acked so the resolve is a
|
|
358
358
|
// no-op today, and it wires up for free when fork ledger parity lands.)
|
|
359
|
+
// Channel-capability env (lark-cli auth) rides EVERY turn's invocation
|
|
360
|
+
// context while granted — Feishu workspace ops (docs/calendar/…) are asked
|
|
361
|
+
// for from ordinary Parall chats, not only from channel turns. Null when
|
|
362
|
+
// ungranted or the mint degrades; never blocks the emit.
|
|
363
|
+
const capabilityEnv = await channelCapabilityEnv(ctx);
|
|
359
364
|
const invocationContext = {
|
|
360
365
|
PRLL_CHAT_ID: chatId,
|
|
361
366
|
PRLL_TRIGGER_MESSAGE_ID: sourceId,
|
|
362
367
|
PRLL_DISPATCH_SOURCE_TYPE: sourceType,
|
|
363
368
|
PRLL_DISPATCH_SOURCE_ID: sourceId,
|
|
364
369
|
...(threadRootId ? { PRLL_THREAD_ROOT_ID: threadRootId } : {}),
|
|
370
|
+
...(capabilityEnv ?? {}),
|
|
365
371
|
};
|
|
366
372
|
const envelope = {
|
|
367
373
|
id: envelopeId,
|
|
@@ -445,18 +451,16 @@ async function effectsForDispatch(data, ctx) {
|
|
|
445
451
|
source_id: sourceId,
|
|
446
452
|
chat_id: chatId,
|
|
447
453
|
});
|
|
448
|
-
return
|
|
449
|
-
{ type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS },
|
|
450
|
-
]);
|
|
454
|
+
return [{ type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS }];
|
|
451
455
|
}
|
|
452
|
-
return
|
|
456
|
+
return [
|
|
453
457
|
{ type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
|
|
454
|
-
]
|
|
458
|
+
];
|
|
455
459
|
}
|
|
456
|
-
return
|
|
460
|
+
return [
|
|
457
461
|
{ type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
|
|
458
462
|
ackEffect(ctx, sourceType, sourceId),
|
|
459
|
-
]
|
|
463
|
+
];
|
|
460
464
|
}
|
|
461
465
|
if (decision.mode === 'spawn') {
|
|
462
466
|
// Main is mid-turn on another conversation — fork. The opening message
|
|
@@ -519,7 +523,14 @@ async function effectsForDispatch(data, ctx) {
|
|
|
519
523
|
}
|
|
520
524
|
await recordSpawn(ctx, subject, decision.childRef, { dispatchId, sourceId });
|
|
521
525
|
await receivedSettled;
|
|
522
|
-
|
|
526
|
+
// Known gap: SpawnChildSessionEffect (#140) carries no invocation
|
|
527
|
+
// context, so the fork's OPENING turn runs without the capability env
|
|
528
|
+
// (lark-cli workspace ops would need a follow-up turn — those arrive
|
|
529
|
+
// as deliverTo envelopes, which inherit the full context). Channel
|
|
530
|
+
// replies are unaffected: channel_message dispatches always ride main.
|
|
531
|
+
// Fix belongs parel-side (context on spawnChildSession) — tracked as a
|
|
532
|
+
// parel ask.
|
|
533
|
+
return [
|
|
523
534
|
{
|
|
524
535
|
type: 'spawnChildSession',
|
|
525
536
|
childRef: decision.childRef,
|
|
@@ -527,7 +538,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
527
538
|
subject,
|
|
528
539
|
},
|
|
529
540
|
...timerEffects,
|
|
530
|
-
]
|
|
541
|
+
];
|
|
531
542
|
}
|
|
532
543
|
// fall through: black-holed spawn rides main below.
|
|
533
544
|
}
|
|
@@ -647,6 +658,15 @@ async function effectsForTypedDispatch(data, ctx) {
|
|
|
647
658
|
}
|
|
648
659
|
}
|
|
649
660
|
}
|
|
661
|
+
// Typed turns carry the capability env too: a task or schedule is exactly
|
|
662
|
+
// where "create the Feishu doc / check the calendar" work is assigned, so
|
|
663
|
+
// the every-turn capability contract covers them (null when ungranted or
|
|
664
|
+
// the mint degrades; never blocks the emit).
|
|
665
|
+
const capabilityEnv = await channelCapabilityEnv(ctx);
|
|
666
|
+
const typedContext = {
|
|
667
|
+
...(plan.chatId ? { PRLL_CHAT_ID: plan.chatId, PRLL_TRIGGER_MESSAGE_ID: sourceId } : {}),
|
|
668
|
+
...(capabilityEnv ?? {}),
|
|
669
|
+
};
|
|
650
670
|
return [
|
|
651
671
|
{
|
|
652
672
|
type: 'emitEvent',
|
|
@@ -673,14 +693,7 @@ async function effectsForTypedDispatch(data, ctx) {
|
|
|
673
693
|
? { chatId: plan.chatId, messageId: sourceId, envelopeId }
|
|
674
694
|
: { envelopeId },
|
|
675
695
|
},
|
|
676
|
-
...(
|
|
677
|
-
? {
|
|
678
|
-
context: {
|
|
679
|
-
PRLL_CHAT_ID: plan.chatId,
|
|
680
|
-
PRLL_TRIGGER_MESSAGE_ID: sourceId,
|
|
681
|
-
},
|
|
682
|
-
}
|
|
683
|
-
: {}),
|
|
696
|
+
...(Object.keys(typedContext).length > 0 ? { context: typedContext } : {}),
|
|
684
697
|
},
|
|
685
698
|
},
|
|
686
699
|
ack(),
|
|
@@ -765,22 +778,21 @@ export function handleParallAgentEvent(event, ctx) {
|
|
|
765
778
|
return serialized(() => handleParallAgentEventInner(event, ctx));
|
|
766
779
|
}
|
|
767
780
|
async function handleParallAgentEventInner(event, ctx) {
|
|
768
|
-
// spawnChildSession's asynchronous error channel — not turn-scoped
|
|
769
|
-
// regardless of observe scopes
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
if (spawnFailed) {
|
|
781
|
+
// spawnChildSession's asynchronous error channel — not turn-scoped and
|
|
782
|
+
// pushed regardless of observe scopes. Handle it before anything
|
|
783
|
+
// subject-gated.
|
|
784
|
+
if (event.type === 'child_spawn_failed') {
|
|
773
785
|
const effects = [];
|
|
774
786
|
try {
|
|
775
|
-
const outcome = await handleChildSpawnFailed(ctx,
|
|
787
|
+
const outcome = await handleChildSpawnFailed(ctx, event);
|
|
776
788
|
if (outcome.cleared) {
|
|
777
789
|
// The spawn's ase_ was already created active with an input step; no
|
|
778
790
|
// child turn will ever emit a terminal event for it — settle it here
|
|
779
791
|
// or the session (and its processing indicator) stays active forever.
|
|
780
|
-
const orphanAse = await ensureChildSession(ctx,
|
|
792
|
+
const orphanAse = await ensureChildSession(ctx, event.childRef);
|
|
781
793
|
if (orphanAse)
|
|
782
794
|
await patchSession(ctx, orphanAse, { status: 'idle' });
|
|
783
|
-
await invalidateChildSession(ctx,
|
|
795
|
+
await invalidateChildSession(ctx, event.childRef);
|
|
784
796
|
}
|
|
785
797
|
// Re-drive the stranded messages WITHOUT waiting for a reconnect sweep
|
|
786
798
|
// (a healthy long-lived socket never re-runs onOpen): park their
|
|
@@ -806,15 +818,14 @@ async function handleParallAgentEventInner(event, ctx) {
|
|
|
806
818
|
}
|
|
807
819
|
return effects;
|
|
808
820
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
return handleChildAgentEvent(withChild, ctx);
|
|
821
|
+
if (event.childRef) {
|
|
822
|
+
return handleChildAgentEvent(event, ctx);
|
|
812
823
|
}
|
|
813
824
|
const chatId = event.subject;
|
|
814
825
|
try {
|
|
815
826
|
// Best-effort busy classifier for fork-on-busy: only MAIN turns feed it
|
|
816
827
|
// (child events took the branch above).
|
|
817
|
-
await noteMainTurnEvent(ctx,
|
|
828
|
+
await noteMainTurnEvent(ctx, event).catch(() => { });
|
|
818
829
|
if (!chatId)
|
|
819
830
|
return [];
|
|
820
831
|
if (event.type === 'turn_completed' || event.type === 'turn_failed') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/parel-channel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
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": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"parel.plugin.json"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@parel/plugin-sdk": "^0.
|
|
26
|
+
"@parel/plugin-sdk": "^0.12.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.0.0",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-capability credential injection — the parel-native form of the
|
|
3
|
+
* feishu-cli broker (multi-channel-architecture-design §8.3).
|
|
4
|
+
*
|
|
5
|
+
* Bridges deliver the capability as a PATH pointer shim that mints a fresh
|
|
6
|
+
* short-lived tenant token per lark-cli invocation. In parel we control
|
|
7
|
+
* neither the sandbox PATH nor a bridge process — but we DO control the
|
|
8
|
+
* per-turn invocation context, which sandbox-e2b flattens into every exec's
|
|
9
|
+
* env. So the connector mints at emit time and injects the official
|
|
10
|
+
* LARKSUITE_CLI_* env set: bare `lark-cli` works ambiently for the whole
|
|
11
|
+
* turn (turn length ≪ token TTL ~2h), the app secret never leaves the
|
|
12
|
+
* platform, and revocation bites on the next emit's mint (403 → no env).
|
|
13
|
+
*
|
|
14
|
+
* The mint endpoint IS the gate (the platform re-evaluates connection state
|
|
15
|
+
* + flag on every request — the no-drift invariant), so the connector needs
|
|
16
|
+
* no declaration-plane fetch: a 403 is the revocation signal and is
|
|
17
|
+
* negative-cached briefly to keep a revoked grant from adding one 403 per
|
|
18
|
+
* turn.
|
|
19
|
+
*/
|
|
20
|
+
import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
21
|
+
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
22
|
+
|
|
23
|
+
const MINT_TIMEOUT_MS = 5_000;
|
|
24
|
+
// Re-mint this long before the token expires so an env set injected at turn
|
|
25
|
+
// start stays valid through a long turn's tail.
|
|
26
|
+
const EXPIRY_SKEW_MS = 10 * 60 * 1000;
|
|
27
|
+
// After a 403 (no grant / revoked / flag off), skip minting for this long —
|
|
28
|
+
// revocation latency stays bounded while per-turn 403 noise does not.
|
|
29
|
+
const DENIED_TTL_MS = 60 * 1000;
|
|
30
|
+
// A GRANTED result is re-validated on the same cadence: the mint endpoint IS
|
|
31
|
+
// the revocation gate, so a positive cache lasting the token TTL (~2h) would
|
|
32
|
+
// extend revocation latency to hours. Re-minting is cheap (the platform
|
|
33
|
+
// Redis-caches the exchanged token and re-evaluates the gate per request);
|
|
34
|
+
// this cache only absorbs bursty emits within a conversation.
|
|
35
|
+
const GRANTED_TTL_MS = 60 * 1000;
|
|
36
|
+
|
|
37
|
+
interface MintedToken {
|
|
38
|
+
app_id: string;
|
|
39
|
+
brand: string;
|
|
40
|
+
token: string;
|
|
41
|
+
expires_at: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CacheEntry {
|
|
45
|
+
env: Record<string, string> | null; // null = denied (negative cache)
|
|
46
|
+
validUntil: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Per-connection cache. The plugin isolate is per-connection, so a plain
|
|
50
|
+
// module map keyed by connectionId is effectively single-entry — the key
|
|
51
|
+
// guards against isolate reuse across connections.
|
|
52
|
+
const cache = new Map<string, CacheEntry>();
|
|
53
|
+
|
|
54
|
+
/** Test hook: the cache is module-level state, so suites reset it between cases. */
|
|
55
|
+
export function resetChannelCapabilityCache(): void {
|
|
56
|
+
cache.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function envFromMint(minted: MintedToken): Record<string, string> {
|
|
60
|
+
return {
|
|
61
|
+
LARKSUITE_CLI_APP_ID: minted.app_id,
|
|
62
|
+
LARKSUITE_CLI_BRAND: minted.brand || 'feishu',
|
|
63
|
+
LARKSUITE_CLI_TENANT_ACCESS_TOKEN: minted.token,
|
|
64
|
+
// Bot lock: the credential is the app's, not a human's — mirror the
|
|
65
|
+
// bridge shim's injection exactly (agent-core channel-exec).
|
|
66
|
+
LARKSUITE_CLI_DEFAULT_AS: 'bot',
|
|
67
|
+
LARKSUITE_CLI_STRICT_MODE: 'bot',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the feishu-cli env set to spread into an envelope's invocation
|
|
73
|
+
* context, or null when the capability is not granted (or the mint degraded
|
|
74
|
+
* — a transient platform error must never block a dispatch emit).
|
|
75
|
+
*/
|
|
76
|
+
export async function channelCapabilityEnv(
|
|
77
|
+
ctx: ConnectorContext,
|
|
78
|
+
): Promise<Record<string, string> | null> {
|
|
79
|
+
const key = ctx.connectionId;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const hit = cache.get(key);
|
|
82
|
+
if (hit && hit.validUntil > now) return hit.env;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(
|
|
86
|
+
`${parallApiUrl(ctx)}/api/v1/orgs/${encodeURIComponent(parallOrgId(ctx))}/agents/me/channel-token`,
|
|
87
|
+
{
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
authorization: `Bearer ${requireAgk(ctx)}`,
|
|
91
|
+
'content-type': 'application/json',
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({ channel_type: 'feishu' }),
|
|
94
|
+
signal: AbortSignal.timeout(MINT_TIMEOUT_MS),
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
if (res.status === 403 || res.status === 404) {
|
|
98
|
+
// No grant (never had it / revoked / flag off) — or an old server
|
|
99
|
+
// without the endpoint. Same agent-facing outcome: no capability.
|
|
100
|
+
cache.set(key, { env: null, validUntil: now + DENIED_TTL_MS });
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
// Transient (5xx/502-mint-upstream): don't cache, don't block the emit.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const minted = (await res.json()) as MintedToken;
|
|
108
|
+
if (!minted?.token || !minted.app_id) return null;
|
|
109
|
+
const env = envFromMint(minted);
|
|
110
|
+
const expiresAt = Date.parse(minted.expires_at);
|
|
111
|
+
// Bounded by BOTH the revalidation cadence (revocation latency) and the
|
|
112
|
+
// token's own remaining life (an injected env must outlive the turn).
|
|
113
|
+
const tokenBound = Number.isFinite(expiresAt) ? expiresAt - EXPIRY_SKEW_MS : now;
|
|
114
|
+
const validUntil = Math.max(now + 5_000, Math.min(now + GRANTED_TTL_MS, tokenBound));
|
|
115
|
+
cache.set(key, { env, validUntil });
|
|
116
|
+
return env;
|
|
117
|
+
} catch {
|
|
118
|
+
// Network failure — degrade to "no env this turn"; the next emit retries.
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/channel-prompt.ts
CHANGED
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
*
|
|
17
17
|
* - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
|
|
18
18
|
* attachment links in the reply (dead links there).
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* - Single-path reply (multi-channel-architecture-design §6): with the
|
|
20
|
+
* feishu-cli capability granted, the reply goes out through the official
|
|
21
|
+
* lark-cli — auth rides the turn's invocation context env
|
|
22
|
+
* (channel-capability.ts), so bare `lark-cli` works in the sandbox.
|
|
23
|
+
* Without the grant there is no outbound path and the hint says so (the
|
|
24
|
+
* provider clip path is retired).
|
|
25
25
|
* - When the inbound external message id is known, offer the in-thread reply
|
|
26
|
-
* form
|
|
27
|
-
*
|
|
26
|
+
* form so threaded conversations aren't nudged toward new top-level
|
|
27
|
+
* messages.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
function sanitizeMeta(value: string): string {
|
|
@@ -90,7 +90,7 @@ export function buildChatPrompt(args: ChatPromptArgs): string {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export interface ChannelPromptArgs {
|
|
93
|
-
/** Provider name (
|
|
93
|
+
/** Provider name (e.g. "feishu"); undefined when unresolved. */
|
|
94
94
|
provider?: string;
|
|
95
95
|
/** Conversation shape as recorded on the ChannelConversation (dm/group). */
|
|
96
96
|
conversationType?: string;
|
|
@@ -99,6 +99,13 @@ export interface ChannelPromptArgs {
|
|
|
99
99
|
senderName: string;
|
|
100
100
|
/** The inbound message text (raw, un-framed). */
|
|
101
101
|
text: string;
|
|
102
|
+
/**
|
|
103
|
+
* Live capability grant: the turn's invocation context carries the minted
|
|
104
|
+
* LARKSUITE_CLI_* env, so bare `lark-cli` works in the sandbox and is THE
|
|
105
|
+
* reply path. False → outbound is disabled for this org (no grant); the
|
|
106
|
+
* hint says so instead of pointing at the retired provider clip.
|
|
107
|
+
*/
|
|
108
|
+
cliCapable?: boolean;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
export function buildChannelPrompt(args: ChannelPromptArgs): string {
|
|
@@ -122,16 +129,22 @@ export function buildChannelPrompt(args: ChannelPromptArgs): string {
|
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
function buildChannelReplyHint(args: ChannelPromptArgs): string {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (args.
|
|
133
|
-
const
|
|
134
|
-
|
|
132
|
+
// Single-path routing (multi-channel-architecture-design §6/§8.3): with
|
|
133
|
+
// the feishu-cli grant, the minted LARKSUITE_CLI_* env rides this turn's
|
|
134
|
+
// invocation context, so the official lark-cli works ambiently in the
|
|
135
|
+
// sandbox and is THE reply path. Without the grant there is no outbound
|
|
136
|
+
// path — say so instead of pointing at the retired provider clip.
|
|
137
|
+
// cliCapable alone decides: the mint is feishu-specific, so a successful
|
|
138
|
+
// grant implies Feishu even when the cosmetic provider-label lookup failed.
|
|
139
|
+
if (args.cliCapable) {
|
|
140
|
+
const convRef = args.externalConversationId
|
|
141
|
+
? `chat_id "${args.externalConversationId}"`
|
|
142
|
+
: 'the conversation id named in this event';
|
|
143
|
+
const threadAlt = args.externalMessageId
|
|
144
|
+
? ` To reply threaded to this specific message, reference message_id "${args.externalMessageId}".`
|
|
145
|
+
: '';
|
|
146
|
+
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>`;
|
|
135
147
|
}
|
|
136
|
-
|
|
148
|
+
const platform = args.provider ?? 'the external platform';
|
|
149
|
+
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>`;
|
|
137
150
|
}
|
package/src/fork.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
2
|
-
|
|
1
|
+
import type { AgentEvent, ConnectorContext } from '@parel/plugin-sdk';
|
|
2
|
+
|
|
3
|
+
/** The child-spawn failure variant of the sdk's AgentEvent union. */
|
|
4
|
+
type ChildSpawnFailedEvent = Extract<AgentEvent, { type: 'child_spawn_failed' }>;
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Fork-on-busy scheduling (the parall-side half of parel F4, parel-mono #140;
|
|
@@ -298,10 +300,7 @@ export async function clearForkState(ctx: ConnectorContext): Promise<string[]> {
|
|
|
298
300
|
* Track main-session busyness from the E1 turn stream. Child events (they
|
|
299
301
|
* carry childRef) must NOT feed this — only the main session's turns do.
|
|
300
302
|
*/
|
|
301
|
-
export async function noteMainTurnEvent(
|
|
302
|
-
ctx: ConnectorContext,
|
|
303
|
-
event: AgentEventWithChild,
|
|
304
|
-
): Promise<void> {
|
|
303
|
+
export async function noteMainTurnEvent(ctx: ConnectorContext, event: AgentEvent): Promise<void> {
|
|
305
304
|
if (event.type === 'turn_started') {
|
|
306
305
|
const row: MainTurnRow = { turnId: event.turnId, subject: event.subject, at: ctx.now() };
|
|
307
306
|
await ctx.store?.set(MAIN_TURN_KEY, row).catch(() => {});
|
|
@@ -323,7 +322,7 @@ export async function noteMainTurnEvent(
|
|
|
323
322
|
*/
|
|
324
323
|
export async function settleChildEvent(
|
|
325
324
|
ctx: ConnectorContext,
|
|
326
|
-
event:
|
|
325
|
+
event: AgentEvent,
|
|
327
326
|
): Promise<{ subject: string; pendingAcks: PendingAck[] } | null> {
|
|
328
327
|
const childRef = event.childRef;
|
|
329
328
|
if (!childRef) return null;
|
package/src/inbound-channel.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConnectorContext, ConnectorEffect } from '@parel/plugin-sdk';
|
|
2
|
+
import { channelCapabilityEnv } from './channel-capability.js';
|
|
2
3
|
import { buildChannelPrompt } from './channel-prompt.js';
|
|
3
4
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
4
5
|
import {
|
|
@@ -154,6 +155,10 @@ export async function effectsForChannelDispatch(
|
|
|
154
155
|
await receivedSettled;
|
|
155
156
|
await writeActiveEmit(ctx, 'channel_message', sourceId, envelopeId);
|
|
156
157
|
|
|
158
|
+
// Live capability grant → this turn's lark-cli auth env (mint gate = the
|
|
159
|
+
// no-drift revocation point) + the hint teaching the single reply path.
|
|
160
|
+
const capabilityEnv = await channelCapabilityEnv(ctx);
|
|
161
|
+
|
|
157
162
|
return [
|
|
158
163
|
{
|
|
159
164
|
type: 'emitEvent',
|
|
@@ -178,6 +183,7 @@ export async function effectsForChannelDispatch(
|
|
|
178
183
|
externalMessageId,
|
|
179
184
|
senderName,
|
|
180
185
|
text,
|
|
186
|
+
cliCapable: capabilityEnv !== null,
|
|
181
187
|
}),
|
|
182
188
|
conversationId,
|
|
183
189
|
channelMessageId: sourceId,
|
|
@@ -185,19 +191,19 @@ export async function effectsForChannelDispatch(
|
|
|
185
191
|
externalConversationId,
|
|
186
192
|
externalMessageId,
|
|
187
193
|
},
|
|
188
|
-
// No chatId on purpose: the reply
|
|
189
|
-
//
|
|
190
|
-
//
|
|
194
|
+
// No chatId on purpose: the reply is the agent's own lark-cli send,
|
|
195
|
+
// NOT deliver() — deliver only runs the redundant session idle for
|
|
196
|
+
// channel routes (see delivery.ts).
|
|
191
197
|
replyRoute: {
|
|
192
198
|
kind: 'provider_http',
|
|
193
199
|
connectionId: ctx.connectionId,
|
|
194
200
|
data: { conversationId, envelopeId },
|
|
195
201
|
},
|
|
196
|
-
//
|
|
197
|
-
// a chat that doesn't exist
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
202
|
+
// Invocation context carries ONLY the capability env: no PRLL_CHAT_ID
|
|
203
|
+
// (it must not point CLI defaults at a chat that doesn't exist) and
|
|
204
|
+
// no PRLL_DISPATCH_SOURCE_* (the external reply is a vendor-CLI send,
|
|
205
|
+
// and this row's ledger close is the turn event's complete-sources).
|
|
206
|
+
...(capabilityEnv ? { context: capabilityEnv } : {}),
|
|
201
207
|
},
|
|
202
208
|
},
|
|
203
209
|
// No ack effect — see the message path (inbound.ts): the row stays
|
package/src/inbound.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ConnectorEffect,
|
|
6
6
|
WebSocketFrame,
|
|
7
7
|
} from '@parel/plugin-sdk';
|
|
8
|
+
import { channelCapabilityEnv } from './channel-capability.js';
|
|
8
9
|
import { buildChatPrompt, type PromptAttachment } from './channel-prompt.js';
|
|
9
10
|
import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
|
|
10
11
|
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
@@ -25,12 +26,6 @@ import {
|
|
|
25
26
|
settleChildEvent,
|
|
26
27
|
trackFollowUpAck,
|
|
27
28
|
} from './fork.js';
|
|
28
|
-
import {
|
|
29
|
-
type AgentEventWithChild,
|
|
30
|
-
asChildSpawnFailed,
|
|
31
|
-
asConnectorEffects,
|
|
32
|
-
type ExtendedConnectorEffect,
|
|
33
|
-
} from './parel-sdk-compat.js';
|
|
34
29
|
import {
|
|
35
30
|
ackByIdEffect,
|
|
36
31
|
ackEffect,
|
|
@@ -496,12 +491,18 @@ async function effectsForDispatch(
|
|
|
496
491
|
// so the server resolves this WorkItem in the reply's own transaction.
|
|
497
492
|
// (Fork turns get it too — their rows are legacy-acked so the resolve is a
|
|
498
493
|
// no-op today, and it wires up for free when fork ledger parity lands.)
|
|
494
|
+
// Channel-capability env (lark-cli auth) rides EVERY turn's invocation
|
|
495
|
+
// context while granted — Feishu workspace ops (docs/calendar/…) are asked
|
|
496
|
+
// for from ordinary Parall chats, not only from channel turns. Null when
|
|
497
|
+
// ungranted or the mint degrades; never blocks the emit.
|
|
498
|
+
const capabilityEnv = await channelCapabilityEnv(ctx);
|
|
499
499
|
const invocationContext: Record<string, string> = {
|
|
500
500
|
PRLL_CHAT_ID: chatId,
|
|
501
501
|
PRLL_TRIGGER_MESSAGE_ID: sourceId,
|
|
502
502
|
PRLL_DISPATCH_SOURCE_TYPE: sourceType,
|
|
503
503
|
PRLL_DISPATCH_SOURCE_ID: sourceId,
|
|
504
504
|
...(threadRootId ? { PRLL_THREAD_ROOT_ID: threadRootId } : {}),
|
|
505
|
+
...(capabilityEnv ?? {}),
|
|
505
506
|
};
|
|
506
507
|
|
|
507
508
|
const envelope = {
|
|
@@ -586,18 +587,16 @@ async function effectsForDispatch(
|
|
|
586
587
|
source_id: sourceId,
|
|
587
588
|
chat_id: chatId,
|
|
588
589
|
});
|
|
589
|
-
return
|
|
590
|
-
{ type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS },
|
|
591
|
-
]);
|
|
590
|
+
return [{ type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS }];
|
|
592
591
|
}
|
|
593
|
-
return
|
|
592
|
+
return [
|
|
594
593
|
{ type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
|
|
595
|
-
]
|
|
594
|
+
];
|
|
596
595
|
}
|
|
597
|
-
return
|
|
596
|
+
return [
|
|
598
597
|
{ type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
|
|
599
598
|
ackEffect(ctx, sourceType, sourceId),
|
|
600
|
-
]
|
|
599
|
+
];
|
|
601
600
|
}
|
|
602
601
|
|
|
603
602
|
if (decision.mode === 'spawn') {
|
|
@@ -625,7 +624,7 @@ async function effectsForDispatch(
|
|
|
625
624
|
// would leave the message stranded with no timer and no bookkeeping
|
|
626
625
|
// until a reconnect).
|
|
627
626
|
let spawnBlackHoled = false;
|
|
628
|
-
const timerEffects:
|
|
627
|
+
const timerEffects: ConnectorEffect[] = [];
|
|
629
628
|
if (dispatchId) {
|
|
630
629
|
const retryKey = `${FORK_RETRY_PREFIX}${dispatchId}`;
|
|
631
630
|
const attempts = await bumpForkRetryAttempts(ctx, retryKey, {
|
|
@@ -659,7 +658,14 @@ async function effectsForDispatch(
|
|
|
659
658
|
}
|
|
660
659
|
await recordSpawn(ctx, subject, decision.childRef, { dispatchId, sourceId });
|
|
661
660
|
await receivedSettled;
|
|
662
|
-
|
|
661
|
+
// Known gap: SpawnChildSessionEffect (#140) carries no invocation
|
|
662
|
+
// context, so the fork's OPENING turn runs without the capability env
|
|
663
|
+
// (lark-cli workspace ops would need a follow-up turn — those arrive
|
|
664
|
+
// as deliverTo envelopes, which inherit the full context). Channel
|
|
665
|
+
// replies are unaffected: channel_message dispatches always ride main.
|
|
666
|
+
// Fix belongs parel-side (context on spawnChildSession) — tracked as a
|
|
667
|
+
// parel ask.
|
|
668
|
+
return [
|
|
663
669
|
{
|
|
664
670
|
type: 'spawnChildSession',
|
|
665
671
|
childRef: decision.childRef,
|
|
@@ -667,7 +673,7 @@ async function effectsForDispatch(
|
|
|
667
673
|
subject,
|
|
668
674
|
},
|
|
669
675
|
...timerEffects,
|
|
670
|
-
]
|
|
676
|
+
];
|
|
671
677
|
}
|
|
672
678
|
// fall through: black-holed spawn rides main below.
|
|
673
679
|
}
|
|
@@ -796,6 +802,16 @@ async function effectsForTypedDispatch(
|
|
|
796
802
|
}
|
|
797
803
|
}
|
|
798
804
|
|
|
805
|
+
// Typed turns carry the capability env too: a task or schedule is exactly
|
|
806
|
+
// where "create the Feishu doc / check the calendar" work is assigned, so
|
|
807
|
+
// the every-turn capability contract covers them (null when ungranted or
|
|
808
|
+
// the mint degrades; never blocks the emit).
|
|
809
|
+
const capabilityEnv = await channelCapabilityEnv(ctx);
|
|
810
|
+
const typedContext: Record<string, string> = {
|
|
811
|
+
...(plan.chatId ? { PRLL_CHAT_ID: plan.chatId, PRLL_TRIGGER_MESSAGE_ID: sourceId } : {}),
|
|
812
|
+
...(capabilityEnv ?? {}),
|
|
813
|
+
};
|
|
814
|
+
|
|
799
815
|
return [
|
|
800
816
|
{
|
|
801
817
|
type: 'emitEvent',
|
|
@@ -822,14 +838,7 @@ async function effectsForTypedDispatch(
|
|
|
822
838
|
? { chatId: plan.chatId, messageId: sourceId, envelopeId }
|
|
823
839
|
: { envelopeId },
|
|
824
840
|
},
|
|
825
|
-
...(
|
|
826
|
-
? {
|
|
827
|
-
context: {
|
|
828
|
-
PRLL_CHAT_ID: plan.chatId,
|
|
829
|
-
PRLL_TRIGGER_MESSAGE_ID: sourceId,
|
|
830
|
-
},
|
|
831
|
-
}
|
|
832
|
-
: {}),
|
|
841
|
+
...(Object.keys(typedContext).length > 0 ? { context: typedContext } : {}),
|
|
833
842
|
},
|
|
834
843
|
},
|
|
835
844
|
ack(),
|
|
@@ -932,21 +941,20 @@ async function handleParallAgentEventInner(
|
|
|
932
941
|
event: AgentEvent,
|
|
933
942
|
ctx: ConnectorContext,
|
|
934
943
|
): Promise<AgentEventEffect[]> {
|
|
935
|
-
// spawnChildSession's asynchronous error channel — not turn-scoped
|
|
936
|
-
// regardless of observe scopes
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
if (spawnFailed) {
|
|
944
|
+
// spawnChildSession's asynchronous error channel — not turn-scoped and
|
|
945
|
+
// pushed regardless of observe scopes. Handle it before anything
|
|
946
|
+
// subject-gated.
|
|
947
|
+
if (event.type === 'child_spawn_failed') {
|
|
940
948
|
const effects: AgentEventEffect[] = [];
|
|
941
949
|
try {
|
|
942
|
-
const outcome = await handleChildSpawnFailed(ctx,
|
|
950
|
+
const outcome = await handleChildSpawnFailed(ctx, event);
|
|
943
951
|
if (outcome.cleared) {
|
|
944
952
|
// The spawn's ase_ was already created active with an input step; no
|
|
945
953
|
// child turn will ever emit a terminal event for it — settle it here
|
|
946
954
|
// or the session (and its processing indicator) stays active forever.
|
|
947
|
-
const orphanAse = await ensureChildSession(ctx,
|
|
955
|
+
const orphanAse = await ensureChildSession(ctx, event.childRef);
|
|
948
956
|
if (orphanAse) await patchSession(ctx, orphanAse, { status: 'idle' });
|
|
949
|
-
await invalidateChildSession(ctx,
|
|
957
|
+
await invalidateChildSession(ctx, event.childRef);
|
|
950
958
|
}
|
|
951
959
|
// Re-drive the stranded messages WITHOUT waiting for a reconnect sweep
|
|
952
960
|
// (a healthy long-lived socket never re-runs onOpen): park their
|
|
@@ -971,15 +979,14 @@ async function handleParallAgentEventInner(
|
|
|
971
979
|
}
|
|
972
980
|
return effects;
|
|
973
981
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
return handleChildAgentEvent(withChild, ctx);
|
|
982
|
+
if (event.childRef) {
|
|
983
|
+
return handleChildAgentEvent(event, ctx);
|
|
977
984
|
}
|
|
978
985
|
const chatId = event.subject;
|
|
979
986
|
try {
|
|
980
987
|
// Best-effort busy classifier for fork-on-busy: only MAIN turns feed it
|
|
981
988
|
// (child events took the branch above).
|
|
982
|
-
await noteMainTurnEvent(ctx,
|
|
989
|
+
await noteMainTurnEvent(ctx, event).catch(() => {});
|
|
983
990
|
if (!chatId) return [];
|
|
984
991
|
if (event.type === 'turn_completed' || event.type === 'turn_failed') {
|
|
985
992
|
// Ledger close FIRST, fenced per source — deliberately NOT behind the
|
|
@@ -1065,7 +1072,7 @@ async function handleParallAgentEventInner(
|
|
|
1065
1072
|
* turnEnvelope marker or the busy classifier.
|
|
1066
1073
|
*/
|
|
1067
1074
|
async function handleChildAgentEvent(
|
|
1068
|
-
event:
|
|
1075
|
+
event: AgentEvent,
|
|
1069
1076
|
ctx: ConnectorContext,
|
|
1070
1077
|
): Promise<AgentEventEffect[]> {
|
|
1071
1078
|
const childRef = event.childRef ?? '';
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { AgentEvent, ChannelEnvelope, ConnectorEffect } from '@parel/plugin-sdk';
|
|
2
|
-
/**
|
|
3
|
-
* Local mirrors of parel host types that shipped in parel-mono #140 (F3
|
|
4
|
-
* injectInFlight / F4 child sessions) but are not yet published in
|
|
5
|
-
* @parel/plugin-sdk (the mirror lives on parel-oss branch
|
|
6
|
-
* feat/channel-child-sessions). The host accepts these shapes today — prod
|
|
7
|
-
* runs #140 — so the connector uses them through this compat layer and casts
|
|
8
|
-
* at the hook boundary.
|
|
9
|
-
*
|
|
10
|
-
* TODO(parel-sdk-#140-types): DELETE this file (and the casts) once
|
|
11
|
-
* @parel/plugin-sdk ships the mirror sitting on parel-oss branch
|
|
12
|
-
* `feat/channel-child-sessions` — check the branch/`@parel/plugin-sdk`
|
|
13
|
-
* changelog on every sdk version bump. Until then the shapes below must stay
|
|
14
|
-
* byte-compatible with parel-mono ts/packages/cloudflare/src/channel-types.ts.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Spawn a fork child session off this connection's main conversation session.
|
|
18
|
-
* Idempotent on childRef (opaque, connector-owned). Host gates: binding must
|
|
19
|
-
* opt in (childSessions) + `main` routing; failures come back asynchronously
|
|
20
|
-
* as a `child_spawn_failed` agent event. The child seeds from the parent
|
|
21
|
-
* transcript at the last turn boundary (never sees in-flight output).
|
|
22
|
-
*/
|
|
23
|
-
export interface SpawnChildSessionEffect {
|
|
24
|
-
type: 'spawnChildSession';
|
|
25
|
-
childRef: string;
|
|
26
|
-
input: string;
|
|
27
|
-
subject?: string;
|
|
28
|
-
}
|
|
29
|
-
/** emitEvent with the #140 deliverTo extension: route to a spawned child. */
|
|
30
|
-
export interface EmitToChildEffect {
|
|
31
|
-
type: 'emitEvent';
|
|
32
|
-
event: ChannelEnvelope;
|
|
33
|
-
deliverTo: {
|
|
34
|
-
childRef: string;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Asynchronous error channel for a failed spawnChildSession effect (pushed
|
|
39
|
-
* regardless of observe scopes). codes: disabled | unsupported_routing |
|
|
40
|
-
* no_binding | invalid_request | depth_limit | concurrency_limit |
|
|
41
|
-
* spawn_failed.
|
|
42
|
-
*/
|
|
43
|
-
export interface ChildSpawnFailedEvent {
|
|
44
|
-
type: 'child_spawn_failed';
|
|
45
|
-
childRef: string;
|
|
46
|
-
code: string;
|
|
47
|
-
error: string;
|
|
48
|
-
}
|
|
49
|
-
/** Effect union the #140 host actually executes. */
|
|
50
|
-
export type ExtendedConnectorEffect = ConnectorEffect | SpawnChildSessionEffect | EmitToChildEffect;
|
|
51
|
-
/**
|
|
52
|
-
* Every event from a connector-spawned child carries the childRef it was
|
|
53
|
-
* spawned with (#140: AgentEventBase.childRef). Absent on main-session
|
|
54
|
-
* events and on sdk versions that predate the field.
|
|
55
|
-
*/
|
|
56
|
-
export type AgentEventWithChild = AgentEvent & {
|
|
57
|
-
childRef?: string;
|
|
58
|
-
};
|
|
59
|
-
/** Narrow an incoming agent event to the not-yet-published failure shape. */
|
|
60
|
-
export declare function asChildSpawnFailed(event: unknown): ChildSpawnFailedEvent | null;
|
|
61
|
-
/** Cast helper: the host executes the extended union; the sdk type lags it. */
|
|
62
|
-
export declare function asConnectorEffects(effects: ExtendedConnectorEffect[]): ConnectorEffect[];
|
|
63
|
-
//# sourceMappingURL=parel-sdk-compat.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parel-sdk-compat.d.ts","sourceRoot":"","sources":["../src/parel-sdk-compat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEtF;;;;;;;;;;;;;GAaG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,oDAAoD;AACpD,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG,uBAAuB,GAAG,iBAAiB,CAAC;AAEpG;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CAS/E;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,EAAE,GAAG,eAAe,EAAE,CAExF"}
|
package/dist/parel-sdk-compat.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** Narrow an incoming agent event to the not-yet-published failure shape. */
|
|
2
|
-
export function asChildSpawnFailed(event) {
|
|
3
|
-
const e = event;
|
|
4
|
-
if (e?.type !== 'child_spawn_failed')
|
|
5
|
-
return null;
|
|
6
|
-
return {
|
|
7
|
-
type: 'child_spawn_failed',
|
|
8
|
-
childRef: typeof e.childRef === 'string' ? e.childRef : '',
|
|
9
|
-
code: typeof e.code === 'string' ? e.code : 'unknown',
|
|
10
|
-
error: typeof e.error === 'string' ? e.error : '',
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
/** Cast helper: the host executes the extended union; the sdk type lags it. */
|
|
14
|
-
export function asConnectorEffects(effects) {
|
|
15
|
-
return effects;
|
|
16
|
-
}
|
package/src/parel-sdk-compat.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { AgentEvent, ChannelEnvelope, ConnectorEffect } from '@parel/plugin-sdk';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Local mirrors of parel host types that shipped in parel-mono #140 (F3
|
|
5
|
-
* injectInFlight / F4 child sessions) but are not yet published in
|
|
6
|
-
* @parel/plugin-sdk (the mirror lives on parel-oss branch
|
|
7
|
-
* feat/channel-child-sessions). The host accepts these shapes today — prod
|
|
8
|
-
* runs #140 — so the connector uses them through this compat layer and casts
|
|
9
|
-
* at the hook boundary.
|
|
10
|
-
*
|
|
11
|
-
* TODO(parel-sdk-#140-types): DELETE this file (and the casts) once
|
|
12
|
-
* @parel/plugin-sdk ships the mirror sitting on parel-oss branch
|
|
13
|
-
* `feat/channel-child-sessions` — check the branch/`@parel/plugin-sdk`
|
|
14
|
-
* changelog on every sdk version bump. Until then the shapes below must stay
|
|
15
|
-
* byte-compatible with parel-mono ts/packages/cloudflare/src/channel-types.ts.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Spawn a fork child session off this connection's main conversation session.
|
|
20
|
-
* Idempotent on childRef (opaque, connector-owned). Host gates: binding must
|
|
21
|
-
* opt in (childSessions) + `main` routing; failures come back asynchronously
|
|
22
|
-
* as a `child_spawn_failed` agent event. The child seeds from the parent
|
|
23
|
-
* transcript at the last turn boundary (never sees in-flight output).
|
|
24
|
-
*/
|
|
25
|
-
export interface SpawnChildSessionEffect {
|
|
26
|
-
type: 'spawnChildSession';
|
|
27
|
-
childRef: string;
|
|
28
|
-
input: string;
|
|
29
|
-
subject?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** emitEvent with the #140 deliverTo extension: route to a spawned child. */
|
|
33
|
-
export interface EmitToChildEffect {
|
|
34
|
-
type: 'emitEvent';
|
|
35
|
-
event: ChannelEnvelope;
|
|
36
|
-
deliverTo: { childRef: string };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Asynchronous error channel for a failed spawnChildSession effect (pushed
|
|
41
|
-
* regardless of observe scopes). codes: disabled | unsupported_routing |
|
|
42
|
-
* no_binding | invalid_request | depth_limit | concurrency_limit |
|
|
43
|
-
* spawn_failed.
|
|
44
|
-
*/
|
|
45
|
-
export interface ChildSpawnFailedEvent {
|
|
46
|
-
type: 'child_spawn_failed';
|
|
47
|
-
childRef: string;
|
|
48
|
-
code: string;
|
|
49
|
-
error: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Effect union the #140 host actually executes. */
|
|
53
|
-
export type ExtendedConnectorEffect = ConnectorEffect | SpawnChildSessionEffect | EmitToChildEffect;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Every event from a connector-spawned child carries the childRef it was
|
|
57
|
-
* spawned with (#140: AgentEventBase.childRef). Absent on main-session
|
|
58
|
-
* events and on sdk versions that predate the field.
|
|
59
|
-
*/
|
|
60
|
-
export type AgentEventWithChild = AgentEvent & { childRef?: string };
|
|
61
|
-
|
|
62
|
-
/** Narrow an incoming agent event to the not-yet-published failure shape. */
|
|
63
|
-
export function asChildSpawnFailed(event: unknown): ChildSpawnFailedEvent | null {
|
|
64
|
-
const e = event as { type?: unknown; childRef?: unknown; code?: unknown; error?: unknown };
|
|
65
|
-
if (e?.type !== 'child_spawn_failed') return null;
|
|
66
|
-
return {
|
|
67
|
-
type: 'child_spawn_failed',
|
|
68
|
-
childRef: typeof e.childRef === 'string' ? e.childRef : '',
|
|
69
|
-
code: typeof e.code === 'string' ? e.code : 'unknown',
|
|
70
|
-
error: typeof e.error === 'string' ? e.error : '',
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Cast helper: the host executes the extended union; the sdk type lags it. */
|
|
75
|
-
export function asConnectorEffects(effects: ExtendedConnectorEffect[]): ConnectorEffect[] {
|
|
76
|
-
return effects as ConnectorEffect[];
|
|
77
|
-
}
|