@parall/parel-channel 1.40.0 → 1.42.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 +13 -4
- package/dist/channel-prompt.d.ts.map +1 -1
- package/dist/channel-prompt.js +21 -3
- package/dist/connect.d.ts +7 -0
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +9 -0
- package/dist/delivery.d.ts +23 -16
- package/dist/delivery.d.ts.map +1 -1
- package/dist/delivery.js +89 -92
- package/dist/fork.d.ts +148 -0
- package/dist/fork.d.ts.map +1 -0
- package/dist/fork.js +323 -0
- package/dist/inbound-channel.d.ts +29 -0
- package/dist/inbound-channel.d.ts.map +1 -0
- package/dist/inbound-channel.js +219 -0
- package/dist/inbound.d.ts +14 -0
- package/dist/inbound.d.ts.map +1 -1
- package/dist/inbound.js +639 -272
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/parel-sdk-compat.d.ts +63 -0
- package/dist/parel-sdk-compat.d.ts.map +1 -0
- package/dist/parel-sdk-compat.js +16 -0
- package/dist/session.d.ts +155 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +391 -13
- package/dist/typed-dispatch.d.ts +82 -0
- package/dist/typed-dispatch.d.ts.map +1 -0
- package/dist/typed-dispatch.js +347 -0
- package/package.json +1 -1
- package/src/channel-prompt.ts +32 -4
- package/src/connect.ts +10 -0
- package/src/delivery.ts +91 -92
- package/src/fork.ts +435 -0
- package/src/inbound-channel.ts +264 -0
- package/src/inbound.ts +725 -308
- package/src/index.ts +7 -1
- package/src/parel-sdk-compat.ts +77 -0
- package/src/session.ts +448 -13
- package/src/typed-dispatch.ts +529 -0
package/dist/channel-prompt.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* chat shares the agent's ONE parel session, so consecutive turns can come
|
|
6
6
|
* from different conversations — each message is framed with its originating
|
|
7
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* with chat A's thread of thought. No per-message reply hint: the system
|
|
9
|
+
* prompt teaches the CLI-only reply contract once (SSOT — plain text is
|
|
10
|
+
* never delivered; reply via `parall messages send`), and per-turn
|
|
11
|
+
* invocation context presets PRLL_CHAT_ID so the bare send hits this chat.
|
|
11
12
|
*
|
|
12
13
|
* External IM channel messages (buildChannelPrompt, channel_message
|
|
13
14
|
* dispatches). Mirrors agent-core's event-format.ts channel branch, adapted
|
|
@@ -25,6 +26,12 @@
|
|
|
25
26
|
* form ({"message_id": …}) so threaded conversations aren't nudged toward
|
|
26
27
|
* new top-level messages.
|
|
27
28
|
*/
|
|
29
|
+
export interface PromptAttachment {
|
|
30
|
+
id: string;
|
|
31
|
+
fileName: string;
|
|
32
|
+
fileSize: number;
|
|
33
|
+
mimeType: string;
|
|
34
|
+
}
|
|
28
35
|
export interface ChatPromptArgs {
|
|
29
36
|
chatId: string;
|
|
30
37
|
/** Display name of the chat; undefined when unresolved (best-effort). */
|
|
@@ -32,8 +39,10 @@ export interface ChatPromptArgs {
|
|
|
32
39
|
/** Sender user id (usr_); display-name enrichment is a follow-up. */
|
|
33
40
|
senderId?: string;
|
|
34
41
|
threadRootId?: string;
|
|
35
|
-
/** The inbound message text (raw, un-framed). */
|
|
42
|
+
/** The inbound message text (raw, un-framed); may be empty (attachment-only). */
|
|
36
43
|
text: string;
|
|
44
|
+
/** Message attachments — rendered as reference lines (agent fetches via CLI). */
|
|
45
|
+
attachments?: PromptAttachment[];
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Frame a Parall chat message with its conversation of origin. Kept to a
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;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,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
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* chat shares the agent's ONE parel session, so consecutive turns can come
|
|
6
6
|
* from different conversations — each message is framed with its originating
|
|
7
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* with chat A's thread of thought. No per-message reply hint: the system
|
|
9
|
+
* prompt teaches the CLI-only reply contract once (SSOT — plain text is
|
|
10
|
+
* never delivered; reply via `parall messages send`), and per-turn
|
|
11
|
+
* invocation context presets PRLL_CHAT_ID so the bare send hits this chat.
|
|
11
12
|
*
|
|
12
13
|
* External IM channel messages (buildChannelPrompt, channel_message
|
|
13
14
|
* dispatches). Mirrors agent-core's event-format.ts channel branch, adapted
|
|
@@ -31,6 +32,22 @@ function sanitizeMeta(value) {
|
|
|
31
32
|
.replace(/[[\]|]/g, ' ')
|
|
32
33
|
.trim();
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* One reference line per attachment, byte-identical to agent-core's
|
|
37
|
+
* event-format rendering: the agent learns the attachment EXISTS and pulls
|
|
38
|
+
* the bytes itself through its sandbox CLI (`parall files download prll://…`)
|
|
39
|
+
* — same discover-then-fetch contract the standard runtimes use.
|
|
40
|
+
*/
|
|
41
|
+
function attachmentLines(attachments) {
|
|
42
|
+
if (!attachments?.length)
|
|
43
|
+
return [];
|
|
44
|
+
return attachments.map((att) => {
|
|
45
|
+
const sizeStr = att.fileSize >= 1048576
|
|
46
|
+
? `${(att.fileSize / 1048576).toFixed(1)}MB`
|
|
47
|
+
: `${Math.round(att.fileSize / 1024)}KB`;
|
|
48
|
+
return `[Attachment: prll://${att.id} | ${sanitizeMeta(att.mimeType)} | ${sizeStr} | ${sanitizeMeta(att.fileName)}]`;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
34
51
|
/**
|
|
35
52
|
* Frame a Parall chat message with its conversation of origin. Kept to a
|
|
36
53
|
* minimal metadata header: the text itself is the user message, and heavier
|
|
@@ -46,6 +63,7 @@ export function buildChatPrompt(args) {
|
|
|
46
63
|
lines.push(`[From: ${sanitizeMeta(args.senderId)}]`);
|
|
47
64
|
if (args.threadRootId)
|
|
48
65
|
lines.push(`[Thread: ${sanitizeMeta(args.threadRootId)}]`);
|
|
66
|
+
lines.push(...attachmentLines(args.attachments));
|
|
49
67
|
lines.push('', args.text);
|
|
50
68
|
return lines.join('\n');
|
|
51
69
|
}
|
package/dist/connect.d.ts
CHANGED
|
@@ -18,5 +18,12 @@ export declare function parallOrgId(ctx: ConnectorContext): string;
|
|
|
18
18
|
* shipped — reporting degrades to off until the agent is redeployed.
|
|
19
19
|
*/
|
|
20
20
|
export declare function parallAgentId(ctx: ConnectorContext): string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether this deployment provisions a sandbox (shell + `parall` CLI) for the
|
|
23
|
+
* agent. Server writes `parallSandbox: "off"` into the connection config only
|
|
24
|
+
* for conversational-only deployments (no platform E2B key) — an absent field
|
|
25
|
+
* means sandbox-enabled (every config generated before the field existed was).
|
|
26
|
+
*/
|
|
27
|
+
export declare function sandboxEnabled(ctx: ConnectorContext): boolean;
|
|
21
28
|
export declare function requireAgk(ctx: ConnectorContext): string;
|
|
22
29
|
//# sourceMappingURL=connect.d.ts.map
|
package/dist/connect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE1E;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAyBlF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE1D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE3D;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAMxD"}
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE1E;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAyBlF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE1D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAE7D;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM,CAMxD"}
|
package/dist/connect.js
CHANGED
|
@@ -48,6 +48,15 @@ export function parallOrgId(ctx) {
|
|
|
48
48
|
export function parallAgentId(ctx) {
|
|
49
49
|
return String(ctx.config.parallAgentId ?? '');
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether this deployment provisions a sandbox (shell + `parall` CLI) for the
|
|
53
|
+
* agent. Server writes `parallSandbox: "off"` into the connection config only
|
|
54
|
+
* for conversational-only deployments (no platform E2B key) — an absent field
|
|
55
|
+
* means sandbox-enabled (every config generated before the field existed was).
|
|
56
|
+
*/
|
|
57
|
+
export function sandboxEnabled(ctx) {
|
|
58
|
+
return String(ctx.config.parallSandbox ?? '') !== 'off';
|
|
59
|
+
}
|
|
51
60
|
export function requireAgk(ctx) {
|
|
52
61
|
const agk = ctx.secrets.parallApiKey;
|
|
53
62
|
if (!agk) {
|
package/dist/delivery.d.ts
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import type { ChannelDelivery, ConnectorContext, ConnectorEffect } from '@parel/plugin-sdk';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Turn-end delivery — which, under the platform reply contract, delivers
|
|
4
|
+
* NOTHING to the chat.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The runtime-standard output contract (agent-dm-loop-prevention.md Layer 0,
|
|
7
|
+
* parel-reply-contract-alignment.md): plain turn output is never projected as
|
|
8
|
+
* a chat message — the agent replies explicitly via `parall messages send`
|
|
9
|
+
* from its sandbox, and that send carries the dispatch_source ledger binding.
|
|
10
|
+
* This hook records the turn-end text as a suppressed audit step on the
|
|
11
|
+
* session (the same posture the claude-agent / openclaw bridges enforce),
|
|
12
|
+
* runs a fence-checked ledger close, and a redundant session idle; nothing
|
|
13
|
+
* else.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* by delivery id + message idempotency_key). The turn's PRIMARY done signal
|
|
14
|
-
* lives in onAgentEvent (turn_completed); the idle here is a deliberate
|
|
15
|
-
* redundant close for bindings without `observe: [turn]` — see the inline
|
|
16
|
-
* comment at the bottom.
|
|
15
|
+
* parel still auto-invokes deliver() at turn end for provider_http reply
|
|
16
|
+
* routes (C4) — ignoring the payload here is the connector's half of the
|
|
17
|
+
* contract; no parel-side change is needed.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* A fork-owned turn (replyRoute carries childRef) records its audit step on
|
|
20
|
+
* the CHILD's ase_ and idles IT — its session panel owns the turn, not
|
|
21
|
+
* main's. A stale childRef (fork purged by New Session / given up on) drops
|
|
22
|
+
* the text entirely: blind-recreating a fork ase_ under the new conversation
|
|
23
|
+
* line would resurrect pre-reset state.
|
|
24
|
+
*
|
|
25
|
+
* External IM turns (channel_message dispatches) route here too with a
|
|
26
|
+
* conversationId and no chatId: their reply goes out through the provider
|
|
27
|
+
* clip, agent-invoked, and their turn-end text was already deliberately
|
|
28
|
+
* undelivered — only the ledger close + redundant idle run for them.
|
|
22
29
|
*/
|
|
23
30
|
export declare function buildParallDelivery(delivery: ChannelDelivery, ctx: ConnectorContext): Promise<ConnectorEffect[]>;
|
|
24
31
|
//# sourceMappingURL=delivery.d.ts.map
|
package/dist/delivery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAa5F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,eAAe,EACzB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAgF5B"}
|
package/dist/delivery.js
CHANGED
|
@@ -1,107 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ensureSession, invalidateSession, patchSession,
|
|
1
|
+
import { childRefKnown } from './fork.js';
|
|
2
|
+
import { completeFencedEnvelopes, createSuppressedTextStep, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, patchSession, TURN_ENVELOPE_KEY, } from './session.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Turn-end delivery — which, under the platform reply contract, delivers
|
|
5
|
+
* NOTHING to the chat.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The runtime-standard output contract (agent-dm-loop-prevention.md Layer 0,
|
|
8
|
+
* parel-reply-contract-alignment.md): plain turn output is never projected as
|
|
9
|
+
* a chat message — the agent replies explicitly via `parall messages send`
|
|
10
|
+
* from its sandbox, and that send carries the dispatch_source ledger binding.
|
|
11
|
+
* This hook records the turn-end text as a suppressed audit step on the
|
|
12
|
+
* session (the same posture the claude-agent / openclaw bridges enforce),
|
|
13
|
+
* runs a fence-checked ledger close, and a redundant session idle; nothing
|
|
14
|
+
* else.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* by delivery id + message idempotency_key). The turn's PRIMARY done signal
|
|
15
|
-
* lives in onAgentEvent (turn_completed); the idle here is a deliberate
|
|
16
|
-
* redundant close for bindings without `observe: [turn]` — see the inline
|
|
17
|
-
* comment at the bottom.
|
|
16
|
+
* parel still auto-invokes deliver() at turn end for provider_http reply
|
|
17
|
+
* routes (C4) — ignoring the payload here is the connector's half of the
|
|
18
|
+
* contract; no parel-side change is needed.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* A fork-owned turn (replyRoute carries childRef) records its audit step on
|
|
21
|
+
* the CHILD's ase_ and idles IT — its session panel owns the turn, not
|
|
22
|
+
* main's. A stale childRef (fork purged by New Session / given up on) drops
|
|
23
|
+
* the text entirely: blind-recreating a fork ase_ under the new conversation
|
|
24
|
+
* line would resurrect pre-reset state.
|
|
25
|
+
*
|
|
26
|
+
* External IM turns (channel_message dispatches) route here too with a
|
|
27
|
+
* conversationId and no chatId: their reply goes out through the provider
|
|
28
|
+
* clip, agent-invoked, and their turn-end text was already deliberately
|
|
29
|
+
* undelivered — only the ledger close + redundant idle run for them.
|
|
23
30
|
*/
|
|
24
31
|
export async function buildParallDelivery(delivery, ctx) {
|
|
25
|
-
const apiUrl = parallApiUrl(ctx);
|
|
26
|
-
const agk = requireAgk(ctx);
|
|
27
|
-
const orgId = parallOrgId(ctx);
|
|
28
32
|
const route = (delivery.replyRoute?.data ?? {});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// not the reply). Only run the redundant idle so a binding without
|
|
35
|
-
// `observe: [turn]` still closes the session.
|
|
36
|
-
if (route.conversationId) {
|
|
37
|
-
const sessionId = await ensureSession(ctx, { timeoutMs: 3_000 });
|
|
38
|
-
if (sessionId) {
|
|
39
|
-
await idleUnlessSuperseded(ctx, route.envelopeId, sessionId);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
33
|
+
// A fork-owned envelope whose bookkeeping is gone is a late/stale delivery
|
|
34
|
+
// (post-reset or post-give-up) — drop it entirely, audit step included.
|
|
35
|
+
const childLive = route.childRef ? await childRefKnown(ctx, route.childRef) : false;
|
|
36
|
+
if (route.childRef && !childLive) {
|
|
37
|
+
console.warn('[parel-channel] dropping turn-end text from stale fork', route.childRef);
|
|
42
38
|
return [];
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
message_type: 'text',
|
|
68
|
-
content: { text },
|
|
69
|
-
idempotency_key: `parel-reply:${delivery.id}`,
|
|
70
|
-
// A reply to a thread message stays in that thread.
|
|
71
|
-
...(route.threadRootId ? { thread_root_id: route.threadRootId } : {}),
|
|
72
|
-
...(sessionId ? { agent_session_id: sessionId } : {}),
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
try {
|
|
76
|
-
const res = await fetch(request.url, {
|
|
77
|
-
method: request.method,
|
|
78
|
-
headers: request.headers,
|
|
79
|
-
body: request.body,
|
|
80
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
// 4xx (bar 429) is permanent — retrying an unprocessable reply burns
|
|
84
|
-
// requests forever. Drop it (logged); turn_completed still closes the
|
|
85
|
-
// turn's indicator.
|
|
86
|
-
const permanent = res.status >= 400 && res.status < 500 && res.status !== 429;
|
|
87
|
-
if (!permanent)
|
|
88
|
-
throw new Error(`reply post failed (${res.status})`);
|
|
89
|
-
console.error(`[parel-channel] reply rejected (${res.status}) — dropping`);
|
|
40
|
+
if (route.chatId) {
|
|
41
|
+
const payload = delivery.payload;
|
|
42
|
+
const text = typeof payload === 'string' ? payload : (payload?.text ?? '');
|
|
43
|
+
if (text) {
|
|
44
|
+
// Audit-only: the session panel shows what the model produced, the
|
|
45
|
+
// chat never does. Short timeout — this hook sits at turn end, nothing
|
|
46
|
+
// user-visible waits on it. Fork turns land the step on the child ase_.
|
|
47
|
+
const sessionId = route.childRef
|
|
48
|
+
? await ensureChildSession(ctx, route.childRef, { timeoutMs: 3_000 })
|
|
49
|
+
: await ensureSession(ctx, { timeoutMs: 3_000 });
|
|
50
|
+
if (sessionId) {
|
|
51
|
+
const result = await createSuppressedTextStep(ctx, sessionId, {
|
|
52
|
+
subject: route.chatId,
|
|
53
|
+
deliveryId: delivery.id,
|
|
54
|
+
text,
|
|
55
|
+
});
|
|
56
|
+
if (result === 'stale') {
|
|
57
|
+
if (route.childRef)
|
|
58
|
+
await invalidateChildSession(ctx, route.childRef);
|
|
59
|
+
else
|
|
60
|
+
await invalidateSession(ctx);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
90
63
|
}
|
|
91
64
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
65
|
+
// Redundant turn-end ledger close, fence-checked — the deliver-side twin of
|
|
66
|
+
// onAgentEvent's complete. Exists for bindings WITHOUT `observe: [turn]`
|
|
67
|
+
// (the npm ^1 fallback path): there turn events never arrive and, with
|
|
68
|
+
// ack-on-emit gone, this hook is the only terminal ledger path — without it
|
|
69
|
+
// a delivered turn's row would stay live and re-emit on every TTL sweep.
|
|
70
|
+
// Under the normal artifact path both hooks run; the fence makes the
|
|
71
|
+
// overlap free (whichever completes first consumes it, the other skips).
|
|
72
|
+
// Fork-handled envelopes never wrote a fence, so this is a no-op for them
|
|
73
|
+
// (their ack machinery owns the ledger there).
|
|
74
|
+
// Known bound of this fallback: the replyRoute snapshot carries ONE
|
|
75
|
+
// envelope id, so a folded turn's OTHER sources heal only via the TTL
|
|
76
|
+
// re-emit here. Reaching that state needs a binding with injectInFlight
|
|
77
|
+
// ON and observe OFF — the platform template always sets both together,
|
|
78
|
+
// so it exists only on a hand-crafted binding.
|
|
79
|
+
if (route.envelopeId) {
|
|
80
|
+
await completeFencedEnvelopes(ctx, [route.envelopeId]);
|
|
95
81
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
82
|
+
const subject = route.chatId ?? route.conversationId;
|
|
83
|
+
if (subject) {
|
|
84
|
+
// Redundant done signal, kept deliberately: turn_completed (onAgentEvent)
|
|
85
|
+
// is the primary close for every turn — including no-reply turns — but a
|
|
86
|
+
// binding deployed without `observe: [turn]` would otherwise never idle.
|
|
87
|
+
// Child turns skip the marker guard entirely — the marker is main's; a
|
|
88
|
+
// child's own turn ending always idles the child ase_.
|
|
89
|
+
const sessionId = route.childRef
|
|
90
|
+
? await ensureChildSession(ctx, route.childRef, { timeoutMs: 3_000 })
|
|
91
|
+
: await ensureSession(ctx, { timeoutMs: 3_000 });
|
|
92
|
+
if (sessionId) {
|
|
93
|
+
if (route.childRef) {
|
|
94
|
+
const result = await patchSession(ctx, sessionId, { status: 'idle' });
|
|
95
|
+
if (result === 'stale')
|
|
96
|
+
await invalidateChildSession(ctx, route.childRef);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
await idleUnlessSuperseded(ctx, route.envelopeId, sessionId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
105
102
|
}
|
|
106
103
|
return [];
|
|
107
104
|
}
|
package/dist/fork.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
2
|
+
import type { AgentEventWithChild, ChildSpawnFailedEvent } from './parel-sdk-compat.js';
|
|
3
|
+
/** Delay before a failed spawn's messages are re-driven through onTimer. */
|
|
4
|
+
export declare const FORK_RETRY_DELAY_MS = 15000;
|
|
5
|
+
export declare const FORK_RETRY_PREFIX = "forkRetry:";
|
|
6
|
+
export interface PendingAck {
|
|
7
|
+
dispatchId: string;
|
|
8
|
+
sourceId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ChildRow {
|
|
11
|
+
childRef: string;
|
|
12
|
+
at: number;
|
|
13
|
+
/**
|
|
14
|
+
* The opening message's replay identity — set at spawn and NEVER cleared
|
|
15
|
+
* while the row lives. pendingAcks can be dropped after MAX_ACK_ATTEMPTS,
|
|
16
|
+
* but a replay of the opening source must still match branch 1a (its input
|
|
17
|
+
* was a spawn payload, not an envelope — a deliverTo replay would bypass
|
|
18
|
+
* ingress dedupe and inject the user's first message twice).
|
|
19
|
+
*/
|
|
20
|
+
openingSourceId?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Un-acked WorkItems this child holds: the spawn opening dispatch plus any
|
|
23
|
+
* follow-ups routed to the child BEFORE its provisioning was confirmed (a
|
|
24
|
+
* deliverTo into a spawn that then fails terminally is ignored by the host
|
|
25
|
+
* — acking those up front would lose them). Acked (by WorkItem id) on the
|
|
26
|
+
* child's observed events; sourceIds double as replay identities (a swept
|
|
27
|
+
* dispatch whose sourceId appears here is one of these very messages).
|
|
28
|
+
* pendingAcks[0] is always the opening message.
|
|
29
|
+
*/
|
|
30
|
+
pendingAcks?: PendingAck[];
|
|
31
|
+
/** Set on the child's first observed event — provisioning demonstrably done. */
|
|
32
|
+
confirmed?: boolean;
|
|
33
|
+
/** Ack re-send counter: acks are re-issued on later events until this hits
|
|
34
|
+
* MAX_ACK_ATTEMPTS (fetch effects are fire-and-forget; one lost ack must
|
|
35
|
+
* not orphan the WorkItem forever). */
|
|
36
|
+
ackAttempts?: number;
|
|
37
|
+
}
|
|
38
|
+
export type ForkDecision = {
|
|
39
|
+
mode: 'main';
|
|
40
|
+
}
|
|
41
|
+
/** trackAck: the child is not yet confirmed — the caller must park the
|
|
42
|
+
* WorkItem ack on the bookkeeping row instead of acking on emit. */
|
|
43
|
+
| {
|
|
44
|
+
mode: 'deliverTo';
|
|
45
|
+
childRef: string;
|
|
46
|
+
trackAck: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
mode: 'spawn';
|
|
49
|
+
childRef: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Decide how to route one inbound chat message. Called on the message hot
|
|
53
|
+
* path — store reads only, never a fetch.
|
|
54
|
+
*
|
|
55
|
+
* Concurrency: the decision-then-record sequence is NOT internally locked —
|
|
56
|
+
* it relies on the parel host executing one connection's hooks serially (the
|
|
57
|
+
* C3 contract; the same guarantee the turnEnvelope marker's read-then-patch
|
|
58
|
+
* ordering already leans on). Two same-chat dispatches therefore cannot both
|
|
59
|
+
* observe "no child" — the second runs after the first's recordSpawn.
|
|
60
|
+
*/
|
|
61
|
+
export declare function planForkDecision(ctx: ConnectorContext, subject: string, sourceId: string): Promise<ForkDecision>;
|
|
62
|
+
/** Record a just-issued spawn so follow-ups route to the child. Idempotent
|
|
63
|
+
* for the 1a replay path: an existing row for the same ref keeps its parked
|
|
64
|
+
* follow-up acks and confirmation state, only refreshing liveness. */
|
|
65
|
+
export declare function recordSpawn(ctx: ConnectorContext, subject: string, childRef: string, opening: PendingAck): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Park a follow-up's WorkItem ack on an unconfirmed child. Returns false when
|
|
68
|
+
* the row is gone or the cap is hit — the caller then acks on emit (the cap
|
|
69
|
+
* bounds the loss window of a pathological never-confirming child; beyond it
|
|
70
|
+
* we prefer at-most-once for the tail over unbounded growth).
|
|
71
|
+
*/
|
|
72
|
+
export declare function trackFollowUpAck(ctx: ConnectorContext, subject: string, childRef: string, ack: PendingAck): Promise<boolean>;
|
|
73
|
+
/** Whether a childRef is still part of live fork bookkeeping. Late child
|
|
74
|
+
* callbacks after a New Session purge (or after give-up) must NOT recreate
|
|
75
|
+
* fork ase_ rows via the blind get-or-create — a pre-reset child's delayed
|
|
76
|
+
* trace/reply would attach a fresh session under the NEW conversation line. */
|
|
77
|
+
export declare function childRefKnown(ctx: ConnectorContext, childRef: string): Promise<boolean>;
|
|
78
|
+
/** Whether a subject's live child has ever produced an observed event. */
|
|
79
|
+
export declare function childConfirmed(ctx: ConnectorContext, subject: string, childRef: string): Promise<boolean>;
|
|
80
|
+
/** Drop one subject's fork routing (give-up path: message re-plans fresh). */
|
|
81
|
+
export declare function clearForkSubject(ctx: ConnectorContext, subject: string, childRef: string): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Bump-and-park a fork retry ledger row (also the spawn-verification
|
|
84
|
+
* coordinate onTimer re-plans). Returns the attempt count INCLUDING this one.
|
|
85
|
+
*/
|
|
86
|
+
export declare function bumpForkRetryAttempts(ctx: ConnectorContext, retryKey: string, data: Record<string, unknown>): Promise<number>;
|
|
87
|
+
/** Give up re-verifying a spawn after this many timer rounds. */
|
|
88
|
+
export declare const MAX_SPAWN_VERIFY_ATTEMPTS = 3;
|
|
89
|
+
/**
|
|
90
|
+
* Purge ALL fork routing state — called on `agent.new_session`. The binding
|
|
91
|
+
* reset rotates main's conversation line; stale child routing surviving it
|
|
92
|
+
* would deliver post-reset messages into pre-reset forks (old context), and
|
|
93
|
+
* a stale busy marker would fork off a session that no longer exists.
|
|
94
|
+
* Returns the childRefs that were live so the caller can also invalidate
|
|
95
|
+
* their cached ase_ rows.
|
|
96
|
+
*/
|
|
97
|
+
export declare function clearForkState(ctx: ConnectorContext): Promise<string[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Track main-session busyness from the E1 turn stream. Child events (they
|
|
100
|
+
* carry childRef) must NOT feed this — only the main session's turns do.
|
|
101
|
+
*/
|
|
102
|
+
export declare function noteMainTurnEvent(ctx: ConnectorContext, event: AgentEventWithChild): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a child event's bookkeeping row (by its subject mirror) and fire
|
|
105
|
+
* the pending spawn ack exactly once. Returns the ack request when due.
|
|
106
|
+
*/
|
|
107
|
+
export declare function settleChildEvent(ctx: ConnectorContext, event: AgentEventWithChild): Promise<{
|
|
108
|
+
subject: string;
|
|
109
|
+
pendingAcks: PendingAck[];
|
|
110
|
+
} | null>;
|
|
111
|
+
/**
|
|
112
|
+
* A spawn failed before the child ran. NEVER acks the opening dispatch — it
|
|
113
|
+
* stays received and the catch-up sweep re-plans it. Failure codes split
|
|
114
|
+
* three ways (parel §18.2 three-state mapping semantics):
|
|
115
|
+
* - `concurrency_limit` is TRANSIENT: the platform keeps the mapping in
|
|
116
|
+
* `provisioning` and the same childRef can retry — keep the bookkeeping so
|
|
117
|
+
* the sweep's replay re-issues the same spawn key (branch 1a) instead of
|
|
118
|
+
* abandoning one provisioning row per attempt.
|
|
119
|
+
* - `disabled` / `unsupported_routing` are configuration-shaped: clear the
|
|
120
|
+
* bookkeeping AND park a no-fork flag so the connector stops re-trying a
|
|
121
|
+
* fork the binding will never allow.
|
|
122
|
+
* - Everything else is terminal for this childRef (the ref is burned): clear
|
|
123
|
+
* the bookkeeping so the sweep's re-plan mints a fresh ref.
|
|
124
|
+
* Returns true when the bookkeeping was cleared (the caller then settles the
|
|
125
|
+
* orphaned child ase_).
|
|
126
|
+
*/
|
|
127
|
+
export declare function handleChildSpawnFailed(ctx: ConnectorContext, event: ChildSpawnFailedEvent): Promise<{
|
|
128
|
+
cleared: boolean;
|
|
129
|
+
subject: string;
|
|
130
|
+
pendingAcks: PendingAck[];
|
|
131
|
+
}>;
|
|
132
|
+
/**
|
|
133
|
+
* Scope guard prepended to a fork child's opening input — the behavioral
|
|
134
|
+
* fallback agent-core applies to every forked dispatch (buildForkScopePrefix
|
|
135
|
+
* equivalent; the platform already excludes in-flight output from the seed,
|
|
136
|
+
* this line keeps the child from "finishing" main's visible work).
|
|
137
|
+
*
|
|
138
|
+
* Replies are CLI-only on EVERY fork turn (the platform reply contract —
|
|
139
|
+
* nothing auto-delivers, parel-reply-contract-alignment.md). The opening
|
|
140
|
+
* turn additionally has no envelope (it starts from the spawn input), so it
|
|
141
|
+
* gets no per-turn invocation context either — the prefix therefore spells
|
|
142
|
+
* out the full CLI reply command with the explicit chat target; follow-ups
|
|
143
|
+
* arrive as deliverTo envelopes whose invocation context presets
|
|
144
|
+
* PRLL_CHAT_ID, so the system prompt's bare `parall messages send` teaching
|
|
145
|
+
* covers them.
|
|
146
|
+
*/
|
|
147
|
+
export declare function buildForkScopePrefix(subject: string, threadRootId?: string): string;
|
|
148
|
+
//# sourceMappingURL=fork.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"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"}
|