@parall/parall 1.51.0 → 1.52.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.
@@ -51594,6 +51594,7 @@ var ENDPOINTS = {
51594
51594
  DISPATCH_BY_MESSAGES: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/by-messages`,
51595
51595
  DISPATCH_CLAIM: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/claim`,
51596
51596
  DISPATCH_STEER: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/steer`,
51597
+ DISPATCH_INPUT_STATE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/input-state`,
51597
51598
  DISPATCH_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/complete`,
51598
51599
  DISPATCH_COMPLETE_SOURCES: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/complete-sources`,
51599
51600
  DISPATCH_RELEASE: (orgId) => `${API_BASE}/orgs/${orgId}/dispatch/release`,
@@ -51691,7 +51692,11 @@ var ENDPOINTS = {
51691
51692
  // Pinix Hub catalog proxy above is a different, id-less surface)
51692
51693
  ORG_CLIP_REGISTRY: (orgId) => `/api/v1/orgs/${orgId}/clip-registry`,
51693
51694
  ORG_CLIP_INSTALL: (orgId) => `/api/v1/orgs/${orgId}/clips/install`,
51694
- ORG_CLIPS_INSTALLED: (orgId) => `/api/v1/orgs/${orgId}/clips/installed`
51695
+ ORG_CLIPS_INSTALLED: (orgId) => `/api/v1/orgs/${orgId}/clips/installed`,
51696
+ // MCP clip server config (cap:clip-mcp; publisher-org only — cross-org gets
51697
+ // 403 MCP_CROSS_ORG_DISABLED on the whole family, reads included)
51698
+ ORG_CLIP_MCP_CONFIG: (orgId, clipId) => `/api/v1/orgs/${orgId}/clip-registry/${clipId}/mcp-config`,
51699
+ ORG_CLIP_MCP_TOOLS_REFRESH: (orgId, clipId) => `/api/v1/orgs/${orgId}/clip-registry/${clipId}/mcp-config/tools/refresh`
51695
51700
  };
51696
51701
  var WS_EVENTS = {
51697
51702
  // Client -> Server
@@ -52879,6 +52884,10 @@ var ParallClient = class _ParallClient {
52879
52884
  async steerDispatch(orgId, req) {
52880
52885
  return this.request("POST", ENDPOINTS.DISPATCH_STEER(orgId), req);
52881
52886
  }
52887
+ /** Advance exact runtime-input lifecycle for members of an explicit lane. */
52888
+ async updateDispatchInputState(orgId, req) {
52889
+ return this.request("POST", ENDPOINTS.DISPATCH_INPUT_STATE(orgId), req);
52890
+ }
52882
52891
  async completeDispatch(orgId, req) {
52883
52892
  return this.request("POST", ENDPOINTS.DISPATCH_COMPLETE(orgId), req);
52884
52893
  }
@@ -53844,12 +53853,17 @@ var ParallClient = class _ParallClient {
53844
53853
  * command RAN and failed — `error`/`error_code` describe why). Everything
53845
53854
  * else throws a typed {@link ApiError}; match on `err.code`:
53846
53855
  *
53847
- * Safe to retry (guaranteed nothing was dispatched):
53848
- * - `EDGE_ACTIVATING` 503 + `Retry-After` — cold cloud profile is starting.
53849
- * Bounded backoff, same `correlation_id` across the loop.
53850
- * - `EDGE_BUSY` 409the device is executing another request.
53851
- * - `EDGE_CONCURRENCY_LIMIT` 429 org at its concurrent-session limit.
53852
- * - `EDGE_UNAVAILABLE` 503session torn down / replaced mid-dispatch.
53856
+ * Safe to retry (provably not executed). Exactly three carry `Retry-After`
53857
+ * pacing ({@link ApiError.retryAfterSeconds}) `EDGE_ACTIVATING`,
53858
+ * `EDGE_BUSY`, `EDGE_CONCURRENCY_LIMIT`:
53859
+ * - `EDGE_ACTIVATING` 503cold cloud profile is starting. Answered before
53860
+ * any dispatch. Bounded backoff, same `correlation_id` across the loop.
53861
+ * - `EDGE_BUSY` 409the device is executing another request; the pod
53862
+ * refused this one before starting any script.
53863
+ * - `EDGE_CONCURRENCY_LIMIT` 429 — org at its concurrent-session limit,
53864
+ * answered before any dispatch.
53865
+ * - `EDGE_UNAVAILABLE` 503 — session torn down / replaced mid-dispatch. No
53866
+ * `Retry-After` (no slot to wait for — retry re-resolves routing).
53853
53867
  *
53854
53868
  * NOT retryable:
53855
53869
  * - `OUTCOME_UNKNOWN` 504 — dispatched, but no result arrived. The command
@@ -53888,6 +53902,60 @@ var ParallClient = class _ParallClient {
53888
53902
  async deleteClipConnection(orgId, connId) {
53889
53903
  return this.request("DELETE", ENDPOINTS.CLIP_CONNECTION(orgId, connId));
53890
53904
  }
53905
+ // ---- MCP clip server config (cap:clip-mcp; publisher-org only) ----
53906
+ /**
53907
+ * Read the redacted MCP config. 404 NOT_FOUND when the clip has none yet;
53908
+ * 403 MCP_CROSS_ORG_DISABLED for a cross-org installed clip (the whole
53909
+ * mcp-config family is publisher-org property, reads included). `version`
53910
+ * is the CAS token the mutations echo via If-Match.
53911
+ */
53912
+ async getClipMCPConfig(orgId, clipId) {
53913
+ return this.request("GET", ENDPOINTS.ORG_CLIP_MCP_CONFIG(orgId, clipId));
53914
+ }
53915
+ /**
53916
+ * Upsert the MCP config (human org-admin JWT only). Pass `expectedVersion`
53917
+ * from the last GET; omit it ONLY on first create (no config exists yet).
53918
+ * Save probes the remote server first — a probe failure persists nothing and
53919
+ * surfaces as MCP_URL_FORBIDDEN / MCP_AUTH_FAILED / MCP_SERVER_UNREACHABLE /
53920
+ * MCP_PROTOCOL_ERROR. Other typed conflicts: MCP_CONFIG_STALE (reload, then
53921
+ * retry with the current version), MCP_CONFIG_BUSY (another change in
53922
+ * flight — retry), MCP_CONNECTION_CONFLICT (a device-targeted default
53923
+ * connection must be unbound first), SECRETBOX_UNCONFIGURED (503 — the
53924
+ * server cannot store credentials safely).
53925
+ */
53926
+ async putClipMCPConfig(orgId, clipId, req, expectedVersion) {
53927
+ return this.request("PUT", ENDPOINTS.ORG_CLIP_MCP_CONFIG(orgId, clipId), req, void 0, false, {
53928
+ headers: expectedVersion ? { "If-Match": `"${expectedVersion}"` } : void 0,
53929
+ // The server probes the remote MCP server inside the request on a fixed
53930
+ // 30s budget; give the HTTP layer headroom past it so a slow-but-valid
53931
+ // save isn't chopped locally into a fake transport error.
53932
+ timeoutMs: 4e4
53933
+ });
53934
+ }
53935
+ /**
53936
+ * Delete the MCP config and every connection routed through it (one
53937
+ * transaction — no orphaned target-less connections). `expectedVersion` is
53938
+ * mandatory: deleting always mutates an existing config.
53939
+ */
53940
+ async deleteClipMCPConfig(orgId, clipId, expectedVersion) {
53941
+ return this.request("DELETE", ENDPOINTS.ORG_CLIP_MCP_CONFIG(orgId, clipId), void 0, void 0, false, { headers: { "If-Match": `"${expectedVersion}"` } });
53942
+ }
53943
+ /**
53944
+ * Re-run tools/list and replace the cached snapshot (org-admin only).
53945
+ * Deliberately does NOT advance the CAS version, so an in-flight edit in
53946
+ * another tab stays valid; on probe failure the old snapshot survives.
53947
+ */
53948
+ async refreshClipMCPTools(orgId, clipId) {
53949
+ return this.request(
53950
+ "POST",
53951
+ ENDPOINTS.ORG_CLIP_MCP_TOOLS_REFRESH(orgId, clipId),
53952
+ void 0,
53953
+ void 0,
53954
+ false,
53955
+ // Same fixed 30s server-side probe budget as PUT (see putClipMCPConfig).
53956
+ { timeoutMs: 4e4 }
53957
+ );
53958
+ }
53891
53959
  };
53892
53960
  function normalizeWikiChangeset(changeset) {
53893
53961
  return {
@@ -54293,10 +54361,221 @@ var ParallWs = class {
54293
54361
  }
54294
54362
  };
54295
54363
 
54364
+ // ../agent-core/dist/event-format.js
54365
+ function sanitizeMeta(value) {
54366
+ return value.replace(/[\r\n]+/g, " ").replace(/[[\]|]/g, " ").trim();
54367
+ }
54368
+ function buildEventBody(event) {
54369
+ const lines = [];
54370
+ if (event.type === "message") {
54371
+ lines.push(`[Event: message.new]`);
54372
+ const chatLabel = event.targetName ? `"${event.targetName}" (prll://${event.targetId})` : `prll://${event.targetId}`;
54373
+ lines.push(`[Chat: ${chatLabel} | type: ${event.targetType ?? "unknown"}]`);
54374
+ lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
54375
+ lines.push(`[Message ID: prll://${event.messageId}]`);
54376
+ if (event.threadRootId) {
54377
+ const threadMeta = [
54378
+ `prll://${event.threadRootId}`,
54379
+ event.threadReplyCount != null ? `${event.threadReplyCount} replies` : null,
54380
+ event.threadUnreadCount != null && event.threadUnreadCount > 0 ? `${event.threadUnreadCount} unread` : null,
54381
+ event.threadUnreadCount != null && event.threadUnreadCount > 0 && event.threadUnreadSince ? `since: prll://${event.threadUnreadSince}` : null
54382
+ ].filter(Boolean).join(" | ");
54383
+ lines.push(`[Thread: ${threadMeta}]`);
54384
+ }
54385
+ if (event.unreadCount != null && event.unreadCount > 1) {
54386
+ const countStr = event.unreadCount >= 1e3 ? "999+" : String(event.unreadCount);
54387
+ const sinceStr = event.unreadSince ? ` | since: prll://${event.unreadSince}` : "";
54388
+ let line = `[Unread: ${countStr} messages${sinceStr}]`;
54389
+ if (event.unreadCount > 50)
54390
+ line += ` \u2014 fetch recent context with --limit, not all`;
54391
+ lines.push(line);
54392
+ }
54393
+ if (event.noReply)
54394
+ lines.push(`[Hint: no_reply]`);
54395
+ if (event.attachments?.length) {
54396
+ for (const att of event.attachments) {
54397
+ const sizeStr = att.fileSize >= 1048576 ? `${(att.fileSize / 1048576).toFixed(1)}MB` : `${Math.round(att.fileSize / 1024)}KB`;
54398
+ lines.push(`[Attachment: prll://${att.id} | ${sanitizeMeta(att.mimeType)} | ${sizeStr} | ${sanitizeMeta(att.fileName)}]`);
54399
+ }
54400
+ }
54401
+ lines.push("", event.body);
54402
+ } else if (event.type === "task_comment") {
54403
+ lines.push(`[Event: task.comment.created]`);
54404
+ const taskLabel = event.targetName ? `${event.targetName} (prll://${event.targetId})` : `prll://${event.targetId}`;
54405
+ lines.push(`[Task: ${taskLabel}]`);
54406
+ if (event.deliveryReason)
54407
+ lines.push(`[Delivery: ${sanitizeMeta(event.deliveryReason)}]`);
54408
+ lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
54409
+ lines.push(`[Comment ID: prll://${event.messageId}]`);
54410
+ lines.push("", event.body);
54411
+ } else if (event.type === "wiki_comment") {
54412
+ lines.push(`[Event: wiki.comment.created]`);
54413
+ const target = event.replyTargetUri ?? `prll://${event.targetId}`;
54414
+ if (event.targetType === "changeset") {
54415
+ lines.push(`[Wiki Changeset: ${target}]`);
54416
+ } else {
54417
+ lines.push(`[Wiki: ${event.targetName ? `${sanitizeMeta(event.targetName)} (${target})` : target}]`);
54418
+ }
54419
+ if (event.deliveryReason)
54420
+ lines.push(`[Delivery: ${sanitizeMeta(event.deliveryReason)}]`);
54421
+ lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
54422
+ lines.push(`[Comment ID: prll://${event.messageId}]`);
54423
+ lines.push("", event.body);
54424
+ } else if (event.type === "approval") {
54425
+ lines.push(`[Event: approval.decided]`);
54426
+ lines.push(`[Approval: prll://${event.messageId}]`);
54427
+ lines.push(`[Chat: prll://${event.targetId}]`);
54428
+ lines.push(`[Decided by: ${event.senderName} (prll://${event.senderId})]`);
54429
+ lines.push("", event.body);
54430
+ } else if (event.type === "schedule") {
54431
+ lines.push(`[Event: schedule.fired]`);
54432
+ lines.push(`[Schedule: prll://${event.targetId}]`);
54433
+ lines.push(`[Run: prll://${event.messageId}]`);
54434
+ if (event.scheduledFireAt)
54435
+ lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
54436
+ if (event.attachedUri)
54437
+ lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
54438
+ lines.push("", event.body);
54439
+ } else if (event.type === "channel_message") {
54440
+ lines.push(`[Event: channel.message]`);
54441
+ const providerLabel = sanitizeMeta(event.channelProvider ?? "external IM");
54442
+ const convLabel = event.channelExternalConversationId ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? "conversation")})` : sanitizeMeta(event.channelConversationType ?? "conversation");
54443
+ lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
54444
+ lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
54445
+ if (event.channelExternalMessageId) {
54446
+ lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
54447
+ }
54448
+ lines.push(`[Audience: this conversation lives on ${providerLabel}, OUTSIDE Parall. Readers cannot open prll:// links, Parall cards, or internal attachments \u2014 never include them in replies. Write plain conversational text.]`);
54449
+ lines.push("", event.body);
54450
+ } else if (event.type === "external_trigger") {
54451
+ lines.push(`[Event: external.trigger]`);
54452
+ lines.push(`[Trigger: prll://${event.targetId}]`);
54453
+ lines.push(`[Run: prll://${event.messageId}]`);
54454
+ if (event.externalConnectionId) {
54455
+ const label = event.externalConnectionDisplayName ? `${sanitizeMeta(event.externalConnectionDisplayName)} (prll://${event.externalConnectionId})` : `prll://${event.externalConnectionId}`;
54456
+ lines.push(`[Connection: ${label}]`);
54457
+ }
54458
+ if (event.externalIngressEventId)
54459
+ lines.push(`[Ingress: prll://${event.externalIngressEventId}]`);
54460
+ if (event.attachedUri)
54461
+ lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
54462
+ if (event.externalConnectionSourceType) {
54463
+ lines.push(`[Source: ${sanitizeMeta(event.externalConnectionSourceType)}]`);
54464
+ }
54465
+ if (event.externalIngressEventType) {
54466
+ lines.push(`[External event: ${sanitizeMeta(event.externalIngressEventType)}]`);
54467
+ }
54468
+ lines.push("", event.body);
54469
+ } else {
54470
+ lines.push(`[Event: task.assigned]`);
54471
+ const taskLabel = event.targetName ? `${event.targetName} (prll://${event.targetId})` : `prll://${event.targetId}`;
54472
+ lines.push(`[Task: ${taskLabel}]`);
54473
+ lines.push(`[Assigned by: ${event.senderName} (prll://${event.senderId})]`);
54474
+ lines.push("", event.body);
54475
+ }
54476
+ return lines.join("\n") + buildSendMessageHint(event);
54477
+ }
54478
+ function buildEventBodyForForkResult(event) {
54479
+ return buildEventBody(event).replace(/\n<system-reminder>[\s\S]*<\/system-reminder>$/, "");
54480
+ }
54481
+ function buildSendMessageHint(event) {
54482
+ if (event.noReply)
54483
+ return "";
54484
+ if (event.type === "wiki_comment" && event.replyTargetUri) {
54485
+ const where = event.targetType === "changeset" ? "this changeset comment" : "this wiki page";
54486
+ return `
54487
+ <system-reminder>To reply on ${where}, run: \`parall comments add --target "${event.replyTargetUri}" --body "..."\` (read the thread first with \`parall comments list --target "${event.replyTargetUri}"\`). To message someone instead, use \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
54488
+ }
54489
+ if (event.targetId.startsWith("cht_")) {
54490
+ if (event.threadRootId) {
54491
+ return `
54492
+ <system-reminder>To reply in this thread, run \`parall messages send prll://${event.targetId} --thread-root-id ${sanitizeMeta(event.threadRootId)} --text-file - <<'EOF'\` \u2026 \`EOF\` \u2014 the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
54493
+ }
54494
+ return `
54495
+ <system-reminder>To reply, run \`parall messages send prll://${event.targetId} --text-file - <<'EOF'\` \u2026 \`EOF\` \u2014 the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
54496
+ }
54497
+ if (event.targetId.startsWith("tsk_")) {
54498
+ return `
54499
+ <system-reminder>To respond, use the CLI: \`parall tasks update\` / \`parall tasks comments add\`. To message someone, use \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
54500
+ }
54501
+ if (event.targetId.startsWith("sch_")) {
54502
+ return `
54503
+ <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
54504
+ }
54505
+ if (event.type === "channel_message") {
54506
+ if (event.channelCliCapable) {
54507
+ if (event.channelProvider === "slack") {
54508
+ const channelArg = event.channelExternalConversationId ? ` --channel "${event.channelExternalConversationId}"` : " --channel <conversation id from this event>";
54509
+ const replyTo = event.channelExternalMessageId ? ` --reply-to "${event.channelExternalMessageId}"` : "";
54510
+ return `
54511
+ <system-reminder>To reply, use the platform verb: \`parall slack send${channelArg}${replyTo} --text <your reply>\`. In channels --reply-to is REQUIRED (the reply lands in that message's thread); in DMs it is optional (DMs are linear). \`parall slack send\` is the ONLY outbound path \u2014 your plain text output is NOT delivered to the external conversation.</system-reminder>`;
54512
+ }
54513
+ if (!event.channelProvider) {
54514
+ return `
54515
+ <system-reminder>To reply, use the channel capability granted in your system prompt \u2014 for Feishu conversations that is \`lark-cli im\`, for Slack it is \`parall slack send\` (pass the message id from this event as --reply-to). That capability is the ONLY outbound path \u2014 your plain text output is NOT delivered to the external conversation.</system-reminder>`;
54516
+ }
54517
+ const convRef = event.channelExternalConversationId ? `chat_id "${event.channelExternalConversationId}"` : "the conversation id named in this event";
54518
+ const threadAlt = event.channelExternalMessageId ? ` To reply threaded to this specific message, reference message_id "${event.channelExternalMessageId}".` : "";
54519
+ return `
54520
+ <system-reminder>To reply, use the official Feishu CLI on your PATH: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned automatically).${threadAlt} lark-cli is the ONLY outbound path \u2014 your plain text output is NOT delivered to the external conversation.</system-reminder>`;
54521
+ }
54522
+ const platform = event.channelProvider ?? "the external platform";
54523
+ return `
54524
+ <system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
54525
+ }
54526
+ if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
54527
+ return `
54528
+ <system-reminder>This external trigger is incoming-only. Your plain text output is not sent back to the external provider. To communicate in Parall, use \`parall messages send\` / \`parall dm\`; provider-specific outbound actions require a separate capability.</system-reminder>`;
54529
+ }
54530
+ return "";
54531
+ }
54532
+ function buildForkScopePrefix(event) {
54533
+ const targetLabel = event.targetName ? `"${event.targetName}" (prll://${event.targetId})` : `prll://${event.targetId}`;
54534
+ return `[Fork scope: You are a parallel worker forked from the main session to handle ${targetLabel}. The conversation history above belongs to the main session's in-progress dispatch \u2014 do NOT continue it or complete it, or produce external output (comments, messages) for work visible there. The main session owns its own output.
54535
+
54536
+ Only act on the event below within your target scope. If your event reveals findings about the main session's work, report them in your summary \u2014 do not post them externally.
54537
+
54538
+ End with a brief "Fork summary" of actions taken and decisions made.]
54539
+
54540
+ `;
54541
+ }
54542
+ function buildForkResultPrefix(results) {
54543
+ if (!results.length)
54544
+ return "";
54545
+ const blocks = results.map((result) => {
54546
+ const lines = [];
54547
+ for (const body of result.eventBodies) {
54548
+ lines.push(body);
54549
+ }
54550
+ lines.push(`[This event was handled by a parallel fork session. Do NOT re-handle, re-reply, or duplicate work for it.]`);
54551
+ lines.push(`[Fork summary: ${result.agentSummary ? sanitizeMeta(result.agentSummary) : "No fork summary available \u2014 the fork completed without producing a text summary. Check the target chat/task for any actions the fork already took before acting."}]`);
54552
+ if (result.actions.length)
54553
+ lines.push(`[Fork actions: ${result.actions.join("; ")}]`);
54554
+ if (result.historyPath)
54555
+ lines.push(`[Fork history: ${result.historyPath}]`);
54556
+ return lines.join("\n");
54557
+ });
54558
+ return blocks.join("\n\n") + "\n\n---\n\n";
54559
+ }
54560
+
54296
54561
  // ../agent-core/dist/lane-ledger.js
54297
54562
  import * as fs from "node:fs";
54298
54563
  var LedgerUnsupportedError = class extends Error {
54299
54564
  };
54565
+ function bindLaneSession(lane, agentSessionId) {
54566
+ if (lane.sessionId && lane.sessionId !== agentSessionId) {
54567
+ lane.sessionAmbiguous = true;
54568
+ } else if (!lane.sessionAmbiguous) {
54569
+ lane.sessionId = agentSessionId;
54570
+ }
54571
+ }
54572
+ function releaseLocalMessageClaims(claims, sourceIds) {
54573
+ const released = sourceIds.filter((sourceId) => claims.delete(sourceId));
54574
+ return () => {
54575
+ for (const sourceId of released)
54576
+ claims.add(sourceId);
54577
+ };
54578
+ }
54300
54579
  function isStaleLane(err) {
54301
54580
  return err instanceof ApiError && err.status === 409 && err.code === "STALE_LANE";
54302
54581
  }
@@ -54346,7 +54625,8 @@ var LaneLedger = class {
54346
54625
  res = await this.opts.client.claimDispatch(this.opts.orgId, {
54347
54626
  target_uri: targetUri,
54348
54627
  thread_root_id: trigger.threadRootId,
54349
- limit: 100
54628
+ limit: 100,
54629
+ coverage_mode: this.opts.coverageMode ?? "implicit"
54350
54630
  });
54351
54631
  } catch (err) {
54352
54632
  if (isEndpointMissing(err))
@@ -54361,12 +54641,20 @@ var LaneLedger = class {
54361
54641
  }
54362
54642
  return null;
54363
54643
  }
54644
+ const requestedCoverage = this.opts.coverageMode ?? "implicit";
54645
+ const actualCoverage = res.coverage_mode ?? "implicit";
54646
+ if (requestedCoverage === "explicit" && actualCoverage !== "explicit") {
54647
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, res.lane).catch(() => {
54648
+ });
54649
+ throw new Error(`server did not negotiate explicit input coverage for ${targetUri}; lane released`);
54650
+ }
54364
54651
  const leaseUntilMs = Date.parse(res.lease_until ?? "");
54365
54652
  lane = {
54366
54653
  laneKey,
54367
54654
  lane: res.lane,
54368
54655
  targetUri,
54369
54656
  threadRootId: trigger.threadRootId,
54657
+ coverageMode: actualCoverage,
54370
54658
  folded: /* @__PURE__ */ new Map(),
54371
54659
  ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
54372
54660
  };
@@ -54408,9 +54696,10 @@ var LaneLedger = class {
54408
54696
  const laneKey = this.laneKeyFor(event);
54409
54697
  const lane = this.lanes.get(laneKey);
54410
54698
  if (!lane)
54411
- return false;
54412
- if (lane.folded.has(event.messageId))
54413
- return true;
54699
+ return null;
54700
+ if (lane.folded.has(event.messageId)) {
54701
+ return { inputLifecycle: this.inputLifecycle(lane, [event]) };
54702
+ }
54414
54703
  try {
54415
54704
  const res = await this.opts.client.steerDispatch(this.opts.orgId, {
54416
54705
  lane: lane.lane,
@@ -54419,16 +54708,64 @@ var LaneLedger = class {
54419
54708
  ...event.dispatchEventId ? { dispatch_event_id: event.dispatchEventId } : { source_type: "message", source_id: event.messageId }
54420
54709
  });
54421
54710
  lane.folded.set(event.messageId, res.dispatch_event_id);
54422
- return true;
54711
+ return { inputLifecycle: this.inputLifecycle(lane, [event]) };
54423
54712
  } catch (err) {
54424
54713
  if (isStaleLane(err)) {
54425
54714
  this.lanes.delete(laneKey);
54426
54715
  } else {
54427
54716
  this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
54428
54717
  }
54429
- return false;
54718
+ return null;
54430
54719
  }
54431
54720
  }
54721
+ /**
54722
+ * Build the exact runtime-input lifecycle for one prompt/injection. A
54723
+ * batched prompt covers every WorkItem represented in that single frame.
54724
+ */
54725
+ inputLifecycle(lane, events) {
54726
+ if (lane.coverageMode !== "explicit")
54727
+ return void 0;
54728
+ const dispatchEventIds = events.map((event) => lane.folded.get(event.messageId)).filter((id) => Boolean(id));
54729
+ if (dispatchEventIds.length !== events.length) {
54730
+ throw new Error(`explicit lane ${lane.lane} is missing a folded WorkItem mapping`);
54731
+ }
54732
+ return {
54733
+ deliveryKey: dispatchEventIds.join(","),
54734
+ dispatchEventIds,
54735
+ update: (state) => this.updateInputState(lane, dispatchEventIds, state)
54736
+ };
54737
+ }
54738
+ async updateInputState(lane, dispatchEventIds, state) {
54739
+ const failed = new Set(state === "failed" ? dispatchEventIds : []);
54740
+ const failedSourceIds = [...lane.folded].filter(([, dispatchEventId]) => failed.has(dispatchEventId)).map(([sourceId]) => sourceId);
54741
+ const restoreLocalClaims = failedSourceIds.length > 0 ? this.opts.releaseLocalClaims?.(failedSourceIds) : void 0;
54742
+ try {
54743
+ const result = await this.opts.client.updateDispatchInputState(this.opts.orgId, {
54744
+ lane: lane.lane,
54745
+ target_uri: lane.targetUri,
54746
+ thread_root_id: lane.threadRootId,
54747
+ dispatch_event_ids: dispatchEventIds,
54748
+ state
54749
+ });
54750
+ if (result.recognized !== dispatchEventIds.length) {
54751
+ throw new Error(`input lifecycle ${state} recognized ${result.recognized}/${dispatchEventIds.length} WorkItems`);
54752
+ }
54753
+ if (state === "failed" && result.released === 0) {
54754
+ restoreLocalClaims?.();
54755
+ return { retry: false };
54756
+ }
54757
+ } catch (err) {
54758
+ restoreLocalClaims?.();
54759
+ throw err;
54760
+ }
54761
+ if (state === "failed") {
54762
+ for (const [sourceId, dispatchEventId] of lane.folded) {
54763
+ if (failed.has(dispatchEventId))
54764
+ lane.folded.delete(sourceId);
54765
+ }
54766
+ }
54767
+ return { retry: state === "failed" };
54768
+ }
54432
54769
  /**
54433
54770
  * Complete the lane when no local work remains for it: the server sweeps
54434
54771
  * still-leased members as no_action, releases the occupancy row, and
@@ -54447,20 +54784,46 @@ var LaneLedger = class {
54447
54784
  if (lane)
54448
54785
  lane.turnError = true;
54449
54786
  }
54787
+ /**
54788
+ * Record that the turn on this lane ended on a self-healing usage limit.
54789
+ * Like markTurnError this is transport state for the lane's final
54790
+ * complete; an error bit set on the same lane outranks it.
54791
+ */
54792
+ markTurnDeferred(laneKey, info) {
54793
+ const lane = this.lanes.get(laneKey);
54794
+ if (lane)
54795
+ lane.turnDeferred = info;
54796
+ }
54797
+ /**
54798
+ * Sticky: the server rejected turn_outcome=deferred (predates it). Deferred
54799
+ * completes fall back to the error form for the rest of the process — the
54800
+ * members still release for retry, just on the redrive budget instead of
54801
+ * the reset-time schedule.
54802
+ */
54803
+ deferredUnsupported = false;
54450
54804
  async completeIfIdle(laneKey, hasMoreLocal) {
54451
54805
  const lane = this.lanes.get(laneKey);
54452
54806
  if (!lane || hasMoreLocal)
54453
54807
  return;
54454
54808
  this.lanes.delete(laneKey);
54455
54809
  this.removeLaneContext(lane);
54810
+ const deferred = !lane.turnError && !this.deferredUnsupported ? lane.turnDeferred : void 0;
54811
+ const outcome = lane.turnError ? "error" : lane.turnDeferred ? this.deferredUnsupported ? "error" : "deferred" : "ok";
54456
54812
  try {
54457
54813
  const res = await this.opts.client.completeDispatch(this.opts.orgId, {
54458
54814
  lane: lane.lane,
54459
54815
  target_uri: lane.targetUri,
54460
54816
  thread_root_id: lane.threadRootId,
54461
- // An error turn releases its members for retry instead of sweeping
54462
- // them as handled (ignored by older servers).
54463
- turn_outcome: lane.turnError ? "error" : "ok"
54817
+ // An error turn releases its members for retry; a deferred turn
54818
+ // re-delivers them at retry_at without burning redrive budget
54819
+ // (ignored by older servers, which 400 on the unknown enum — see the
54820
+ // fallback below).
54821
+ turn_outcome: outcome,
54822
+ ...deferred ? {
54823
+ outcome_class: deferred.outcomeClass,
54824
+ ...deferred.retryAt ? { retry_at: deferred.retryAt } : {}
54825
+ } : {},
54826
+ session_id: lane.sessionAmbiguous ? void 0 : lane.sessionId
54464
54827
  });
54465
54828
  if (res.swept_no_action > 0 || res.redriven) {
54466
54829
  this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
@@ -54470,6 +54833,23 @@ var LaneLedger = class {
54470
54833
  this.opts.log?.info(`lane complete skipped for ${lane.targetUri} \u2014 taken over`);
54471
54834
  return;
54472
54835
  }
54836
+ if (outcome === "deferred" && err instanceof ApiError && err.status === 400) {
54837
+ this.deferredUnsupported = true;
54838
+ this.opts.log?.warn(`server rejected turn_outcome=deferred for ${lane.targetUri} \u2014 falling back to error completes`);
54839
+ try {
54840
+ await this.opts.client.completeDispatch(this.opts.orgId, {
54841
+ lane: lane.lane,
54842
+ target_uri: lane.targetUri,
54843
+ thread_root_id: lane.threadRootId,
54844
+ turn_outcome: "error"
54845
+ });
54846
+ } catch (fallbackErr) {
54847
+ if (isStaleLane(fallbackErr))
54848
+ return;
54849
+ this.opts.log?.warn(`lane complete (deferred fallback) failed for ${lane.targetUri}: ${String(fallbackErr)}`);
54850
+ }
54851
+ return;
54852
+ }
54473
54853
  this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
54474
54854
  }
54475
54855
  }
@@ -54571,6 +54951,7 @@ var LaneLedger = class {
54571
54951
  laneKey: laneKeyForTarget(targetUri),
54572
54952
  lane: res.lane,
54573
54953
  targetUri,
54954
+ coverageMode: "implicit",
54574
54955
  folded: /* @__PURE__ */ new Map([[workItem.source_id, workItem.id]]),
54575
54956
  typedDispatchEventId: workItem.id,
54576
54957
  ...Number.isNaN(leaseUntilMs) ? {} : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 6e4) }
@@ -54610,6 +54991,16 @@ var LaneLedger = class {
54610
54991
  };
54611
54992
 
54612
54993
  // ../agent-core/dist/gateway-lane-flow.js
54994
+ async function steerLaneMessage(host, event) {
54995
+ const { laneLedger: ledger, opts } = host;
54996
+ const adapter = opts.dispatchAdapter;
54997
+ if (!ledger || !adapter.enqueueDuringDispatch)
54998
+ return;
54999
+ const folded = await ledger.steerLive(event);
55000
+ if (folded && await adapter.enqueueDuringDispatch(opts.runtimeKey, buildEventBody(event), folded.inputLifecycle)) {
55001
+ opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
55002
+ }
55003
+ }
54613
55004
  async function dispatchLaneGroup(host, opts) {
54614
55005
  const ledger = host.laneLedger;
54615
55006
  const event = opts.events[opts.events.length - 1];
@@ -54642,7 +55033,8 @@ async function dispatchLaneGroup(host, opts) {
54642
55033
  host.noteSessionLane(opts.sessionKey, lane.laneKey);
54643
55034
  let dispatched = false;
54644
55035
  try {
54645
- dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText);
55036
+ const inputLifecycle = ledger.inputLifecycle(lane, [...opts.earlier, event]);
55037
+ dispatched = await host.runDispatch(event, opts.sessionKey, opts.body, opts.earlier, opts.captureText, inputLifecycle);
54646
55038
  } catch (err) {
54647
55039
  host.noteSessionLane(opts.sessionKey, null);
54648
55040
  await ledger.release(lane.laneKey).catch(() => {
@@ -54655,8 +55047,16 @@ async function dispatchLaneGroup(host, opts) {
54655
55047
  if (!dispatched) {
54656
55048
  return "shutdown";
54657
55049
  }
54658
- if (host.consumeTurnError(opts.sessionKey)) {
54659
- ledger.markTurnError(lane.laneKey);
55050
+ const settled = host.consumeTurnOutcome(opts.sessionKey);
55051
+ if (settled) {
55052
+ if (settled.kind === "deferred") {
55053
+ ledger.markTurnDeferred(lane.laneKey, {
55054
+ outcomeClass: settled.outcomeClass,
55055
+ ...settled.retryAt ? { retryAt: settled.retryAt } : {}
55056
+ });
55057
+ } else {
55058
+ ledger.markTurnError(lane.laneKey);
55059
+ }
54660
55060
  for (const msgId of lane.folded.keys()) {
54661
55061
  host.dispatchedMessages.delete(msgId);
54662
55062
  }
@@ -54665,7 +55065,7 @@ async function dispatchLaneGroup(host, opts) {
54665
55065
  } catch {
54666
55066
  }
54667
55067
  await ledger.completeIfIdle(lane.laneKey, false);
54668
- return "failed";
55068
+ return settled.kind === "deferred" ? "deferred" : "failed";
54669
55069
  }
54670
55070
  const pendingInjections = host.opts.dispatchAdapter.hasPendingInjections?.(opts.sessionKey) ?? false;
54671
55071
  await ledger.completeIfIdle(lane.laneKey, pendingInjections || opts.hasMoreLocal());
@@ -54866,6 +55266,8 @@ async function consumeMessageWorkItem(host, item) {
54866
55266
  return;
54867
55267
  if (!host.tryClaimMessage(item.source_id))
54868
55268
  return;
55269
+ if (host.dispatchState.mainBuffer.some((event) => event.messageId === item.source_id))
55270
+ return;
54869
55271
  const ackItem = () => {
54870
55272
  if (host.laneLedger && !host.ledgerDisabled && !host.typedByIdCompleteUnsupported) {
54871
55273
  void resolveDispatchByID(host, item.id).then((outcome) => {
@@ -55036,203 +55438,6 @@ function routeTrigger(event, state, strategy = defaultRoutingStrategy) {
55036
55438
  return strategy(event, state);
55037
55439
  }
55038
55440
 
55039
- // ../agent-core/dist/event-format.js
55040
- function sanitizeMeta(value) {
55041
- return value.replace(/[\r\n]+/g, " ").replace(/[[\]|]/g, " ").trim();
55042
- }
55043
- function buildEventBody(event) {
55044
- const lines = [];
55045
- if (event.type === "message") {
55046
- lines.push(`[Event: message.new]`);
55047
- const chatLabel = event.targetName ? `"${event.targetName}" (prll://${event.targetId})` : `prll://${event.targetId}`;
55048
- lines.push(`[Chat: ${chatLabel} | type: ${event.targetType ?? "unknown"}]`);
55049
- lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
55050
- lines.push(`[Message ID: prll://${event.messageId}]`);
55051
- if (event.threadRootId) {
55052
- const threadMeta = [
55053
- `prll://${event.threadRootId}`,
55054
- event.threadReplyCount != null ? `${event.threadReplyCount} replies` : null,
55055
- event.threadUnreadCount != null && event.threadUnreadCount > 0 ? `${event.threadUnreadCount} unread` : null,
55056
- event.threadUnreadCount != null && event.threadUnreadCount > 0 && event.threadUnreadSince ? `since: prll://${event.threadUnreadSince}` : null
55057
- ].filter(Boolean).join(" | ");
55058
- lines.push(`[Thread: ${threadMeta}]`);
55059
- }
55060
- if (event.unreadCount != null && event.unreadCount > 1) {
55061
- const countStr = event.unreadCount >= 1e3 ? "999+" : String(event.unreadCount);
55062
- const sinceStr = event.unreadSince ? ` | since: prll://${event.unreadSince}` : "";
55063
- let line = `[Unread: ${countStr} messages${sinceStr}]`;
55064
- if (event.unreadCount > 50)
55065
- line += ` \u2014 fetch recent context with --limit, not all`;
55066
- lines.push(line);
55067
- }
55068
- if (event.noReply)
55069
- lines.push(`[Hint: no_reply]`);
55070
- if (event.attachments?.length) {
55071
- for (const att of event.attachments) {
55072
- const sizeStr = att.fileSize >= 1048576 ? `${(att.fileSize / 1048576).toFixed(1)}MB` : `${Math.round(att.fileSize / 1024)}KB`;
55073
- lines.push(`[Attachment: prll://${att.id} | ${sanitizeMeta(att.mimeType)} | ${sizeStr} | ${sanitizeMeta(att.fileName)}]`);
55074
- }
55075
- }
55076
- lines.push("", event.body);
55077
- } else if (event.type === "task_comment") {
55078
- lines.push(`[Event: task.comment.created]`);
55079
- const taskLabel = event.targetName ? `${event.targetName} (prll://${event.targetId})` : `prll://${event.targetId}`;
55080
- lines.push(`[Task: ${taskLabel}]`);
55081
- if (event.deliveryReason)
55082
- lines.push(`[Delivery: ${sanitizeMeta(event.deliveryReason)}]`);
55083
- lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
55084
- lines.push(`[Comment ID: prll://${event.messageId}]`);
55085
- lines.push("", event.body);
55086
- } else if (event.type === "wiki_comment") {
55087
- lines.push(`[Event: wiki.comment.created]`);
55088
- const target = event.replyTargetUri ?? `prll://${event.targetId}`;
55089
- if (event.targetType === "changeset") {
55090
- lines.push(`[Wiki Changeset: ${target}]`);
55091
- } else {
55092
- lines.push(`[Wiki: ${event.targetName ? `${sanitizeMeta(event.targetName)} (${target})` : target}]`);
55093
- }
55094
- if (event.deliveryReason)
55095
- lines.push(`[Delivery: ${sanitizeMeta(event.deliveryReason)}]`);
55096
- lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
55097
- lines.push(`[Comment ID: prll://${event.messageId}]`);
55098
- lines.push("", event.body);
55099
- } else if (event.type === "approval") {
55100
- lines.push(`[Event: approval.decided]`);
55101
- lines.push(`[Approval: prll://${event.messageId}]`);
55102
- lines.push(`[Chat: prll://${event.targetId}]`);
55103
- lines.push(`[Decided by: ${event.senderName} (prll://${event.senderId})]`);
55104
- lines.push("", event.body);
55105
- } else if (event.type === "schedule") {
55106
- lines.push(`[Event: schedule.fired]`);
55107
- lines.push(`[Schedule: prll://${event.targetId}]`);
55108
- lines.push(`[Run: prll://${event.messageId}]`);
55109
- if (event.scheduledFireAt)
55110
- lines.push(`[Scheduled at: ${sanitizeMeta(event.scheduledFireAt)}]`);
55111
- if (event.attachedUri)
55112
- lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
55113
- lines.push("", event.body);
55114
- } else if (event.type === "channel_message") {
55115
- lines.push(`[Event: channel.message]`);
55116
- const providerLabel = sanitizeMeta(event.channelProvider ?? "external IM");
55117
- const convLabel = event.channelExternalConversationId ? `${sanitizeMeta(event.channelExternalConversationId)} (${sanitizeMeta(event.channelConversationType ?? "conversation")})` : sanitizeMeta(event.channelConversationType ?? "conversation");
55118
- lines.push(`[Channel: ${providerLabel} | conversation: ${convLabel}]`);
55119
- lines.push(`[From: ${sanitizeMeta(event.senderName)} (external user, not a Parall member)]`);
55120
- if (event.channelExternalMessageId) {
55121
- lines.push(`[External message ID: ${sanitizeMeta(event.channelExternalMessageId)}]`);
55122
- }
55123
- lines.push(`[Audience: this conversation lives on ${providerLabel}, OUTSIDE Parall. Readers cannot open prll:// links, Parall cards, or internal attachments \u2014 never include them in replies. Write plain conversational text.]`);
55124
- lines.push("", event.body);
55125
- } else if (event.type === "external_trigger") {
55126
- lines.push(`[Event: external.trigger]`);
55127
- lines.push(`[Trigger: prll://${event.targetId}]`);
55128
- lines.push(`[Run: prll://${event.messageId}]`);
55129
- if (event.externalConnectionId) {
55130
- const label = event.externalConnectionDisplayName ? `${sanitizeMeta(event.externalConnectionDisplayName)} (prll://${event.externalConnectionId})` : `prll://${event.externalConnectionId}`;
55131
- lines.push(`[Connection: ${label}]`);
55132
- }
55133
- if (event.externalIngressEventId)
55134
- lines.push(`[Ingress: prll://${event.externalIngressEventId}]`);
55135
- if (event.attachedUri)
55136
- lines.push(`[Attached: ${sanitizeMeta(event.attachedUri)}]`);
55137
- if (event.externalConnectionSourceType) {
55138
- lines.push(`[Source: ${sanitizeMeta(event.externalConnectionSourceType)}]`);
55139
- }
55140
- if (event.externalIngressEventType) {
55141
- lines.push(`[External event: ${sanitizeMeta(event.externalIngressEventType)}]`);
55142
- }
55143
- lines.push("", event.body);
55144
- } else {
55145
- lines.push(`[Event: task.assigned]`);
55146
- const taskLabel = event.targetName ? `${event.targetName} (prll://${event.targetId})` : `prll://${event.targetId}`;
55147
- lines.push(`[Task: ${taskLabel}]`);
55148
- lines.push(`[Assigned by: ${event.senderName} (prll://${event.senderId})]`);
55149
- lines.push("", event.body);
55150
- }
55151
- return lines.join("\n") + buildSendMessageHint(event);
55152
- }
55153
- function buildEventBodyForForkResult(event) {
55154
- return buildEventBody(event).replace(/\n<system-reminder>[\s\S]*<\/system-reminder>$/, "");
55155
- }
55156
- function buildSendMessageHint(event) {
55157
- if (event.noReply)
55158
- return "";
55159
- if (event.type === "wiki_comment" && event.replyTargetUri) {
55160
- const where = event.targetType === "changeset" ? "this changeset comment" : "this wiki page";
55161
- return `
55162
- <system-reminder>To reply on ${where}, run: \`parall comments add --target "${event.replyTargetUri}" --body "..."\` (read the thread first with \`parall comments list --target "${event.replyTargetUri}"\`). To message someone instead, use \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
55163
- }
55164
- if (event.targetId.startsWith("cht_")) {
55165
- if (event.threadRootId) {
55166
- return `
55167
- <system-reminder>To reply in this thread, run \`parall messages send prll://${event.targetId} --thread-root-id ${sanitizeMeta(event.threadRootId)} --text-file - <<'EOF'\` \u2026 \`EOF\` \u2014 the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
55168
- }
55169
- return `
55170
- <system-reminder>To reply, run \`parall messages send prll://${event.targetId} --text-file - <<'EOF'\` \u2026 \`EOF\` \u2014 the quoted heredoc keeps \`$\`, backticks and apostrophes literal (plain \`--text "$1,000"\` sends \`,000\`). Your plain text output is not delivered to the chat.</system-reminder>`;
55171
- }
55172
- if (event.targetId.startsWith("tsk_")) {
55173
- return `
55174
- <system-reminder>To respond, use the CLI: \`parall tasks update\` / \`parall tasks comments add\`. To message someone, use \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
55175
- }
55176
- if (event.targetId.startsWith("sch_")) {
55177
- return `
55178
- <system-reminder>To communicate, use the CLI: \`parall messages send\` / \`parall dm\`. Your plain text output is not delivered.</system-reminder>`;
55179
- }
55180
- if (event.type === "channel_message") {
55181
- if (event.channelCliCapable) {
55182
- if (event.channelProvider === "slack") {
55183
- const channelArg = event.channelExternalConversationId ? ` --channel "${event.channelExternalConversationId}"` : " --channel <conversation id from this event>";
55184
- const replyTo = event.channelExternalMessageId ? ` --reply-to "${event.channelExternalMessageId}"` : "";
55185
- return `
55186
- <system-reminder>To reply, use the platform verb: \`parall slack send${channelArg}${replyTo} --text <your reply>\`. In channels --reply-to is REQUIRED (the reply lands in that message's thread); in DMs it is optional (DMs are linear). \`parall slack send\` is the ONLY outbound path \u2014 your plain text output is NOT delivered to the external conversation.</system-reminder>`;
55187
- }
55188
- if (!event.channelProvider) {
55189
- return `
55190
- <system-reminder>To reply, use the channel capability granted in your system prompt \u2014 for Feishu conversations that is \`lark-cli im\`, for Slack it is \`parall slack send\` (pass the message id from this event as --reply-to). That capability is the ONLY outbound path \u2014 your plain text output is NOT delivered to the external conversation.</system-reminder>`;
55191
- }
55192
- const convRef = event.channelExternalConversationId ? `chat_id "${event.channelExternalConversationId}"` : "the conversation id named in this event";
55193
- const threadAlt = event.channelExternalMessageId ? ` To reply threaded to this specific message, reference message_id "${event.channelExternalMessageId}".` : "";
55194
- return `
55195
- <system-reminder>To reply, use the official Feishu CLI on your PATH: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned automatically).${threadAlt} lark-cli is the ONLY outbound path \u2014 your plain text output is NOT delivered to the external conversation.</system-reminder>`;
55196
- }
55197
- const platform = event.channelProvider ?? "the external platform";
55198
- return `
55199
- <system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
55200
- }
55201
- if (event.type === "external_trigger" || event.targetId.startsWith("xtr_")) {
55202
- return `
55203
- <system-reminder>This external trigger is incoming-only. Your plain text output is not sent back to the external provider. To communicate in Parall, use \`parall messages send\` / \`parall dm\`; provider-specific outbound actions require a separate capability.</system-reminder>`;
55204
- }
55205
- return "";
55206
- }
55207
- function buildForkScopePrefix(event) {
55208
- const targetLabel = event.targetName ? `"${event.targetName}" (prll://${event.targetId})` : `prll://${event.targetId}`;
55209
- return `[Fork scope: You are a parallel worker forked from the main session to handle ${targetLabel}. The conversation history above belongs to the main session's in-progress dispatch \u2014 do NOT continue it or complete it, or produce external output (comments, messages) for work visible there. The main session owns its own output.
55210
-
55211
- Only act on the event below within your target scope. If your event reveals findings about the main session's work, report them in your summary \u2014 do not post them externally.
55212
-
55213
- End with a brief "Fork summary" of actions taken and decisions made.]
55214
-
55215
- `;
55216
- }
55217
- function buildForkResultPrefix(results) {
55218
- if (!results.length)
55219
- return "";
55220
- const blocks = results.map((result) => {
55221
- const lines = [];
55222
- for (const body of result.eventBodies) {
55223
- lines.push(body);
55224
- }
55225
- lines.push(`[This event was handled by a parallel fork session. Do NOT re-handle, re-reply, or duplicate work for it.]`);
55226
- lines.push(`[Fork summary: ${result.agentSummary ? sanitizeMeta(result.agentSummary) : "No fork summary available \u2014 the fork completed without producing a text summary. Check the target chat/task for any actions the fork already took before acting."}]`);
55227
- if (result.actions.length)
55228
- lines.push(`[Fork actions: ${result.actions.join("; ")}]`);
55229
- if (result.historyPath)
55230
- lines.push(`[Fork history: ${result.historyPath}]`);
55231
- return lines.join("\n");
55232
- });
55233
- return blocks.join("\n\n") + "\n\n---\n\n";
55234
- }
55235
-
55236
55441
  // ../agent-core/dist/prompt-fragments.js
55237
55442
  function identityFromMe(me) {
55238
55443
  return {
@@ -55619,6 +55824,28 @@ function buildErrorStepContent(message) {
55619
55824
  return { text: message, suppressed: false, status: "error" };
55620
55825
  }
55621
55826
 
55827
+ // ../agent-core/dist/redact.js
55828
+ function redactSecrets(s, knownValues = []) {
55829
+ let out = s;
55830
+ for (const v of knownValues) {
55831
+ if (typeof v === "string" && v.length >= 6)
55832
+ out = out.split(v).join("***");
55833
+ }
55834
+ return out.replace(/\b(agk|mck|cpk)_[A-Za-z0-9_-]+/g, "$1_***").replace(/\b(sk|pk|rk)-[A-Za-z0-9_-]{8,}/g, "$1-***").replace(/\bAKIA[0-9A-Z]{16}\b/g, "AKIA***").replace(/\b(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, "$1***").replace(/[A-Za-z0-9_-]{32,}/g, "***");
55835
+ }
55836
+ function redactTurnOutcome(event, knownValues) {
55837
+ const redacted = { ...event };
55838
+ if (redacted.detail)
55839
+ redacted.detail = redactSecrets(redacted.detail, knownValues);
55840
+ if (redacted.raw) {
55841
+ redacted.raw = Object.fromEntries(Object.entries(redacted.raw).map(([k, v]) => [
55842
+ k,
55843
+ typeof v === "string" ? redactSecrets(v, knownValues) : v
55844
+ ]));
55845
+ }
55846
+ return redacted;
55847
+ }
55848
+
55622
55849
  // ../agent-core/dist/gateway-base.js
55623
55850
  import * as os from "node:os";
55624
55851
  import * as fs3 from "node:fs";
@@ -56480,6 +56707,8 @@ var tracer = null;
56480
56707
  var dispatchCounter = null;
56481
56708
  var dispatchDuration = null;
56482
56709
  var missingReplyCounter = null;
56710
+ var turnTokensCounter = null;
56711
+ var turnCostCounter = null;
56483
56712
  var otelLogger = null;
56484
56713
  function resolveTargetType(targetId) {
56485
56714
  if (targetId.startsWith("cht_"))
@@ -56554,6 +56783,12 @@ async function initAgentTelemetry(serviceName, runtimeType) {
56554
56783
  missingReplyCounter = meter.createCounter("parall.dispatch.missing_reply", {
56555
56784
  description: "Dispatches where agent produced text but sent no reply message"
56556
56785
  });
56786
+ turnTokensCounter = meter.createCounter("parall.turn.tokens", {
56787
+ description: "LLM tokens consumed per turn, by kind (input/output/cache_read/cache_creation)"
56788
+ });
56789
+ turnCostCounter = meter.createCounter("parall.turn.cost_usd", {
56790
+ description: "LLM cost per turn in USD (when the runtime reports it)"
56791
+ });
56557
56792
  initialized = true;
56558
56793
  shutdownFn = async () => {
56559
56794
  await tracerProvider.forceFlush();
@@ -56587,7 +56822,7 @@ function startDispatchSpan(event, runtimeType, sessionKey) {
56587
56822
  }
56588
56823
  });
56589
56824
  }
56590
- function endDispatchSpan(span, metricsSnapshot, error) {
56825
+ function endDispatchSpan(span, metricsSnapshot, error, turnOutcome) {
56591
56826
  if (!span)
56592
56827
  return;
56593
56828
  if (metricsSnapshot) {
@@ -56601,27 +56836,77 @@ function endDispatchSpan(span, metricsSnapshot, error) {
56601
56836
  "dispatch.duration_ms": Date.now() - metricsSnapshot.started_at
56602
56837
  });
56603
56838
  }
56839
+ if (turnOutcome) {
56840
+ span.setAttribute("dispatch.outcome", turnOutcome.outcome);
56841
+ if (turnOutcome.detail)
56842
+ span.setAttribute("dispatch.outcome_detail", turnOutcome.detail);
56843
+ if (turnOutcome.retryAt)
56844
+ span.setAttribute("dispatch.retry_at", turnOutcome.retryAt);
56845
+ if (turnOutcome.model)
56846
+ span.setAttribute("dispatch.model", turnOutcome.model);
56847
+ if (turnOutcome.raw && Object.keys(turnOutcome.raw).length > 0) {
56848
+ try {
56849
+ span.setAttribute("dispatch.outcome_raw", JSON.stringify(turnOutcome.raw));
56850
+ } catch {
56851
+ }
56852
+ }
56853
+ const u = turnOutcome.usage;
56854
+ if (u) {
56855
+ if (u.inputTokens !== void 0)
56856
+ span.setAttribute("dispatch.tokens_input", u.inputTokens);
56857
+ if (u.outputTokens !== void 0)
56858
+ span.setAttribute("dispatch.tokens_output", u.outputTokens);
56859
+ if (u.cacheReadTokens !== void 0)
56860
+ span.setAttribute("dispatch.tokens_cache_read", u.cacheReadTokens);
56861
+ if (u.cacheCreationTokens !== void 0)
56862
+ span.setAttribute("dispatch.tokens_cache_creation", u.cacheCreationTokens);
56863
+ if (u.costUsd !== void 0)
56864
+ span.setAttribute("dispatch.cost_usd", u.costUsd);
56865
+ if (u.durationApiMs !== void 0)
56866
+ span.setAttribute("dispatch.duration_api_ms", u.durationApiMs);
56867
+ }
56868
+ }
56604
56869
  if (error) {
56605
- span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
56606
- span.recordException(error instanceof Error ? error : new Error(String(error)));
56870
+ const safe = redactSecrets(String(error));
56871
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safe });
56872
+ span.recordException(error instanceof Error ? new Error(safe) : new Error(safe));
56607
56873
  }
56608
56874
  span.end();
56609
56875
  }
56610
- function recordDispatchMetric(event, runtimeType, durationMs) {
56876
+ function recordDispatchMetric(event, runtimeType, durationMs, outcome = "ok") {
56611
56877
  if (!initialized)
56612
56878
  return;
56613
56879
  const attrs = {
56614
56880
  target_type: resolveTargetType(event.targetId),
56615
56881
  event_type: event.type,
56616
- runtime_type: runtimeType
56882
+ runtime_type: runtimeType,
56883
+ outcome
56617
56884
  };
56618
56885
  dispatchCounter?.add(1, attrs);
56619
56886
  dispatchDuration?.record(durationMs, attrs);
56620
56887
  }
56621
- function recordMissingReply(runtimeType) {
56888
+ function recordMissingReply(runtimeType, outcome = "ok") {
56622
56889
  if (!initialized)
56623
56890
  return;
56624
- missingReplyCounter?.add(1, { runtime_type: runtimeType });
56891
+ missingReplyCounter?.add(1, { runtime_type: runtimeType, outcome });
56892
+ }
56893
+ function recordTurnUsage(usage, runtimeType) {
56894
+ if (!initialized || !usage)
56895
+ return;
56896
+ const kinds = [
56897
+ ["input", usage.inputTokens],
56898
+ ["output", usage.outputTokens],
56899
+ ["cache_read", usage.cacheReadTokens],
56900
+ ["cache_creation", usage.cacheCreationTokens]
56901
+ ];
56902
+ for (const [kind, value] of kinds) {
56903
+ if (value !== void 0 && value > 0) {
56904
+ turnTokensCounter?.add(value, { kind, runtime_type: runtimeType });
56905
+ }
56906
+ }
56907
+ if (usage.costUsd !== void 0 && usage.costUsd > 0) {
56908
+ turnCostCounter?.add(usage.costUsd, { runtime_type: runtimeType });
56909
+ }
56625
56910
  }
56626
56911
  var sessionKeyStorage = new AsyncLocalStorage();
56627
56912
  function runWithSessionKey(sessionKey, fn) {
@@ -56821,7 +57106,9 @@ var ParallAgentGateway = class {
56821
57106
  client: opts.client,
56822
57107
  orgId: opts.config.org_id,
56823
57108
  contextDir: opts.dispatchContextDir,
56824
- log: opts.log
57109
+ log: opts.log,
57110
+ coverageMode: opts.dispatchAdapter.inputLifecycleMode ?? "implicit",
57111
+ releaseLocalClaims: (sourceIds) => releaseLocalMessageClaims(this.dispatchedMessages, sourceIds)
56825
57112
  });
56826
57113
  }
56827
57114
  this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 6e4;
@@ -57095,17 +57382,49 @@ var ParallAgentGateway = class {
57095
57382
  if (laneKey)
57096
57383
  this.laneLedger?.renewByKey(laneKey);
57097
57384
  }
57098
- /** Sessions whose in-flight turn surfaced a runtime error event. */
57099
- turnErrorSessions = /* @__PURE__ */ new Set();
57100
57385
  /**
57101
- * Consume (read-and-clear) the error marker for sessionKey's last turn.
57386
+ * Settled LLM-layer outcome of each session's last turn — an error to
57387
+ * release lane members on the redrive budget, or a deferred usage-limit
57388
+ * wait (agent-turn-outcome-design.md). Absent = clean turn.
57389
+ */
57390
+ turnOutcomes = /* @__PURE__ */ new Map();
57391
+ /**
57392
+ * Consume (read-and-clear) the settled outcome for sessionKey's last turn.
57102
57393
  * Feeds complete's turn_outcome so an error turn's lane members are
57103
- * released for retry instead of no_action-swept (design §3). Consuming
57104
- * (rather than peeking) keeps one-shot fork session keys from accumulating
57105
- * in the set forever.
57394
+ * released for retry (and a deferred turn's members re-deliver at retryAt)
57395
+ * instead of being no_action-swept (design §3). Consuming (rather than
57396
+ * peeking) keeps one-shot fork session keys from accumulating forever.
57397
+ */
57398
+ consumeTurnOutcome(sessionKey) {
57399
+ const outcome = this.turnOutcomes.get(sessionKey);
57400
+ this.turnOutcomes.delete(sessionKey);
57401
+ return outcome;
57402
+ }
57403
+ /**
57404
+ * Boolean view of consumeTurnOutcome for the typed/fork call sites, which
57405
+ * have no deferred semantics: ANY non-clean outcome (error or deferred)
57406
+ * counts as an errored turn there — release-for-retry beats a false
57407
+ * "handled".
57106
57408
  */
57107
57409
  consumeTurnError(sessionKey) {
57108
- return this.turnErrorSessions.delete(sessionKey);
57410
+ return this.consumeTurnOutcome(sessionKey) !== void 0;
57411
+ }
57412
+ /**
57413
+ * Fold turn signals into the session's settled outcome. Two sources, one
57414
+ * precedence rule: the bridge's explicit turn_outcome classification always
57415
+ * wins (it may refine the SAME failure a generic `error` event already
57416
+ * reported — e.g. Claude's result frame yields both), while a bare `error`
57417
+ * event only fills the slot when no classification exists. Wire order makes
57418
+ * this safe: every bridge emits its error events before the turn-boundary
57419
+ * turn_outcome.
57420
+ */
57421
+ recordTurnErrorSignal(sessionKey) {
57422
+ if (!this.turnOutcomes.has(sessionKey)) {
57423
+ this.turnOutcomes.set(sessionKey, { kind: "error" });
57424
+ }
57425
+ }
57426
+ recordTurnClassification(sessionKey, next) {
57427
+ this.turnOutcomes.set(sessionKey, next);
57109
57428
  }
57110
57429
  async emitDispatchReceived(event) {
57111
57430
  const sourceType = event.ackSourceType ?? (event.type === "task" ? "task_activity" : "message");
@@ -57275,7 +57594,14 @@ var ParallAgentGateway = class {
57275
57594
  channel_message_id: event.messageId,
57276
57595
  provider: event.channelProvider,
57277
57596
  external_conversation_id: event.channelExternalConversationId
57278
- } : event.type === "approval" ? { approval_id: event.messageId } : { message_id: event.messageId },
57597
+ } : event.type === "approval" ? { approval_id: event.messageId } : {
57598
+ message_id: event.messageId,
57599
+ // Thread context for the session→chat backlink:
57600
+ // a thread reply can only be navigated to with
57601
+ // its thread root (the chat surface rejects
57602
+ // bare thread-message targets).
57603
+ ...event.threadRootId ? { thread_root_id: event.threadRootId } : {}
57604
+ },
57279
57605
  sender_id: event.senderId,
57280
57606
  sender_name: event.senderName,
57281
57607
  summary: event.body.substring(0, 200),
@@ -57482,7 +57808,7 @@ var ParallAgentGateway = class {
57482
57808
  // are shutting down. Callers MUST treat `false` as "not dispatched" and
57483
57809
  // skip the server-side ack so the event stays in the dispatch queue for
57484
57810
  // catch-up on the replacement pod — otherwise we silently drop work.
57485
- async runDispatch(event, sessionKey, bodyForAgent, earlierEvents = [], captureText) {
57811
+ async runDispatch(event, sessionKey, bodyForAgent, earlierEvents = [], captureText, inputLifecycle) {
57486
57812
  if (this.shuttingDown) {
57487
57813
  this.opts.log?.info(`skipping dispatch for ${event.messageId} (shutting down) \u2014 leaving unacked for catch-up on replacement pod`);
57488
57814
  return false;
@@ -57492,7 +57818,7 @@ var ParallAgentGateway = class {
57492
57818
  this.pendingRestartNotification = null;
57493
57819
  }
57494
57820
  resetDispatchMetrics(sessionKey);
57495
- this.turnErrorSessions.delete(sessionKey);
57821
+ this.turnOutcomes.delete(sessionKey);
57496
57822
  return runWithSessionKey(sessionKey, async () => {
57497
57823
  let dispatchSpan = null;
57498
57824
  setSessionChatId(sessionKey, event.targetId);
@@ -57537,6 +57863,8 @@ var ParallAgentGateway = class {
57537
57863
  let inputStepsCreated = false;
57538
57864
  let turnHandle;
57539
57865
  let dispatchError;
57866
+ let turnOutcomeEvent;
57867
+ let sawErrorEvent = false;
57540
57868
  const pendingSendCallIds = /* @__PURE__ */ new Set();
57541
57869
  const ensureTurnBegun = async () => {
57542
57870
  if (turnHandle || !binding)
@@ -57550,11 +57878,15 @@ var ParallAgentGateway = class {
57550
57878
  earlierEvents,
57551
57879
  bodyForAgent,
57552
57880
  sessionKey,
57553
- context: dispatchContext
57881
+ context: dispatchContext,
57882
+ inputLifecycle
57554
57883
  })) {
57555
57884
  if (runtimeEvent.type === "runtime_session") {
57556
57885
  const priorAgentSessionId = binding?.agentSessionId;
57557
57886
  binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath, laneContextFilePath2);
57887
+ if (activeLane) {
57888
+ bindLaneSession(activeLane, binding.agentSessionId);
57889
+ }
57558
57890
  if (event.targetType === "channel_conversation" && binding.agentSessionId !== priorAgentSessionId) {
57559
57891
  try {
57560
57892
  await this.opts.client.setChannelConversationSession(this.opts.config.org_id, event.targetId, binding.agentSessionId);
@@ -57572,6 +57904,36 @@ var ParallAgentGateway = class {
57572
57904
  }
57573
57905
  continue;
57574
57906
  }
57907
+ if (runtimeEvent.type === "turn_outcome") {
57908
+ const outcomeEvent = redactTurnOutcome(runtimeEvent, [this.opts.config.api_key]);
57909
+ turnOutcomeEvent = outcomeEvent;
57910
+ if (outcomeEvent.outcome === "ok") {
57911
+ this.turnOutcomes.delete(sessionKey);
57912
+ continue;
57913
+ }
57914
+ this.recordTurnClassification(sessionKey, outcomeEvent.outcome === "usage_limit" ? {
57915
+ kind: "deferred",
57916
+ outcomeClass: outcomeEvent.outcome,
57917
+ ...outcomeEvent.retryAt ? { retryAt: outcomeEvent.retryAt } : {}
57918
+ } : { kind: "error", outcomeClass: outcomeEvent.outcome });
57919
+ const retryNote = outcomeEvent.retryAt ? `, retry at ${outcomeEvent.retryAt}` : "";
57920
+ this.opts.log?.warn(`turn outcome: ${outcomeEvent.outcome}${retryNote}${outcomeEvent.detail ? ` \u2014 ${outcomeEvent.detail}` : ""}`);
57921
+ if (binding) {
57922
+ await ensureTurnBegun();
57923
+ if (!inputStepsCreated) {
57924
+ if (earlierEvents.length > 0) {
57925
+ await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
57926
+ }
57927
+ await this.createInputStep(binding.agentSessionId, event);
57928
+ inputStepsCreated = true;
57929
+ }
57930
+ await this.createRuntimeStep(binding.agentSessionId, event, {
57931
+ type: "error",
57932
+ message: `LLM turn ${outcomeEvent.outcome}${retryNote}${outcomeEvent.detail ? `: ${outcomeEvent.detail}` : ""}`
57933
+ }, stepIdFilePath, contextFilePath, laneContextFilePath2);
57934
+ }
57935
+ continue;
57936
+ }
57575
57937
  if (!binding) {
57576
57938
  const detail = runtimeEvent.type === "error" ? `: ${runtimeEvent.message}` : "";
57577
57939
  throw new Error(`runtime emitted ${runtimeEvent.type} before runtime_session${detail}`);
@@ -57604,7 +57966,8 @@ var ParallAgentGateway = class {
57604
57966
  recordMessageSend(sessionKey, !runtimeEvent.error);
57605
57967
  }
57606
57968
  if (runtimeEvent.type === "error") {
57607
- this.turnErrorSessions.add(sessionKey);
57969
+ sawErrorEvent = true;
57970
+ this.recordTurnErrorSignal(sessionKey);
57608
57971
  }
57609
57972
  await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath, laneContextFilePath2);
57610
57973
  }
@@ -57658,10 +58021,12 @@ var ParallAgentGateway = class {
57658
58021
  clearTimeout(deadlineTimer);
57659
58022
  const metricsSnapshot = getDispatchMetrics(sessionKey);
57660
58023
  const durationMs = metricsSnapshot ? Date.now() - metricsSnapshot.started_at : 0;
57661
- endDispatchSpan(dispatchSpan, metricsSnapshot, dispatchError);
57662
- recordDispatchMetric(event, this.opts.runtimeType, durationMs);
58024
+ const effectiveOutcome = turnOutcomeEvent?.outcome ?? (dispatchError || sawErrorEvent ? "runtime_crash" : "ok");
58025
+ endDispatchSpan(dispatchSpan, metricsSnapshot, dispatchError, turnOutcomeEvent);
58026
+ recordDispatchMetric(event, this.opts.runtimeType, durationMs, effectiveOutcome);
58027
+ recordTurnUsage(turnOutcomeEvent?.usage, this.opts.runtimeType);
57663
58028
  if (metricsSnapshot && !dispatchError && event.type === "message" && event.targetId?.startsWith("cht_") && !event.noReply && metricsSnapshot.deliver_text_chunks > 0 && metricsSnapshot.message_send_successes === 0 && !metricsSnapshot.no_reply_called) {
57664
- recordMissingReply(this.opts.runtimeType);
58029
+ recordMissingReply(this.opts.runtimeType, effectiveOutcome);
57665
58030
  }
57666
58031
  clearDispatchMetrics(sessionKey);
57667
58032
  if (turnHandle) {
@@ -57769,7 +58134,7 @@ var ParallAgentGateway = class {
57769
58134
  item.resolve(false);
57770
58135
  break;
57771
58136
  }
57772
- if (outcome === "failed") {
58137
+ if (outcome === "failed" || outcome === "deferred") {
57773
58138
  for (const item of items)
57774
58139
  item.resolve(false);
57775
58140
  continue;
@@ -57813,7 +58178,7 @@ var ParallAgentGateway = class {
57813
58178
  remaining.resolve(false);
57814
58179
  }
57815
58180
  } finally {
57816
- this.turnErrorSessions.delete(fork.fork.sessionKey);
58181
+ this.turnOutcomes.delete(fork.fork.sessionKey);
57817
58182
  if (fork.deadlineTimer) {
57818
58183
  clearTimeout(fork.deadlineTimer);
57819
58184
  fork.deadlineTimer = null;
@@ -57928,7 +58293,7 @@ var ParallAgentGateway = class {
57928
58293
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
57929
58294
  continue;
57930
58295
  }
57931
- if (outcome === "failed") {
58296
+ if (outcome === "failed" || outcome === "deferred") {
57932
58297
  this.dispatchState.pendingForkResults.unshift(...pendingFork);
57933
58298
  continue;
57934
58299
  }
@@ -58075,8 +58440,8 @@ var ParallAgentGateway = class {
58075
58440
  this.dispatchState.mainBuffer.push(event);
58076
58441
  const typedAheadInBuffer = this.dispatchState.mainBuffer.some((e) => e.type !== "message");
58077
58442
  if (this.usesLaneLedger(event)) {
58078
- if (!typedAheadInBuffer && this.mainCurrentGroupKey === this.dispatchGroupKey(event) && this.opts.dispatchAdapter.enqueueDuringDispatch != null && await this.laneLedger?.steerLive(event) && await this.opts.dispatchAdapter.enqueueDuringDispatch(this.opts.runtimeKey, buildEventBody(event))) {
58079
- this.opts.log?.info(`steer folded+injected for ${event.messageId} (will drain for bookkeeping)`);
58443
+ if (!typedAheadInBuffer && this.mainCurrentGroupKey === this.dispatchGroupKey(event) && this.opts.dispatchAdapter.enqueueDuringDispatch != null) {
58444
+ await steerLaneMessage(this.laneFlowHost(), event);
58080
58445
  }
58081
58446
  } else if (
58082
58447
  // Message events only. A typed event (task_comment/schedule/…)
@@ -60406,6 +60771,24 @@ ${event.body}` : event.body
60406
60771
  };
60407
60772
  });
60408
60773
  }
60774
+ var OC_AUTH_TEXT = /unauthorized|401|403|invalid api key|authentication/i;
60775
+ var OC_CONTEXT_TEXT = /context (window|length)|prompt is too long|request too large|413/i;
60776
+ var OC_API_TEXT = /overloaded|429|rate limit|5\d\d|bad gateway|service unavailable|timeout/i;
60777
+ function classifyOpenClawFailure(err) {
60778
+ const message = err instanceof Error ? err.message : String(err);
60779
+ let outcome = "runtime_crash";
60780
+ if (OC_AUTH_TEXT.test(message))
60781
+ outcome = "auth";
60782
+ else if (OC_CONTEXT_TEXT.test(message))
60783
+ outcome = "context_overflow";
60784
+ else if (OC_API_TEXT.test(message))
60785
+ outcome = "api_error";
60786
+ return {
60787
+ type: "turn_outcome",
60788
+ outcome,
60789
+ ...message ? { detail: message.slice(0, 500) } : {}
60790
+ };
60791
+ }
60409
60792
  function createRuntimeEventStream() {
60410
60793
  const queue = [];
60411
60794
  const waiters = [];
@@ -60560,7 +60943,10 @@ function createOpenClawDispatchAdapter(opts) {
60560
60943
  }
60561
60944
  }
60562
60945
  });
60563
- run.then(() => stream.end()).catch((err) => stream.fail(err));
60946
+ run.then(() => stream.end()).catch((err) => {
60947
+ stream.push(classifyOpenClawFailure(err));
60948
+ stream.fail(err);
60949
+ });
60564
60950
  let sessionId;
60565
60951
  try {
60566
60952
  sessionId = await Promise.race([