@parall/parel-channel 1.39.0 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;AAgD3B,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAuDrF;AAkdD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA6C7B"}
1
+ {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AAiD3B,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5B;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAuDrF;AAkdD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyD7B"}
package/dist/inbound.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { buildChannelPrompt, buildChatPrompt } from './channel-prompt.js';
2
2
  import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
3
- import { createChannelInputStep, createInputStep, createTraceStep, ensureSession, invalidateSession, markDispatchReceived, patchSession, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, } from './session.js';
3
+ import { createChannelInputStep, createErrorStep, createInputStep, createTraceStep, ensureSession, invalidateSession, markDispatchReceived, patchSession, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, } from './session.js';
4
4
  export async function handleParallFrame(frame, ctx) {
5
5
  let event;
6
6
  try {
@@ -525,10 +525,22 @@ export async function handleParallAgentEvent(event, ctx) {
525
525
  }
526
526
  const sessionId = await ensureSession(ctx);
527
527
  if (sessionId) {
528
+ // Error step BEFORE the idle patch so the panel orders failure →
529
+ // done; the step is what makes a failed turn visible at all (the
530
+ // reply never arrives — see createErrorStep).
531
+ let stale = false;
532
+ if (event.type === 'turn_failed') {
533
+ const stepResult = await createErrorStep(ctx, sessionId, {
534
+ subject: chatId,
535
+ turnId: event.turnId,
536
+ error: event.error,
537
+ });
538
+ stale = stepResult === 'stale';
539
+ }
528
540
  const result = await patchSession(ctx, sessionId, { status: 'idle' });
529
541
  // A closed session is already terminal (frontend shows done) — just
530
542
  // drop the cache so the next dispatch resolves a fresh one.
531
- if (result === 'stale')
543
+ if (stale || result === 'stale')
532
544
  await invalidateSession(ctx);
533
545
  }
534
546
  return [];
package/dist/session.d.ts CHANGED
@@ -87,6 +87,37 @@ export declare function createChannelInputStep(ctx: ConnectorContext, sessionId:
87
87
  text: string;
88
88
  sentAt?: string;
89
89
  }): Promise<ReportResult>;
90
+ /**
91
+ * Credential redaction for runtime error strings that become user-visible
92
+ * step content. Two layers: every secret VALUE the connector actually holds
93
+ * (`ctx.secrets`) is replaced exactly — the strongest guarantee for the
94
+ * secrets in this connector's scope — then credential-shaped patterns are
95
+ * masked (parall key prefixes, common vendor prefixes, AWS access key ids,
96
+ * bearer tokens, and any long unbroken token as a fallback: credentials are
97
+ * long random strings, while prose and paths break on spaces and slashes
98
+ * first). Pattern matching can't catch every conceivable secret (e.g. a
99
+ * short password echoed by a foreign exception), which is the accepted
100
+ * trade-off for keeping the error text diagnosable — the audience is the
101
+ * org-internal session panel.
102
+ */
103
+ export declare function redactSecrets(s: string, knownValues?: string[]): string;
104
+ /**
105
+ * turn_failed → an error step in the session panel, following the
106
+ * cross-runtime error-step contract (text step + content.status:'error',
107
+ * same shape agent-core's dispatch-failure step uses — pinned by
108
+ * ts/protocol-vectors/agent-steps.json). Without this the turn's failure is
109
+ * invisible on the parall side: the 2026-07-05 parel regression was fully
110
+ * silent because this connector dropped event.error.
111
+ *
112
+ * The error string comes from parel's runtime (exception messages) — length
113
+ * is unbounded and it can echo credential-shaped values, so it is redacted
114
+ * (see redactSecrets) and truncated before leaving the connector.
115
+ */
116
+ export declare function createErrorStep(ctx: ConnectorContext, sessionId: string, args: {
117
+ subject: string;
118
+ turnId: string;
119
+ error: string;
120
+ }): Promise<ReportResult>;
90
121
  /**
91
122
  * E2 step-trace events → parall AgentSteps, shaped exactly like agent-core's
92
123
  * createRuntimeStep so the session panel renders them identically to CC's.
@@ -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
@@ -194,6 +194,78 @@ export async function createChannelInputStep(ctx, sessionId, args) {
194
194
  return 'failed';
195
195
  }
196
196
  }
197
+ const ERROR_TEXT_MAX = 500;
198
+ /**
199
+ * Credential redaction for runtime error strings that become user-visible
200
+ * step content. Two layers: every secret VALUE the connector actually holds
201
+ * (`ctx.secrets`) is replaced exactly — the strongest guarantee for the
202
+ * secrets in this connector's scope — then credential-shaped patterns are
203
+ * masked (parall key prefixes, common vendor prefixes, AWS access key ids,
204
+ * bearer tokens, and any long unbroken token as a fallback: credentials are
205
+ * long random strings, while prose and paths break on spaces and slashes
206
+ * first). Pattern matching can't catch every conceivable secret (e.g. a
207
+ * short password echoed by a foreign exception), which is the accepted
208
+ * trade-off for keeping the error text diagnosable — the audience is the
209
+ * org-internal session panel.
210
+ */
211
+ export function redactSecrets(s, knownValues = []) {
212
+ let out = s;
213
+ for (const v of knownValues) {
214
+ // Skip tiny values: replacing e.g. a 3-char string would shred prose.
215
+ if (typeof v === 'string' && v.length >= 6)
216
+ out = out.split(v).join('***');
217
+ }
218
+ return out
219
+ .replace(/\b(agk|mck|cpk)_[A-Za-z0-9_-]+/g, '$1_***')
220
+ .replace(/\b(sk|pk|rk)-[A-Za-z0-9_-]{8,}/g, '$1-***')
221
+ .replace(/\bAKIA[0-9A-Z]{16}\b/g, 'AKIA***')
222
+ .replace(/\b(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, '$1***')
223
+ .replace(/[A-Za-z0-9_-]{32,}/g, '***');
224
+ }
225
+ /**
226
+ * turn_failed → an error step in the session panel, following the
227
+ * cross-runtime error-step contract (text step + content.status:'error',
228
+ * same shape agent-core's dispatch-failure step uses — pinned by
229
+ * ts/protocol-vectors/agent-steps.json). Without this the turn's failure is
230
+ * invisible on the parall side: the 2026-07-05 parel regression was fully
231
+ * silent because this connector dropped event.error.
232
+ *
233
+ * The error string comes from parel's runtime (exception messages) — length
234
+ * is unbounded and it can echo credential-shaped values, so it is redacted
235
+ * (see redactSecrets) and truncated before leaving the connector.
236
+ */
237
+ export async function createErrorStep(ctx, sessionId, args) {
238
+ const knownSecrets = Object.values((ctx.secrets ?? {})).filter((v) => typeof v === 'string');
239
+ const text = `Turn failed: ${redactSecrets(args.error, knownSecrets)}`.slice(0, ERROR_TEXT_MAX);
240
+ try {
241
+ const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions/${sessionId}/steps`;
242
+ // Same subject-prefix line createTraceStep draws: chv_ turns must not
243
+ // target 'chat' or the broadcast lands on a channel no client watches.
244
+ const targetType = args.subject.startsWith('chv_') ? 'channel_conversation' : 'chat';
245
+ const res = await fetch(url, {
246
+ method: 'POST',
247
+ headers: jsonHeaders(ctx),
248
+ body: JSON.stringify({
249
+ step_type: 'text',
250
+ target_type: targetType,
251
+ target_id: args.subject,
252
+ // At-most-once today (agent-event pushes are not replayed), but the
253
+ // turnId key makes a future retry path free of duplicate error rows.
254
+ idempotency_key: `error:${args.turnId}`,
255
+ content: { text, suppressed: false, status: 'error' },
256
+ }),
257
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
258
+ });
259
+ if (res.ok)
260
+ return 'ok';
261
+ console.error(`[parel-channel] error step failed (${res.status})`);
262
+ return resultForStatus(res.status);
263
+ }
264
+ catch (err) {
265
+ console.error('[parel-channel] error step failed', err);
266
+ return 'failed';
267
+ }
268
+ }
197
269
  // Subject-scoped: one connector serves many per-subject sessions, and
198
270
  // provider call ids are only unique within a turn — never let two subjects'
199
271
  // concurrent calls overwrite each other's parked name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parel-channel",
3
- "version": "1.39.0",
3
+ "version": "1.40.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/inbound.ts CHANGED
@@ -9,6 +9,7 @@ import { buildChannelPrompt, buildChatPrompt } from './channel-prompt.js';
9
9
  import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
10
10
  import {
11
11
  createChannelInputStep,
12
+ createErrorStep,
12
13
  createInputStep,
13
14
  createTraceStep,
14
15
  ensureSession,
@@ -638,10 +639,22 @@ export async function handleParallAgentEvent(
638
639
  }
639
640
  const sessionId = await ensureSession(ctx);
640
641
  if (sessionId) {
642
+ // Error step BEFORE the idle patch so the panel orders failure →
643
+ // done; the step is what makes a failed turn visible at all (the
644
+ // reply never arrives — see createErrorStep).
645
+ let stale = false;
646
+ if (event.type === 'turn_failed') {
647
+ const stepResult = await createErrorStep(ctx, sessionId, {
648
+ subject: chatId,
649
+ turnId: event.turnId,
650
+ error: event.error,
651
+ });
652
+ stale = stepResult === 'stale';
653
+ }
641
654
  const result = await patchSession(ctx, sessionId, { status: 'idle' });
642
655
  // A closed session is already terminal (frontend shows done) — just
643
656
  // drop the cache so the next dispatch resolves a fresh one.
644
- if (result === 'stale') await invalidateSession(ctx);
657
+ if (stale || result === 'stale') await invalidateSession(ctx);
645
658
  }
646
659
  return [];
647
660
  }
package/src/session.ts CHANGED
@@ -230,6 +230,84 @@ export async function createChannelInputStep(
230
230
  }
231
231
  }
232
232
 
233
+ const ERROR_TEXT_MAX = 500;
234
+
235
+ /**
236
+ * Credential redaction for runtime error strings that become user-visible
237
+ * step content. Two layers: every secret VALUE the connector actually holds
238
+ * (`ctx.secrets`) is replaced exactly — the strongest guarantee for the
239
+ * secrets in this connector's scope — then credential-shaped patterns are
240
+ * masked (parall key prefixes, common vendor prefixes, AWS access key ids,
241
+ * bearer tokens, and any long unbroken token as a fallback: credentials are
242
+ * long random strings, while prose and paths break on spaces and slashes
243
+ * first). Pattern matching can't catch every conceivable secret (e.g. a
244
+ * short password echoed by a foreign exception), which is the accepted
245
+ * trade-off for keeping the error text diagnosable — the audience is the
246
+ * org-internal session panel.
247
+ */
248
+ export function redactSecrets(s: string, knownValues: string[] = []): string {
249
+ let out = s;
250
+ for (const v of knownValues) {
251
+ // Skip tiny values: replacing e.g. a 3-char string would shred prose.
252
+ if (typeof v === 'string' && v.length >= 6) out = out.split(v).join('***');
253
+ }
254
+ return out
255
+ .replace(/\b(agk|mck|cpk)_[A-Za-z0-9_-]+/g, '$1_***')
256
+ .replace(/\b(sk|pk|rk)-[A-Za-z0-9_-]{8,}/g, '$1-***')
257
+ .replace(/\bAKIA[0-9A-Z]{16}\b/g, 'AKIA***')
258
+ .replace(/\b(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, '$1***')
259
+ .replace(/[A-Za-z0-9_-]{32,}/g, '***');
260
+ }
261
+
262
+ /**
263
+ * turn_failed → an error step in the session panel, following the
264
+ * cross-runtime error-step contract (text step + content.status:'error',
265
+ * same shape agent-core's dispatch-failure step uses — pinned by
266
+ * ts/protocol-vectors/agent-steps.json). Without this the turn's failure is
267
+ * invisible on the parall side: the 2026-07-05 parel regression was fully
268
+ * silent because this connector dropped event.error.
269
+ *
270
+ * The error string comes from parel's runtime (exception messages) — length
271
+ * is unbounded and it can echo credential-shaped values, so it is redacted
272
+ * (see redactSecrets) and truncated before leaving the connector.
273
+ */
274
+ export async function createErrorStep(
275
+ ctx: ConnectorContext,
276
+ sessionId: string,
277
+ args: { subject: string; turnId: string; error: string },
278
+ ): Promise<ReportResult> {
279
+ const knownSecrets = Object.values((ctx.secrets ?? {}) as Record<string, unknown>).filter(
280
+ (v): v is string => typeof v === 'string',
281
+ );
282
+ const text = `Turn failed: ${redactSecrets(args.error, knownSecrets)}`.slice(0, ERROR_TEXT_MAX);
283
+ try {
284
+ const url = `${parallApiUrl(ctx)}/api/v1/orgs/${parallOrgId(ctx)}/agents/${parallAgentId(ctx)}/sessions/${sessionId}/steps`;
285
+ // Same subject-prefix line createTraceStep draws: chv_ turns must not
286
+ // target 'chat' or the broadcast lands on a channel no client watches.
287
+ const targetType = args.subject.startsWith('chv_') ? 'channel_conversation' : 'chat';
288
+ const res = await fetch(url, {
289
+ method: 'POST',
290
+ headers: jsonHeaders(ctx),
291
+ body: JSON.stringify({
292
+ step_type: 'text',
293
+ target_type: targetType,
294
+ target_id: args.subject,
295
+ // At-most-once today (agent-event pushes are not replayed), but the
296
+ // turnId key makes a future retry path free of duplicate error rows.
297
+ idempotency_key: `error:${args.turnId}`,
298
+ content: { text, suppressed: false, status: 'error' },
299
+ }),
300
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
301
+ });
302
+ if (res.ok) return 'ok';
303
+ console.error(`[parel-channel] error step failed (${res.status})`);
304
+ return resultForStatus(res.status);
305
+ } catch (err) {
306
+ console.error('[parel-channel] error step failed', err);
307
+ return 'failed';
308
+ }
309
+ }
310
+
233
311
  // Subject-scoped: one connector serves many per-subject sessions, and
234
312
  // provider call ids are only unique within a turn — never let two subjects'
235
313
  // concurrent calls overwrite each other's parked name.