@rallycry/conveyor-agent 11.0.5 → 11.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  mapChatHistory,
3
- readAgentVersion
4
- } from "./chunk-XORJ6SII.js";
3
+ readAgentVersion,
4
+ refreshSkillsAfterCheckout
5
+ } from "./chunk-SR66HQKB.js";
5
6
  import {
6
7
  MAX_BETWEEN_TURN_BUFFER,
7
8
  MAX_DIAGNOSTIC_OUTPUT,
@@ -28,7 +29,7 @@ import {
28
29
  spawnOptionsFingerprint,
29
30
  transcriptSize,
30
31
  turnOptionsFrom
31
- } from "./chunk-W6VILPQE.js";
32
+ } from "./chunk-JG3VMWRQ.js";
32
33
  import {
33
34
  AgentConnection,
34
35
  CodespacePortVisibility,
@@ -44,6 +45,7 @@ import {
44
45
  forceFreshCooldownNotice,
45
46
  forceFreshMintBlocked,
46
47
  getCurrentBranch,
48
+ git,
47
49
  hasUncommittedChanges,
48
50
  hasUnpushedCommits,
49
51
  pushToOrigin,
@@ -57,7 +59,7 @@ import {
57
59
  statWorkspacePath,
58
60
  updateRemoteToken,
59
61
  verifyGitCredential
60
- } from "./chunk-SLUBOIYE.js";
62
+ } from "./chunk-APP6Y2D7.js";
61
63
  import {
62
64
  registerBootMilestoneSocketFallback,
63
65
  reportBootMilestone
@@ -78,7 +80,7 @@ import {
78
80
  } from "./chunk-IA45XHOA.js";
79
81
  import {
80
82
  getWorkbenchClient
81
- } from "./chunk-EXQ6AHOY.js";
83
+ } from "./chunk-TOTCEWJD.js";
82
84
  import {
83
85
  workbenchEnabled
84
86
  } from "./chunk-KMB3BU4S.js";
@@ -4640,6 +4642,12 @@ var PtySession = class {
4640
4642
  // synthetic id so the card flips to answered.
4641
4643
  pendingSyntheticQuestionIds = [];
4642
4644
  questionResultRemap = /* @__PURE__ */ new Map();
4645
+ // An AskUserQuestion questionnaire is parked on screen (PreToolUse hook
4646
+ // fired, no PostToolUse envelope yet). While set the parked process must
4647
+ // not be reused: feeding follow-up prompt bytes into the live dialog would
4648
+ // type chat text into the questionnaire. Fail-safe — a stale flag only
4649
+ // costs a teardown + respawn (`--resume` recovers the conversation).
4650
+ questionnairePending = false;
4643
4651
  // Per-turn state: the prompt to feed and the per-turn options subset. Both
4644
4652
  // start from the constructor args (turn 1) and are replaced by beginTurn.
4645
4653
  turnPrompt;
@@ -4732,7 +4740,9 @@ var PtySession = class {
4732
4740
  */
4733
4741
  canReuse(resume, fingerprint) {
4734
4742
  const resumeMatches = resume === void 0 ? this.reportedOpenCodeId !== null : resume === this.sessionUuid;
4735
- return !this._toreDown && !this.exited && this.activeQueue === null && resumeMatches && fingerprint === this.spawnFingerprint;
4743
+ return !this._toreDown && !this.exited && this.activeQueue === null && // A parked questionnaire owns the terminal — a fed prompt would type
4744
+ // into the dialog. Force teardown + respawn instead.
4745
+ !this.questionnairePending && resumeMatches && fingerprint === this.spawnFingerprint;
4736
4746
  }
4737
4747
  get hookSocketPath() {
4738
4748
  return this.socket ? this.socket.socketPath : null;
@@ -4807,6 +4817,7 @@ var PtySession = class {
4807
4817
  this.deliveredTexts = [];
4808
4818
  this.disarmSubmitNudge();
4809
4819
  this.disarmPlanDialogAutoAccept();
4820
+ this.questionnairePending = false;
4810
4821
  this.closeSyntheticQuestionCards();
4811
4822
  }
4812
4823
  /**
@@ -5116,6 +5127,7 @@ var PtySession = class {
5116
5127
  async teardown() {
5117
5128
  if (this._toreDown) return;
5118
5129
  this._toreDown = true;
5130
+ this.questionnairePending = false;
5119
5131
  this.disarmPlanDialogAutoAccept();
5120
5132
  this.disarmSubmitNudge();
5121
5133
  this.closeSyntheticQuestionCards();
@@ -5391,6 +5403,7 @@ var PtySession = class {
5391
5403
  if (this.pendingPlanApproval && progress.tool_name === "ExitPlanMode") {
5392
5404
  this.disarmPlanDialogAutoAccept();
5393
5405
  }
5406
+ if (progress.tool_name === "AskUserQuestion") this.questionnairePending = false;
5394
5407
  if (this.pendingSubmitNudge) this.disarmSubmitNudge();
5395
5408
  this.pushEvent({
5396
5409
  type: "tool_progress",
@@ -5411,6 +5424,7 @@ var PtySession = class {
5411
5424
  if (request.tool_name === "AskUserQuestion") {
5412
5425
  this.disarmSubmitNudge();
5413
5426
  this.disarmPlanDialogAutoAccept();
5427
+ this.questionnairePending = true;
5414
5428
  const questions = parseUserQuestions(request.tool_input);
5415
5429
  this.pushEvent({ type: "user_question", questions });
5416
5430
  this.emitSyntheticQuestionCard(questions);
@@ -5519,6 +5533,7 @@ var PtySession = class {
5519
5533
  async finalizeOnExit(exitCode) {
5520
5534
  this.coalescer?.flush();
5521
5535
  this.exited = true;
5536
+ this.questionnairePending = false;
5522
5537
  if (this._toreDown) return;
5523
5538
  if (this.tailer) {
5524
5539
  this.tailer.close();
@@ -12539,16 +12554,22 @@ function questionTextFromEvent(event) {
12539
12554
  }
12540
12555
  async function armQuestionPending(host, state, event) {
12541
12556
  state.questionPending = true;
12557
+ const questionText = questionTextFromEvent(event);
12558
+ host.callbacks.onUserQuestion?.({
12559
+ kind: "armed",
12560
+ ...questionText === void 0 ? {} : { questionText }
12561
+ });
12542
12562
  await host.connection.emitStatus(
12543
12563
  "waiting_for_input",
12544
12564
  AGENT_STATUS_REASON_USER_QUESTION,
12545
- questionTextFromEvent(event)
12565
+ questionText
12546
12566
  );
12547
12567
  await host.callbacks.onStatusChange("waiting_for_input");
12548
12568
  }
12549
12569
  async function clearQuestionPending(host, state, options) {
12550
12570
  if (!state.questionPending) return;
12551
12571
  state.questionPending = false;
12572
+ host.callbacks.onUserQuestion?.({ kind: options.emitRunning ? "answered" : "turn_ended" });
12552
12573
  if (options.emitRunning) {
12553
12574
  await host.connection.emitStatus("running");
12554
12575
  await host.callbacks.onStatusChange("running");
@@ -14740,6 +14761,15 @@ function findLiveChild(sources) {
14740
14761
  var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery", "chat"]);
14741
14762
  var CHILD_DEFER_RECHECK_MS = 60 * 1e3;
14742
14763
  var ORPHAN_WAKE_GRACE_MS = 3 * 60 * 1e3;
14764
+ var QUESTION_GRACE_MS = 10 * 6e4;
14765
+ var QUESTION_HOLD_SLACK_MS = 6e4;
14766
+ function resolveQuestionGraceMs() {
14767
+ const raw = Number(process.env.CONVEYOR_QUESTION_GRACE_MS ?? "");
14768
+ return Number.isFinite(raw) && raw > 0 ? raw : QUESTION_GRACE_MS;
14769
+ }
14770
+ var QUESTION_TYPING_RECENT_MS = 6e4;
14771
+ var QUESTION_TYPING_DEFER_MS = 2 * 6e4;
14772
+ var QUESTION_TIMEOUT_PROMPT = 'No one answered your clarifying questions within 10 minutes. Do this now: (1) Save your findings-so-far as the card plan (`update_task`). (2) Post the unanswered questions to chat with `post_to_chat` using `milestone: "blocked"`, stating the assumption you would make for each. (3) If you can safely proceed on your best-judgment answers, state the assumptions and continue; otherwise end your turn \u2014 answers posted in chat will be delivered when you resume.';
14743
14773
  var SessionRunner = class _SessionRunner {
14744
14774
  connection;
14745
14775
  mode;
@@ -14793,6 +14823,18 @@ var SessionRunner = class _SessionRunner {
14793
14823
  /** Guards the one-shot `agent_live` boot-milestone report — fired on the
14794
14824
  * first harness event (createQueryBridge's onEvent hook). */
14795
14825
  agentLiveReported = false;
14826
+ /** Epoch ms since a TUI questionnaire has been parked on a human with no
14827
+ * answer; null when none is pending. While set (and within the grace +
14828
+ * slack bound) `resolveLoopStatus` reports `waiting`, which reaches the API
14829
+ * as `active` and keeps the workspace activity clock fresh — the pod must
14830
+ * not be slept out from under an unanswered question. Deliberately KEPT
14831
+ * when the turn ends with the questionnaire still parked (the Shape-B
14832
+ * silent death this hold exists for). */
14833
+ questionPendingSince = null;
14834
+ /** Single-shot unref'd timer driving `handleQuestionGraceExpiry`. The
14835
+ * liveness hold above is bounded by timestamp arithmetic, so a lost timer
14836
+ * still expires the hold — it just skips the un-park injection. */
14837
+ questionGraceTimer = null;
14796
14838
  constructor(config, callbacks, deps = {}) {
14797
14839
  this.config = config;
14798
14840
  this.callbacks = callbacks;
@@ -15148,6 +15190,7 @@ var SessionRunner = class _SessionRunner {
15148
15190
  process.stderr.write("[conveyor-agent] WARNING: task-branch checkout failed\n");
15149
15191
  }
15150
15192
  if (ok) void reportBootMilestone({ key: "branch_ready" });
15193
+ await this.refreshSkillsForCheckout();
15151
15194
  }
15152
15195
  if (!this.stopped) {
15153
15196
  this.lifecycle.startGitFlush();
@@ -15207,6 +15250,40 @@ var SessionRunner = class _SessionRunner {
15207
15250
  await this.run();
15208
15251
  }
15209
15252
  // ── Core loop ──────────────────────────────────────────────────────
15253
+ /**
15254
+ * Re-run the skill floor against the tree `ensureOnTaskBranch` just produced.
15255
+ *
15256
+ * Split-mode pods keep the repo in the WORKBENCH container, so this routes
15257
+ * over the launcher: running it in-process here would materialize skills into
15258
+ * the AGENT container's own copy of /workspaces/repo — still on the base
15259
+ * branch, already complete — and look like it worked while Claude spawned on
15260
+ * the untouched real tree. Codespaces/local (no workbench) run it directly.
15261
+ *
15262
+ * Best-effort by design: a failure warns and boot continues, exactly like the
15263
+ * pre-ready binds it repeats.
15264
+ */
15265
+ async refreshSkillsForCheckout() {
15266
+ const write = (msg) => void process.stderr.write(`[conveyor-agent] ${msg}
15267
+ `);
15268
+ try {
15269
+ const result = workbenchEnabled() ? await getWorkbenchClient().refreshSkills() : await refreshSkillsAfterCheckout(
15270
+ this.config.workspaceDir,
15271
+ async (args, opts) => ({
15272
+ stdout: await git(opts?.cwd ?? this.config.workspaceDir, args, opts?.timeoutMs)
15273
+ }),
15274
+ { info: write, warn: write, error: write }
15275
+ );
15276
+ const { filled, shadowed, stale } = result;
15277
+ if (filled.length + shadowed.length + stale.length === 0) return;
15278
+ write(
15279
+ `post-checkout skill refresh: filled=[${filled.join(", ")}] shadowed=[${shadowed.join(", ")}] unusable=[${stale.join(", ")}]`
15280
+ );
15281
+ } catch (err) {
15282
+ write(
15283
+ `WARNING: post-checkout skill refresh failed: ${err instanceof Error ? err.message : String(err)}`
15284
+ );
15285
+ }
15286
+ }
15210
15287
  // oxlint-disable-next-line complexity -- dormant idle paths add branches but are tightly coupled to the loop
15211
15288
  async coreLoop() {
15212
15289
  while (!this.stopped) {
@@ -15495,6 +15572,7 @@ var SessionRunner = class _SessionRunner {
15495
15572
  }
15496
15573
  /** Inject a message (from connection callback or external source) */
15497
15574
  injectMessage(msg) {
15575
+ this.clearQuestionHold();
15498
15576
  if (this.inputResolver) {
15499
15577
  const resolve = this.inputResolver;
15500
15578
  this.inputResolver = null;
@@ -15702,6 +15780,7 @@ var SessionRunner = class _SessionRunner {
15702
15780
  }
15703
15781
  stop() {
15704
15782
  this.stopped = true;
15783
+ this.clearQuestionHold();
15705
15784
  this.queryBridge?.stop();
15706
15785
  void this.queryBridge?.dispose().catch(() => {
15707
15786
  });
@@ -15796,7 +15875,8 @@ var SessionRunner = class _SessionRunner {
15796
15875
  void this.connection.sendHeartbeat();
15797
15876
  }
15798
15877
  return this.callbacks.onEvent(event);
15799
- }
15878
+ },
15879
+ onUserQuestion: (signal) => this.handleUserQuestionSignal(signal)
15800
15880
  });
15801
15881
  bridge.isParentTask = this.fullContext?.isParentTask ?? false;
15802
15882
  bridge.onBackgroundTaskDone = () => this.backgroundWork.noteCompletion();
@@ -15947,8 +16027,72 @@ ${outcome.failures.join("\n")}
15947
16027
  */
15948
16028
  resolveLoopStatus() {
15949
16029
  if (loopStatusForRunnerStatus(this._state) === "active") return "active";
16030
+ if (this.questionPendingSince !== null && Date.now() - this.questionPendingSince < resolveQuestionGraceMs() + QUESTION_HOLD_SLACK_MS) {
16031
+ return "waiting";
16032
+ }
15950
16033
  return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
15951
16034
  }
16035
+ // ── Question grace hold ────────────────────────────────────────────
16036
+ /**
16037
+ * Question-pending transitions from the event loop. `armed` starts the
16038
+ * bounded liveness hold + the grace timer; `answered` clears both.
16039
+ * `turn_ended` — the turn's result arrived with the questionnaire still
16040
+ * parked — deliberately KEEPS them: that is the Shape-B silent death
16041
+ * (idle heartbeats stop bumping the activity clock and the reconciler
16042
+ * sleeps the pod minutes later) this hold exists to prevent.
16043
+ */
16044
+ handleUserQuestionSignal(signal) {
16045
+ if (signal.kind === "armed") {
16046
+ this.questionPendingSince = Date.now();
16047
+ this.armQuestionGraceTimer(resolveQuestionGraceMs());
16048
+ this.refreshLoopStatus();
16049
+ } else if (signal.kind === "answered") {
16050
+ this.clearQuestionHold();
16051
+ }
16052
+ }
16053
+ armQuestionGraceTimer(delayMs) {
16054
+ if (this.questionGraceTimer) clearTimeout(this.questionGraceTimer);
16055
+ this.questionGraceTimer = setTimeout(() => this.handleQuestionGraceExpiry(), delayMs);
16056
+ this.questionGraceTimer.unref();
16057
+ }
16058
+ clearQuestionHold() {
16059
+ if (this.questionGraceTimer) {
16060
+ clearTimeout(this.questionGraceTimer);
16061
+ this.questionGraceTimer = null;
16062
+ }
16063
+ if (this.questionPendingSince === null) return;
16064
+ this.questionPendingSince = null;
16065
+ this.refreshLoopStatus();
16066
+ }
16067
+ /**
16068
+ * The grace expired with the questionnaire still unanswered. Un-park the
16069
+ * agent by injecting the timeout prompt as a superseding system message —
16070
+ * the proven supersede path (mid-turn: queue + queryBridge.stop(), teardown
16071
+ * kills the parked dialog and the respawn `--resume`s cleanly; idle: the
16072
+ * inputResolver picks it up). Never synthetic keypresses: Enter would
16073
+ * fabricate an answer and Esc handling is CLI-version-dependent.
16074
+ *
16075
+ * A human actively typing in the Connected-TUI defers the expiry — they may
16076
+ * be mid-answer, and superseding would yank the dialog out from under them.
16077
+ */
16078
+ handleQuestionGraceExpiry() {
16079
+ this.questionGraceTimer = null;
16080
+ if (this.questionPendingSince === null || this.stopped) return;
16081
+ const lastInput = this.connection.lastPtyInputAt;
16082
+ if (lastInput !== null && Date.now() - lastInput < QUESTION_TYPING_RECENT_MS) {
16083
+ this.questionPendingSince = Date.now();
16084
+ this.armQuestionGraceTimer(QUESTION_TYPING_DEFER_MS);
16085
+ return;
16086
+ }
16087
+ process.stderr.write(
16088
+ "[conveyor-agent] Question grace expired with no answer \u2014 un-parking the agent\n"
16089
+ );
16090
+ this.injectMessage({
16091
+ content: QUESTION_TIMEOUT_PROMPT,
16092
+ userId: "system",
16093
+ source: "question_timeout"
16094
+ });
16095
+ }
15952
16096
  /** Mirror the resolved status into the loop-lag buffer so the starvation-proof
15953
16097
  * heartbeat worker reports it too. Returns what it wrote. */
15954
16098
  refreshLoopStatus() {
@@ -15977,6 +16121,7 @@ ${outcome.failures.join("\n")}
15977
16121
  process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
15978
16122
  `);
15979
16123
  this.connection.sendEvent({ type: "shutdown", reason: finalState });
16124
+ this.clearQuestionHold();
15980
16125
  this.portDiscovery.stop();
15981
16126
  this.backgroundWork.clear();
15982
16127
  this.loopLag.stop();