@parall/parel-channel 1.43.0 → 1.45.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/delivery.js +1 -1
- package/dist/inbound-channel.js +6 -6
- package/dist/inbound.d.ts.map +1 -1
- package/dist/inbound.js +24 -13
- package/dist/session.d.ts +43 -19
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +75 -36
- package/package.json +1 -1
- package/src/delivery.ts +1 -1
- package/src/inbound-channel.ts +7 -7
- package/src/inbound.ts +31 -15
- package/src/session.ts +81 -34
package/dist/delivery.js
CHANGED
|
@@ -77,7 +77,7 @@ export async function buildParallDelivery(delivery, ctx) {
|
|
|
77
77
|
// ON and observe OFF — the platform template always sets both together,
|
|
78
78
|
// so it exists only on a hand-crafted binding.
|
|
79
79
|
if (route.envelopeId) {
|
|
80
|
-
await completeFencedEnvelopes(ctx, [route.envelopeId]);
|
|
80
|
+
await completeFencedEnvelopes(ctx, [route.envelopeId], 'ok');
|
|
81
81
|
}
|
|
82
82
|
const subject = route.chatId ?? route.conversationId;
|
|
83
83
|
if (subject) {
|
package/dist/inbound-channel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { channelCapabilityEnv } from './channel-capability.js';
|
|
2
2
|
import { buildChannelPrompt } from './channel-prompt.js';
|
|
3
3
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
4
|
-
import {
|
|
4
|
+
import { claimDispatchSource, completeSourceEffect, createChannelInputStep, ensureSession, invalidateSession, nextEnvelopeId, patchSession, readActiveEmit, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
|
|
5
5
|
/**
|
|
6
6
|
* External IM inbound (channel_message dispatches — Feishu/Slack via the
|
|
7
7
|
* platform-mediated channel, external-im-channel-design.md). Split from
|
|
@@ -41,7 +41,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
41
41
|
if (m.status === 404 || m.status === 403) {
|
|
42
42
|
// Deleted / inaccessible (also the shape a disabled org feature flag
|
|
43
43
|
// takes) — permanently unusable, drop it.
|
|
44
|
-
return [
|
|
44
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
45
45
|
}
|
|
46
46
|
return []; // transient: keep pending, the catch-up sweep retries
|
|
47
47
|
}
|
|
@@ -50,18 +50,18 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
50
50
|
const text = msg?.text ?? '';
|
|
51
51
|
if (!conversationId) {
|
|
52
52
|
// No conversation → no session key and no reply target: unusable.
|
|
53
|
-
return [
|
|
53
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
54
54
|
}
|
|
55
55
|
if (!text) {
|
|
56
56
|
// The ingress adapter only persists mechanically parsed text; a row with
|
|
57
57
|
// none will never grow any (same reasoning as attachment-only chat
|
|
58
58
|
// messages) — drop rather than let it pile up in the pending queue.
|
|
59
|
-
return [
|
|
59
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
60
60
|
}
|
|
61
61
|
const c = await get(`/channel-conversations/${conversationId}`);
|
|
62
62
|
if (!c.ok) {
|
|
63
63
|
if (c.status === 404 || c.status === 403) {
|
|
64
|
-
return [
|
|
64
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
65
65
|
}
|
|
66
66
|
return [];
|
|
67
67
|
}
|
|
@@ -79,7 +79,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
|
|
|
79
79
|
// (see the comments there): received settles before this function returns;
|
|
80
80
|
// the turnEnvelope marker goes down before any session write; the row
|
|
81
81
|
// stays 'received' until the turn event's complete-sources.
|
|
82
|
-
const receivedSettled =
|
|
82
|
+
const receivedSettled = claimDispatchSource(ctx, 'channel_message', sourceId);
|
|
83
83
|
const priorEmit = await readActiveEmit(ctx, 'channel_message', sourceId);
|
|
84
84
|
const envelopeId = nextEnvelopeId('channel_message', sourceId, priorEmit);
|
|
85
85
|
let sessionId = await ensureSession(ctx);
|
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;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;
|
|
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;AAkrBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}
|
package/dist/inbound.js
CHANGED
|
@@ -3,7 +3,7 @@ import { buildChatPrompt } from './channel-prompt.js';
|
|
|
3
3
|
import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
|
|
4
4
|
import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
|
|
5
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';
|
|
6
|
-
import { ackByIdEffect,
|
|
6
|
+
import { ackByIdEffect, claimDispatchSource, completeFencedEnvelopes, completeSourceEffect, conversationKey, createErrorStep, fetchMainSessionFresh, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, 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
|
/**
|
|
9
9
|
* Connection-local hook serialization. The parel ConnectorHost dispatches
|
|
@@ -274,7 +274,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
274
274
|
return [];
|
|
275
275
|
if (!chatId) {
|
|
276
276
|
// A message dispatch with no chat can never be routed or replied to.
|
|
277
|
-
return [
|
|
277
|
+
return [completeSourceEffect(ctx, sourceType, sourceId)];
|
|
278
278
|
}
|
|
279
279
|
if (await shouldSkipLiveRow(ctx, sourceType, sourceId))
|
|
280
280
|
return [];
|
|
@@ -289,7 +289,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
289
289
|
if (!m.ok) {
|
|
290
290
|
if (m.status === 404 || m.status === 403) {
|
|
291
291
|
// Deleted message / lost chat access — permanently unusable, drop it.
|
|
292
|
-
return [
|
|
292
|
+
return [completeSourceEffect(ctx, sourceType, sourceId)];
|
|
293
293
|
}
|
|
294
294
|
// Transient failure: keep pending, the catch-up sweep retries.
|
|
295
295
|
return [];
|
|
@@ -312,7 +312,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
312
312
|
// Truly empty messages never grow content — ack-and-drop rather than let
|
|
313
313
|
// them pile up in the pending queue forever. (Attachment-only messages
|
|
314
314
|
// used to take this path; they now ride the attachment lines instead.)
|
|
315
|
-
return [
|
|
315
|
+
return [completeSourceEffect(ctx, sourceType, sourceId)];
|
|
316
316
|
}
|
|
317
317
|
// Frame the message with its chat of origin: all chats share the agent's
|
|
318
318
|
// one main session, so the model must see WHERE each message comes from or
|
|
@@ -400,7 +400,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
400
400
|
// Progress reporting lands on the CHILD's ase_ — its turn does the work,
|
|
401
401
|
// and the replyRoute carries childRef so deliver() links the reply to the
|
|
402
402
|
// child session (and idles IT, not main).
|
|
403
|
-
const receivedSettled =
|
|
403
|
+
const receivedSettled = claimDispatchSource(ctx, sourceType, sourceId);
|
|
404
404
|
const childAse = await ensureChildSession(ctx, decision.childRef);
|
|
405
405
|
if (childAse) {
|
|
406
406
|
const results = await Promise.all([
|
|
@@ -459,7 +459,9 @@ async function effectsForDispatch(data, ctx) {
|
|
|
459
459
|
}
|
|
460
460
|
return [
|
|
461
461
|
{ type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
|
|
462
|
-
|
|
462
|
+
// Fork-handled rows keep the legacy ack machinery (ledger parity is a
|
|
463
|
+
// declared follow-up); by id — the precise form.
|
|
464
|
+
dispatchId ? ackByIdEffect(ctx, dispatchId) : completeSourceEffect(ctx, sourceType, sourceId),
|
|
463
465
|
];
|
|
464
466
|
}
|
|
465
467
|
if (decision.mode === 'spawn') {
|
|
@@ -506,7 +508,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
506
508
|
}
|
|
507
509
|
}
|
|
508
510
|
if (!spawnBlackHoled) {
|
|
509
|
-
const receivedSettled =
|
|
511
|
+
const receivedSettled = claimDispatchSource(ctx, sourceType, sourceId);
|
|
510
512
|
const childAse = await ensureChildSession(ctx, decision.childRef);
|
|
511
513
|
if (childAse) {
|
|
512
514
|
const results = await Promise.all([
|
|
@@ -552,7 +554,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
552
554
|
// pending→received transition. It is deliberately NOT gated on the
|
|
553
555
|
// session resolving: a transient session failure must not also cost the
|
|
554
556
|
// indicator.
|
|
555
|
-
const receivedSettled =
|
|
557
|
+
const receivedSettled = claimDispatchSource(ctx, sourceType, sourceId);
|
|
556
558
|
let sessionId = await ensureSession(ctx);
|
|
557
559
|
if (sessionId) {
|
|
558
560
|
// The guard marker goes down BEFORE any session write: a stale
|
|
@@ -615,7 +617,7 @@ async function effectsForDispatch(data, ctx) {
|
|
|
615
617
|
* them on the orchestrator preserves task/schedule context continuity.
|
|
616
618
|
*/
|
|
617
619
|
async function effectsForTypedDispatch(data, ctx) {
|
|
618
|
-
// No
|
|
620
|
+
// No claimDispatchSource here ON PURPOSE: typed dispatches have no
|
|
619
621
|
// message-level activity indicator (DispatchResolvedData: "typed
|
|
620
622
|
// dispatches have no indicator"), so there is no pending ring to light —
|
|
621
623
|
// the typed input step below is the session panel's timeline entry.
|
|
@@ -625,12 +627,21 @@ async function effectsForTypedDispatch(data, ctx) {
|
|
|
625
627
|
// Typed WorkItems ack by id, never by source: sibling rows (a task
|
|
626
628
|
// mutation's task_assign + task_update) share (source_type, source_id),
|
|
627
629
|
// and the source ack would terminally resolve work that was never emitted.
|
|
628
|
-
const ack = () =>
|
|
630
|
+
const ack = () => {
|
|
631
|
+
if (!dispatchId) {
|
|
632
|
+
// No WorkItem id (malformed payload): the by-source form would
|
|
633
|
+
// terminally resolve SIBLING typed rows sharing this source — leave
|
|
634
|
+
// the row pending for the sweep instead.
|
|
635
|
+
console.error(`[parel-channel] typed dispatch without id (${sourceType}:${sourceId}) left pending`);
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
return [ackByIdEffect(ctx, dispatchId)];
|
|
639
|
+
};
|
|
629
640
|
const plan = await planTypedDispatch(data, ctx);
|
|
630
641
|
if (plan.outcome === 'pending')
|
|
631
642
|
return [];
|
|
632
643
|
if (plan.outcome === 'stale')
|
|
633
|
-
return
|
|
644
|
+
return ack();
|
|
634
645
|
const envelopeId = dispatchId || `${sourceType}:${sourceId}`;
|
|
635
646
|
let sessionId = await ensureSession(ctx);
|
|
636
647
|
if (sessionId) {
|
|
@@ -696,7 +707,7 @@ async function effectsForTypedDispatch(data, ctx) {
|
|
|
696
707
|
...(Object.keys(typedContext).length > 0 ? { context: typedContext } : {}),
|
|
697
708
|
},
|
|
698
709
|
},
|
|
699
|
-
ack(),
|
|
710
|
+
...ack(),
|
|
700
711
|
];
|
|
701
712
|
}
|
|
702
713
|
/**
|
|
@@ -842,7 +853,7 @@ async function handleParallAgentEventInner(event, ctx) {
|
|
|
842
853
|
// the absorbing turn's envelopeIds and settle here naturally. The
|
|
843
854
|
// server decides reply broad-cover vs no_action per batch; turn_failed
|
|
844
855
|
// sweeps the same way with the error step below as the visible record.
|
|
845
|
-
await completeFencedEnvelopes(ctx, event.envelopeIds);
|
|
856
|
+
await completeFencedEnvelopes(ctx, event.envelopeIds, event.type === 'turn_failed' ? 'error' : 'ok');
|
|
846
857
|
// Staleness guard: only idle if this event's turn covers the AGENT'S
|
|
847
858
|
// latest emitted envelope (the marker is agent-global — all chats share
|
|
848
859
|
// the one main session, so a done signal for chat A's turn must not
|
package/dist/session.d.ts
CHANGED
|
@@ -216,12 +216,14 @@ export declare function createTraceStep(ctx: ConnectorContext, sessionId: string
|
|
|
216
216
|
outputPreview?: string;
|
|
217
217
|
}): Promise<ReportResult>;
|
|
218
218
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
219
|
+
* Terminally drop ONE source's live WorkItem via the unified complete's
|
|
220
|
+
* source form (no covering reply Effect exists for a dropped row, so the
|
|
221
|
+
* server sweeps it to no_action — the drop semantics; idempotent). Typed as
|
|
222
|
+
* the fetch effect so it can ride BOTH hook channels (ConnectorEffect[] and
|
|
223
|
+
* AgentEventEffect[]). Replaces the legacy by-source ack for drop cases —
|
|
224
|
+
* that endpoint retires at S3 (dispatch-convergence-design.md §3).
|
|
223
225
|
*/
|
|
224
|
-
export declare function
|
|
226
|
+
export declare function completeSourceEffect(ctx: ConnectorContext, sourceType: string, sourceId: string): Extract<ConnectorEffect, {
|
|
225
227
|
type: 'fetch';
|
|
226
228
|
}>;
|
|
227
229
|
/**
|
|
@@ -236,12 +238,16 @@ export declare function ackByIdEffect(ctx: ConnectorContext, dispatchId: string)
|
|
|
236
238
|
type: 'fetch';
|
|
237
239
|
}>;
|
|
238
240
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
241
|
+
* Claim the source's WorkItem before the turn (server folds the row into its
|
|
242
|
+
* message lane, flips it pending→received and broadcasts dispatch.received —
|
|
243
|
+
* the pending ring rides the claim side effect now; the retired received
|
|
244
|
+
* endpoint was a bare receipt). Best-effort like the receipt was: a refused
|
|
245
|
+
* or failed claim never blocks the emit — the row stays pending and the
|
|
246
|
+
* turn's complete resolves it just the same. Must COMPLETE before any
|
|
247
|
+
* settling effect is returned so a same-batch complete can't race the
|
|
248
|
+
* pending→received transition.
|
|
243
249
|
*/
|
|
244
|
-
export declare function
|
|
250
|
+
export declare function claimDispatchSource(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<void>;
|
|
245
251
|
/**
|
|
246
252
|
* How long an emitted source may stay unresolved before the reconnect sweep
|
|
247
253
|
* re-emits it (crash / lost turn_completed self-heal — the successor of the
|
|
@@ -263,6 +269,14 @@ export type ActiveEmit = {
|
|
|
263
269
|
export declare function readActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<ActiveEmit | null>;
|
|
264
270
|
export declare function writeActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string, envelopeId: string): Promise<void>;
|
|
265
271
|
export declare function clearActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<void>;
|
|
272
|
+
/**
|
|
273
|
+
* Burn the fence after an ERROR close: keep the attempt id (so the re-driven
|
|
274
|
+
* emit's nextEnvelopeId picks a fresh `#r{n+1}` — a bare repeat of the base
|
|
275
|
+
* id could be deduped by parel and strand the retry) but zero the timestamp
|
|
276
|
+
* (so the redelivery-skip window is already over and the server's re-drive
|
|
277
|
+
* hint re-emits immediately instead of hiding behind a 30-min TTL).
|
|
278
|
+
*/
|
|
279
|
+
export declare function burnActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<void>;
|
|
266
280
|
/**
|
|
267
281
|
* Deterministic envelope id for a source: `{sourceType}:{sourceId}` on the
|
|
268
282
|
* first attempt, `…#r{n}` on re-emits. Attempt-suffixed ids are unique per
|
|
@@ -275,6 +289,8 @@ export declare function sourceFromEnvelopeId(envelopeId: string): {
|
|
|
275
289
|
sourceType: string;
|
|
276
290
|
sourceId: string;
|
|
277
291
|
} | null;
|
|
292
|
+
/** Turn outcome for the ledger close — mirrors the server's turn_outcome. */
|
|
293
|
+
export type TurnOutcome = 'ok' | 'error';
|
|
278
294
|
/**
|
|
279
295
|
* Fence-checked turn-end ledger close for a set of envelope ids — the shared
|
|
280
296
|
* core of BOTH turn-end hooks: onAgentEvent (turn_completed/turn_failed, the
|
|
@@ -282,21 +298,29 @@ export declare function sourceFromEnvelopeId(envelopeId: string): {
|
|
|
282
298
|
* never arrive and this is the ONLY terminal ledger hook — without it a
|
|
283
299
|
* received row would re-emit on every TTL sweep forever). Only sources whose
|
|
284
300
|
* `active:{source}` store row matches the envelope id EXACTLY are completed
|
|
285
|
-
* (attempt fence)
|
|
286
|
-
*
|
|
301
|
+
* (attempt fence). Both hooks running is harmless: whichever completes first
|
|
302
|
+
* consumes the fence, the other skips.
|
|
303
|
+
*
|
|
304
|
+
* Fence disposal follows the outcome: an "ok" close clears the fences (the
|
|
305
|
+
* sources are terminal); an "error" close BURNS them instead — the server
|
|
306
|
+
* released the uncovered members back to pending and its re-drive hint
|
|
307
|
+
* re-emits them, so the burned fence must neither hide the retry behind the
|
|
308
|
+
* redelivery-skip TTL nor let it reuse a dedupe-prone envelope id.
|
|
287
309
|
*/
|
|
288
|
-
export declare function completeFencedEnvelopes(ctx: ConnectorContext, envelopeIds: string[]): Promise<void>;
|
|
310
|
+
export declare function completeFencedEnvelopes(ctx: ConnectorContext, envelopeIds: string[], outcome?: TurnOutcome): Promise<void>;
|
|
289
311
|
/**
|
|
290
|
-
* Turn-end ledger close: resolves the turn's folded sources on the server
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
312
|
+
* Turn-end ledger close: resolves the turn's folded sources on the server via
|
|
313
|
+
* the unified complete's source form (POST /dispatch/complete, idempotent) —
|
|
314
|
+
* broad-cover to the turn's reply Effect when one exists; otherwise a
|
|
315
|
+
* no_action sweep on "ok", or a release back to pending on the redrive budget
|
|
316
|
+
* on "error" (a failed turn must not silently consume work it never handled).
|
|
317
|
+
* One immediate retry on failure; a still-failed call leaves the rows live
|
|
318
|
+
* and the catch-up TTL re-emit self-heals.
|
|
295
319
|
*/
|
|
296
320
|
export declare function completeSources(ctx: ConnectorContext, sources: {
|
|
297
321
|
source_type: string;
|
|
298
322
|
source_id: string;
|
|
299
|
-
}[]): Promise<boolean>;
|
|
323
|
+
}[], outcome?: TurnOutcome): Promise<boolean>;
|
|
300
324
|
/**
|
|
301
325
|
* Turn-end plain text as a suppressed audit step — the runtime-standard
|
|
302
326
|
* Layer 0 posture (agent-dm-loop-prevention.md): recorded on the session,
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/dist/session.js
CHANGED
|
@@ -523,24 +523,26 @@ export async function createTraceStep(ctx, sessionId, subject, event) {
|
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
/**
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
526
|
+
* Terminally drop ONE source's live WorkItem via the unified complete's
|
|
527
|
+
* source form (no covering reply Effect exists for a dropped row, so the
|
|
528
|
+
* server sweeps it to no_action — the drop semantics; idempotent). Typed as
|
|
529
|
+
* the fetch effect so it can ride BOTH hook channels (ConnectorEffect[] and
|
|
530
|
+
* AgentEventEffect[]). Replaces the legacy by-source ack for drop cases —
|
|
531
|
+
* that endpoint retires at S3 (dispatch-convergence-design.md §3).
|
|
530
532
|
*/
|
|
531
|
-
export function
|
|
533
|
+
export function completeSourceEffect(ctx, sourceType, sourceId) {
|
|
532
534
|
return {
|
|
533
535
|
type: 'fetch',
|
|
534
536
|
request: {
|
|
535
|
-
url: `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/
|
|
537
|
+
url: `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete`,
|
|
536
538
|
method: 'POST',
|
|
537
539
|
headers: {
|
|
538
540
|
Authorization: `Bearer ${requireAgk(ctx)}`,
|
|
539
541
|
'Content-Type': 'application/json',
|
|
540
542
|
},
|
|
541
|
-
body: JSON.stringify({ source_type: sourceType, source_id: sourceId }),
|
|
543
|
+
body: JSON.stringify({ sources: [{ source_type: sourceType, source_id: sourceId }] }),
|
|
542
544
|
},
|
|
543
|
-
idempotencyKey: `
|
|
545
|
+
idempotencyKey: `complete:src:${sourceType}:${sourceId}`,
|
|
544
546
|
};
|
|
545
547
|
}
|
|
546
548
|
/**
|
|
@@ -567,18 +569,22 @@ export function ackByIdEffect(ctx, dispatchId) {
|
|
|
567
569
|
};
|
|
568
570
|
}
|
|
569
571
|
/**
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
572
|
+
* Claim the source's WorkItem before the turn (server folds the row into its
|
|
573
|
+
* message lane, flips it pending→received and broadcasts dispatch.received —
|
|
574
|
+
* the pending ring rides the claim side effect now; the retired received
|
|
575
|
+
* endpoint was a bare receipt). Best-effort like the receipt was: a refused
|
|
576
|
+
* or failed claim never blocks the emit — the row stays pending and the
|
|
577
|
+
* turn's complete resolves it just the same. Must COMPLETE before any
|
|
578
|
+
* settling effect is returned so a same-batch complete can't race the
|
|
579
|
+
* pending→received transition.
|
|
574
580
|
*/
|
|
575
|
-
export async function
|
|
581
|
+
export async function claimDispatchSource(ctx, sourceType, sourceId) {
|
|
576
582
|
// Deliberately NOT gated on reportingEnabled: this is ledger surface, not
|
|
577
|
-
// session reporting — the URL needs no agent id, and
|
|
578
|
-
//
|
|
579
|
-
//
|
|
583
|
+
// session reporting — the URL needs no agent id, and the claim transition
|
|
584
|
+
// is what keeps the indicator lifecycle working even for agent-id-less old
|
|
585
|
+
// configs.
|
|
580
586
|
try {
|
|
581
|
-
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/
|
|
587
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/claim`;
|
|
582
588
|
const res = await fetch(url, {
|
|
583
589
|
method: 'POST',
|
|
584
590
|
headers: jsonHeaders(ctx),
|
|
@@ -586,10 +592,10 @@ export async function markDispatchReceived(ctx, sourceType, sourceId) {
|
|
|
586
592
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
587
593
|
});
|
|
588
594
|
if (!res.ok)
|
|
589
|
-
console.error(`[parel-channel]
|
|
595
|
+
console.error(`[parel-channel] claim source failed (${res.status})`);
|
|
590
596
|
}
|
|
591
597
|
catch (err) {
|
|
592
|
-
console.error('[parel-channel]
|
|
598
|
+
console.error('[parel-channel] claim source failed', err);
|
|
593
599
|
}
|
|
594
600
|
}
|
|
595
601
|
// ── By-source ledger surface ────────────────────────────
|
|
@@ -632,6 +638,21 @@ export async function writeActiveEmit(ctx, sourceType, sourceId, envelopeId) {
|
|
|
632
638
|
export async function clearActiveEmit(ctx, sourceType, sourceId) {
|
|
633
639
|
await ctx.store?.delete(activeEmitKey(sourceType, sourceId)).catch(() => { });
|
|
634
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Burn the fence after an ERROR close: keep the attempt id (so the re-driven
|
|
643
|
+
* emit's nextEnvelopeId picks a fresh `#r{n+1}` — a bare repeat of the base
|
|
644
|
+
* id could be deduped by parel and strand the retry) but zero the timestamp
|
|
645
|
+
* (so the redelivery-skip window is already over and the server's re-drive
|
|
646
|
+
* hint re-emits immediately instead of hiding behind a 30-min TTL).
|
|
647
|
+
*/
|
|
648
|
+
export async function burnActiveEmit(ctx, sourceType, sourceId) {
|
|
649
|
+
const prior = await readActiveEmit(ctx, sourceType, sourceId);
|
|
650
|
+
if (!prior)
|
|
651
|
+
return;
|
|
652
|
+
await ctx.store
|
|
653
|
+
?.set(activeEmitKey(sourceType, sourceId), { id: prior.id, at: 0 })
|
|
654
|
+
.catch(() => { });
|
|
655
|
+
}
|
|
635
656
|
/**
|
|
636
657
|
* Deterministic envelope id for a source: `{sourceType}:{sourceId}` on the
|
|
637
658
|
* first attempt, `…#r{n}` on re-emits. Attempt-suffixed ids are unique per
|
|
@@ -661,47 +682,65 @@ export function sourceFromEnvelopeId(envelopeId) {
|
|
|
661
682
|
* never arrive and this is the ONLY terminal ledger hook — without it a
|
|
662
683
|
* received row would re-emit on every TTL sweep forever). Only sources whose
|
|
663
684
|
* `active:{source}` store row matches the envelope id EXACTLY are completed
|
|
664
|
-
* (attempt fence)
|
|
665
|
-
*
|
|
685
|
+
* (attempt fence). Both hooks running is harmless: whichever completes first
|
|
686
|
+
* consumes the fence, the other skips.
|
|
687
|
+
*
|
|
688
|
+
* Fence disposal follows the outcome: an "ok" close clears the fences (the
|
|
689
|
+
* sources are terminal); an "error" close BURNS them instead — the server
|
|
690
|
+
* released the uncovered members back to pending and its re-drive hint
|
|
691
|
+
* re-emits them, so the burned fence must neither hide the retry behind the
|
|
692
|
+
* redelivery-skip TTL nor let it reuse a dedupe-prone envelope id.
|
|
666
693
|
*/
|
|
667
|
-
export async function completeFencedEnvelopes(ctx, envelopeIds) {
|
|
694
|
+
export async function completeFencedEnvelopes(ctx, envelopeIds, outcome = 'ok') {
|
|
668
695
|
const fenced = [];
|
|
669
696
|
for (const envId of envelopeIds) {
|
|
670
697
|
const src = sourceFromEnvelopeId(envId);
|
|
671
698
|
if (!src)
|
|
672
699
|
continue; // pre-alignment envelope id (dsp_*) — old rows were acked on emit
|
|
673
700
|
const active = await readActiveEmit(ctx, src.sourceType, src.sourceId);
|
|
674
|
-
|
|
701
|
+
// A BURNED fence (at === 0) still carries the attempt id but the attempt
|
|
702
|
+
// is already error-closed — its members are back in pending awaiting the
|
|
703
|
+
// re-drive. The redundant deliver()-side close for the same envelope
|
|
704
|
+
// must skip here, or its "ok" would no_action-sweep the very rows the
|
|
705
|
+
// error close just released (the retry hole S3a closes).
|
|
706
|
+
if (active?.id === envId && active.at > 0) {
|
|
675
707
|
fenced.push({ source_type: src.sourceType, source_id: src.sourceId });
|
|
676
708
|
}
|
|
677
709
|
}
|
|
678
|
-
if (fenced.length > 0 && (await completeSources(ctx, fenced))) {
|
|
710
|
+
if (fenced.length > 0 && (await completeSources(ctx, fenced, outcome))) {
|
|
679
711
|
for (const src of fenced) {
|
|
680
|
-
|
|
712
|
+
if (outcome === 'error') {
|
|
713
|
+
await burnActiveEmit(ctx, src.source_type, src.source_id);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
await clearActiveEmit(ctx, src.source_type, src.source_id);
|
|
717
|
+
}
|
|
681
718
|
}
|
|
682
719
|
}
|
|
683
720
|
}
|
|
684
721
|
/**
|
|
685
|
-
* Turn-end ledger close: resolves the turn's folded sources on the server
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
722
|
+
* Turn-end ledger close: resolves the turn's folded sources on the server via
|
|
723
|
+
* the unified complete's source form (POST /dispatch/complete, idempotent) —
|
|
724
|
+
* broad-cover to the turn's reply Effect when one exists; otherwise a
|
|
725
|
+
* no_action sweep on "ok", or a release back to pending on the redrive budget
|
|
726
|
+
* on "error" (a failed turn must not silently consume work it never handled).
|
|
727
|
+
* One immediate retry on failure; a still-failed call leaves the rows live
|
|
728
|
+
* and the catch-up TTL re-emit self-heals.
|
|
690
729
|
*/
|
|
691
|
-
export async function completeSources(ctx, sources) {
|
|
692
|
-
// Ledger surface — not gated on reportingEnabled (see
|
|
730
|
+
export async function completeSources(ctx, sources, outcome = 'ok') {
|
|
731
|
+
// Ledger surface — not gated on reportingEnabled (see claimDispatchSource).
|
|
693
732
|
if (sources.length === 0)
|
|
694
733
|
return true;
|
|
695
|
-
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete
|
|
734
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete`;
|
|
696
735
|
const attempt = async () => {
|
|
697
736
|
const res = await fetch(url, {
|
|
698
737
|
method: 'POST',
|
|
699
738
|
headers: jsonHeaders(ctx),
|
|
700
|
-
body: JSON.stringify({ sources }),
|
|
739
|
+
body: JSON.stringify({ sources, turn_outcome: outcome }),
|
|
701
740
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
702
741
|
});
|
|
703
742
|
if (!res.ok)
|
|
704
|
-
console.error(`[parel-channel] complete
|
|
743
|
+
console.error(`[parel-channel] complete (sources) failed (${res.status})`);
|
|
705
744
|
return res.ok;
|
|
706
745
|
};
|
|
707
746
|
try {
|
|
@@ -715,7 +754,7 @@ export async function completeSources(ctx, sources) {
|
|
|
715
754
|
return await attempt();
|
|
716
755
|
}
|
|
717
756
|
catch (err) {
|
|
718
|
-
console.error('[parel-channel] complete
|
|
757
|
+
console.error('[parel-channel] complete (sources) failed', err);
|
|
719
758
|
return false;
|
|
720
759
|
}
|
|
721
760
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/parel-channel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.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": {
|
package/src/delivery.ts
CHANGED
|
@@ -99,7 +99,7 @@ export async function buildParallDelivery(
|
|
|
99
99
|
// ON and observe OFF — the platform template always sets both together,
|
|
100
100
|
// so it exists only on a hand-crafted binding.
|
|
101
101
|
if (route.envelopeId) {
|
|
102
|
-
await completeFencedEnvelopes(ctx, [route.envelopeId]);
|
|
102
|
+
await completeFencedEnvelopes(ctx, [route.envelopeId], 'ok');
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const subject = route.chatId ?? route.conversationId;
|
package/src/inbound-channel.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { channelCapabilityEnv } from './channel-capability.js';
|
|
|
3
3
|
import { buildChannelPrompt } from './channel-prompt.js';
|
|
4
4
|
import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
claimDispatchSource,
|
|
7
|
+
completeSourceEffect,
|
|
7
8
|
createChannelInputStep,
|
|
8
9
|
ensureSession,
|
|
9
10
|
invalidateSession,
|
|
10
|
-
markDispatchReceived,
|
|
11
11
|
nextEnvelopeId,
|
|
12
12
|
patchSession,
|
|
13
13
|
readActiveEmit,
|
|
@@ -62,7 +62,7 @@ export async function effectsForChannelDispatch(
|
|
|
62
62
|
if (m.status === 404 || m.status === 403) {
|
|
63
63
|
// Deleted / inaccessible (also the shape a disabled org feature flag
|
|
64
64
|
// takes) — permanently unusable, drop it.
|
|
65
|
-
return [
|
|
65
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
66
66
|
}
|
|
67
67
|
return []; // transient: keep pending, the catch-up sweep retries
|
|
68
68
|
}
|
|
@@ -78,19 +78,19 @@ export async function effectsForChannelDispatch(
|
|
|
78
78
|
const text = msg?.text ?? '';
|
|
79
79
|
if (!conversationId) {
|
|
80
80
|
// No conversation → no session key and no reply target: unusable.
|
|
81
|
-
return [
|
|
81
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
82
82
|
}
|
|
83
83
|
if (!text) {
|
|
84
84
|
// The ingress adapter only persists mechanically parsed text; a row with
|
|
85
85
|
// none will never grow any (same reasoning as attachment-only chat
|
|
86
86
|
// messages) — drop rather than let it pile up in the pending queue.
|
|
87
|
-
return [
|
|
87
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const c = await get(`/channel-conversations/${conversationId}`);
|
|
91
91
|
if (!c.ok) {
|
|
92
92
|
if (c.status === 404 || c.status === 403) {
|
|
93
|
-
return [
|
|
93
|
+
return [completeSourceEffect(ctx, 'channel_message', sourceId)];
|
|
94
94
|
}
|
|
95
95
|
return [];
|
|
96
96
|
}
|
|
@@ -115,7 +115,7 @@ export async function effectsForChannelDispatch(
|
|
|
115
115
|
// (see the comments there): received settles before this function returns;
|
|
116
116
|
// the turnEnvelope marker goes down before any session write; the row
|
|
117
117
|
// stays 'received' until the turn event's complete-sources.
|
|
118
|
-
const receivedSettled =
|
|
118
|
+
const receivedSettled = claimDispatchSource(ctx, 'channel_message', sourceId);
|
|
119
119
|
const priorEmit = await readActiveEmit(ctx, 'channel_message', sourceId);
|
|
120
120
|
const envelopeId = nextEnvelopeId('channel_message', sourceId, priorEmit);
|
|
121
121
|
let sessionId = await ensureSession(ctx);
|
package/src/inbound.ts
CHANGED
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
} from './fork.js';
|
|
29
29
|
import {
|
|
30
30
|
ackByIdEffect,
|
|
31
|
-
|
|
31
|
+
claimDispatchSource,
|
|
32
32
|
completeFencedEnvelopes,
|
|
33
|
+
completeSourceEffect,
|
|
33
34
|
conversationKey,
|
|
34
35
|
createErrorStep,
|
|
35
36
|
fetchMainSessionFresh,
|
|
@@ -41,7 +42,6 @@ import {
|
|
|
41
42
|
ensureSession,
|
|
42
43
|
invalidateChildSession,
|
|
43
44
|
invalidateSession,
|
|
44
|
-
markDispatchReceived,
|
|
45
45
|
nextEnvelopeId,
|
|
46
46
|
patchSession,
|
|
47
47
|
readActiveEmit,
|
|
@@ -395,7 +395,7 @@ async function effectsForDispatch(
|
|
|
395
395
|
if (sourceType !== 'message' || !sourceId) return [];
|
|
396
396
|
if (!chatId) {
|
|
397
397
|
// A message dispatch with no chat can never be routed or replied to.
|
|
398
|
-
return [
|
|
398
|
+
return [completeSourceEffect(ctx, sourceType, sourceId)];
|
|
399
399
|
}
|
|
400
400
|
if (await shouldSkipLiveRow(ctx, sourceType, sourceId)) return [];
|
|
401
401
|
|
|
@@ -411,7 +411,7 @@ async function effectsForDispatch(
|
|
|
411
411
|
if (!m.ok) {
|
|
412
412
|
if (m.status === 404 || m.status === 403) {
|
|
413
413
|
// Deleted message / lost chat access — permanently unusable, drop it.
|
|
414
|
-
return [
|
|
414
|
+
return [completeSourceEffect(ctx, sourceType, sourceId)];
|
|
415
415
|
}
|
|
416
416
|
// Transient failure: keep pending, the catch-up sweep retries.
|
|
417
417
|
return [];
|
|
@@ -444,7 +444,7 @@ async function effectsForDispatch(
|
|
|
444
444
|
// Truly empty messages never grow content — ack-and-drop rather than let
|
|
445
445
|
// them pile up in the pending queue forever. (Attachment-only messages
|
|
446
446
|
// used to take this path; they now ride the attachment lines instead.)
|
|
447
|
-
return [
|
|
447
|
+
return [completeSourceEffect(ctx, sourceType, sourceId)];
|
|
448
448
|
}
|
|
449
449
|
|
|
450
450
|
// Frame the message with its chat of origin: all chats share the agent's
|
|
@@ -537,7 +537,7 @@ async function effectsForDispatch(
|
|
|
537
537
|
// Progress reporting lands on the CHILD's ase_ — its turn does the work,
|
|
538
538
|
// and the replyRoute carries childRef so deliver() links the reply to the
|
|
539
539
|
// child session (and idles IT, not main).
|
|
540
|
-
const receivedSettled =
|
|
540
|
+
const receivedSettled = claimDispatchSource(ctx, sourceType, sourceId);
|
|
541
541
|
const childAse = await ensureChildSession(ctx, decision.childRef);
|
|
542
542
|
if (childAse) {
|
|
543
543
|
const results = await Promise.all([
|
|
@@ -595,7 +595,9 @@ async function effectsForDispatch(
|
|
|
595
595
|
}
|
|
596
596
|
return [
|
|
597
597
|
{ type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
|
|
598
|
-
|
|
598
|
+
// Fork-handled rows keep the legacy ack machinery (ledger parity is a
|
|
599
|
+
// declared follow-up); by id — the precise form.
|
|
600
|
+
dispatchId ? ackByIdEffect(ctx, dispatchId) : completeSourceEffect(ctx, sourceType, sourceId),
|
|
599
601
|
];
|
|
600
602
|
}
|
|
601
603
|
|
|
@@ -642,7 +644,7 @@ async function effectsForDispatch(
|
|
|
642
644
|
}
|
|
643
645
|
}
|
|
644
646
|
if (!spawnBlackHoled) {
|
|
645
|
-
const receivedSettled =
|
|
647
|
+
const receivedSettled = claimDispatchSource(ctx, sourceType, sourceId);
|
|
646
648
|
const childAse = await ensureChildSession(ctx, decision.childRef);
|
|
647
649
|
if (childAse) {
|
|
648
650
|
const results = await Promise.all([
|
|
@@ -688,7 +690,7 @@ async function effectsForDispatch(
|
|
|
688
690
|
// pending→received transition. It is deliberately NOT gated on the
|
|
689
691
|
// session resolving: a transient session failure must not also cost the
|
|
690
692
|
// indicator.
|
|
691
|
-
const receivedSettled =
|
|
693
|
+
const receivedSettled = claimDispatchSource(ctx, sourceType, sourceId);
|
|
692
694
|
let sessionId = await ensureSession(ctx);
|
|
693
695
|
if (sessionId) {
|
|
694
696
|
// The guard marker goes down BEFORE any session write: a stale
|
|
@@ -757,7 +759,7 @@ async function effectsForTypedDispatch(
|
|
|
757
759
|
data: DispatchData,
|
|
758
760
|
ctx: ConnectorContext,
|
|
759
761
|
): Promise<ConnectorEffect[]> {
|
|
760
|
-
// No
|
|
762
|
+
// No claimDispatchSource here ON PURPOSE: typed dispatches have no
|
|
761
763
|
// message-level activity indicator (DispatchResolvedData: "typed
|
|
762
764
|
// dispatches have no indicator"), so there is no pending ring to light —
|
|
763
765
|
// the typed input step below is the session panel's timeline entry.
|
|
@@ -767,11 +769,21 @@ async function effectsForTypedDispatch(
|
|
|
767
769
|
// Typed WorkItems ack by id, never by source: sibling rows (a task
|
|
768
770
|
// mutation's task_assign + task_update) share (source_type, source_id),
|
|
769
771
|
// and the source ack would terminally resolve work that was never emitted.
|
|
770
|
-
const ack = () =>
|
|
771
|
-
|
|
772
|
+
const ack = (): ConnectorEffect[] => {
|
|
773
|
+
if (!dispatchId) {
|
|
774
|
+
// No WorkItem id (malformed payload): the by-source form would
|
|
775
|
+
// terminally resolve SIBLING typed rows sharing this source — leave
|
|
776
|
+
// the row pending for the sweep instead.
|
|
777
|
+
console.error(
|
|
778
|
+
`[parel-channel] typed dispatch without id (${sourceType}:${sourceId}) left pending`,
|
|
779
|
+
);
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
return [ackByIdEffect(ctx, dispatchId)];
|
|
783
|
+
};
|
|
772
784
|
const plan = await planTypedDispatch(data, ctx);
|
|
773
785
|
if (plan.outcome === 'pending') return [];
|
|
774
|
-
if (plan.outcome === 'stale') return
|
|
786
|
+
if (plan.outcome === 'stale') return ack();
|
|
775
787
|
|
|
776
788
|
const envelopeId = dispatchId || `${sourceType}:${sourceId}`;
|
|
777
789
|
let sessionId = await ensureSession(ctx);
|
|
@@ -841,7 +853,7 @@ async function effectsForTypedDispatch(
|
|
|
841
853
|
...(Object.keys(typedContext).length > 0 ? { context: typedContext } : {}),
|
|
842
854
|
},
|
|
843
855
|
},
|
|
844
|
-
ack(),
|
|
856
|
+
...ack(),
|
|
845
857
|
];
|
|
846
858
|
}
|
|
847
859
|
|
|
@@ -1002,7 +1014,11 @@ async function handleParallAgentEventInner(
|
|
|
1002
1014
|
// the absorbing turn's envelopeIds and settle here naturally. The
|
|
1003
1015
|
// server decides reply broad-cover vs no_action per batch; turn_failed
|
|
1004
1016
|
// sweeps the same way with the error step below as the visible record.
|
|
1005
|
-
await completeFencedEnvelopes(
|
|
1017
|
+
await completeFencedEnvelopes(
|
|
1018
|
+
ctx,
|
|
1019
|
+
event.envelopeIds,
|
|
1020
|
+
event.type === 'turn_failed' ? 'error' : 'ok',
|
|
1021
|
+
);
|
|
1006
1022
|
|
|
1007
1023
|
// Staleness guard: only idle if this event's turn covers the AGENT'S
|
|
1008
1024
|
// latest emitted envelope (the marker is agent-global — all chats share
|
package/src/session.ts
CHANGED
|
@@ -590,12 +590,14 @@ export async function createTraceStep(
|
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
593
|
+
* Terminally drop ONE source's live WorkItem via the unified complete's
|
|
594
|
+
* source form (no covering reply Effect exists for a dropped row, so the
|
|
595
|
+
* server sweeps it to no_action — the drop semantics; idempotent). Typed as
|
|
596
|
+
* the fetch effect so it can ride BOTH hook channels (ConnectorEffect[] and
|
|
597
|
+
* AgentEventEffect[]). Replaces the legacy by-source ack for drop cases —
|
|
598
|
+
* that endpoint retires at S3 (dispatch-convergence-design.md §3).
|
|
597
599
|
*/
|
|
598
|
-
export function
|
|
600
|
+
export function completeSourceEffect(
|
|
599
601
|
ctx: ConnectorContext,
|
|
600
602
|
sourceType: string,
|
|
601
603
|
sourceId: string,
|
|
@@ -603,15 +605,15 @@ export function ackEffect(
|
|
|
603
605
|
return {
|
|
604
606
|
type: 'fetch',
|
|
605
607
|
request: {
|
|
606
|
-
url: `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/
|
|
608
|
+
url: `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete`,
|
|
607
609
|
method: 'POST',
|
|
608
610
|
headers: {
|
|
609
611
|
Authorization: `Bearer ${requireAgk(ctx)}`,
|
|
610
612
|
'Content-Type': 'application/json',
|
|
611
613
|
},
|
|
612
|
-
body: JSON.stringify({ source_type: sourceType, source_id: sourceId }),
|
|
614
|
+
body: JSON.stringify({ sources: [{ source_type: sourceType, source_id: sourceId }] }),
|
|
613
615
|
},
|
|
614
|
-
idempotencyKey: `
|
|
616
|
+
idempotencyKey: `complete:src:${sourceType}:${sourceId}`,
|
|
615
617
|
};
|
|
616
618
|
}
|
|
617
619
|
|
|
@@ -643,31 +645,35 @@ export function ackByIdEffect(
|
|
|
643
645
|
}
|
|
644
646
|
|
|
645
647
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
648
|
+
* Claim the source's WorkItem before the turn (server folds the row into its
|
|
649
|
+
* message lane, flips it pending→received and broadcasts dispatch.received —
|
|
650
|
+
* the pending ring rides the claim side effect now; the retired received
|
|
651
|
+
* endpoint was a bare receipt). Best-effort like the receipt was: a refused
|
|
652
|
+
* or failed claim never blocks the emit — the row stays pending and the
|
|
653
|
+
* turn's complete resolves it just the same. Must COMPLETE before any
|
|
654
|
+
* settling effect is returned so a same-batch complete can't race the
|
|
655
|
+
* pending→received transition.
|
|
650
656
|
*/
|
|
651
|
-
export async function
|
|
657
|
+
export async function claimDispatchSource(
|
|
652
658
|
ctx: ConnectorContext,
|
|
653
659
|
sourceType: string,
|
|
654
660
|
sourceId: string,
|
|
655
661
|
): Promise<void> {
|
|
656
662
|
// Deliberately NOT gated on reportingEnabled: this is ledger surface, not
|
|
657
|
-
// session reporting — the URL needs no agent id, and
|
|
658
|
-
//
|
|
659
|
-
//
|
|
663
|
+
// session reporting — the URL needs no agent id, and the claim transition
|
|
664
|
+
// is what keeps the indicator lifecycle working even for agent-id-less old
|
|
665
|
+
// configs.
|
|
660
666
|
try {
|
|
661
|
-
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/
|
|
667
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/claim`;
|
|
662
668
|
const res = await fetch(url, {
|
|
663
669
|
method: 'POST',
|
|
664
670
|
headers: jsonHeaders(ctx),
|
|
665
671
|
body: JSON.stringify({ source_type: sourceType, source_id: sourceId }),
|
|
666
672
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
667
673
|
});
|
|
668
|
-
if (!res.ok) console.error(`[parel-channel]
|
|
674
|
+
if (!res.ok) console.error(`[parel-channel] claim source failed (${res.status})`);
|
|
669
675
|
} catch (err) {
|
|
670
|
-
console.error('[parel-channel]
|
|
676
|
+
console.error('[parel-channel] claim source failed', err);
|
|
671
677
|
}
|
|
672
678
|
}
|
|
673
679
|
|
|
@@ -735,6 +741,25 @@ export async function clearActiveEmit(
|
|
|
735
741
|
await ctx.store?.delete(activeEmitKey(sourceType, sourceId)).catch(() => {});
|
|
736
742
|
}
|
|
737
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Burn the fence after an ERROR close: keep the attempt id (so the re-driven
|
|
746
|
+
* emit's nextEnvelopeId picks a fresh `#r{n+1}` — a bare repeat of the base
|
|
747
|
+
* id could be deduped by parel and strand the retry) but zero the timestamp
|
|
748
|
+
* (so the redelivery-skip window is already over and the server's re-drive
|
|
749
|
+
* hint re-emits immediately instead of hiding behind a 30-min TTL).
|
|
750
|
+
*/
|
|
751
|
+
export async function burnActiveEmit(
|
|
752
|
+
ctx: ConnectorContext,
|
|
753
|
+
sourceType: string,
|
|
754
|
+
sourceId: string,
|
|
755
|
+
): Promise<void> {
|
|
756
|
+
const prior = await readActiveEmit(ctx, sourceType, sourceId);
|
|
757
|
+
if (!prior) return;
|
|
758
|
+
await ctx.store
|
|
759
|
+
?.set(activeEmitKey(sourceType, sourceId), { id: prior.id, at: 0 })
|
|
760
|
+
.catch(() => {});
|
|
761
|
+
}
|
|
762
|
+
|
|
738
763
|
/**
|
|
739
764
|
* Deterministic envelope id for a source: `{sourceType}:{sourceId}` on the
|
|
740
765
|
* first attempt, `…#r{n}` on re-emits. Attempt-suffixed ids are unique per
|
|
@@ -763,6 +788,9 @@ export function sourceFromEnvelopeId(
|
|
|
763
788
|
return { sourceType: bare.slice(0, i), sourceId: bare.slice(i + 1) };
|
|
764
789
|
}
|
|
765
790
|
|
|
791
|
+
/** Turn outcome for the ledger close — mirrors the server's turn_outcome. */
|
|
792
|
+
export type TurnOutcome = 'ok' | 'error';
|
|
793
|
+
|
|
766
794
|
/**
|
|
767
795
|
* Fence-checked turn-end ledger close for a set of envelope ids — the shared
|
|
768
796
|
* core of BOTH turn-end hooks: onAgentEvent (turn_completed/turn_failed, the
|
|
@@ -770,51 +798,70 @@ export function sourceFromEnvelopeId(
|
|
|
770
798
|
* never arrive and this is the ONLY terminal ledger hook — without it a
|
|
771
799
|
* received row would re-emit on every TTL sweep forever). Only sources whose
|
|
772
800
|
* `active:{source}` store row matches the envelope id EXACTLY are completed
|
|
773
|
-
* (attempt fence)
|
|
774
|
-
*
|
|
801
|
+
* (attempt fence). Both hooks running is harmless: whichever completes first
|
|
802
|
+
* consumes the fence, the other skips.
|
|
803
|
+
*
|
|
804
|
+
* Fence disposal follows the outcome: an "ok" close clears the fences (the
|
|
805
|
+
* sources are terminal); an "error" close BURNS them instead — the server
|
|
806
|
+
* released the uncovered members back to pending and its re-drive hint
|
|
807
|
+
* re-emits them, so the burned fence must neither hide the retry behind the
|
|
808
|
+
* redelivery-skip TTL nor let it reuse a dedupe-prone envelope id.
|
|
775
809
|
*/
|
|
776
810
|
export async function completeFencedEnvelopes(
|
|
777
811
|
ctx: ConnectorContext,
|
|
778
812
|
envelopeIds: string[],
|
|
813
|
+
outcome: TurnOutcome = 'ok',
|
|
779
814
|
): Promise<void> {
|
|
780
815
|
const fenced: { source_type: string; source_id: string }[] = [];
|
|
781
816
|
for (const envId of envelopeIds) {
|
|
782
817
|
const src = sourceFromEnvelopeId(envId);
|
|
783
818
|
if (!src) continue; // pre-alignment envelope id (dsp_*) — old rows were acked on emit
|
|
784
819
|
const active = await readActiveEmit(ctx, src.sourceType, src.sourceId);
|
|
785
|
-
|
|
820
|
+
// A BURNED fence (at === 0) still carries the attempt id but the attempt
|
|
821
|
+
// is already error-closed — its members are back in pending awaiting the
|
|
822
|
+
// re-drive. The redundant deliver()-side close for the same envelope
|
|
823
|
+
// must skip here, or its "ok" would no_action-sweep the very rows the
|
|
824
|
+
// error close just released (the retry hole S3a closes).
|
|
825
|
+
if (active?.id === envId && active.at > 0) {
|
|
786
826
|
fenced.push({ source_type: src.sourceType, source_id: src.sourceId });
|
|
787
827
|
}
|
|
788
828
|
}
|
|
789
|
-
if (fenced.length > 0 && (await completeSources(ctx, fenced))) {
|
|
829
|
+
if (fenced.length > 0 && (await completeSources(ctx, fenced, outcome))) {
|
|
790
830
|
for (const src of fenced) {
|
|
791
|
-
|
|
831
|
+
if (outcome === 'error') {
|
|
832
|
+
await burnActiveEmit(ctx, src.source_type, src.source_id);
|
|
833
|
+
} else {
|
|
834
|
+
await clearActiveEmit(ctx, src.source_type, src.source_id);
|
|
835
|
+
}
|
|
792
836
|
}
|
|
793
837
|
}
|
|
794
838
|
}
|
|
795
839
|
|
|
796
840
|
/**
|
|
797
|
-
* Turn-end ledger close: resolves the turn's folded sources on the server
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
841
|
+
* Turn-end ledger close: resolves the turn's folded sources on the server via
|
|
842
|
+
* the unified complete's source form (POST /dispatch/complete, idempotent) —
|
|
843
|
+
* broad-cover to the turn's reply Effect when one exists; otherwise a
|
|
844
|
+
* no_action sweep on "ok", or a release back to pending on the redrive budget
|
|
845
|
+
* on "error" (a failed turn must not silently consume work it never handled).
|
|
846
|
+
* One immediate retry on failure; a still-failed call leaves the rows live
|
|
847
|
+
* and the catch-up TTL re-emit self-heals.
|
|
802
848
|
*/
|
|
803
849
|
export async function completeSources(
|
|
804
850
|
ctx: ConnectorContext,
|
|
805
851
|
sources: { source_type: string; source_id: string }[],
|
|
852
|
+
outcome: TurnOutcome = 'ok',
|
|
806
853
|
): Promise<boolean> {
|
|
807
|
-
// Ledger surface — not gated on reportingEnabled (see
|
|
854
|
+
// Ledger surface — not gated on reportingEnabled (see claimDispatchSource).
|
|
808
855
|
if (sources.length === 0) return true;
|
|
809
|
-
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete
|
|
856
|
+
const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete`;
|
|
810
857
|
const attempt = async (): Promise<boolean> => {
|
|
811
858
|
const res = await fetch(url, {
|
|
812
859
|
method: 'POST',
|
|
813
860
|
headers: jsonHeaders(ctx),
|
|
814
|
-
body: JSON.stringify({ sources }),
|
|
861
|
+
body: JSON.stringify({ sources, turn_outcome: outcome }),
|
|
815
862
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
816
863
|
});
|
|
817
|
-
if (!res.ok) console.error(`[parel-channel] complete
|
|
864
|
+
if (!res.ok) console.error(`[parel-channel] complete (sources) failed (${res.status})`);
|
|
818
865
|
return res.ok;
|
|
819
866
|
};
|
|
820
867
|
try {
|
|
@@ -825,7 +872,7 @@ export async function completeSources(
|
|
|
825
872
|
try {
|
|
826
873
|
return await attempt();
|
|
827
874
|
} catch (err) {
|
|
828
|
-
console.error('[parel-channel] complete
|
|
875
|
+
console.error('[parel-channel] complete (sources) failed', err);
|
|
829
876
|
return false;
|
|
830
877
|
}
|
|
831
878
|
}
|