@parall/parel-channel 1.40.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel-prompt.d.ts +9 -1
- package/dist/channel-prompt.d.ts.map +1 -1
- package/dist/channel-prompt.js +17 -0
- 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.map +1 -1
- package/dist/delivery.js +38 -10
- package/dist/fork.d.ts +145 -0
- package/dist/fork.d.ts.map +1 -0
- package/dist/fork.js +320 -0
- package/dist/inbound-channel.d.ts +29 -0
- package/dist/inbound-channel.d.ts.map +1 -0
- package/dist/inbound-channel.js +213 -0
- package/dist/inbound.d.ts +14 -0
- package/dist/inbound.d.ts.map +1 -1
- package/dist/inbound.js +571 -271
- 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 +88 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +225 -11
- package/dist/typed-dispatch.d.ts +76 -0
- package/dist/typed-dispatch.d.ts.map +1 -0
- package/dist/typed-dispatch.js +345 -0
- package/package.json +1 -1
- package/src/channel-prompt.ts +28 -1
- package/src/connect.ts +10 -0
- package/src/delivery.ts +38 -9
- package/src/fork.ts +432 -0
- package/src/inbound-channel.ts +256 -0
- package/src/inbound.ts +634 -303
- package/src/index.ts +7 -1
- package/src/parel-sdk-compat.ts +77 -0
- package/src/session.ts +247 -12
- package/src/typed-dispatch.ts +521 -0
package/src/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ChannelConnector } from '@parel/plugin-sdk';
|
|
2
2
|
import { connectParall } from './connect.js';
|
|
3
3
|
import { buildParallDelivery } from './delivery.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
catchUpParall,
|
|
6
|
+
handleParallAgentEvent,
|
|
7
|
+
handleParallFrame,
|
|
8
|
+
handleParallTimer,
|
|
9
|
+
} from './inbound.js';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Parall channel plugin for the parel runtime.
|
|
@@ -21,6 +26,7 @@ const connector: ChannelConnector = {
|
|
|
21
26
|
connect: connectParall,
|
|
22
27
|
onOpen: catchUpParall,
|
|
23
28
|
onMessage: handleParallFrame,
|
|
29
|
+
onTimer: handleParallTimer,
|
|
24
30
|
onAgentEvent: handleParallAgentEvent,
|
|
25
31
|
deliver: buildParallDelivery,
|
|
26
32
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConnectorContext } from '@parel/plugin-sdk';
|
|
1
|
+
import type { ConnectorContext, ConnectorEffect } from '@parel/plugin-sdk';
|
|
2
2
|
import { parallAgentId, parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -41,7 +41,51 @@ export function reportingEnabled(ctx: ConnectorContext): boolean {
|
|
|
41
41
|
* simply never read again.)
|
|
42
42
|
*/
|
|
43
43
|
export const TURN_ENVELOPE_KEY = `turnEnvelope:${MAIN_SESSION_KEY}`;
|
|
44
|
-
const SESSION_CACHE_KEY = `session:${MAIN_SESSION_KEY}`;
|
|
44
|
+
export const SESSION_CACHE_KEY = `session:${MAIN_SESSION_KEY}`;
|
|
45
|
+
const childSessionCacheKey = (childRef: string) => `sessionChild:${childRef}`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Conversation key for envelope subjects and fork routing. Thread-aware:
|
|
49
|
+
* parel's turn grouping (F2) and mid-turn injection (F3) key on the envelope
|
|
50
|
+
* subject, and a turn carries ONE reply route — grouping two threads of the
|
|
51
|
+
* same chat under one subject would absorb thread B into thread A's turn and
|
|
52
|
+
* deliver its reply to the wrong thread (agent-core's lane keys draw the
|
|
53
|
+
* same thread/channel line). `~` is outside both nanoid and ULID alphabets,
|
|
54
|
+
* so the composite never collides with a plain id.
|
|
55
|
+
*/
|
|
56
|
+
export function conversationKey(chatId: string, threadRootId?: string): string {
|
|
57
|
+
return threadRootId ? `${chatId}~${threadRootId}` : chatId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The addressable entity id inside a (possibly thread-composite) subject.
|
|
62
|
+
* Splits ONLY the `cht_…~msg_…` composite conversationKey produces — typed
|
|
63
|
+
* wiki subjects are scheme-stripped paths where `~` is a legal character
|
|
64
|
+
* (e.g. `wik_1/docs/~draft.md`) and must pass through untouched.
|
|
65
|
+
*/
|
|
66
|
+
export function subjectEntityId(subject: string): string {
|
|
67
|
+
const sep = subject.indexOf('~');
|
|
68
|
+
if (sep === -1) return subject;
|
|
69
|
+
const head = subject.slice(0, sep);
|
|
70
|
+
const tail = subject.slice(sep + 1);
|
|
71
|
+
return head.startsWith('cht_') && tail.startsWith('msg_') ? head : subject;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Step broadcast routing by subject shape: chat ids and channel conversations
|
|
76
|
+
* have live subscribers; task/schedule/trigger/wiki subjects are stored-only
|
|
77
|
+
* targets (the server publishes step WS events for chat/task targets and
|
|
78
|
+
* simply persists the rest). Mirrors agent-core's resolveStepTarget so both
|
|
79
|
+
* runtimes' session panels read identically.
|
|
80
|
+
*/
|
|
81
|
+
export function stepTargetTypeFor(subject: string): string {
|
|
82
|
+
if (subject.startsWith('chv_')) return 'channel_conversation';
|
|
83
|
+
if (subject.startsWith('tsk_')) return 'task';
|
|
84
|
+
if (subject.startsWith('sch_')) return 'schedule';
|
|
85
|
+
if (subject.startsWith('xtr_')) return 'external_trigger';
|
|
86
|
+
if (subject.startsWith('wcs_') || subject.startsWith('wik_')) return 'wiki';
|
|
87
|
+
return 'chat';
|
|
88
|
+
}
|
|
45
89
|
|
|
46
90
|
/**
|
|
47
91
|
* 'stale' means the session id no longer accepts writes (closed/superseded —
|
|
@@ -105,6 +149,89 @@ export async function ensureSession(
|
|
|
105
149
|
}
|
|
106
150
|
}
|
|
107
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the CURRENT main ase_ from the server, bypassing the cache — the
|
|
154
|
+
* onOpen generation check needs the authoritative id to compare against the
|
|
155
|
+
* cached one (New Session rotates it). Updates the cache on success; returns
|
|
156
|
+
* null on failure WITHOUT touching the cache (the caller must not lose its
|
|
157
|
+
* only generation token to a transient error).
|
|
158
|
+
*/
|
|
159
|
+
export async function fetchMainSessionFresh(ctx: ConnectorContext): Promise<string | null> {
|
|
160
|
+
if (!reportingEnabled(ctx)) return null;
|
|
161
|
+
try {
|
|
162
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions`;
|
|
163
|
+
const res = await fetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: jsonHeaders(ctx),
|
|
166
|
+
body: JSON.stringify({ runtime_type: 'parel', runtime_session_id: MAIN_SESSION_KEY }),
|
|
167
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) return null;
|
|
170
|
+
const body = (await res.json().catch(() => null)) as { id?: string } | null;
|
|
171
|
+
const id = body?.id;
|
|
172
|
+
if (!id) return null;
|
|
173
|
+
await ctx.store?.set(SESSION_CACHE_KEY, id).catch(() => {});
|
|
174
|
+
return id;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get-or-create the parall AgentSession mirroring one fork child session
|
|
182
|
+
* (ase_ with parent_session_id → the main ase_, runtime_session_id = the
|
|
183
|
+
* opaque childRef — the connector never sees parel's internal session id).
|
|
184
|
+
* Same caching/degradation posture as ensureSession; the parent resolve
|
|
185
|
+
* reuses the main-session cache so the extra cost is one store read.
|
|
186
|
+
*/
|
|
187
|
+
export async function ensureChildSession(
|
|
188
|
+
ctx: ConnectorContext,
|
|
189
|
+
childRef: string,
|
|
190
|
+
opts: { timeoutMs?: number } = {},
|
|
191
|
+
): Promise<string | null> {
|
|
192
|
+
if (!reportingEnabled(ctx)) return null;
|
|
193
|
+
const cacheKey = childSessionCacheKey(childRef);
|
|
194
|
+
const cached = await ctx.store?.get(cacheKey).catch(() => null);
|
|
195
|
+
if (typeof cached === 'string' && cached) return cached;
|
|
196
|
+
const parentId = await ensureSession(ctx, opts);
|
|
197
|
+
try {
|
|
198
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions`;
|
|
199
|
+
const res = await fetch(url, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: jsonHeaders(ctx),
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
runtime_type: 'parel',
|
|
204
|
+
runtime_session_id: childRef,
|
|
205
|
+
// Forks sit outside the main-lane uniqueness (parent set), mirroring
|
|
206
|
+
// the standard runtimes' fork ase_ semantics.
|
|
207
|
+
runtime_lane_key: `agent:fork:parel:${childRef}`,
|
|
208
|
+
...(parentId ? { parent_session_id: parentId } : {}),
|
|
209
|
+
}),
|
|
210
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? REQUEST_TIMEOUT_MS),
|
|
211
|
+
});
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
console.error(`[parel-channel] child session get-or-create failed (${res.status})`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const body = (await res.json().catch(() => null)) as { id?: string } | null;
|
|
217
|
+
const id = body?.id;
|
|
218
|
+
if (!id) return null;
|
|
219
|
+
await ctx.store?.set(cacheKey, id).catch(() => {});
|
|
220
|
+
return id;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error('[parel-channel] child session get-or-create failed', err);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Drop one child session's cache row (its ase_ was closed under us). */
|
|
228
|
+
export async function invalidateChildSession(
|
|
229
|
+
ctx: ConnectorContext,
|
|
230
|
+
childRef: string,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
await ctx.store?.delete(childSessionCacheKey(childRef)).catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
|
|
108
235
|
/** PATCH the session (status / trigger_message_id). Never throws. */
|
|
109
236
|
export async function patchSession(
|
|
110
237
|
ctx: ConnectorContext,
|
|
@@ -230,6 +357,57 @@ export async function createChannelInputStep(
|
|
|
230
357
|
}
|
|
231
358
|
}
|
|
232
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Input step for a typed (non-message) dispatch — task/schedule/trigger/
|
|
362
|
+
* wiki-comment/approval events. Same processing-ring/panel role as the chat
|
|
363
|
+
* input step; the target routes the record to the source entity instead of a
|
|
364
|
+
* chat (typed dispatches have no message-level indicator, so the step is the
|
|
365
|
+
* panel timeline entry, not an indicator driver).
|
|
366
|
+
*/
|
|
367
|
+
export async function createTypedInputStep(
|
|
368
|
+
ctx: ConnectorContext,
|
|
369
|
+
sessionId: string,
|
|
370
|
+
args: {
|
|
371
|
+
target: { type: string; id: string } | null;
|
|
372
|
+
triggerType: string;
|
|
373
|
+
/** WorkItem id — the replay identity. source_id is NOT unique per event
|
|
374
|
+
* for task WorkItems (it can be the task id), so keying the step on it
|
|
375
|
+
* would suppress every later event's input step for the same task. */
|
|
376
|
+
dispatchId: string;
|
|
377
|
+
sourceId: string;
|
|
378
|
+
summary: string;
|
|
379
|
+
senderId?: string;
|
|
380
|
+
},
|
|
381
|
+
): Promise<ReportResult> {
|
|
382
|
+
try {
|
|
383
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions/${sessionId}/steps`;
|
|
384
|
+
const res = await fetch(url, {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: jsonHeaders(ctx),
|
|
387
|
+
body: JSON.stringify({
|
|
388
|
+
step_type: 'input',
|
|
389
|
+
...(args.target ? { target_type: args.target.type, target_id: args.target.id } : {}),
|
|
390
|
+
// Session-scoped dedup — a failed ack leaves the dispatch replayable
|
|
391
|
+
// by the catch-up sweep, same rationale as the chat input step.
|
|
392
|
+
idempotency_key: `input:${args.dispatchId || args.sourceId}`,
|
|
393
|
+
content: {
|
|
394
|
+
trigger_type: args.triggerType,
|
|
395
|
+
trigger_ref: { source_id: args.sourceId },
|
|
396
|
+
...(args.senderId ? { sender_id: args.senderId } : {}),
|
|
397
|
+
summary: args.summary.slice(0, 200),
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
401
|
+
});
|
|
402
|
+
if (res.ok) return 'ok';
|
|
403
|
+
console.error(`[parel-channel] typed input step failed (${res.status})`);
|
|
404
|
+
return resultForStatus(res.status);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.error('[parel-channel] typed input step failed', err);
|
|
407
|
+
return 'failed';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
233
411
|
const ERROR_TEXT_MAX = 500;
|
|
234
412
|
|
|
235
413
|
/**
|
|
@@ -282,16 +460,18 @@ export async function createErrorStep(
|
|
|
282
460
|
const text = `Turn failed: ${redactSecrets(args.error, knownSecrets)}`.slice(0, ERROR_TEXT_MAX);
|
|
283
461
|
try {
|
|
284
462
|
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions/${sessionId}/steps`;
|
|
285
|
-
// Same subject-
|
|
286
|
-
// target 'chat' or the broadcast lands on a channel no client
|
|
287
|
-
|
|
463
|
+
// Same subject-shape line createTraceStep draws: non-chat subjects must
|
|
464
|
+
// not target 'chat' or the broadcast lands on a channel no client
|
|
465
|
+
// watches. Thread-composite subjects address the chat itself.
|
|
466
|
+
const targetId = subjectEntityId(args.subject);
|
|
467
|
+
const targetType = stepTargetTypeFor(targetId);
|
|
288
468
|
const res = await fetch(url, {
|
|
289
469
|
method: 'POST',
|
|
290
470
|
headers: jsonHeaders(ctx),
|
|
291
471
|
body: JSON.stringify({
|
|
292
472
|
step_type: 'text',
|
|
293
473
|
target_type: targetType,
|
|
294
|
-
target_id:
|
|
474
|
+
target_id: targetId,
|
|
295
475
|
// At-most-once today (agent-event pushes are not replayed), but the
|
|
296
476
|
// turnId key makes a future retry path free of duplicate error rows.
|
|
297
477
|
idempotency_key: `error:${args.turnId}`,
|
|
@@ -387,15 +567,17 @@ export async function createTraceStep(
|
|
|
387
567
|
}
|
|
388
568
|
try {
|
|
389
569
|
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions/${sessionId}/steps`;
|
|
390
|
-
// The subject
|
|
391
|
-
// steps must not target 'chat' or the server would
|
|
392
|
-
// a messages:{
|
|
393
|
-
// resolveStepTarget draws the same line).
|
|
394
|
-
|
|
570
|
+
// The subject shape tells chat turns from external IM / typed-entity
|
|
571
|
+
// turns — non-chat steps must not target 'chat' or the server would
|
|
572
|
+
// broadcast them onto a messages:{…} channel no client subscribes to
|
|
573
|
+
// (agent-core's resolveStepTarget draws the same line). Thread-composite
|
|
574
|
+
// subjects address the chat itself.
|
|
575
|
+
const targetId = subjectEntityId(subject);
|
|
576
|
+
const targetType = stepTargetTypeFor(targetId);
|
|
395
577
|
const res = await fetch(url, {
|
|
396
578
|
method: 'POST',
|
|
397
579
|
headers: jsonHeaders(ctx),
|
|
398
|
-
body: JSON.stringify({ ...body, target_type: targetType, target_id:
|
|
580
|
+
body: JSON.stringify({ ...body, target_type: targetType, target_id: targetId }),
|
|
399
581
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
400
582
|
});
|
|
401
583
|
if (res.ok) return 'ok';
|
|
@@ -407,6 +589,59 @@ export async function createTraceStep(
|
|
|
407
589
|
}
|
|
408
590
|
}
|
|
409
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Ack a dispatch by source so it leaves the pending queue (idempotent).
|
|
594
|
+
* Typed as the fetch effect so it can ride BOTH hook channels (onMessage's
|
|
595
|
+
* ConnectorEffect[] and onAgentEvent's AgentEventEffect[] — the fork spawn
|
|
596
|
+
* ack fires from a child agent event).
|
|
597
|
+
*/
|
|
598
|
+
export function ackEffect(
|
|
599
|
+
ctx: ConnectorContext,
|
|
600
|
+
sourceType: string,
|
|
601
|
+
sourceId: string,
|
|
602
|
+
): Extract<ConnectorEffect, { type: 'fetch' }> {
|
|
603
|
+
return {
|
|
604
|
+
type: 'fetch',
|
|
605
|
+
request: {
|
|
606
|
+
url: `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/ack`,
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: {
|
|
609
|
+
Authorization: `Bearer ${requireAgk(ctx)}`,
|
|
610
|
+
'Content-Type': 'application/json',
|
|
611
|
+
},
|
|
612
|
+
body: JSON.stringify({ source_type: sourceType, source_id: sourceId }),
|
|
613
|
+
},
|
|
614
|
+
idempotencyKey: `ack:${sourceType}:${sourceId}`,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Ack ONE dispatch WorkItem by its id. Typed dispatches must use this form:
|
|
620
|
+
* sibling WorkItems (e.g. a task mutation's task_assign + task_update rows)
|
|
621
|
+
* share (source_type, source_id), so the by-source ack would terminally
|
|
622
|
+
* resolve work that was never emitted. Message dispatches keep the by-source
|
|
623
|
+
* form (one WorkItem per message; the source ack is the established legacy
|
|
624
|
+
* contract).
|
|
625
|
+
*/
|
|
626
|
+
export function ackByIdEffect(
|
|
627
|
+
ctx: ConnectorContext,
|
|
628
|
+
dispatchId: string,
|
|
629
|
+
): Extract<ConnectorEffect, { type: 'fetch' }> {
|
|
630
|
+
return {
|
|
631
|
+
type: 'fetch',
|
|
632
|
+
request: {
|
|
633
|
+
url: `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/${dispatchId}/ack`,
|
|
634
|
+
method: 'POST',
|
|
635
|
+
headers: {
|
|
636
|
+
Authorization: `Bearer ${requireAgk(ctx)}`,
|
|
637
|
+
'Content-Type': 'application/json',
|
|
638
|
+
},
|
|
639
|
+
body: '{}',
|
|
640
|
+
},
|
|
641
|
+
idempotencyKey: `ackid:${dispatchId}`,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
410
645
|
/**
|
|
411
646
|
* Lights the pending ring (server broadcasts dispatch.received). Must COMPLETE
|
|
412
647
|
* before the ack effect is returned: MarkReceived only transitions rows still
|