@parall/parel-channel 1.38.0 → 1.40.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-prompt.d.ts +28 -1
- package/dist/channel-prompt.d.ts.map +1 -1
- package/dist/channel-prompt.js +29 -1
- package/dist/delivery.js +14 -13
- package/dist/inbound.d.ts.map +1 -1
- package/dist/inbound.js +131 -71
- package/dist/session.d.ts +63 -15
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +117 -23
- package/package.json +1 -1
- package/src/channel-prompt.ts +39 -1
- package/src/delivery.ts +13 -13
- package/src/inbound.ts +148 -72
- package/src/session.ts +126 -25
package/dist/channel-prompt.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt framing for
|
|
2
|
+
* Prompt framing for inbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Chat messages (buildChatPrompt): under `main` channel routing every Parall
|
|
5
|
+
* chat shares the agent's ONE parel session, so consecutive turns can come
|
|
6
|
+
* from different conversations — each message is framed with its originating
|
|
7
|
+
* chat so the model registers the context switch instead of answering chat B
|
|
8
|
+
* with chat A's thread of thought. No reply hint: the turn-end reply is
|
|
9
|
+
* auto-delivered to the right chat by replyRoute (deliver()), and sandbox CLI
|
|
10
|
+
* calls get the right PRLL_CHAT_ID via per-turn invocation context.
|
|
11
|
+
*
|
|
12
|
+
* External IM channel messages (buildChannelPrompt, channel_message
|
|
3
13
|
* dispatches). Mirrors agent-core's event-format.ts channel branch, adapted
|
|
4
14
|
* to the parel runtime's tool surface:
|
|
5
15
|
*
|
|
@@ -15,6 +25,23 @@
|
|
|
15
25
|
* form ({"message_id": …}) so threaded conversations aren't nudged toward
|
|
16
26
|
* new top-level messages.
|
|
17
27
|
*/
|
|
28
|
+
export interface ChatPromptArgs {
|
|
29
|
+
chatId: string;
|
|
30
|
+
/** Display name of the chat; undefined when unresolved (best-effort). */
|
|
31
|
+
chatName?: string;
|
|
32
|
+
/** Sender user id (usr_); display-name enrichment is a follow-up. */
|
|
33
|
+
senderId?: string;
|
|
34
|
+
threadRootId?: string;
|
|
35
|
+
/** The inbound message text (raw, un-framed). */
|
|
36
|
+
text: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Frame a Parall chat message with its conversation of origin. Kept to a
|
|
40
|
+
* minimal metadata header: the text itself is the user message, and heavier
|
|
41
|
+
* context (unread counts, membership) is the standard runtimes' enrichment —
|
|
42
|
+
* not re-created here.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildChatPrompt(args: ChatPromptArgs): string;
|
|
18
45
|
export interface ChannelPromptArgs {
|
|
19
46
|
/** Provider name (= clip alias, e.g. "feishu"); undefined when unresolved. */
|
|
20
47
|
provider?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH,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,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAS5D;AAED,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,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;CACd;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAkBlE"}
|
package/dist/channel-prompt.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt framing for
|
|
2
|
+
* Prompt framing for inbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Chat messages (buildChatPrompt): under `main` channel routing every Parall
|
|
5
|
+
* chat shares the agent's ONE parel session, so consecutive turns can come
|
|
6
|
+
* from different conversations — each message is framed with its originating
|
|
7
|
+
* chat so the model registers the context switch instead of answering chat B
|
|
8
|
+
* with chat A's thread of thought. No reply hint: the turn-end reply is
|
|
9
|
+
* auto-delivered to the right chat by replyRoute (deliver()), and sandbox CLI
|
|
10
|
+
* calls get the right PRLL_CHAT_ID via per-turn invocation context.
|
|
11
|
+
*
|
|
12
|
+
* External IM channel messages (buildChannelPrompt, channel_message
|
|
3
13
|
* dispatches). Mirrors agent-core's event-format.ts channel branch, adapted
|
|
4
14
|
* to the parel runtime's tool surface:
|
|
5
15
|
*
|
|
@@ -21,6 +31,24 @@ function sanitizeMeta(value) {
|
|
|
21
31
|
.replace(/[[\]|]/g, ' ')
|
|
22
32
|
.trim();
|
|
23
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Frame a Parall chat message with its conversation of origin. Kept to a
|
|
36
|
+
* minimal metadata header: the text itself is the user message, and heavier
|
|
37
|
+
* context (unread counts, membership) is the standard runtimes' enrichment —
|
|
38
|
+
* not re-created here.
|
|
39
|
+
*/
|
|
40
|
+
export function buildChatPrompt(args) {
|
|
41
|
+
const chatLabel = args.chatName
|
|
42
|
+
? `${sanitizeMeta(args.chatId)} — ${sanitizeMeta(args.chatName)}`
|
|
43
|
+
: sanitizeMeta(args.chatId);
|
|
44
|
+
const lines = [`[Chat: ${chatLabel}]`];
|
|
45
|
+
if (args.senderId)
|
|
46
|
+
lines.push(`[From: ${sanitizeMeta(args.senderId)}]`);
|
|
47
|
+
if (args.threadRootId)
|
|
48
|
+
lines.push(`[Thread: ${sanitizeMeta(args.threadRootId)}]`);
|
|
49
|
+
lines.push('', args.text);
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
24
52
|
export function buildChannelPrompt(args) {
|
|
25
53
|
const providerLabel = sanitizeMeta(args.provider ?? 'external IM');
|
|
26
54
|
const convLabel = args.externalConversationId
|
package/dist/delivery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
2
|
-
import { ensureSession, invalidateSession, patchSession, REQUEST_TIMEOUT_MS,
|
|
2
|
+
import { ensureSession, invalidateSession, patchSession, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, } from './session.js';
|
|
3
3
|
/**
|
|
4
4
|
* Post the agent's reply back to the Parall chat. The chat id rides the
|
|
5
5
|
* replyRoute.data set in inbound.ts.
|
|
@@ -34,9 +34,9 @@ export async function buildParallDelivery(delivery, ctx) {
|
|
|
34
34
|
// not the reply). Only run the redundant idle so a binding without
|
|
35
35
|
// `observe: [turn]` still closes the session.
|
|
36
36
|
if (route.conversationId) {
|
|
37
|
-
const sessionId = await ensureSession(ctx,
|
|
37
|
+
const sessionId = await ensureSession(ctx, { timeoutMs: 3_000 });
|
|
38
38
|
if (sessionId) {
|
|
39
|
-
await idleUnlessSuperseded(ctx, route.
|
|
39
|
+
await idleUnlessSuperseded(ctx, route.envelopeId, sessionId);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
return [];
|
|
@@ -46,9 +46,9 @@ export async function buildParallDelivery(delivery, ctx) {
|
|
|
46
46
|
if (!text)
|
|
47
47
|
return [];
|
|
48
48
|
// Short timeout: this sits in front of the user-visible reply POST. Cache
|
|
49
|
-
// hit (every
|
|
50
|
-
// we degrade to a reply without the session link rather than delay it.
|
|
51
|
-
const sessionId = await ensureSession(ctx,
|
|
49
|
+
// hit (every dispatch after the agent's first) is a store read; on a slow
|
|
50
|
+
// miss we degrade to a reply without the session link rather than delay it.
|
|
51
|
+
const sessionId = await ensureSession(ctx, { timeoutMs: 3_000 });
|
|
52
52
|
const request = {
|
|
53
53
|
url: `${apiUrl}/api/v1/orgs/${orgId}/chats/${chatId}/messages`,
|
|
54
54
|
method: 'POST',
|
|
@@ -101,20 +101,21 @@ export async function buildParallDelivery(delivery, ctx) {
|
|
|
101
101
|
// run the same turnEnvelope staleness guard and idle is idempotent, so the
|
|
102
102
|
// overlap is harmless.
|
|
103
103
|
if (sessionId) {
|
|
104
|
-
await idleUnlessSuperseded(ctx,
|
|
104
|
+
await idleUnlessSuperseded(ctx, route.envelopeId, sessionId);
|
|
105
105
|
}
|
|
106
106
|
return [];
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
* Idle the
|
|
110
|
-
* turnEnvelope staleness guard onAgentEvent runs
|
|
111
|
-
*
|
|
109
|
+
* Idle the agent's main session unless a newer envelope owns the turn (the
|
|
110
|
+
* same agent-global turnEnvelope staleness guard onAgentEvent runs — any
|
|
111
|
+
* chat's later message supersedes this turn's idle). Fail-open when the
|
|
112
|
+
* marker is unavailable — a stuck ring is worse than a blink.
|
|
112
113
|
*/
|
|
113
|
-
async function idleUnlessSuperseded(ctx,
|
|
114
|
-
const current = await ctx.store?.get(
|
|
114
|
+
async function idleUnlessSuperseded(ctx, envelopeId, sessionId) {
|
|
115
|
+
const current = await ctx.store?.get(TURN_ENVELOPE_KEY).catch(() => null);
|
|
115
116
|
if (!(typeof current === 'string' && current) || !envelopeId || current === envelopeId) {
|
|
116
117
|
const result = await patchSession(ctx, sessionId, { status: 'idle' });
|
|
117
118
|
if (result === 'stale')
|
|
118
|
-
await invalidateSession(ctx
|
|
119
|
+
await invalidateSession(ctx);
|
|
119
120
|
}
|
|
120
121
|
}
|
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;AAiD3B,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAuDrF;AAkdD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyD7B"}
|
package/dist/inbound.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { buildChannelPrompt } from './channel-prompt.js';
|
|
1
|
+
import { buildChannelPrompt, buildChatPrompt } from './channel-prompt.js';
|
|
2
2
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
3
|
-
import { createChannelInputStep, createInputStep, createTraceStep, ensureSession, invalidateSession, markDispatchReceived, patchSession, REQUEST_TIMEOUT_MS,
|
|
3
|
+
import { createChannelInputStep, createErrorStep, createInputStep, createTraceStep, ensureSession, invalidateSession, markDispatchReceived, patchSession, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, } from './session.js';
|
|
4
4
|
export async function handleParallFrame(frame, ctx) {
|
|
5
5
|
let event;
|
|
6
6
|
try {
|
|
@@ -52,48 +52,21 @@ export async function catchUpParall(ctx) {
|
|
|
52
52
|
if (!res.ok)
|
|
53
53
|
break;
|
|
54
54
|
const body = (await res.json().catch(() => null));
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// (the session key) is only known after fetching the message — so they
|
|
64
|
-
// all share ONE sequential chain: per-conversation order is preserved
|
|
65
|
-
// at the cost of serializing across conversations, which is fine for
|
|
66
|
-
// the small external-IM backlogs a reconnect sweep sees.
|
|
67
|
-
const byChat = new Map();
|
|
68
|
-
const unkeyed = [];
|
|
55
|
+
// ALL dispatches replay SEQUENTIALLY, in page order (FIFO): every
|
|
56
|
+
// envelope feeds the agent's one main session, so cross-chat replay
|
|
57
|
+
// concurrency would scramble the session's input order AND race the
|
|
58
|
+
// agent-global turnEnvelope marker (a slower old dispatch could
|
|
59
|
+
// overwrite a newer one's marker and mis-arm the done-signal guard).
|
|
60
|
+
// Reconnect backlogs are small; serialization is the correctness
|
|
61
|
+
// posture, not a throughput concern. A failed dispatch resolves to []
|
|
62
|
+
// and stays pending for the next sweep.
|
|
69
63
|
for (const d of body?.data ?? []) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!cid) {
|
|
76
|
-
unkeyed.push(d);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const group = byChat.get(cid);
|
|
80
|
-
if (group)
|
|
81
|
-
group.push(d);
|
|
82
|
-
else
|
|
83
|
-
byChat.set(cid, [d]);
|
|
64
|
+
const out = await effectsForDispatch(d, ctx).catch((err) => {
|
|
65
|
+
console.error('[parel-channel] catch-up dispatch failed', err);
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
effects.push(...out);
|
|
84
69
|
}
|
|
85
|
-
const runOne = (d) => effectsForDispatch(d, ctx).catch((err) => {
|
|
86
|
-
console.error('[parel-channel] catch-up dispatch failed', err);
|
|
87
|
-
return [];
|
|
88
|
-
});
|
|
89
|
-
const chains = [...byChat.values()].map(async (group) => {
|
|
90
|
-
const out = [];
|
|
91
|
-
for (const d of group)
|
|
92
|
-
out.push(...(await runOne(d)));
|
|
93
|
-
return out;
|
|
94
|
-
});
|
|
95
|
-
const pageEffects = await Promise.all([...chains, ...unkeyed.map(runOne)]);
|
|
96
|
-
effects.push(...pageEffects.flat());
|
|
97
70
|
if (!body?.next_cursor)
|
|
98
71
|
break;
|
|
99
72
|
cursor = body.next_cursor;
|
|
@@ -109,11 +82,6 @@ export async function catchUpParall(ctx) {
|
|
|
109
82
|
}
|
|
110
83
|
return effects;
|
|
111
84
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Synthetic sweep group key for channel_message dispatches (no chat_id).
|
|
114
|
-
* Chat ids are cht_-prefixed nanoids, so this cannot collide with one.
|
|
115
|
-
*/
|
|
116
|
-
const CHANNEL_SWEEP_CHAIN = 'sweep:channel_message';
|
|
117
85
|
async function effectsForDispatch(data, ctx) {
|
|
118
86
|
const sourceType = String(data.source_type ?? '');
|
|
119
87
|
const sourceId = data.source_id ? String(data.source_id) : undefined;
|
|
@@ -171,16 +139,18 @@ async function effectsForDispatch(data, ctx) {
|
|
|
171
139
|
// pending ring (a later successful turn's idle clears agent-level pendings).
|
|
172
140
|
const receivedSettled = markDispatchReceived(ctx, sourceType, sourceId);
|
|
173
141
|
const envelopeId = dispatchId || `${sourceType}:${sourceId}`;
|
|
174
|
-
let sessionId = await ensureSession(ctx
|
|
142
|
+
let sessionId = await ensureSession(ctx);
|
|
175
143
|
if (sessionId) {
|
|
176
144
|
// The guard marker goes down BEFORE any session write: a stale
|
|
177
145
|
// turn_completed for the PREVIOUS turn (outbox retry, late event) checks
|
|
178
146
|
// turnEnvelope to decide whether its idle is still safe — if the active
|
|
179
147
|
// patch below could land first, that event would still see the old
|
|
180
|
-
// envelope id and idle the session out from under this message.
|
|
148
|
+
// envelope id and idle the session out from under this message. The
|
|
149
|
+
// marker is agent-global: chats share the one main session, so a chat-A
|
|
150
|
+
// done signal must not idle it while this (later) message is pending.
|
|
181
151
|
// (Residual read-then-patch overlap is zero when the host runs a
|
|
182
152
|
// connection's hooks serially, per the C3 contract / C3b confirm item.)
|
|
183
|
-
await ctx.store?.set(
|
|
153
|
+
await ctx.store?.set(TURN_ENVELOPE_KEY, envelopeId).catch(() => { });
|
|
184
154
|
const report = (sid) => Promise.all([
|
|
185
155
|
patchSession(ctx, sid, { status: 'active', trigger_message_id: sourceId }),
|
|
186
156
|
createInputStep(ctx, sid, {
|
|
@@ -195,8 +165,8 @@ async function effectsForDispatch(data, ctx) {
|
|
|
195
165
|
// The cached session was closed under us (e.g. New Session). The
|
|
196
166
|
// get-or-create falls through terminal rows, so one rebuild + retry
|
|
197
167
|
// repairs reporting for this and every later dispatch.
|
|
198
|
-
await invalidateSession(ctx
|
|
199
|
-
const fresh = await ensureSession(ctx
|
|
168
|
+
await invalidateSession(ctx);
|
|
169
|
+
const fresh = await ensureSession(ctx);
|
|
200
170
|
if (fresh && fresh !== sessionId) {
|
|
201
171
|
await report(fresh);
|
|
202
172
|
sessionId = fresh;
|
|
@@ -212,6 +182,22 @@ async function effectsForDispatch(data, ctx) {
|
|
|
212
182
|
PRLL_TRIGGER_MESSAGE_ID: sourceId,
|
|
213
183
|
...(threadRootId ? { PRLL_THREAD_ROOT_ID: threadRootId } : {}),
|
|
214
184
|
};
|
|
185
|
+
// Frame the message with its chat of origin: all chats share the agent's
|
|
186
|
+
// one main session, so the model must see WHERE each message comes from or
|
|
187
|
+
// it answers chat B inside chat A's train of thought. The name is cosmetic
|
|
188
|
+
// garnish and must never block the emit/ack hot path (the catch-up sweep is
|
|
189
|
+
// fully serialized — one slow lookup would stall the whole backlog): only
|
|
190
|
+
// the cached name is used, and a miss kicks off a fire-and-forget prefetch
|
|
191
|
+
// so the NEXT message from this chat gets the name. The id alone already
|
|
192
|
+
// marks the context switch.
|
|
193
|
+
const chatName = await cachedChatName(ctx, chatId);
|
|
194
|
+
const framedText = buildChatPrompt({
|
|
195
|
+
chatId,
|
|
196
|
+
chatName,
|
|
197
|
+
senderId: msg?.sender_id ? String(msg.sender_id) : undefined,
|
|
198
|
+
threadRootId,
|
|
199
|
+
text,
|
|
200
|
+
});
|
|
215
201
|
const effects = [
|
|
216
202
|
{
|
|
217
203
|
type: 'emitEvent',
|
|
@@ -221,10 +207,12 @@ async function effectsForDispatch(data, ctx) {
|
|
|
221
207
|
connectionId: ctx.connectionId,
|
|
222
208
|
source: 'parall',
|
|
223
209
|
type: 'message',
|
|
224
|
-
//
|
|
225
|
-
// session
|
|
210
|
+
// The binding's `main` routing sends every envelope to the agent's
|
|
211
|
+
// one session; subject still carries the chat so parel's turn input
|
|
212
|
+
// grouping (F2) never mixes conversations in one turn and agent
|
|
213
|
+
// events mirror it back for per-chat correlation.
|
|
226
214
|
subject: chatId,
|
|
227
|
-
data: { text, chatId, messageId: sourceId, threadRootId },
|
|
215
|
+
data: { text: framedText, chatId, messageId: sourceId, threadRootId },
|
|
228
216
|
// chat id + thread anchor ride replyRoute.data so deliver() knows
|
|
229
217
|
// where to post back (same shape channel-slack-socket uses for its
|
|
230
218
|
// channel id).
|
|
@@ -308,9 +296,9 @@ async function effectsForChannelDispatch(sourceId, dispatchId, ctx) {
|
|
|
308
296
|
// returned; the turnEnvelope marker goes down before any session write.
|
|
309
297
|
const receivedSettled = markDispatchReceived(ctx, 'channel_message', sourceId);
|
|
310
298
|
const envelopeId = dispatchId || `channel_message:${sourceId}`;
|
|
311
|
-
let sessionId = await ensureSession(ctx
|
|
299
|
+
let sessionId = await ensureSession(ctx);
|
|
312
300
|
if (sessionId) {
|
|
313
|
-
await ctx.store?.set(
|
|
301
|
+
await ctx.store?.set(TURN_ENVELOPE_KEY, envelopeId).catch(() => { });
|
|
314
302
|
const report = (sid) => Promise.all([
|
|
315
303
|
patchSession(ctx, sid, { status: 'active', trigger_message_id: sourceId }),
|
|
316
304
|
createChannelInputStep(ctx, sid, {
|
|
@@ -326,8 +314,8 @@ async function effectsForChannelDispatch(sourceId, dispatchId, ctx) {
|
|
|
326
314
|
]);
|
|
327
315
|
const results = await report(sessionId);
|
|
328
316
|
if (results.includes('stale')) {
|
|
329
|
-
await invalidateSession(ctx
|
|
330
|
-
const fresh = await ensureSession(ctx
|
|
317
|
+
await invalidateSession(ctx);
|
|
318
|
+
const fresh = await ensureSession(ctx);
|
|
331
319
|
if (fresh && fresh !== sessionId) {
|
|
332
320
|
await report(fresh);
|
|
333
321
|
sessionId = fresh;
|
|
@@ -354,9 +342,9 @@ async function effectsForChannelDispatch(sourceId, dispatchId, ctx) {
|
|
|
354
342
|
// message — the external-audience framing rides inside it (parel has
|
|
355
343
|
// no separate prompt channel), so 'message' is the right type here.
|
|
356
344
|
type: 'message',
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
345
|
+
// main routing sends this to the agent's one session too; subject
|
|
346
|
+
// still carries the external conversation (chv_) for parel's turn
|
|
347
|
+
// input grouping (F2) and per-conversation event correlation.
|
|
360
348
|
subject: conversationId,
|
|
361
349
|
data: {
|
|
362
350
|
text: buildChannelPrompt({
|
|
@@ -399,6 +387,64 @@ async function effectsForChannelDispatch(sourceId, dispatchId, ctx) {
|
|
|
399
387
|
* degrades to neutral rather than inventing a clip alias.
|
|
400
388
|
*/
|
|
401
389
|
const PROVIDER_LOOKUP_TIMEOUT_MS = 3_000;
|
|
390
|
+
/**
|
|
391
|
+
* How long a cached chat name is served without revalidation. Chat renames
|
|
392
|
+
* are rare and the frame is the model's cross-chat context anchor, so serve
|
|
393
|
+
* stale-while-revalidate: past the TTL the cached name is still returned
|
|
394
|
+
* immediately (never block the hot path) and a background refresh updates
|
|
395
|
+
* the row for later messages.
|
|
396
|
+
*/
|
|
397
|
+
const CHAT_NAME_TTL_MS = 60 * 60 * 1000;
|
|
398
|
+
/**
|
|
399
|
+
* chat id → display name for prompt framing, NON-BLOCKING: returns only what
|
|
400
|
+
* the durable store already holds; a miss fires a background prefetch (tight
|
|
401
|
+
* timeout, any failure ignored) so later messages get the name. The first
|
|
402
|
+
* message from a chat is framed id-only — acceptable, the id is the
|
|
403
|
+
* correctness anchor and the name is garnish; a blocking lookup here would
|
|
404
|
+
* sit on the emit/ack hot path and stall the fully-serialized catch-up sweep
|
|
405
|
+
* one dispatch at a time. DMs have no name — the empty name is cached as a
|
|
406
|
+
* "resolved, nameless" sentinel so they don't re-prefetch on every message.
|
|
407
|
+
* Rows carry the fetch timestamp; past CHAT_NAME_TTL_MS the stale name is
|
|
408
|
+
* served AND revalidated in the background, so a renamed chat converges
|
|
409
|
+
* within one TTL window. (Pre-TTL rows were plain strings — served once and
|
|
410
|
+
* upgraded by the same background refresh.)
|
|
411
|
+
*/
|
|
412
|
+
async function cachedChatName(ctx, chatId) {
|
|
413
|
+
const cacheKey = `chatName:${chatId}`;
|
|
414
|
+
const cached = await ctx.store?.get(cacheKey).catch(() => null);
|
|
415
|
+
if (cached &&
|
|
416
|
+
typeof cached === 'object' &&
|
|
417
|
+
typeof cached.name === 'string') {
|
|
418
|
+
const { name, at } = cached;
|
|
419
|
+
if (typeof at !== 'number' || ctx.now() - at > CHAT_NAME_TTL_MS) {
|
|
420
|
+
void prefetchChatName(ctx, chatId, cacheKey).catch(() => { });
|
|
421
|
+
}
|
|
422
|
+
return name || undefined;
|
|
423
|
+
}
|
|
424
|
+
if (typeof cached === 'string') {
|
|
425
|
+
// Legacy plain-string row: serve it, upgrade to the timestamped shape.
|
|
426
|
+
void prefetchChatName(ctx, chatId, cacheKey).catch(() => { });
|
|
427
|
+
return cached || undefined;
|
|
428
|
+
}
|
|
429
|
+
void prefetchChatName(ctx, chatId, cacheKey).catch(() => { });
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
async function prefetchChatName(ctx, chatId, cacheKey) {
|
|
433
|
+
try {
|
|
434
|
+
const res = await fetch(`${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/chats/${chatId}`, {
|
|
435
|
+
headers: { Authorization: `Bearer ${requireAgk(ctx)}` },
|
|
436
|
+
signal: AbortSignal.timeout(PROVIDER_LOOKUP_TIMEOUT_MS),
|
|
437
|
+
});
|
|
438
|
+
if (!res.ok)
|
|
439
|
+
return;
|
|
440
|
+
const body = (await res.json().catch(() => null));
|
|
441
|
+
const name = typeof body?.name === 'string' ? body.name : '';
|
|
442
|
+
await ctx.store?.set(cacheKey, { name, at: ctx.now() }).catch(() => { });
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Cosmetic prefetch — the next message from this chat retries.
|
|
446
|
+
}
|
|
447
|
+
}
|
|
402
448
|
async function resolveChannelProvider(ctx, connectionId, get) {
|
|
403
449
|
const cacheKey = `channelProvider:${connectionId}`;
|
|
404
450
|
const cached = await ctx.store?.get(cacheKey).catch(() => null);
|
|
@@ -467,34 +513,48 @@ export async function handleParallAgentEvent(event, ctx) {
|
|
|
467
513
|
return [];
|
|
468
514
|
try {
|
|
469
515
|
if (event.type === 'turn_completed' || event.type === 'turn_failed') {
|
|
470
|
-
// Staleness guard: only idle if this event's turn
|
|
471
|
-
// latest emitted envelope
|
|
472
|
-
//
|
|
516
|
+
// Staleness guard: only idle if this event's turn covers the AGENT'S
|
|
517
|
+
// latest emitted envelope (the marker is agent-global — all chats share
|
|
518
|
+
// the one main session, so a done signal for chat A's turn must not
|
|
519
|
+
// clear the indicator while a later chat-B message is queued/running).
|
|
520
|
+
// A late/retried event for a superseded turn is dropped. Fail-open
|
|
473
521
|
// when the marker is unavailable (a stuck ring is worse than a blink).
|
|
474
|
-
const current = await ctx.store?.get(
|
|
522
|
+
const current = await ctx.store?.get(TURN_ENVELOPE_KEY).catch(() => null);
|
|
475
523
|
if (typeof current === 'string' && current && !event.envelopeIds.includes(current)) {
|
|
476
524
|
return [];
|
|
477
525
|
}
|
|
478
|
-
const sessionId = await ensureSession(ctx
|
|
526
|
+
const sessionId = await ensureSession(ctx);
|
|
479
527
|
if (sessionId) {
|
|
528
|
+
// Error step BEFORE the idle patch so the panel orders failure →
|
|
529
|
+
// done; the step is what makes a failed turn visible at all (the
|
|
530
|
+
// reply never arrives — see createErrorStep).
|
|
531
|
+
let stale = false;
|
|
532
|
+
if (event.type === 'turn_failed') {
|
|
533
|
+
const stepResult = await createErrorStep(ctx, sessionId, {
|
|
534
|
+
subject: chatId,
|
|
535
|
+
turnId: event.turnId,
|
|
536
|
+
error: event.error,
|
|
537
|
+
});
|
|
538
|
+
stale = stepResult === 'stale';
|
|
539
|
+
}
|
|
480
540
|
const result = await patchSession(ctx, sessionId, { status: 'idle' });
|
|
481
541
|
// A closed session is already terminal (frontend shows done) — just
|
|
482
542
|
// drop the cache so the next dispatch resolves a fresh one.
|
|
483
|
-
if (result === 'stale')
|
|
484
|
-
await invalidateSession(ctx
|
|
543
|
+
if (stale || result === 'stale')
|
|
544
|
+
await invalidateSession(ctx);
|
|
485
545
|
}
|
|
486
546
|
return [];
|
|
487
547
|
}
|
|
488
548
|
if (event.type === 'model_reasoning' ||
|
|
489
549
|
event.type === 'tool_call' ||
|
|
490
550
|
event.type === 'tool_result') {
|
|
491
|
-
const sessionId = await ensureSession(ctx
|
|
551
|
+
const sessionId = await ensureSession(ctx);
|
|
492
552
|
if (sessionId) {
|
|
493
553
|
const result = await createTraceStep(ctx, sessionId, chatId, event);
|
|
494
554
|
// Dropped step is fine (display feed); uncache so the NEXT event
|
|
495
555
|
// resolves a live session instead of failing forever.
|
|
496
556
|
if (result === 'stale')
|
|
497
|
-
await invalidateSession(ctx
|
|
557
|
+
await invalidateSession(ctx);
|
|
498
558
|
}
|
|
499
559
|
return [];
|
|
500
560
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -7,23 +7,32 @@ import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
|
7
7
|
* Every function catches its own failures and returns a degraded value: the
|
|
8
8
|
* message pipeline (emit / reply) must never block on indicator reporting.
|
|
9
9
|
*
|
|
10
|
-
* Session identity is deterministic — (runtime_type='parel',
|
|
11
|
-
* runtime_session_id=
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* connector ever knowing parel's internal session id.
|
|
10
|
+
* Session identity is deterministic and agent-global — (runtime_type='parel',
|
|
11
|
+
* runtime_session_id='main'). The binding's `main` routing keys EVERY
|
|
12
|
+
* envelope to one parel session per agent (the single-orchestrator model,
|
|
13
|
+
* aligned with the standard runtimes; parel-session-alignment-plan.md), so
|
|
14
|
+
* the parall-side AgentSession mirrors it 1:1: one ase_ per agent, without
|
|
15
|
+
* the connector ever knowing parel's internal session id. Envelopes still
|
|
16
|
+
* carry subject=chatId/chv_ — parel groups a turn's inputs by it (F2) and
|
|
17
|
+
* mirrors it on agent events, so steps and turn signals stay chat-addressed.
|
|
16
18
|
*/
|
|
17
19
|
export declare const REQUEST_TIMEOUT_MS = 10000;
|
|
20
|
+
/** The agent-global runtime session identity under `main` channel routing. */
|
|
21
|
+
export declare const MAIN_SESSION_KEY = "main";
|
|
18
22
|
/** Reporting needs the agent id in the endpoint path; old configs lack it. */
|
|
19
23
|
export declare function reportingEnabled(ctx: ConnectorContext): boolean;
|
|
20
24
|
/**
|
|
21
|
-
* The
|
|
25
|
+
* The agent's most recently emitted envelope id — the staleness guard for
|
|
22
26
|
* turn-level done signals: a turn_completed/failed whose envelopeIds don't
|
|
23
27
|
* include the current value belongs to a superseded turn and must not idle
|
|
24
|
-
* the session out from under the newer one.
|
|
28
|
+
* the session out from under the newer one. Agent-global (NOT per-subject):
|
|
29
|
+
* all chats share the one main session, so a completed turn for chat A must
|
|
30
|
+
* not idle the session while a later-emitted chat-B message is queued or
|
|
31
|
+
* running — only the turn covering the LAST emitted envelope may idle.
|
|
32
|
+
* (Pre-main-routing store keys were `turnEnvelope:{subject}`; those rows are
|
|
33
|
+
* simply never read again.)
|
|
25
34
|
*/
|
|
26
|
-
export declare const
|
|
35
|
+
export declare const TURN_ENVELOPE_KEY = "turnEnvelope:main";
|
|
27
36
|
/**
|
|
28
37
|
* 'stale' means the session id no longer accepts writes (closed/superseded —
|
|
29
38
|
* e.g. the New Session control ended it): callers invalidate the cache and
|
|
@@ -32,15 +41,16 @@ export declare const turnEnvelopeKey: (subject: string) => string;
|
|
|
32
41
|
*/
|
|
33
42
|
export type ReportResult = 'ok' | 'stale' | 'failed';
|
|
34
43
|
/** Drop the cached session id so the next ensureSession re-resolves it. */
|
|
35
|
-
export declare function invalidateSession(ctx: ConnectorContext
|
|
44
|
+
export declare function invalidateSession(ctx: ConnectorContext): Promise<void>;
|
|
36
45
|
/**
|
|
37
|
-
* Get-or-create the
|
|
46
|
+
* Get-or-create the agent's parall AgentSession (ase_), cached in the
|
|
38
47
|
* durable per-connection store. Returns null when reporting is disabled or
|
|
39
48
|
* the call fails — callers skip their report and move on. Callers on a
|
|
40
49
|
* latency-sensitive path (deliver blocks the user-visible reply on this) pass
|
|
41
50
|
* a shorter timeoutMs; the cache-hit fast path is a store read either way.
|
|
51
|
+
* (Pre-main-routing store keys were `session:{subject}`; never read again.)
|
|
42
52
|
*/
|
|
43
|
-
export declare function ensureSession(ctx: ConnectorContext,
|
|
53
|
+
export declare function ensureSession(ctx: ConnectorContext, opts?: {
|
|
44
54
|
timeoutMs?: number;
|
|
45
55
|
}): Promise<string | null>;
|
|
46
56
|
/** PATCH the session (status / trigger_message_id). Never throws. */
|
|
@@ -77,24 +87,62 @@ export declare function createChannelInputStep(ctx: ConnectorContext, sessionId:
|
|
|
77
87
|
text: string;
|
|
78
88
|
sentAt?: string;
|
|
79
89
|
}): Promise<ReportResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Credential redaction for runtime error strings that become user-visible
|
|
92
|
+
* step content. Two layers: every secret VALUE the connector actually holds
|
|
93
|
+
* (`ctx.secrets`) is replaced exactly — the strongest guarantee for the
|
|
94
|
+
* secrets in this connector's scope — then credential-shaped patterns are
|
|
95
|
+
* masked (parall key prefixes, common vendor prefixes, AWS access key ids,
|
|
96
|
+
* bearer tokens, and any long unbroken token as a fallback: credentials are
|
|
97
|
+
* long random strings, while prose and paths break on spaces and slashes
|
|
98
|
+
* first). Pattern matching can't catch every conceivable secret (e.g. a
|
|
99
|
+
* short password echoed by a foreign exception), which is the accepted
|
|
100
|
+
* trade-off for keeping the error text diagnosable — the audience is the
|
|
101
|
+
* org-internal session panel.
|
|
102
|
+
*/
|
|
103
|
+
export declare function redactSecrets(s: string, knownValues?: string[]): string;
|
|
104
|
+
/**
|
|
105
|
+
* turn_failed → an error step in the session panel, following the
|
|
106
|
+
* cross-runtime error-step contract (text step + content.status:'error',
|
|
107
|
+
* same shape agent-core's dispatch-failure step uses — pinned by
|
|
108
|
+
* ts/protocol-vectors/agent-steps.json). Without this the turn's failure is
|
|
109
|
+
* invisible on the parall side: the 2026-07-05 parel regression was fully
|
|
110
|
+
* silent because this connector dropped event.error.
|
|
111
|
+
*
|
|
112
|
+
* The error string comes from parel's runtime (exception messages) — length
|
|
113
|
+
* is unbounded and it can echo credential-shaped values, so it is redacted
|
|
114
|
+
* (see redactSecrets) and truncated before leaving the connector.
|
|
115
|
+
*/
|
|
116
|
+
export declare function createErrorStep(ctx: ConnectorContext, sessionId: string, args: {
|
|
117
|
+
subject: string;
|
|
118
|
+
turnId: string;
|
|
119
|
+
error: string;
|
|
120
|
+
}): Promise<ReportResult>;
|
|
80
121
|
/**
|
|
81
122
|
* E2 step-trace events → parall AgentSteps, shaped exactly like agent-core's
|
|
82
123
|
* createRuntimeStep so the session panel renders them identically to CC's.
|
|
83
124
|
* tool_call/tool_result carry a session-scoped idempotency key (events are
|
|
84
|
-
* at-most-once today, but
|
|
85
|
-
*
|
|
86
|
-
*
|
|
125
|
+
* at-most-once today, but the key is free insurance). Provider call ids are
|
|
126
|
+
* only unique WITHIN a turn, and the shared main session makes step
|
|
127
|
+
* idempotency agent-wide — so the key is turnId-scoped (`tc:{turnId}:{callId}`),
|
|
128
|
+
* or two turns that both emit `call_1` would swallow the later step as a
|
|
129
|
+
* duplicate. Reasoning has no natural key and parel already aggregates it
|
|
130
|
+
* host-side. The E2 tool_result event has no tool name, so tool_call parks
|
|
131
|
+
* it in the store by callId.
|
|
87
132
|
*/
|
|
88
133
|
export declare function createTraceStep(ctx: ConnectorContext, sessionId: string, subject: string, event: {
|
|
89
134
|
type: 'model_reasoning';
|
|
135
|
+
turnId?: string;
|
|
90
136
|
text: string;
|
|
91
137
|
} | {
|
|
92
138
|
type: 'tool_call';
|
|
139
|
+
turnId?: string;
|
|
93
140
|
callId: string;
|
|
94
141
|
name: string;
|
|
95
142
|
input: unknown;
|
|
96
143
|
} | {
|
|
97
144
|
type: 'tool_result';
|
|
145
|
+
turnId?: string;
|
|
98
146
|
callId: string;
|
|
99
147
|
status: 'ok' | 'error';
|
|
100
148
|
durationMs?: number;
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|