@parall/parel-channel 1.41.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/inbound.js CHANGED
@@ -3,7 +3,7 @@ import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect
3
3
  import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
4
4
  import { buildForkScopePrefix, bumpForkRetryAttempts, childConfirmed, childRefKnown, clearForkState, clearForkSubject, FORK_RETRY_DELAY_MS, FORK_RETRY_PREFIX, handleChildSpawnFailed, MAX_SPAWN_VERIFY_ATTEMPTS, noteMainTurnEvent, planForkDecision, recordSpawn, settleChildEvent, trackFollowUpAck, } from './fork.js';
5
5
  import { asChildSpawnFailed, asConnectorEffects, } from './parel-sdk-compat.js';
6
- import { ackByIdEffect, ackEffect, conversationKey, createErrorStep, fetchMainSessionFresh, createInputStep, createTraceStep, createTypedInputStep, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, markDispatchReceived, patchSession, reportingEnabled, SESSION_CACHE_KEY, TURN_ENVELOPE_KEY, } from './session.js';
6
+ import { ackByIdEffect, ackEffect, completeFencedEnvelopes, conversationKey, createErrorStep, fetchMainSessionFresh, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, markDispatchReceived, nextEnvelopeId, patchSession, readActiveEmit, reportingEnabled, SESSION_CACHE_KEY, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
7
7
  import { isTypedEventType, planTypedDispatch } from './typed-dispatch.js';
8
8
  /**
9
9
  * Connection-local hook serialization. The parel ConnectorHost dispatches
@@ -212,6 +212,25 @@ async function catchUpParallInner(ctx) {
212
212
  }
213
213
  return effects;
214
214
  }
215
+ /**
216
+ * Turn-in-flight guard for redelivery (catch-up sweep AND repeated
217
+ * dispatch.new pushes). Main-path rows stay live (not acked) for the whole
218
+ * turn now, and GET /dispatch lists pending AND received rows — so redelivery
219
+ * re-sees every in-flight source. Skip any row whose attempt fence is younger
220
+ * than EMIT_TTL_MS, REGARDLESS of row status: a transiently-failed
221
+ * /dispatch/received leaves the row `pending` while the emit succeeded (fence
222
+ * written), and re-emitting it inside the TTL would duplicate the in-flight
223
+ * turn — complete-sources resolves pending rows just as well, so the fence
224
+ * alone is the truth. Past the TTL (crashed emit, lost turn events) fall
225
+ * through and re-emit with a fresh attempt-suffixed envelope id. Brand-new
226
+ * sources have no fence and never skip; fork-handled rows never write a
227
+ * fence (their ack machinery owns redelivery there).
228
+ * Design: parel-reply-contract-alignment.md §5.3.
229
+ */
230
+ async function shouldSkipLiveRow(ctx, sourceType, sourceId) {
231
+ const active = await readActiveEmit(ctx, sourceType, sourceId);
232
+ return active !== null && ctx.now() - active.at < EMIT_TTL_MS;
233
+ }
215
234
  async function effectsForDispatch(data, ctx) {
216
235
  const sourceType = String(data.source_type ?? '');
217
236
  const sourceId = data.source_id ? String(data.source_id) : undefined;
@@ -220,7 +239,9 @@ async function effectsForDispatch(data, ctx) {
220
239
  if (sourceType === 'channel_message' && sourceId) {
221
240
  // External IM message (chat_id is NULL by design — the conversation
222
241
  // lives on the provider, not in a Parall chat).
223
- return effectsForChannelDispatch(sourceId, dispatchId, ctx);
242
+ if (await shouldSkipLiveRow(ctx, sourceType, sourceId))
243
+ return [];
244
+ return effectsForChannelDispatch(sourceId, ctx);
224
245
  }
225
246
  const eventType = String(data.event_type ?? '');
226
247
  if (sourceId && isTypedEventType(eventType)) {
@@ -233,10 +254,13 @@ async function effectsForDispatch(data, ctx) {
233
254
  return [];
234
255
  }
235
256
  // Conversational-only deployments (no sandbox → no `parall` CLI) cannot
236
- // act on CLI-only typed work — emitting AND acking it would silently
237
- // consume real work. Keep it pending (same posture as unknown types);
238
- // approval_decided is exempt (it lives in a chat, replies auto-deliver).
239
- if (!sandboxEnabled(ctx) && eventType !== 'approval_decided') {
257
+ // act on typed work AT ALL under the CLI-only reply contract — nothing
258
+ // auto-delivers anymore, approval_decided included — so emitting AND
259
+ // acking would silently consume real work. Keep it pending (same
260
+ // posture as unknown types). Defensive-only: the server's mandatory
261
+ // sandbox gate (422 PAREL_SANDBOX_REQUIRED) means no such managed
262
+ // agent can be provisioned.
263
+ if (!sandboxEnabled(ctx)) {
240
264
  return [];
241
265
  }
242
266
  return effectsForTypedDispatch(data, ctx);
@@ -252,6 +276,8 @@ async function effectsForDispatch(data, ctx) {
252
276
  // A message dispatch with no chat can never be routed or replied to.
253
277
  return [ackEffect(ctx, sourceType, sourceId)];
254
278
  }
279
+ if (await shouldSkipLiveRow(ctx, sourceType, sourceId))
280
+ return [];
255
281
  const apiUrl = parallApiUrl(ctx);
256
282
  const agk = requireAgk(ctx);
257
283
  const orgId = parallOrgId(ctx);
@@ -306,7 +332,11 @@ async function effectsForDispatch(data, ctx) {
306
332
  text,
307
333
  attachments,
308
334
  });
309
- const envelopeId = dispatchId || `${sourceType}:${sourceId}`;
335
+ // Deterministic source-derived envelope id (+ attempt suffix on re-emits):
336
+ // complete-sources strips the suffix back to the source, and every re-emit
337
+ // is globally unique so parel's dedupe (either way) can't strand a retry.
338
+ const priorEmit = await readActiveEmit(ctx, sourceType, sourceId);
339
+ const envelopeId = nextEnvelopeId(sourceType, sourceId, priorEmit);
310
340
  // Input-step summary must never be empty (attachment-only messages).
311
341
  const stepText = text || `[${attachments.length} attachment(s)]`;
312
342
  // Thread-aware conversation identity: F2 grouping / F3 injection / fork
@@ -321,9 +351,16 @@ async function effectsForDispatch(data, ctx) {
321
351
  // Per-turn invocation context: parel snapshots this at turn start and
322
352
  // consume-gated plugins receive it — sandbox-e2b flattens it into per-exec
323
353
  // env, so `parall` CLI calls inside the sandbox land on the right chat.
354
+ // The PRLL_DISPATCH_SOURCE_* pair is the CLI's by-source ledger binding: a
355
+ // send back into the exact trigger (chat, thread) carries dispatch_source
356
+ // so the server resolves this WorkItem in the reply's own transaction.
357
+ // (Fork turns get it too — their rows are legacy-acked so the resolve is a
358
+ // no-op today, and it wires up for free when fork ledger parity lands.)
324
359
  const invocationContext = {
325
360
  PRLL_CHAT_ID: chatId,
326
361
  PRLL_TRIGGER_MESSAGE_ID: sourceId,
362
+ PRLL_DISPATCH_SOURCE_TYPE: sourceType,
363
+ PRLL_DISPATCH_SOURCE_ID: sourceId,
327
364
  ...(threadRootId ? { PRLL_THREAD_ROOT_ID: threadRootId } : {}),
328
365
  };
329
366
  const envelope = {
@@ -494,15 +531,16 @@ async function effectsForDispatch(data, ctx) {
494
531
  }
495
532
  // fall through: black-holed spawn rides main below.
496
533
  }
497
- // Main path. Progress reporting (best-effort,
498
- // parall-runtime-indicator-plan.md §3): received lights the pending ring,
499
- // the active+trigger patch arms the done signal, the input step flips the
500
- // ring to processing. received MUST settle before this function returns —
501
- // the ack effect below moves the dispatch row out of 'pending', and
502
- // MarkReceived only transitions pending rows. It is deliberately NOT gated
503
- // on the session resolving: it only touches the dispatch row, and a
504
- // transient session failure must not also cost the pending ring (a later
505
- // successful turn's idle clears agent-level pendings).
534
+ // Main path. Progress reporting (best-effort): received flips the row out
535
+ // of 'pending' and lights the indicator (dispatch.received → active); the
536
+ // row then STAYS 'received' for the whole turn — resolution comes from the
537
+ // CLI reply's dispatch_source or the turn event's complete-sources, never
538
+ // from an ack here (the old ack-on-emit is what killed the indicator
539
+ // early; parel-reply-contract-alignment.md §1.1). received still settles
540
+ // before this function returns so a same-batch reply can never race the
541
+ // pending→received transition. It is deliberately NOT gated on the
542
+ // session resolving: a transient session failure must not also cost the
543
+ // indicator.
506
544
  const receivedSettled = markDispatchReceived(ctx, sourceType, sourceId);
507
545
  let sessionId = await ensureSession(ctx);
508
546
  if (sessionId) {
@@ -540,7 +578,20 @@ async function effectsForDispatch(data, ctx) {
540
578
  }
541
579
  }
542
580
  await receivedSettled;
543
- return [{ type: 'emitEvent', event: envelope }, ackEffect(ctx, sourceType, sourceId)];
581
+ // Arm the attempt fence BEFORE the emit effect is returned: turn events
582
+ // complete a source only while `active:{source}` matches their envelope id
583
+ // exactly, and the redelivery-skip window starts at this timestamp. Only
584
+ // the main path writes it — fork paths keep their own ack machinery.
585
+ // Deliberate ordering tradeoff: a crash between this write and the host
586
+ // executing the emit leaves an armed fence for an un-emitted source, so
587
+ // the reconnect sweep hides it until the TTL re-emit (≤30min delay). The
588
+ // reverse order (fence after emit) would instead double-emit on a crash —
589
+ // a duplicate agent turn. Delay heals; duplicates don't.
590
+ await writeActiveEmit(ctx, sourceType, sourceId, envelopeId);
591
+ // No ack effect: the row stays 'received' until the reply (dispatch
592
+ // _source) or the turn event (complete-sources) resolves it — the
593
+ // indicator's whole lifecycle rides on that.
594
+ return [{ type: 'emitEvent', event: envelope }];
544
595
  }
545
596
  /**
546
597
  * Typed (non-message) dispatch → main-session envelope. The heavy lifting
@@ -613,9 +664,11 @@ async function effectsForTypedDispatch(data, ctx) {
613
664
  replyRoute: {
614
665
  kind: 'provider_http',
615
666
  connectionId: ctx.connectionId,
616
- // approval_decided replies deliver to its chat; every other typed
617
- // event has no delivery target (deliver() only runs the redundant
618
- // idle off envelopeId) — the agent acts via its sandbox CLI.
667
+ // Replies are CLI-only for every typed event (nothing
668
+ // auto-delivers). chatId — only approval_decided has one — merely
669
+ // routes deliver()'s suppressed turn-end audit step to the chat;
670
+ // chat-less routes carry just the envelopeId for the redundant
671
+ // ledger close + idle.
619
672
  data: plan.chatId
620
673
  ? { chatId: plan.chatId, messageId: sourceId, envelopeId }
621
674
  : { envelopeId },
@@ -765,6 +818,20 @@ async function handleParallAgentEventInner(event, ctx) {
765
818
  if (!chatId)
766
819
  return [];
767
820
  if (event.type === 'turn_completed' || event.type === 'turn_failed') {
821
+ // Ledger close FIRST, fenced per source — deliberately NOT behind the
822
+ // agent-global marker guard below: that guard protects the shared
823
+ // session's idle (last-emit-wins), while the ledger rows are
824
+ // per-source facts that must close whenever THEIR envelope is the one
825
+ // this turn consumed (a concurrent conversation-B emit must not leave
826
+ // conversation-A's resolution hanging). The attempt fence inside
827
+ // completeFencedEnvelopes drops late events from a superseded attempt
828
+ // (the TTL re-emit took the source over) AND fork-handled envelopes
829
+ // (no fence written), so it can never close the in-flight retry's
830
+ // work nor double-close fork acks. F3-absorbed envelopes appear in
831
+ // the absorbing turn's envelopeIds and settle here naturally. The
832
+ // server decides reply broad-cover vs no_action per batch; turn_failed
833
+ // sweeps the same way with the error step below as the visible record.
834
+ await completeFencedEnvelopes(ctx, event.envelopeIds);
768
835
  // Staleness guard: only idle if this event's turn covers the AGENT'S
769
836
  // latest emitted envelope (the marker is agent-global — all chats share
770
837
  // the one main session, so a done signal for chat A's turn must not
package/dist/session.d.ts CHANGED
@@ -242,4 +242,71 @@ export declare function ackByIdEffect(ctx: ConnectorContext, dispatchId: string)
242
242
  * means the ring never appears.
243
243
  */
244
244
  export declare function markDispatchReceived(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<void>;
245
+ /**
246
+ * How long an emitted source may stay unresolved before the reconnect sweep
247
+ * re-emits it (crash / lost turn_completed self-heal — the successor of the
248
+ * retired turnwatch timer). Must comfortably exceed any legitimate turn.
249
+ */
250
+ export declare const EMIT_TTL_MS: number;
251
+ /** The current emit attempt for a source: its envelope id + emit time. */
252
+ export type ActiveEmit = {
253
+ id: string;
254
+ at: number;
255
+ };
256
+ /**
257
+ * Per-source attempt fence. Written before emit, cleared by the matching
258
+ * turn event's complete: a turn event only closes a source whose stored id
259
+ * EXACTLY matches one of its envelopeIds — a late turn_completed from a
260
+ * superseded attempt (the source was re-emitted after the TTL) must not
261
+ * close the in-flight retry's work (codex P2 attempt fence).
262
+ */
263
+ export declare function readActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<ActiveEmit | null>;
264
+ export declare function writeActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string, envelopeId: string): Promise<void>;
265
+ export declare function clearActiveEmit(ctx: ConnectorContext, sourceType: string, sourceId: string): Promise<void>;
266
+ /**
267
+ * Deterministic envelope id for a source: `{sourceType}:{sourceId}` on the
268
+ * first attempt, `…#r{n}` on re-emits. Attempt-suffixed ids are unique per
269
+ * attempt, so a re-emit is a NEW envelope to parel regardless of any dedupe
270
+ * behavior — the retry always produces a fresh turn.
271
+ */
272
+ export declare function nextEnvelopeId(sourceType: string, sourceId: string, prior: ActiveEmit | null): string;
273
+ /** envelope id → source pointer (strips the attempt suffix). */
274
+ export declare function sourceFromEnvelopeId(envelopeId: string): {
275
+ sourceType: string;
276
+ sourceId: string;
277
+ } | null;
278
+ /**
279
+ * Fence-checked turn-end ledger close for a set of envelope ids — the shared
280
+ * core of BOTH turn-end hooks: onAgentEvent (turn_completed/turn_failed, the
281
+ * primary) and deliver (the no-observe npm-fallback path, where turn events
282
+ * never arrive and this is the ONLY terminal ledger hook — without it a
283
+ * received row would re-emit on every TTL sweep forever). Only sources whose
284
+ * `active:{source}` store row matches the envelope id EXACTLY are completed
285
+ * (attempt fence); the fence rows clear on success. Both hooks running is
286
+ * harmless: whichever completes first consumes the fence, the other skips.
287
+ */
288
+ export declare function completeFencedEnvelopes(ctx: ConnectorContext, envelopeIds: string[]): Promise<void>;
289
+ /**
290
+ * Turn-end ledger close: resolves the turn's folded sources on the server —
291
+ * broad-cover to the turn's reply Effect when one exists, no_action sweep
292
+ * otherwise (the server decides; POST /dispatch/complete-sources, idempotent).
293
+ * One immediate retry on failure; a still-failed call leaves the rows
294
+ * received and the catch-up TTL re-emit self-heals.
295
+ */
296
+ export declare function completeSources(ctx: ConnectorContext, sources: {
297
+ source_type: string;
298
+ source_id: string;
299
+ }[]): Promise<boolean>;
300
+ /**
301
+ * Turn-end plain text as a suppressed audit step — the runtime-standard
302
+ * Layer 0 posture (agent-dm-loop-prevention.md): recorded on the session,
303
+ * never delivered to the chat. No `projection` field, so the server's
304
+ * projection gate keeps it panel-only; `suppressed: true` is the content
305
+ * marker the standard bridges write for undelivered text.
306
+ */
307
+ export declare function createSuppressedTextStep(ctx: ConnectorContext, sessionId: string, args: {
308
+ subject: string;
309
+ deliveryId: string;
310
+ text: string;
311
+ }): Promise<ReportResult>;
245
312
  //# sourceMappingURL=session.d.ts.map
@@ -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
@@ -573,8 +573,10 @@ export function ackByIdEffect(ctx, dispatchId) {
573
573
  * means the ring never appears.
574
574
  */
575
575
  export async function markDispatchReceived(ctx, sourceType, sourceId) {
576
- if (!reportingEnabled(ctx))
577
- return;
576
+ // Deliberately NOT gated on reportingEnabled: this is ledger surface, not
577
+ // session reporting — the URL needs no agent id, and with ack-on-emit gone
578
+ // the received transition is what keeps the reconnect sweep's TTL window
579
+ // (and the indicator lifecycle) working even for agent-id-less old configs.
578
580
  try {
579
581
  const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/received`;
580
582
  const res = await fetch(url, {
@@ -590,3 +592,165 @@ export async function markDispatchReceived(ctx, sourceType, sourceId) {
590
592
  console.error('[parel-channel] mark received failed', err);
591
593
  }
592
594
  }
595
+ // ── By-source ledger surface ────────────────────────────
596
+ //
597
+ // The connector consumes dispatches without claiming lanes (parel's own
598
+ // queue serializes turns), so its ledger writes address WorkItems by source
599
+ // pointer, and its liveness bookkeeping is a per-source attempt fence in the
600
+ // durable store. Design: parel-reply-contract-alignment.md §5.3.
601
+ /**
602
+ * How long an emitted source may stay unresolved before the reconnect sweep
603
+ * re-emits it (crash / lost turn_completed self-heal — the successor of the
604
+ * retired turnwatch timer). Must comfortably exceed any legitimate turn.
605
+ */
606
+ export const EMIT_TTL_MS = 30 * 60 * 1000;
607
+ function activeEmitKey(sourceType, sourceId) {
608
+ return `active:${sourceType}:${sourceId}`;
609
+ }
610
+ /**
611
+ * Per-source attempt fence. Written before emit, cleared by the matching
612
+ * turn event's complete: a turn event only closes a source whose stored id
613
+ * EXACTLY matches one of its envelopeIds — a late turn_completed from a
614
+ * superseded attempt (the source was re-emitted after the TTL) must not
615
+ * close the in-flight retry's work (codex P2 attempt fence).
616
+ */
617
+ export async function readActiveEmit(ctx, sourceType, sourceId) {
618
+ const raw = await ctx.store?.get(activeEmitKey(sourceType, sourceId)).catch(() => null);
619
+ if (raw &&
620
+ typeof raw === 'object' &&
621
+ typeof raw.id === 'string' &&
622
+ typeof raw.at === 'number') {
623
+ return raw;
624
+ }
625
+ return null;
626
+ }
627
+ export async function writeActiveEmit(ctx, sourceType, sourceId, envelopeId) {
628
+ await ctx.store
629
+ ?.set(activeEmitKey(sourceType, sourceId), { id: envelopeId, at: ctx.now() })
630
+ .catch(() => { });
631
+ }
632
+ export async function clearActiveEmit(ctx, sourceType, sourceId) {
633
+ await ctx.store?.delete(activeEmitKey(sourceType, sourceId)).catch(() => { });
634
+ }
635
+ /**
636
+ * Deterministic envelope id for a source: `{sourceType}:{sourceId}` on the
637
+ * first attempt, `…#r{n}` on re-emits. Attempt-suffixed ids are unique per
638
+ * attempt, so a re-emit is a NEW envelope to parel regardless of any dedupe
639
+ * behavior — the retry always produces a fresh turn.
640
+ */
641
+ export function nextEnvelopeId(sourceType, sourceId, prior) {
642
+ const base = `${sourceType}:${sourceId}`;
643
+ if (!prior)
644
+ return base;
645
+ const m = prior.id.match(/#r(\d+)$/);
646
+ const attempt = m ? Number(m[1]) + 1 : 1;
647
+ return `${base}#r${attempt}`;
648
+ }
649
+ /** envelope id → source pointer (strips the attempt suffix). */
650
+ export function sourceFromEnvelopeId(envelopeId) {
651
+ const bare = envelopeId.replace(/#r\d+$/, '');
652
+ const i = bare.indexOf(':');
653
+ if (i <= 0 || i === bare.length - 1)
654
+ return null;
655
+ return { sourceType: bare.slice(0, i), sourceId: bare.slice(i + 1) };
656
+ }
657
+ /**
658
+ * Fence-checked turn-end ledger close for a set of envelope ids — the shared
659
+ * core of BOTH turn-end hooks: onAgentEvent (turn_completed/turn_failed, the
660
+ * primary) and deliver (the no-observe npm-fallback path, where turn events
661
+ * never arrive and this is the ONLY terminal ledger hook — without it a
662
+ * received row would re-emit on every TTL sweep forever). Only sources whose
663
+ * `active:{source}` store row matches the envelope id EXACTLY are completed
664
+ * (attempt fence); the fence rows clear on success. Both hooks running is
665
+ * harmless: whichever completes first consumes the fence, the other skips.
666
+ */
667
+ export async function completeFencedEnvelopes(ctx, envelopeIds) {
668
+ const fenced = [];
669
+ for (const envId of envelopeIds) {
670
+ const src = sourceFromEnvelopeId(envId);
671
+ if (!src)
672
+ continue; // pre-alignment envelope id (dsp_*) — old rows were acked on emit
673
+ const active = await readActiveEmit(ctx, src.sourceType, src.sourceId);
674
+ if (active?.id === envId) {
675
+ fenced.push({ source_type: src.sourceType, source_id: src.sourceId });
676
+ }
677
+ }
678
+ if (fenced.length > 0 && (await completeSources(ctx, fenced))) {
679
+ for (const src of fenced) {
680
+ await clearActiveEmit(ctx, src.source_type, src.source_id);
681
+ }
682
+ }
683
+ }
684
+ /**
685
+ * Turn-end ledger close: resolves the turn's folded sources on the server —
686
+ * broad-cover to the turn's reply Effect when one exists, no_action sweep
687
+ * otherwise (the server decides; POST /dispatch/complete-sources, idempotent).
688
+ * One immediate retry on failure; a still-failed call leaves the rows
689
+ * received and the catch-up TTL re-emit self-heals.
690
+ */
691
+ export async function completeSources(ctx, sources) {
692
+ // Ledger surface — not gated on reportingEnabled (see markDispatchReceived).
693
+ if (sources.length === 0)
694
+ return true;
695
+ const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/dispatch/complete-sources`;
696
+ const attempt = async () => {
697
+ const res = await fetch(url, {
698
+ method: 'POST',
699
+ headers: jsonHeaders(ctx),
700
+ body: JSON.stringify({ sources }),
701
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
702
+ });
703
+ if (!res.ok)
704
+ console.error(`[parel-channel] complete-sources failed (${res.status})`);
705
+ return res.ok;
706
+ };
707
+ try {
708
+ if (await attempt())
709
+ return true;
710
+ }
711
+ catch {
712
+ // fall through to the single immediate retry
713
+ }
714
+ try {
715
+ return await attempt();
716
+ }
717
+ catch (err) {
718
+ console.error('[parel-channel] complete-sources failed', err);
719
+ return false;
720
+ }
721
+ }
722
+ /**
723
+ * Turn-end plain text as a suppressed audit step — the runtime-standard
724
+ * Layer 0 posture (agent-dm-loop-prevention.md): recorded on the session,
725
+ * never delivered to the chat. No `projection` field, so the server's
726
+ * projection gate keeps it panel-only; `suppressed: true` is the content
727
+ * marker the standard bridges write for undelivered text.
728
+ */
729
+ export async function createSuppressedTextStep(ctx, sessionId, args) {
730
+ try {
731
+ const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions/${sessionId}/steps`;
732
+ // Same subject-prefix line the other step writers draw: chv_ turns must
733
+ // not target 'chat' or the broadcast lands on a channel no client watches.
734
+ const targetType = args.subject.startsWith('chv_') ? 'channel_conversation' : 'chat';
735
+ const res = await fetch(url, {
736
+ method: 'POST',
737
+ headers: jsonHeaders(ctx),
738
+ body: JSON.stringify({
739
+ step_type: 'text',
740
+ target_type: targetType,
741
+ target_id: args.subject,
742
+ idempotency_key: `text:${args.deliveryId}`,
743
+ content: { text: args.text, suppressed: true },
744
+ }),
745
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
746
+ });
747
+ if (res.ok)
748
+ return 'ok';
749
+ console.error(`[parel-channel] suppressed text step failed (${res.status})`);
750
+ return resultForStatus(res.status);
751
+ }
752
+ catch (err) {
753
+ console.error('[parel-channel] suppressed text step failed', err);
754
+ return 'failed';
755
+ }
756
+ }
@@ -19,13 +19,15 @@ import type { ConnectorContext } from '@parel/plugin-sdk';
19
19
  * attached_to_uri legitimately revokes access to a delivered run snapshot;
20
20
  * docs/primitives/schedule.md § Snapshot 不变性) — warn + ack + drop.
21
21
  *
22
- * Reply semantics differ from chat messages ON PURPOSE: a typed turn's
23
- * turn-end text has no chat to deliver to (deliver() only runs the redundant
24
- * idle), and the agent acts through its sandbox CLI instead — identical to
25
- * the standard runtimes, where typed events are answered with CLI actions,
26
- * not auto-delivered text. approval_decided is the one exception: it lives
27
- * in a chat, so its plan carries the chatId and the reply auto-delivers
28
- * there like any chat message.
22
+ * Reply semantics are uniform with the rest of the connector: NOTHING
23
+ * auto-delivers (the CLI-only reply contract,
24
+ * parel-reply-contract-alignment.md). A typed turn's turn-end text is never
25
+ * posted anywhere; the agent acts through its sandbox CLI — identical to
26
+ * the standard runtimes, where typed events are answered with CLI actions.
27
+ * approval_decided differs only in ROUTING metadata: it lives in a chat, so
28
+ * its plan carries the chatId — that anchors the suppressed turn-end audit
29
+ * step and tells the agent where to `parall messages send`; the reply
30
+ * itself is still an explicit CLI send.
29
31
  */
30
32
  export interface TypedDispatchData {
31
33
  id?: unknown;
@@ -55,7 +57,11 @@ export interface TypedEventPlan {
55
57
  /** Input-step content bits. */
56
58
  triggerType: string;
57
59
  summary: string;
58
- /** Set only for approval_decided: the reply auto-delivers to this chat. */
60
+ /**
61
+ * Set only for approval_decided: anchors the suppressed turn-end audit
62
+ * step and tells the agent where to `parall messages send` — replies are
63
+ * CLI-only, nothing auto-delivers.
64
+ */
59
65
  chatId?: string;
60
66
  }
61
67
  export type TypedDispatchResult = TypedEventPlan
@@ -1 +1 @@
1
- {"version":3,"file":"typed-dispatch.d.ts","sourceRoot":"","sources":["../src/typed-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAI1D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,kIAQpB,CAAC;AAEX,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,mBAAmB,GAC3B,cAAc;AAChB,2CAA2C;GACzC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE;AACtB,gEAAgE;GAC9D;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC;AAY3B;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,iBAAiB,EACvB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,mBAAmB,CAAC,CAwC9B"}
1
+ {"version":3,"file":"typed-dispatch.d.ts","sourceRoot":"","sources":["../src/typed-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAI1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,kIAQpB,CAAC;AAEX,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,mBAAmB,GAC3B,cAAc;AAChB,2CAA2C;GACzC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE;AACtB,gEAAgE;GAC9D;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC;AAY3B;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,iBAAiB,EACvB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,mBAAmB,CAAC,CAwC9B"}
@@ -140,8 +140,9 @@ async function planTask(ctx, taskId, eventType, actor) {
140
140
  };
141
141
  }
142
142
  /**
143
- * schedule.fire / external_trigger turns have NO delivery target — the
144
- * managed-agent base behavior ("your reply auto-delivers") does not apply,
143
+ * schedule.fire / external_trigger turns have NO conversation attached at
144
+ * all — plain text output is never delivered anywhere (the CLI-only reply
145
+ * contract), and unlike chat turns there is no obvious chat to send into,
145
146
  * so without this line a normal completion silently disappears.
146
147
  */
147
148
  function cliOnlyOutputHint(kind) {
@@ -334,7 +335,8 @@ async function planApprovalDecided(ctx, approvalId, chatId, actor) {
334
335
  return {
335
336
  outcome: 'emit',
336
337
  // The decision belongs to the chat conversation it happened in — group
337
- // and correlate there, and let the turn-end reply deliver to that chat.
338
+ // and correlate there; a follow-up reply is the agent's explicit CLI
339
+ // send into that chat (turn-end text never auto-delivers).
338
340
  subject: resolvedChat ?? approval.id,
339
341
  body: lines.join('\n'),
340
342
  stepTarget: resolvedChat ? { type: 'chat', id: resolvedChat } : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parel-channel",
3
- "version": "1.41.0",
3
+ "version": "1.42.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": {
@@ -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 turn-end reply is
9
- * auto-delivered to the right chat by replyRoute (deliver()), and sandbox CLI
10
- * calls get the right PRLL_CHAT_ID via per-turn invocation context.
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