@rallycry/conveyor-agent 11.0.28 → 11.0.30

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.
@@ -8,7 +8,7 @@ import {
8
8
  readWorkspaceBytes,
9
9
  statWorkspacePath,
10
10
  workspacePathExists
11
- } from "./chunk-GHNYP67H.js";
11
+ } from "./chunk-7KG7CDVU.js";
12
12
  import {
13
13
  reportBootMilestone
14
14
  } from "./chunk-Q4FQOJ7D.js";
@@ -583,6 +583,10 @@ var WorkspaceCommandSupervisor = class {
583
583
  this.reportBootMilestoneFn("start_command_launched");
584
584
  const forwardPorts = await this.loadForwardPortsFn(this.workspaceDir);
585
585
  if (this.stopped) return;
586
+ this.reportSetupComplete(startCommandRunning, forwardPorts);
587
+ }
588
+ /** Send `setup_complete` with the devcontainer preview hints and links. */
589
+ reportSetupComplete(startCommandRunning, forwardPorts) {
586
590
  const previewPorts = buildSessionPreviewPorts(forwardPorts);
587
591
  this.connection.sendEvent({
588
592
  type: "setup_complete",
@@ -590,7 +594,8 @@ var WorkspaceCommandSupervisor = class {
590
594
  // Reported separately so the server can tell "nothing to run" apart from
591
595
  // "the dev server failed to launch" — both report startCommandRunning:false.
592
596
  startCommandConfigured: Boolean(this.config?.startCommand),
593
- ...previewPorts.length > 0 ? { previewPorts } : {}
597
+ ...previewPorts.length > 0 ? { previewPorts } : {},
598
+ ...forwardPorts.links.length > 0 ? { previewLinks: forwardPorts.links } : {}
594
599
  });
595
600
  }
596
601
  /** Run the dependency sync, forwarding its output as setup output so it lands
@@ -240,10 +240,43 @@ function syncBundleGithubToken(token) {
240
240
  syncGithubTokenFiles(token);
241
241
  }
242
242
 
243
+ // src/connection/event-batch.ts
244
+ var MAX_EVENT_BATCH_BYTES = 256 * 1024;
245
+ var CLIPPED_FIELD_CHARS = 16 * 1024;
246
+ function sizeOf(event) {
247
+ return JSON.stringify(event).length;
248
+ }
249
+ function fitEvent(event) {
250
+ const size = sizeOf(event);
251
+ if (size <= MAX_EVENT_BATCH_BYTES) return { event, size };
252
+ const clipped = { ...event };
253
+ for (const [key, value] of Object.entries(clipped)) {
254
+ if (typeof value === "string" && value.length > CLIPPED_FIELD_CHARS) {
255
+ const dropped = value.length - CLIPPED_FIELD_CHARS;
256
+ clipped[key] = `${value.slice(0, CLIPPED_FIELD_CHARS)}
257
+ [conveyor-agent] truncated ${dropped} chars
258
+ `;
259
+ }
260
+ }
261
+ const clippedSize = sizeOf(clipped);
262
+ return clippedSize <= MAX_EVENT_BATCH_BYTES ? { event: clipped, size: clippedSize } : null;
263
+ }
264
+ function batchLength(entries) {
265
+ let total = 0;
266
+ for (let i = 0; i < entries.length; i++) {
267
+ const entry = entries[i];
268
+ const size = entry.size ?? sizeOf(entry.event);
269
+ total += size + 1;
270
+ if (total > MAX_EVENT_BATCH_BYTES && i > 0) return i;
271
+ }
272
+ return entries.length;
273
+ }
274
+
243
275
  // src/connection/agent-connection.ts
244
276
  var logger = createServiceLogger("agent-connection");
245
277
  var EVENT_BATCH_MS = 500;
246
278
  var MAX_EVENT_BUFFER = 5e3;
279
+ var PTY_INPUT_REPORT_INTERVAL_MS = 3e4;
247
280
  var TOKEN_REFRESH_INTERVAL_MS = 45 * 60 * 1e3;
248
281
  var AgentConnection = class _AgentConnection {
249
282
  socket = null;
@@ -291,6 +324,9 @@ var AgentConnection = class _AgentConnection {
291
324
  * question-grace expiry reads it to defer un-parking while a human is
292
325
  * actively typing into the Connected-TUI (they may be mid-answer). */
293
326
  _lastPtyInputAt = null;
327
+ /** Runner-side subscribers to keystrokes from EITHER transport. */
328
+ ptyInputActivityListeners = /* @__PURE__ */ new Set();
329
+ lastReportedDirectInputAt = Number.NEGATIVE_INFINITY;
294
330
  constructor(config) {
295
331
  this.config = config;
296
332
  }
@@ -479,7 +515,7 @@ var AgentConnection = class _AgentConnection {
479
515
  });
480
516
  this.socket.on("pty:input", (data) => {
481
517
  if (data.sessionId && data.sessionId !== this.config.sessionId) return;
482
- this._lastPtyInputAt = Date.now();
518
+ this.notePtyInput("relay");
483
519
  this.ptyInputCallback?.(data.data);
484
520
  });
485
521
  this.socket.on("pty:resize", (data) => {
@@ -946,7 +982,32 @@ var AgentConnection = class _AgentConnection {
946
982
  }).catch(() => {
947
983
  });
948
984
  }
949
- /** Epoch ms of the last relayed TUI keystroke, or null before any. */
985
+ /**
986
+ * A human typed into the TUI. Stamps `lastPtyInputAt` and notifies the
987
+ * runner's input-activity listeners for both transports. Direct-stream
988
+ * keystrokes never cross the API, so for those this also tells the server
989
+ * (`reportPtyInput`, throttled, fire-and-forget) — the relay path's server
990
+ * already bumps in `relayPtyInput`. An old API that doesn't know the method
991
+ * rejects harmlessly.
992
+ */
993
+ notePtyInput(origin = "direct") {
994
+ const now = Date.now();
995
+ this._lastPtyInputAt = now;
996
+ for (const listener of this.ptyInputActivityListeners) listener();
997
+ if (origin !== "direct" || !this.socket) return;
998
+ if (now - this.lastReportedDirectInputAt < PTY_INPUT_REPORT_INTERVAL_MS) return;
999
+ this.lastReportedDirectInputAt = now;
1000
+ void this.call("reportPtyInput", { sessionId: this.config.sessionId }).catch(() => {
1001
+ });
1002
+ }
1003
+ /** Subscribe to keystroke activity from either transport. Returns an unsubscribe fn. */
1004
+ onPtyInputActivity(listener) {
1005
+ this.ptyInputActivityListeners.add(listener);
1006
+ return () => {
1007
+ this.ptyInputActivityListeners.delete(listener);
1008
+ };
1009
+ }
1010
+ /** Epoch ms of the last TUI keystroke (relay or direct), or null before any. */
950
1011
  get lastPtyInputAt() {
951
1012
  return this._lastPtyInputAt;
952
1013
  }
@@ -1402,7 +1463,15 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1402
1463
  // ── Event buffering ────────────────────────────────────────────────
1403
1464
  sendEvent(event) {
1404
1465
  if (!this.socket) return;
1405
- this.enqueueEvents([{ event }], false);
1466
+ const fitted = fitEvent(event);
1467
+ if (!fitted) {
1468
+ process.stderr.write(
1469
+ `[conveyor-agent] dropped oversized ${event.type} event (over the emit batch limit)
1470
+ `
1471
+ );
1472
+ return;
1473
+ }
1474
+ this.enqueueEvents([fitted], false);
1406
1475
  }
1407
1476
  /** Append (or, on `toFront`, prepend for a failed-flush re-queue) events to
1408
1477
  * the buffer, then cap + arm the flush timer. Single owner of the overflow
@@ -1429,14 +1498,15 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1429
1498
  clearTimeout(this.flushTimer);
1430
1499
  this.flushTimer = null;
1431
1500
  }
1432
- if (!this.socket || this.eventBuffer.length === 0) return;
1433
- const entries = this.eventBuffer;
1434
- this.eventBuffer = [];
1435
- const events = entries.map((entry) => entry.event);
1436
- try {
1437
- await this.call("emitAgentEvent", { sessionId: this.config.sessionId, events });
1438
- } catch {
1439
- this.requeueFailedEvents(entries);
1501
+ while (this.socket && this.eventBuffer.length > 0) {
1502
+ const entries = this.eventBuffer.splice(0, batchLength(this.eventBuffer));
1503
+ const events = entries.map((entry) => entry.event);
1504
+ try {
1505
+ await this.call("emitAgentEvent", { sessionId: this.config.sessionId, events });
1506
+ } catch {
1507
+ this.requeueFailedEvents(entries);
1508
+ return;
1509
+ }
1440
1510
  }
1441
1511
  }
1442
1512
  /** Put a failed flush's events back at the FRONT of the buffer, preserving
@@ -1446,7 +1516,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1446
1516
  }
1447
1517
  };
1448
1518
 
1449
- // ../shared/dist/chunk-ZSXAYKD4.js
1519
+ // ../shared/dist/chunk-3D2OVVIS.js
1450
1520
  import { z } from "zod";
1451
1521
  import { z as z2 } from "zod";
1452
1522
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
@@ -1455,6 +1525,15 @@ var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
1455
1525
  var FABLE_MODEL = "claude-fable-5-1";
1456
1526
  var PREVIOUS_SONNET_MODEL = "claude-sonnet-4-6";
1457
1527
  var PREVIOUS_OPUS_MODEL = "claude-opus-5";
1528
+ function formatModelId(provider, model) {
1529
+ return `${provider}/${model}`;
1530
+ }
1531
+ var JEV_MODEL = "jev-1.13.0";
1532
+ var JEV_MODEL_ID = formatModelId("typesafe", JEV_MODEL);
1533
+ var JEV_ZEN_MODEL = "jev-1.13";
1534
+ var JEV_ZEN_MODEL_ID = formatModelId("zen", JEV_ZEN_MODEL);
1535
+ var JEV_INPUT_PRICE_PER_TOKEN = 0.042 / 1e6;
1536
+ var JEV_STATE_MAX_CHARS = 1e5;
1458
1537
  var PTY_STREAM_PORT_BASE = 7420;
1459
1538
  var PTY_STREAM_PORT_ATTEMPTS = 8;
1460
1539
  function encodePtyStreamFrame(frame) {
@@ -1508,6 +1587,11 @@ var PREVIEW_PORT_DENY_LIST = [
1508
1587
  9200,
1509
1588
  ...Array.from({ length: PTY_STREAM_PORT_ATTEMPTS }, (_, i) => PTY_STREAM_PORT_BASE + i)
1510
1589
  ];
1590
+ function isAllowablePreviewPort(port) {
1591
+ if (typeof port !== "number" || !Number.isInteger(port)) return false;
1592
+ if (port < 1 || port > 65535) return false;
1593
+ return !PREVIEW_PORT_DENY_LIST.includes(port);
1594
+ }
1511
1595
  function normalizeCheckpointPath(value) {
1512
1596
  let normalized = value.trim().replace(/\/{2,}/g, "/");
1513
1597
  normalized = normalized.split("/").filter((segment) => segment !== ".").join("/");
@@ -1973,7 +2057,9 @@ var KnownAgentEventSchema = z22.discriminatedUnion("type", [
1973
2057
  startCommandRunning: z22.boolean().optional(),
1974
2058
  startCommandConfigured: z22.boolean().optional(),
1975
2059
  // Sanitized server-side by sanitizeSessionPreviewPorts — stays unknown.
1976
- previewPorts: z22.unknown().optional()
2060
+ previewPorts: z22.unknown().optional(),
2061
+ // Sanitized server-side by sanitizeSessionPreviewLinks — stays unknown.
2062
+ previewLinks: z22.unknown().optional()
1977
2063
  }).passthrough(),
1978
2064
  z22.object({ type: z22.literal("setup_error"), message: z22.string() }).passthrough(),
1979
2065
  z22.object({ type: z22.literal("start_command_started") }).passthrough(),
@@ -2451,6 +2537,9 @@ var ReportPtyStreamRequestSchema = z32.object({
2451
2537
  sessionId: z32.string(),
2452
2538
  port: z32.number().int().positive().max(65535).nullable()
2453
2539
  });
2540
+ var ReportPtyInputRequestSchema = z32.object({
2541
+ sessionId: z32.string()
2542
+ });
2454
2543
  var GetPtyStreamEndpointRequestSchema = z32.object({
2455
2544
  sessionId: z32.string()
2456
2545
  });
@@ -3275,6 +3364,30 @@ var CRITICAL_AUTOMATED_SOURCES = /* @__PURE__ */ new Set([
3275
3364
  // clear the completion guard.
3276
3365
  "ci_result"
3277
3366
  ]);
3367
+ var MAX_PREVIEW_LINKS = 8;
3368
+ var MAX_PREVIEW_LINK_LABEL = 64;
3369
+ var MAX_PREVIEW_LINK_PATH = 512;
3370
+ function isSafePreviewPath(path) {
3371
+ if (!path.startsWith("/") || path.startsWith("//")) return false;
3372
+ if (path.length > MAX_PREVIEW_LINK_PATH) return false;
3373
+ return !/[\s\\\u0000-\u001f\u007f]/.test(path);
3374
+ }
3375
+ function sanitizeSessionPreviewLinks(raw) {
3376
+ if (!Array.isArray(raw)) return [];
3377
+ const out = [];
3378
+ for (const entry of raw) {
3379
+ if (out.length >= MAX_PREVIEW_LINKS) break;
3380
+ if (!entry || typeof entry !== "object") continue;
3381
+ const { label, port, path } = entry;
3382
+ if (typeof label !== "string") continue;
3383
+ const trimmed = label.trim().slice(0, MAX_PREVIEW_LINK_LABEL);
3384
+ if (trimmed.length === 0 || !isAllowablePreviewPort(port)) continue;
3385
+ const resolvedPath = path === void 0 ? "/" : path;
3386
+ if (typeof resolvedPath !== "string" || !isSafePreviewPath(resolvedPath)) continue;
3387
+ out.push({ label: trimmed, port, path: resolvedPath });
3388
+ }
3389
+ return out;
3390
+ }
3278
3391
  var DECISION_STATUS_LABELS = ["Open", "Decided", "Cancelled"];
3279
3392
  var DECISION_OPTION_KINDS = ["current", "planned", "proposed"];
3280
3393
  var DECISION_VOTE_SOURCES = ["web", "slack", "discord", "agent"];
@@ -3448,13 +3561,6 @@ var UpdateProjectDecisionRequestSchema = z10.object({
3448
3561
  status: z10.enum(["Cancelled", "Open"]).optional(),
3449
3562
  requestingUserId: z10.string().optional()
3450
3563
  });
3451
- function formatModelId(provider, model) {
3452
- return `${provider}/${model}`;
3453
- }
3454
- var JEV_MODEL = "jev-1.13.0";
3455
- var JEV_MODEL_ID = formatModelId("typesafe", JEV_MODEL);
3456
- var JEV_INPUT_PRICE_PER_TOKEN = 0.042 / 1e6;
3457
- var JEV_STATE_MAX_CHARS = 1e5;
3458
3564
  var EVALUATE_MAX_QUESTIONS = 32;
3459
3565
  var EVALUATE_MAX_ITEMS = 100;
3460
3566
  var EVALUATE_ITEM_MAX_CHARS = 8e3;
@@ -4624,21 +4730,30 @@ async function loadForwardPorts(workspaceDir) {
4624
4730
  if (value.visibility === "public" || value.visibility === "private") {
4625
4731
  entry.visibility = value.visibility;
4626
4732
  }
4733
+ if (value.onAutoForward === "ignore") entry.hidden = true;
4627
4734
  attributes[key] = entry;
4628
4735
  }
4629
- return { ports, attributes };
4736
+ const links = sanitizeSessionPreviewLinks(parsed.customizations?.conveyor?.previewLinks);
4737
+ return { ports, attributes, links };
4630
4738
  } catch {
4631
- return { ports: [], attributes: {} };
4739
+ return { ports: [], attributes: {}, links: [] };
4632
4740
  }
4633
4741
  }
4634
4742
  function buildSessionPreviewPorts(result) {
4635
- return result.ports.filter((port) => !DEVCONTAINER_PORT_DENY_LIST.has(port)).map((port) => {
4743
+ const toEntry = (port) => {
4636
4744
  const attr = result.attributes[String(port)];
4637
4745
  const entry = { port };
4638
4746
  if (attr?.label) entry.label = attr.label;
4639
4747
  if (attr?.visibility) entry.visibility = attr.visibility;
4748
+ if (attr?.hidden) entry.hidden = true;
4640
4749
  return entry;
4641
- });
4750
+ };
4751
+ const forwarded = result.ports.filter((port) => !DEVCONTAINER_PORT_DENY_LIST.has(port));
4752
+ const listed = new Set(forwarded);
4753
+ const hiddenOnly = Object.entries(result.attributes).filter(([, attr]) => attr.hidden).map(([key]) => Number(key)).filter(
4754
+ (port) => Number.isInteger(port) && port >= 1 && port <= 65535 && !listed.has(port) && !DEVCONTAINER_PORT_DENY_LIST.has(port)
4755
+ );
4756
+ return [...forwarded, ...hiddenOnly].map(toEntry);
4642
4757
  }
4643
4758
  function loadConveyorConfig() {
4644
4759
  const envStart = process.env.CONVEYOR_START_COMMAND;
@@ -4741,12 +4856,19 @@ var Lifecycle = class {
4741
4856
  this.clearIdleTimers();
4742
4857
  const delay2 = Math.max(0, overrideMs ?? this.config.idleTimeoutMs);
4743
4858
  this.idleTimer = setTimeout(() => {
4859
+ this.idleTimer = null;
4744
4860
  this.callbacks.onIdleTimeout();
4745
4861
  }, delay2);
4746
4862
  }
4747
4863
  cancelIdleTimer() {
4748
4864
  this.clearIdleTimers();
4749
4865
  }
4866
+ /** Restart an ARMED idle timer with the full window; a no-op when none is
4867
+ * armed. Human activity uses this, so a keystroke during a running turn
4868
+ * (timer cancelled) can never arm one. */
4869
+ touchIdleTimer() {
4870
+ if (this.idleTimer) this.startIdleTimer();
4871
+ }
4750
4872
  // ── Dormant timer ──────────────────────────────────────────────────
4751
4873
  /** Start (or restart) the dormant timer.
4752
4874
  * @param overrideMs Optional custom delay in ms. When provided, the timer
@@ -95,7 +95,7 @@ import {
95
95
  statWorkspacePath,
96
96
  updateRemoteToken,
97
97
  verifyGitCredential
98
- } from "./chunk-GHNYP67H.js";
98
+ } from "./chunk-7KG7CDVU.js";
99
99
  import {
100
100
  registerBootMilestoneSocketFallback,
101
101
  reportBootMilestone
@@ -550,6 +550,8 @@ var MAX_TRACKED_MESSAGES = 200;
550
550
  var CHAT_TEXT_MAX = 16e3;
551
551
  var CHAT_TOOL_INPUT_MAX = 1900;
552
552
  var CHAT_TOOL_OUTPUT_MAX = 1900;
553
+ var OPENCODE_LIVENESS_THROTTLE_MS = 5e3;
554
+ var MAX_TRACKED_CHILD_SESSIONS = 100;
553
555
  function truncate(text, max) {
554
556
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
555
557
  }
@@ -585,6 +587,9 @@ var OpenCodeEventSource = class {
585
587
  relayedTextParts = /* @__PURE__ */ new Set();
586
588
  relayedToolUses = /* @__PURE__ */ new Set();
587
589
  relayedToolResults = /* @__PURE__ */ new Set();
590
+ /** Session ids opencode reported with a `parentID` — subagent sessions. */
591
+ childSessions = /* @__PURE__ */ new Set();
592
+ lastLivenessAt = Number.NEGATIVE_INFINITY;
588
593
  /** The opencode-assigned session id, once any event has carried it. */
589
594
  get sessionId() {
590
595
  return this.latchedSessionId;
@@ -598,11 +603,21 @@ var OpenCodeEventSource = class {
598
603
  handleRecord(raw) {
599
604
  if (typeof raw !== "object" || raw === null) return;
600
605
  const event = raw;
606
+ this.trackChildSession(event);
601
607
  this.latchSessionId(event);
608
+ if (this.isForeignSession(event)) {
609
+ if (this.active) this.emitLiveness();
610
+ return;
611
+ }
602
612
  const info = busInfoOf(event);
603
613
  if (info?.id && info.role) this.trackRole(info.id, info.role);
604
614
  if (isBusyEvent(event)) {
605
615
  if (!this.active) this.beginBusTurn();
616
+ this.emitLiveness();
617
+ return;
618
+ }
619
+ if (isRetryEvent(event)) {
620
+ if (this.active) this.emitLiveness();
606
621
  return;
607
622
  }
608
623
  if (isIdleEvent(event)) {
@@ -629,15 +644,58 @@ var OpenCodeEventSource = class {
629
644
  this.relayAssistantPart(part);
630
645
  accumulateUsage({ part }, this.usage);
631
646
  const mapped = mapOpenCodeEvent({ part });
632
- if (!mapped) return;
647
+ if (!mapped) {
648
+ this.emitLiveness();
649
+ return;
650
+ }
633
651
  this.emit(mapped);
634
652
  if (mapped.type === "assistant") {
635
653
  const block = mapped.message.content[0];
636
654
  if (block?.type === "text" && block.text) this.assistantText += block.text;
637
655
  }
638
656
  }
657
+ /**
658
+ * Local-only proof the turn is live. Throttled per turn; `beginBusTurn`
659
+ * resets the floor so every turn's `busy` gets through.
660
+ */
661
+ emitLiveness() {
662
+ const now = Date.now();
663
+ if (now - this.lastLivenessAt < OPENCODE_LIVENESS_THROTTLE_MS) return;
664
+ this.lastLivenessAt = now;
665
+ this.emit({ type: "tool_progress", tool_name: "OpenCode", elapsed_time_seconds: 0 });
666
+ }
667
+ /**
668
+ * A record from another session. Known children (a `parentID` was seen)
669
+ * always are; an unknown session counts as foreign only mid-turn, where
670
+ * the only other session on the bus is a subagent. Outside a turn it is
671
+ * processed as before, and one that goes `busy` becomes the new root — a
672
+ * human's `/new` in the parked TUI opens a passive turn and later spawns
673
+ * resume into it.
674
+ */
675
+ isForeignSession(event) {
676
+ const id = busSessionIdOf(event);
677
+ if (!id || !this.latchedSessionId || id === this.latchedSessionId) return false;
678
+ if (this.childSessions.has(id) || this.active) return true;
679
+ if (isBusyEvent(event)) {
680
+ this.latchedSessionId = id;
681
+ this.onSessionId?.(id);
682
+ }
683
+ return false;
684
+ }
685
+ trackChildSession(event) {
686
+ if (event.type !== "session.created" && event.type !== "session.updated") return;
687
+ const info = event.properties?.info;
688
+ if (typeof info?.id !== "string" || typeof info.parentID !== "string") return;
689
+ if (!info.parentID) return;
690
+ this.childSessions.add(info.id);
691
+ if (this.childSessions.size > MAX_TRACKED_CHILD_SESSIONS) {
692
+ const oldest = this.childSessions.values().next().value;
693
+ if (oldest !== void 0) this.childSessions.delete(oldest);
694
+ }
695
+ }
639
696
  beginBusTurn() {
640
697
  this.active = true;
698
+ this.lastLivenessAt = Number.NEGATIVE_INFINITY;
641
699
  this.usage = { inputTokens: 0, outputTokens: 0, totalCostUsd: 0 };
642
700
  this.assistantText = "";
643
701
  this.relayedTextParts.clear();
@@ -705,7 +763,7 @@ var OpenCodeEventSource = class {
705
763
  latchSessionId(event) {
706
764
  if (this.latchedSessionId) return;
707
765
  const id = busSessionIdOf(event);
708
- if (!id) return;
766
+ if (!id || this.childSessions.has(id)) return;
709
767
  this.latchedSessionId = id;
710
768
  this.onSessionId?.(id);
711
769
  }
@@ -717,6 +775,9 @@ var OpenCodeEventSource = class {
717
775
  }
718
776
  }
719
777
  };
778
+ function isRetryEvent(event) {
779
+ return event.type === "session.status" && event.properties?.status?.type === "retry";
780
+ }
720
781
  function describeSessionError(event) {
721
782
  const error = event.properties?.error;
722
783
  if (error && typeof error.message === "string") return error.message;
@@ -1008,6 +1069,7 @@ var JsonlTailer = class {
1008
1069
 
1009
1070
  // src/harness/codex/event-source.ts
1010
1071
  var USAGE_CAP_PATTERN = /usage_limit_(?:reached|exceeded)|rate_limit_reached|UsageLimitReached|hit your usage limit/i;
1072
+ var CODEX_LIVENESS_THROTTLE_MS = 5e3;
1011
1073
  var WEEKLY_WINDOW_MINUTES = 24 * 60;
1012
1074
  function isUsageCap(error) {
1013
1075
  return [error.codex_error_info, error.code, error.type, error.message].some(
@@ -1024,6 +1086,9 @@ var CodexEventSource = class {
1024
1086
  onSession;
1025
1087
  chat;
1026
1088
  sessionId = null;
1089
+ /** UserPromptSubmit seen and no completion/interrupt since. */
1090
+ turnOpen = false;
1091
+ lastLivenessAt = Number.NEGATIVE_INFINITY;
1027
1092
  completed = /* @__PURE__ */ new Set();
1028
1093
  transcript = null;
1029
1094
  /** Last reported reading per rate-limit window, keyed by `five_hour` / `seven_day`. */
@@ -1037,13 +1102,18 @@ var CodexEventSource = class {
1037
1102
  this.sessionId = id;
1038
1103
  this.onSession(id);
1039
1104
  }
1040
- if (id !== this.sessionId) return;
1105
+ if (id !== this.sessionId) {
1106
+ this.emitLiveness();
1107
+ return;
1108
+ }
1041
1109
  if (e.hook_event_name === "SessionStart") this.watchTranscript(e);
1042
1110
  if (e.type === "agent-turn-complete") {
1043
1111
  this.finish(e);
1044
1112
  return;
1045
1113
  }
1046
1114
  if (e.hook_event_name === "UserPromptSubmit") {
1115
+ this.turnOpen = true;
1116
+ this.lastLivenessAt = Date.now();
1047
1117
  this.emit({ type: "tool_progress", tool_name: "Codex", elapsed_time_seconds: 0 });
1048
1118
  if (typeof e.prompt === "string")
1049
1119
  this.chat({ kind: "user_text", text: e.prompt.slice(0, 16e3) });
@@ -1056,6 +1126,7 @@ var CodexEventSource = class {
1056
1126
  elapsed_time_seconds: 0
1057
1127
  });
1058
1128
  } else if (e.hook_event_name === "Interrupt") {
1129
+ this.turnOpen = false;
1059
1130
  this.emit({ type: "result", subtype: "error", errors: ["Codex turn interrupted"] });
1060
1131
  this.chat({ kind: "turn_end" });
1061
1132
  }
@@ -1065,6 +1136,14 @@ var CodexEventSource = class {
1065
1136
  await this.transcript?.flush();
1066
1137
  this.transcript = null;
1067
1138
  }
1139
+ /** Throttled local-only liveness; silent unless a turn is open. */
1140
+ emitLiveness() {
1141
+ if (!this.turnOpen) return;
1142
+ const now = Date.now();
1143
+ if (now - this.lastLivenessAt < CODEX_LIVENESS_THROTTLE_MS) return;
1144
+ this.lastLivenessAt = now;
1145
+ this.emit({ type: "tool_progress", tool_name: "Codex", elapsed_time_seconds: 0 });
1146
+ }
1068
1147
  watchTranscript(e) {
1069
1148
  if (this.transcript || typeof e.transcript_path !== "string" || typeof e.transcript_offset !== "number" || e.transcript_offset < 0)
1070
1149
  return;
@@ -1079,9 +1158,10 @@ var CodexEventSource = class {
1079
1158
  handleTranscriptRecord(raw) {
1080
1159
  if (!raw || typeof raw !== "object") return;
1081
1160
  const record = raw;
1082
- if (record.type !== "event_msg" || !record.payload || typeof record.payload !== "object")
1083
- return;
1161
+ if (!record.payload || typeof record.payload !== "object") return;
1084
1162
  const event = record.payload;
1163
+ if (event.type !== "task_complete") this.emitLiveness();
1164
+ if (record.type !== "event_msg") return;
1085
1165
  if (event.type === "token_count") {
1086
1166
  this.handleTokenCount(event);
1087
1167
  return;
@@ -1090,6 +1170,7 @@ var CodexEventSource = class {
1090
1170
  const turn = event.turn_id;
1091
1171
  if (typeof turn !== "string" || this.completed.has(turn)) return;
1092
1172
  this.rememberTurn(turn);
1173
+ this.turnOpen = false;
1093
1174
  const error = event.error;
1094
1175
  if (isUsageCap(error)) this.emitUsageCap();
1095
1176
  this.emit({
@@ -1178,6 +1259,7 @@ var CodexEventSource = class {
1178
1259
  const turn = e["turn-id"];
1179
1260
  if (typeof turn !== "string" || this.completed.has(turn)) return;
1180
1261
  this.rememberTurn(turn);
1262
+ this.turnOpen = false;
1181
1263
  const text = typeof e["last-assistant-message"] === "string" ? e["last-assistant-message"] : "";
1182
1264
  if (text) {
1183
1265
  this.emit({
@@ -2643,6 +2725,7 @@ var ClaudeTuiAdapter = class {
2643
2725
  };
2644
2726
 
2645
2727
  // src/harness/pty/session.ts
2728
+ var SUBMIT_PROVING_PROGRESS_ADAPTERS = /* @__PURE__ */ new Set(["codex", "opencode"]);
2646
2729
  var PtySession = class {
2647
2730
  constructor(prompt, options, resume, bridge, adapter = new ClaudeTuiAdapter()) {
2648
2731
  this.options = options;
@@ -3702,7 +3785,7 @@ var PtySession = class {
3702
3785
  if (this.pendingPlanApproval && event.type === "result") {
3703
3786
  this.disarmPlanDialogAutoAccept();
3704
3787
  }
3705
- if (this.pendingSubmitNudge && (event.type === "assistant" || event.type === "result" || this.adapter.id === "codex" && event.type === "tool_progress")) {
3788
+ if (this.pendingSubmitNudge && (event.type === "assistant" || event.type === "result" || SUBMIT_PROVING_PROGRESS_ADAPTERS.has(this.adapter.id) && event.type === "tool_progress")) {
3706
3789
  this.disarmSubmitNudge();
3707
3790
  }
3708
3791
  this.synthesizeRateLimitFromBanner(event);
@@ -4919,6 +5002,13 @@ var CodexTuiAdapter = class {
4919
5002
  // The pod image owns CLI upgrades. An interactive update would try to
4920
5003
  // replace the root-owned global package from the conveyor user account.
4921
5004
  check_for_update_on_startup: false,
5005
+ // Acknowledge 0.157.0's model promotions so unattended sessions start.
5006
+ // This dismisses the notices; --model below still selects the saved model.
5007
+ "notice.model_migrations": {
5008
+ "gpt-5.6-terra": "gpt-6-sol",
5009
+ "gpt-5.6-sol": "gpt-6-sol",
5010
+ "gpt-5.6-luna": "gpt-6-luna"
5011
+ },
4922
5012
  sqlite_home: codexSqliteHome(this.env),
4923
5013
  approval_policy: "never",
4924
5014
  // Always full access, plan mode included. Codex's own read-only mode is
@@ -5217,7 +5307,10 @@ var DirectStreamController = class {
5217
5307
  this.starting = true;
5218
5308
  const created = new PtyStreamServer({
5219
5309
  sessionId: this.reporter.sessionId,
5220
- onInput: (data) => this.inputHandler?.(data),
5310
+ onInput: (data) => {
5311
+ this.reporter.notePtyInput();
5312
+ this.inputHandler?.(data);
5313
+ },
5221
5314
  onResize: (cols, rows) => {
5222
5315
  this.directDims = { cols, rows };
5223
5316
  this.applyDims();
@@ -8597,7 +8690,7 @@ var waitForChecksContract = defineToolContract({
8597
8690
  var ciWaitContracts = [waitForChecksContract];
8598
8691
  var DESCRIPTION = [
8599
8692
  "Ask typed questions about text and get probabilities back, never text: yes/no (noul \u2192 probability of yes), pick-one (choice \u2192 the chosen option, per-option probabilities, and a confidence), or a graded scale (score \u2192 the level, per-level probabilities, and a confidence).",
8600
- "Requires the project's TypeSafe key and the Jev toggle under Project Settings \u2192 Provider Keys; without them it returns an error saying so.",
8693
+ "Requires Jev to be switched on under Project Settings \u2192 AI Provider Keys (a TypeSafe key, or an OpenCode Zen key); without it the tool returns an error saying so.",
8601
8694
  "The state is data, not a guardrail: adversarial text inside it can move the answers, so never use a result to authorize an action.",
8602
8695
  "Ask one simple judgment per question and split compound questions; accuracy falls when the state carries irrelevant text.",
8603
8696
  "Pass items to judge up to 100 separate texts with the same questions in one call; answers come back in item order.",
@@ -12926,13 +13019,17 @@ async function runPassiveTurn(host, context) {
12926
13019
  host.activeQuery = null;
12927
13020
  }
12928
13021
  }
13022
+ function parkedTuiWatchOptions(harness, cwd) {
13023
+ if (harness.emitsStructuredEvents === false) return { disableWedgeAbort: true };
13024
+ if (harness.ownsClaudeConfigHome === false) return {};
13025
+ return { probeMountDead: () => isConfigHomeMountDead(cwd) };
13026
+ }
12929
13027
  async function trackAndRun(host, context, options, agentQuery) {
12930
13028
  if (host.harnessKind === "pty" && options.promptDelivery !== "prefill") {
12931
- const rawRelay = host.harness.emitsStructuredEvents === false;
12932
13029
  agentQuery = watchForParkedTui(
12933
13030
  agentQuery,
12934
13031
  host,
12935
- rawRelay ? { disableWedgeAbort: true } : { probeMountDead: () => isConfigHomeMountDead(options.cwd) }
13032
+ parkedTuiWatchOptions(host.harness, options.cwd)
12936
13033
  );
12937
13034
  }
12938
13035
  host.activeQuery = agentQuery;
@@ -14320,6 +14417,7 @@ var SessionRunner = class _SessionRunner {
14320
14417
  onGitFlush: () => void this.periodicGitFlush(),
14321
14418
  onUsageSample: () => void this.sampleAndReportKeyUsage()
14322
14419
  });
14420
+ this.connection.onPtyInputActivity(() => this.lifecycle.touchIdleTimer());
14323
14421
  }
14324
14422
  get state() {
14325
14423
  return this._state;
@@ -15656,7 +15754,9 @@ ${outcome.failures.join("\n")}
15656
15754
  * turn: a background launch, or an armed `ScheduleWakeup`. */
15657
15755
  noteToolUseForBackgroundWork(event) {
15658
15756
  if (event.type !== "tool_use" || typeof event.tool !== "string") return;
15659
- this.backgroundWork.noteToolUse(event.tool, event.input);
15757
+ if (resolveCardTui(this.config.runnerMode) === "claude-code") {
15758
+ this.backgroundWork.noteToolUse(event.tool, event.input);
15759
+ }
15660
15760
  this.scheduledWakeup.noteToolUse(event.tool, event.input);
15661
15761
  }
15662
15762
  async setState(status) {
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-DMTBXIA4.js";
10
+ } from "./chunk-2BHXJ37W.js";
11
11
  import {
12
12
  PtyHarness,
13
13
  SessionRunner,
@@ -25,7 +25,7 @@ import {
25
25
  runUsageProbe,
26
26
  sampleKeyUsage,
27
27
  usesNativeUsageReporting
28
- } from "./chunk-ND5INHM4.js";
28
+ } from "./chunk-HYXAXQHN.js";
29
29
  import "./chunk-37J5MMQT.js";
30
30
  import {
31
31
  inheritedEnv,
@@ -44,7 +44,7 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-GHNYP67H.js";
47
+ } from "./chunk-7KG7CDVU.js";
48
48
  import "./chunk-Q4FQOJ7D.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
50
  import "./chunk-372R6E4C.js";
@@ -1458,7 +1458,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1458
1458
  process.exit(1);
1459
1459
  }
1460
1460
  if (CONVEYOR_MODE === "serving") {
1461
- const { runServingSession } = await import("./serve-boot-CYVW7Z27.js");
1461
+ const { runServingSession } = await import("./serve-boot-F2G7ZXFF.js");
1462
1462
  exitContext.runnerMode = "serving";
1463
1463
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1464
1464
  const outcome = await runServingSession({
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _project_shared from '@project/shared';
2
- import { RunnerMode, AgentSessionServiceMethods, AgentMode, PtyChatEventPayload, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus } from '@project/shared';
2
+ import { RunnerMode, AgentSessionServiceMethods, AgentMode, PtyChatEventPayload, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, SessionPreviewLink } from '@project/shared';
3
3
  export * from '@project/shared';
4
4
  import { ChildProcess } from 'node:child_process';
5
5
 
@@ -135,6 +135,9 @@ declare class AgentConnection {
135
135
  * question-grace expiry reads it to defer un-parking while a human is
136
136
  * actively typing into the Connected-TUI (they may be mid-answer). */
137
137
  private _lastPtyInputAt;
138
+ /** Runner-side subscribers to keystrokes from EITHER transport. */
139
+ private readonly ptyInputActivityListeners;
140
+ private lastReportedDirectInputAt;
138
141
  constructor(config: AgentConnectionConfig);
139
142
  get sessionId(): string;
140
143
  get connected(): boolean;
@@ -294,7 +297,18 @@ declare class AgentConnection {
294
297
  * just stays on the relay transport.
295
298
  */
296
299
  reportPtyStream(port: number | null): void;
297
- /** Epoch ms of the last relayed TUI keystroke, or null before any. */
300
+ /**
301
+ * A human typed into the TUI. Stamps `lastPtyInputAt` and notifies the
302
+ * runner's input-activity listeners for both transports. Direct-stream
303
+ * keystrokes never cross the API, so for those this also tells the server
304
+ * (`reportPtyInput`, throttled, fire-and-forget) — the relay path's server
305
+ * already bumps in `relayPtyInput`. An old API that doesn't know the method
306
+ * rejects harmlessly.
307
+ */
308
+ notePtyInput(origin?: "relay" | "direct"): void;
309
+ /** Subscribe to keystroke activity from either transport. Returns an unsubscribe fn. */
310
+ onPtyInputActivity(listener: () => void): () => void;
311
+ /** Epoch ms of the last TUI keystroke (relay or direct), or null before any. */
298
312
  get lastPtyInputAt(): number | null;
299
313
  /** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
300
314
  onPtyInput(handler: (data: string) => void): () => void;
@@ -620,6 +634,10 @@ declare class Lifecycle {
620
634
  * window every time it defers. */
621
635
  startIdleTimer(overrideMs?: number): void;
622
636
  cancelIdleTimer(): void;
637
+ /** Restart an ARMED idle timer with the full window; a no-op when none is
638
+ * armed. Human activity uses this, so a keystroke during a running turn
639
+ * (timer cancelled) can never arm one. */
640
+ touchIdleTimer(): void;
623
641
  /** Start (or restart) the dormant timer.
624
642
  * @param overrideMs Optional custom delay in ms. When provided, the timer
625
643
  * fires after exactly that delay instead of `dormantTimeoutMs`. SessionRunner
@@ -1294,12 +1312,16 @@ declare function removeWorktree(projectDir: string, taskId: string): Promise<voi
1294
1312
  interface ConveyorConfig {
1295
1313
  startCommand?: string;
1296
1314
  }
1315
+ type PortAttributes = {
1316
+ label?: string;
1317
+ visibility?: "public" | "private";
1318
+ hidden?: boolean;
1319
+ };
1297
1320
  interface ForwardPortsResult {
1298
1321
  ports: number[];
1299
- attributes: Record<string, {
1300
- label?: string;
1301
- visibility?: "public" | "private";
1302
- }>;
1322
+ attributes: Record<string, PortAttributes>;
1323
+ /** `customizations.conveyor.previewLinks`, sanitized. */
1324
+ links: SessionPreviewLink[];
1303
1325
  }
1304
1326
  declare function loadForwardPorts(workspaceDir: string): Promise<ForwardPortsResult>;
1305
1327
  /** Load config from env vars (project-level settings injected via bootstrap).
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-ND5INHM4.js";
4
+ } from "./chunk-HYXAXQHN.js";
5
5
  import "./chunk-37J5MMQT.js";
6
6
  import "./chunk-WCJ6HRVU.js";
7
7
  import {
@@ -17,7 +17,7 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-GHNYP67H.js";
20
+ } from "./chunk-7KG7CDVU.js";
21
21
  import "./chunk-Q4FQOJ7D.js";
22
22
  import {
23
23
  runAuthTokenCommand,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WorkspaceCommandSupervisor
3
- } from "./chunk-DMTBXIA4.js";
3
+ } from "./chunk-2BHXJ37W.js";
4
4
  import {
5
5
  AgentConnection,
6
6
  CodespacePortVisibility,
@@ -11,7 +11,7 @@ import {
11
11
  createServiceLogger,
12
12
  ensureOnTaskBranch,
13
13
  loadConveyorConfig
14
- } from "./chunk-GHNYP67H.js";
14
+ } from "./chunk-7KG7CDVU.js";
15
15
  import "./chunk-Q4FQOJ7D.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.28",
3
+ "version": "11.0.30",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",
@@ -151,6 +151,17 @@ labels. Each tag carries a `description` (≤255 — the summary), an `overview`
151
151
  work. The loop idles on `conveyor-wait`, a CLI in `@rallycry/conveyor-mcp`
152
152
  that blocks until a card becomes claimable, so an idle loop wakes on a board
153
153
  event instead of a timer.
154
+ - **From an app, with a project token (no MCP, no user)**: an external service
155
+ holding an incident-scoped project token files AND starts a card in one
156
+ call — `POST /api/incidents/report` with `type: "task"`, a `plan`, and
157
+ `start: true` (optionally `tags: [<glossary names>]`; an unknown name is a
158
+ 400). The build starts through the same path as `start_task`, as the token's
159
+ owner, and the response carries `{ taskId, slug, url, status, buildStarted,
160
+ buildError? }`. A failed start still returns 201 with `buildStarted: false`
161
+ and leaves the card for a human — do not retry the report, that files a
162
+ second card. Poll `GET /api/incidents/tasks/<taskId>` with the same token for
163
+ `{ status, githubPRUrl, githubPRNumber, agentRunnerStatus }`. `start` is
164
+ rate-limited to 30 per token per 10 minutes.
154
165
  - **Reserved-branch trap**: a card with an assigned agent may have a reserved
155
166
  `githubBranch`. Check `get_task` before pushing: if set, push to THAT
156
167
  branch; a PR from any other branch gets auto-closed and unlinked.