@rallycry/conveyor-agent 11.0.4 → 11.0.6

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,6 +1,6 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-UBDSLM44.js";
3
+ } from "./chunk-D57S4UR5.js";
4
4
  import {
5
5
  TOMBSTONE_MESSAGE,
6
6
  isLegacyEntrypointLaunch
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  readAgentVersion
10
10
  } from "./chunk-XORJ6SII.js";
11
- import "./chunk-3F4ZZKCA.js";
11
+ import "./chunk-W6VILPQE.js";
12
12
  import {
13
13
  GitPrepJob,
14
14
  defaultGit,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadPtySpawn
3
- } from "./chunk-3F4ZZKCA.js";
3
+ } from "./chunk-W6VILPQE.js";
4
4
  import {
5
5
  terminateProcessGroup
6
6
  } from "./chunk-W4LZ7R6Z.js";
@@ -9,6 +9,7 @@ import {
9
9
  buildPromptBytes,
10
10
  buildSpawnArgs,
11
11
  cleanTerminalOutput,
12
+ ensureSessionTarget,
12
13
  inheritedEnv,
13
14
  killPtyWithEscalation,
14
15
  needsRawReadyGate,
@@ -27,7 +28,7 @@ import {
27
28
  spawnOptionsFingerprint,
28
29
  transcriptSize,
29
30
  turnOptionsFrom
30
- } from "./chunk-3F4ZZKCA.js";
31
+ } from "./chunk-W6VILPQE.js";
31
32
  import {
32
33
  AgentConnection,
33
34
  CodespacePortVisibility,
@@ -56,7 +57,7 @@ import {
56
57
  statWorkspacePath,
57
58
  updateRemoteToken,
58
59
  verifyGitCredential
59
- } from "./chunk-SLUBOIYE.js";
60
+ } from "./chunk-HFUVAHII.js";
60
61
  import {
61
62
  registerBootMilestoneSocketFallback,
62
63
  reportBootMilestone
@@ -4639,6 +4640,12 @@ var PtySession = class {
4639
4640
  // synthetic id so the card flips to answered.
4640
4641
  pendingSyntheticQuestionIds = [];
4641
4642
  questionResultRemap = /* @__PURE__ */ new Map();
4643
+ // An AskUserQuestion questionnaire is parked on screen (PreToolUse hook
4644
+ // fired, no PostToolUse envelope yet). While set the parked process must
4645
+ // not be reused: feeding follow-up prompt bytes into the live dialog would
4646
+ // type chat text into the questionnaire. Fail-safe — a stale flag only
4647
+ // costs a teardown + respawn (`--resume` recovers the conversation).
4648
+ questionnairePending = false;
4642
4649
  // Per-turn state: the prompt to feed and the per-turn options subset. Both
4643
4650
  // start from the constructor args (turn 1) and are replaced by beginTurn.
4644
4651
  turnPrompt;
@@ -4731,7 +4738,9 @@ var PtySession = class {
4731
4738
  */
4732
4739
  canReuse(resume, fingerprint) {
4733
4740
  const resumeMatches = resume === void 0 ? this.reportedOpenCodeId !== null : resume === this.sessionUuid;
4734
- return !this._toreDown && !this.exited && this.activeQueue === null && resumeMatches && fingerprint === this.spawnFingerprint;
4741
+ return !this._toreDown && !this.exited && this.activeQueue === null && // A parked questionnaire owns the terminal — a fed prompt would type
4742
+ // into the dialog. Force teardown + respawn instead.
4743
+ !this.questionnairePending && resumeMatches && fingerprint === this.spawnFingerprint;
4735
4744
  }
4736
4745
  get hookSocketPath() {
4737
4746
  return this.socket ? this.socket.socketPath : null;
@@ -4806,6 +4815,7 @@ var PtySession = class {
4806
4815
  this.deliveredTexts = [];
4807
4816
  this.disarmSubmitNudge();
4808
4817
  this.disarmPlanDialogAutoAccept();
4818
+ this.questionnairePending = false;
4809
4819
  this.closeSyntheticQuestionCards();
4810
4820
  }
4811
4821
  /**
@@ -5115,6 +5125,7 @@ var PtySession = class {
5115
5125
  async teardown() {
5116
5126
  if (this._toreDown) return;
5117
5127
  this._toreDown = true;
5128
+ this.questionnairePending = false;
5118
5129
  this.disarmPlanDialogAutoAccept();
5119
5130
  this.disarmSubmitNudge();
5120
5131
  this.closeSyntheticQuestionCards();
@@ -5390,6 +5401,7 @@ var PtySession = class {
5390
5401
  if (this.pendingPlanApproval && progress.tool_name === "ExitPlanMode") {
5391
5402
  this.disarmPlanDialogAutoAccept();
5392
5403
  }
5404
+ if (progress.tool_name === "AskUserQuestion") this.questionnairePending = false;
5393
5405
  if (this.pendingSubmitNudge) this.disarmSubmitNudge();
5394
5406
  this.pushEvent({
5395
5407
  type: "tool_progress",
@@ -5410,6 +5422,7 @@ var PtySession = class {
5410
5422
  if (request.tool_name === "AskUserQuestion") {
5411
5423
  this.disarmSubmitNudge();
5412
5424
  this.disarmPlanDialogAutoAccept();
5425
+ this.questionnairePending = true;
5413
5426
  const questions = parseUserQuestions(request.tool_input);
5414
5427
  this.pushEvent({ type: "user_question", questions });
5415
5428
  this.emitSyntheticQuestionCard(questions);
@@ -5518,6 +5531,7 @@ var PtySession = class {
5518
5531
  async finalizeOnExit(exitCode) {
5519
5532
  this.coalescer?.flush();
5520
5533
  this.exited = true;
5534
+ this.questionnairePending = false;
5521
5535
  if (this._toreDown) return;
5522
5536
  if (this.tailer) {
5523
5537
  this.tailer.close();
@@ -5720,13 +5734,19 @@ var PtyHarness = class _PtyHarness {
5720
5734
  }
5721
5735
  async *executeQuery(opts) {
5722
5736
  const want = opts.resume ?? opts.options.resume ?? this.parked?.reportedSessionId ?? void 0;
5723
- const fingerprint = this.fingerprintOf(opts.options);
5737
+ const options = ensureSessionTarget(opts.options, want);
5738
+ if (options !== opts.options) {
5739
+ _PtyHarness.log.warn("spawn had no session target \u2014 minted one", {
5740
+ sessionId: options.sessionId
5741
+ });
5742
+ }
5743
+ const fingerprint = this.fingerprintOf(options);
5724
5744
  let session;
5725
- if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(opts.options)) {
5745
+ if (this.parked?.canReuse(want, fingerprint) && !await this.parkedHomeDied(options)) {
5726
5746
  session = this.parked;
5727
5747
  this.parked = null;
5728
5748
  this.cancelEndedTimer();
5729
- await session.beginTurn(opts.prompt, opts.options);
5749
+ await session.beginTurn(opts.prompt, options);
5730
5750
  } else {
5731
5751
  if (this.parked) {
5732
5752
  const stale = this.parked;
@@ -5734,10 +5754,10 @@ var PtyHarness = class _PtyHarness {
5734
5754
  this.cancelEndedTimer();
5735
5755
  await stale.teardown();
5736
5756
  }
5737
- session = await this.spawnSession(opts.prompt, opts.options, want);
5757
+ session = await this.spawnSession(opts.prompt, options, want);
5738
5758
  }
5739
5759
  yield* this.drain(session);
5740
- yield* this.recoverFailedDelivery(session, opts.options, want);
5760
+ yield* this.recoverFailedDelivery(session, options, want);
5741
5761
  }
5742
5762
  /**
5743
5763
  * Did the shared `~/.claude` mount die while this session sat parked?
@@ -6768,8 +6788,8 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
6768
6788
  }
6769
6789
 
6770
6790
  // src/execution/query-executor.ts
6771
- import { createHash as createHash2 } from "crypto";
6772
- import { existsSync, readFileSync as readFileSync2, truncateSync } from "fs";
6791
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
6792
+ import { existsSync, readFileSync as readFileSync2, renameSync, truncateSync } from "fs";
6773
6793
 
6774
6794
  // src/execution/chat-instructions.ts
6775
6795
  function buildChatInstructions(context, scenario, newMessages) {
@@ -7678,11 +7698,8 @@ function tagContextIntro(runnerMode) {
7678
7698
  return `These docs match this task's tags. They auto-load when you edit matching files, and you can Read any of them the moment you need its detail \u2014 do NOT read them all up front.`;
7679
7699
  }
7680
7700
  function formatResolvedTags(resolved, subProject, mentioned, runnerMode) {
7681
- const parts = [
7682
- `
7683
- ## Reference Guides (load on demand)`,
7684
- tagContextIntro(runnerMode)
7685
- ];
7701
+ const parts = [`
7702
+ ## Reference Guides (load on demand)`, tagContextIntro(runnerMode)];
7686
7703
  for (const tag of resolved) {
7687
7704
  if (tag.entries.length === 0 && !tag.hasOverview) continue;
7688
7705
  const desc = tag.description ? ` \u2014 ${tag.description}` : "";
@@ -8122,7 +8139,10 @@ function buildRelaunchWithSession(mode, context, agentMode, isAuto) {
8122
8139
  parts.push(`You were relaunched but no new instructions have been given since your last run.`);
8123
8140
  if (agentMode === "auto" || agentMode === "building" || isAuto) {
8124
8141
  parts.push(
8125
- ...builderKickoff(context, `You are the Builder on this card \u2014 pick up where you left off.`)
8142
+ ...builderKickoff(
8143
+ context,
8144
+ `You are the Builder on this card \u2014 pick up where you left off.`
8145
+ )
8126
8146
  );
8127
8147
  } else {
8128
8148
  parts.push(
@@ -9405,7 +9425,9 @@ var driveListFilesContract = defineToolContract({
9405
9425
  fields: {
9406
9426
  folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
9407
9427
  search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
9408
- limit: f.optional(f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 }))
9428
+ limit: f.optional(
9429
+ f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 })
9430
+ )
9409
9431
  }
9410
9432
  },
9411
9433
  mcp: {
@@ -9414,7 +9436,9 @@ var driveListFilesContract = defineToolContract({
9414
9436
  projectId: mcpProjectId,
9415
9437
  folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
9416
9438
  search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
9417
- limit: f.optional(f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 }))
9439
+ limit: f.optional(
9440
+ f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 })
9441
+ )
9418
9442
  }
9419
9443
  }
9420
9444
  });
@@ -9517,7 +9541,12 @@ var listMeetingsContract = defineToolContract({
9517
9541
  description: `List this project's meetings, newest first \u2014 each with its title, date, source, status, participants, and a short summary preview. Start here when you are asked about "the meeting", "what did we decide", or "what came out of that call". ${SUMMARY_NOTE}`,
9518
9542
  fields: {
9519
9543
  limit: f.optional(
9520
- f.number({ desc: "How many to return (default 20, maximum 50).", int: true, min: 1, max: 50 })
9544
+ f.number({
9545
+ desc: "How many to return (default 20, maximum 50).",
9546
+ int: true,
9547
+ min: 1,
9548
+ max: 50
9549
+ })
9521
9550
  ),
9522
9551
  search: f.optional(
9523
9552
  f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
@@ -9529,7 +9558,12 @@ var listMeetingsContract = defineToolContract({
9529
9558
  fields: {
9530
9559
  projectId: mcpProjectId,
9531
9560
  limit: f.optional(
9532
- f.number({ desc: "How many to return (default 20, maximum 50).", int: true, min: 1, max: 50 })
9561
+ f.number({
9562
+ desc: "How many to return (default 20, maximum 50).",
9563
+ int: true,
9564
+ min: 1,
9565
+ max: 50
9566
+ })
9533
9567
  ),
9534
9568
  search: f.optional(
9535
9569
  f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
@@ -9583,9 +9617,7 @@ var SINCE_MINUTES = f.optional(
9583
9617
  max: 10080
9584
9618
  })
9585
9619
  );
9586
- var START_TIME = f.optional(
9587
- f.string({ desc: "ISO 8601 lower bound (overrides sinceMinutes)" })
9588
- );
9620
+ var START_TIME = f.optional(f.string({ desc: "ISO 8601 lower bound (overrides sinceMinutes)" }));
9589
9621
  var END_TIME = f.optional(f.string({ desc: "ISO 8601 upper bound (default now)" }));
9590
9622
  var LIMIT = f.optional(
9591
9623
  f.number({ desc: "Max entries per page (default 50)", int: true, min: 1, max: 200 })
@@ -9619,7 +9651,10 @@ var gcpFields = {
9619
9651
  })
9620
9652
  ),
9621
9653
  search: f.optional(
9622
- f.string({ desc: "Free-text search across all log fields (exact substring, not regex)", max: 256 })
9654
+ f.string({
9655
+ desc: "Free-text search across all log fields (exact substring, not regex)",
9656
+ max: 256
9657
+ })
9623
9658
  ),
9624
9659
  filter: f.optional(
9625
9660
  f.string({
@@ -11246,7 +11281,9 @@ var CONTEXT_PATH_SHAPE = z18.object({
11246
11281
  locator: z18.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional().describe(
11247
11282
  'Verified-link tether: text that must keep existing in the file. With locatorType "test" it must appear inside a real it/test/describe TITLE; with "code" anywhere in the file. Validated at write time against the checkout and re-checked by the periodic sweep \u2014 a rename/delete flags the link stale. Locators containing <> are placeholders and never checked.'
11248
11283
  ),
11249
- locatorType: z18.enum(["test", "code"]).optional().describe("How the locator must match \u2014 required iff locator is set; not valid on folder links")
11284
+ locatorType: z18.enum(["test", "code"]).optional().describe(
11285
+ "How the locator must match \u2014 required iff locator is set; not valid on folder links"
11286
+ )
11250
11287
  }).refine((link) => link.locator === void 0 === (link.locatorType === void 0), {
11251
11288
  message: "locator and locatorType must be provided together"
11252
11289
  }).refine((link) => link.locator === void 0 || link.type !== "folder", {
@@ -11640,52 +11677,43 @@ function buildDriveCreateFileTool(connection, projectId) {
11640
11677
  );
11641
11678
  }
11642
11679
  function buildDriveUpdateFileTool(connection, projectId) {
11643
- return defineContractTool(
11644
- driveUpdateFileContract,
11645
- async ({ fileId, content, mimeType }) => {
11646
- try {
11647
- const file = await connection.call("updateProjectDriveFile", {
11648
- projectId,
11649
- fileId,
11650
- content,
11651
- mimeType
11652
- });
11653
- return textResult(`Updated "${file.name}" (${file.id})`);
11654
- } catch (error) {
11655
- return errText2("Failed to update the Google Drive file", error);
11656
- }
11680
+ return defineContractTool(driveUpdateFileContract, async ({ fileId, content, mimeType }) => {
11681
+ try {
11682
+ const file = await connection.call("updateProjectDriveFile", {
11683
+ projectId,
11684
+ fileId,
11685
+ content,
11686
+ mimeType
11687
+ });
11688
+ return textResult(`Updated "${file.name}" (${file.id})`);
11689
+ } catch (error) {
11690
+ return errText2("Failed to update the Google Drive file", error);
11657
11691
  }
11658
- );
11692
+ });
11659
11693
  }
11660
11694
  function buildDriveDeleteFileTool(connection, projectId) {
11661
- return defineContractTool(
11662
- driveDeleteFileContract,
11663
- async ({ fileId }) => {
11664
- try {
11665
- const result = await connection.call("deleteProjectDriveFile", { projectId, fileId });
11666
- return textResult(`Moved "${result.name}" (${result.id}) to the Google Drive trash`);
11667
- } catch (error) {
11668
- return errText2("Failed to delete the Google Drive file", error);
11669
- }
11695
+ return defineContractTool(driveDeleteFileContract, async ({ fileId }) => {
11696
+ try {
11697
+ const result = await connection.call("deleteProjectDriveFile", { projectId, fileId });
11698
+ return textResult(`Moved "${result.name}" (${result.id}) to the Google Drive trash`);
11699
+ } catch (error) {
11700
+ return errText2("Failed to delete the Google Drive file", error);
11670
11701
  }
11671
- );
11702
+ });
11672
11703
  }
11673
11704
  function buildDriveCreateFolderTool(connection, projectId) {
11674
- return defineContractTool(
11675
- driveCreateFolderContract,
11676
- async ({ name, folderId }) => {
11677
- try {
11678
- const folder = await connection.call("createProjectDriveFolder", {
11679
- projectId,
11680
- name,
11681
- folderId
11682
- });
11683
- return textResult(`Created folder "${folder.name}" (${folder.id})`);
11684
- } catch (error) {
11685
- return errText2("Failed to create the Google Drive folder", error);
11686
- }
11705
+ return defineContractTool(driveCreateFolderContract, async ({ name, folderId }) => {
11706
+ try {
11707
+ const folder = await connection.call("createProjectDriveFolder", {
11708
+ projectId,
11709
+ name,
11710
+ folderId
11711
+ });
11712
+ return textResult(`Created folder "${folder.name}" (${folder.id})`);
11713
+ } catch (error) {
11714
+ return errText2("Failed to create the Google Drive folder", error);
11687
11715
  }
11688
- );
11716
+ });
11689
11717
  }
11690
11718
  function buildDriveTools(connection, projectId) {
11691
11719
  return [
@@ -12524,16 +12552,22 @@ function questionTextFromEvent(event) {
12524
12552
  }
12525
12553
  async function armQuestionPending(host, state, event) {
12526
12554
  state.questionPending = true;
12555
+ const questionText = questionTextFromEvent(event);
12556
+ host.callbacks.onUserQuestion?.({
12557
+ kind: "armed",
12558
+ ...questionText === void 0 ? {} : { questionText }
12559
+ });
12527
12560
  await host.connection.emitStatus(
12528
12561
  "waiting_for_input",
12529
12562
  AGENT_STATUS_REASON_USER_QUESTION,
12530
- questionTextFromEvent(event)
12563
+ questionText
12531
12564
  );
12532
12565
  await host.callbacks.onStatusChange("waiting_for_input");
12533
12566
  }
12534
12567
  async function clearQuestionPending(host, state, options) {
12535
12568
  if (!state.questionPending) return;
12536
12569
  state.questionPending = false;
12570
+ host.callbacks.onUserQuestion?.({ kind: options.emitRunning ? "answered" : "turn_ended" });
12537
12571
  if (options.emitRunning) {
12538
12572
  await host.connection.emitStatus("running");
12539
12573
  await host.callbacks.onStatusChange("running");
@@ -13681,6 +13715,29 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
13681
13715
  }
13682
13716
  await trackAndRun(host, context, queryOptions, agentQuery);
13683
13717
  }
13718
+ function rotateSessionFile(cwd, sessionUuid) {
13719
+ const path = sessionTranscriptPath(cwd, sessionUuid);
13720
+ try {
13721
+ if (!existsSync(path)) return true;
13722
+ renameSync(path, `${path.replace(/\.jsonl$/, "")}.aborted-${Date.now()}.jsonl`);
13723
+ return true;
13724
+ } catch (error) {
13725
+ logger5.warn("Could not rotate an aborted transcript aside", {
13726
+ path,
13727
+ error: error instanceof Error ? error.message : String(error)
13728
+ });
13729
+ return false;
13730
+ }
13731
+ }
13732
+ function freshSessionOptions(host, context, options) {
13733
+ const lineageUuid = taskIdToSessionUuid(
13734
+ sessionLineageKey(context.taskId, host.agentMode, host.config.mode)
13735
+ );
13736
+ if (rotateSessionFile(host.config.workspaceDir, lineageUuid)) {
13737
+ return { ...options, sessionId: lineageUuid };
13738
+ }
13739
+ return { ...options, sessionId: randomUUID2() };
13740
+ }
13684
13741
  async function buildRetryQuery(host, context, options, lastErrorWasImage) {
13685
13742
  if (lastErrorWasImage) {
13686
13743
  host.connection.postChatMessage(
@@ -13700,10 +13757,7 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
13700
13757
  );
13701
13758
  return host.harness.executeQuery({
13702
13759
  prompt: host.createInputStream(retryPrompt),
13703
- // Strip sessionId on retry — if the failing query partially created a
13704
- // session with that ID, passing it again would error. Let the SDK
13705
- // auto-generate on retry attempts.
13706
- options: { ...options, sessionId: void 0 },
13760
+ options: freshSessionOptions(host, context, options),
13707
13761
  resume: void 0
13708
13762
  });
13709
13763
  }
@@ -13735,7 +13789,7 @@ async function handleAuthError(context, host, options) {
13735
13789
  );
13736
13790
  const freshQuery = host.harness.executeQuery({
13737
13791
  prompt: host.createInputStream(freshPrompt),
13738
- options: { ...options, sessionId: void 0 },
13792
+ options: freshSessionOptions(host, context, options),
13739
13793
  resume: void 0
13740
13794
  });
13741
13795
  return runWithRetry(freshQuery, context, host, options);
@@ -13756,7 +13810,7 @@ async function handleStaleSession(context, host, options) {
13756
13810
  );
13757
13811
  const freshQuery = host.harness.executeQuery({
13758
13812
  prompt: host.createInputStream(freshPrompt),
13759
- options: { ...options, sessionId: void 0 },
13813
+ options: freshSessionOptions(host, context, options),
13760
13814
  resume: void 0
13761
13815
  });
13762
13816
  return runWithRetry(freshQuery, context, host, options);
@@ -13856,7 +13910,7 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
13856
13910
  );
13857
13911
  const freshQuery = host.harness.executeQuery({
13858
13912
  prompt: host.createInputStream(freshPrompt),
13859
- options: { ...options, sessionId: void 0 },
13913
+ options: freshSessionOptions(host, context, options),
13860
13914
  resume: void 0
13861
13915
  });
13862
13916
  return runWithRetry(freshQuery, context, host, options);
@@ -14654,10 +14708,8 @@ var BackgroundWorkTracker = class {
14654
14708
  this.prune(now);
14655
14709
  if (this.deadlines.length === 0) return;
14656
14710
  this.deadlines.shift();
14657
- this.log(
14658
- `[conveyor-agent] background work finished; ${this.deadlines.length} outstanding
14659
- `
14660
- );
14711
+ this.log(`[conveyor-agent] background work finished; ${this.deadlines.length} outstanding
14712
+ `);
14661
14713
  this.onChange?.();
14662
14714
  }
14663
14715
  /** Is any non-expired background work outstanding? */
@@ -14707,6 +14759,15 @@ function findLiveChild(sources) {
14707
14759
  var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery", "chat"]);
14708
14760
  var CHILD_DEFER_RECHECK_MS = 60 * 1e3;
14709
14761
  var ORPHAN_WAKE_GRACE_MS = 3 * 60 * 1e3;
14762
+ var QUESTION_GRACE_MS = 10 * 6e4;
14763
+ var QUESTION_HOLD_SLACK_MS = 6e4;
14764
+ function resolveQuestionGraceMs() {
14765
+ const raw = Number(process.env.CONVEYOR_QUESTION_GRACE_MS ?? "");
14766
+ return Number.isFinite(raw) && raw > 0 ? raw : QUESTION_GRACE_MS;
14767
+ }
14768
+ var QUESTION_TYPING_RECENT_MS = 6e4;
14769
+ var QUESTION_TYPING_DEFER_MS = 2 * 6e4;
14770
+ 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.';
14710
14771
  var SessionRunner = class _SessionRunner {
14711
14772
  connection;
14712
14773
  mode;
@@ -14760,6 +14821,18 @@ var SessionRunner = class _SessionRunner {
14760
14821
  /** Guards the one-shot `agent_live` boot-milestone report — fired on the
14761
14822
  * first harness event (createQueryBridge's onEvent hook). */
14762
14823
  agentLiveReported = false;
14824
+ /** Epoch ms since a TUI questionnaire has been parked on a human with no
14825
+ * answer; null when none is pending. While set (and within the grace +
14826
+ * slack bound) `resolveLoopStatus` reports `waiting`, which reaches the API
14827
+ * as `active` and keeps the workspace activity clock fresh — the pod must
14828
+ * not be slept out from under an unanswered question. Deliberately KEPT
14829
+ * when the turn ends with the questionnaire still parked (the Shape-B
14830
+ * silent death this hold exists for). */
14831
+ questionPendingSince = null;
14832
+ /** Single-shot unref'd timer driving `handleQuestionGraceExpiry`. The
14833
+ * liveness hold above is bounded by timestamp arithmetic, so a lost timer
14834
+ * still expires the hold — it just skips the un-park injection. */
14835
+ questionGraceTimer = null;
14763
14836
  constructor(config, callbacks, deps = {}) {
14764
14837
  this.config = config;
14765
14838
  this.callbacks = callbacks;
@@ -14886,8 +14959,10 @@ var SessionRunner = class _SessionRunner {
14886
14959
  if (this.backgroundWork.hasPending()) holding = "background work outstanding";
14887
14960
  else if (isHeavyGateActive()) holding = "a gate process is running";
14888
14961
  } catch (err) {
14889
- process.stderr.write(`[conveyor-agent] Background-work probe failed, not deferring: ${err}
14890
- `);
14962
+ process.stderr.write(
14963
+ `[conveyor-agent] Background-work probe failed, not deferring: ${err}
14964
+ `
14965
+ );
14891
14966
  return false;
14892
14967
  }
14893
14968
  if (!holding) {
@@ -15460,6 +15535,7 @@ var SessionRunner = class _SessionRunner {
15460
15535
  }
15461
15536
  /** Inject a message (from connection callback or external source) */
15462
15537
  injectMessage(msg) {
15538
+ this.clearQuestionHold();
15463
15539
  if (this.inputResolver) {
15464
15540
  const resolve = this.inputResolver;
15465
15541
  this.inputResolver = null;
@@ -15667,6 +15743,7 @@ var SessionRunner = class _SessionRunner {
15667
15743
  }
15668
15744
  stop() {
15669
15745
  this.stopped = true;
15746
+ this.clearQuestionHold();
15670
15747
  this.queryBridge?.stop();
15671
15748
  void this.queryBridge?.dispose().catch(() => {
15672
15749
  });
@@ -15761,7 +15838,8 @@ var SessionRunner = class _SessionRunner {
15761
15838
  void this.connection.sendHeartbeat();
15762
15839
  }
15763
15840
  return this.callbacks.onEvent(event);
15764
- }
15841
+ },
15842
+ onUserQuestion: (signal) => this.handleUserQuestionSignal(signal)
15765
15843
  });
15766
15844
  bridge.isParentTask = this.fullContext?.isParentTask ?? false;
15767
15845
  bridge.onBackgroundTaskDone = () => this.backgroundWork.noteCompletion();
@@ -15912,8 +15990,72 @@ ${outcome.failures.join("\n")}
15912
15990
  */
15913
15991
  resolveLoopStatus() {
15914
15992
  if (loopStatusForRunnerStatus(this._state) === "active") return "active";
15993
+ if (this.questionPendingSince !== null && Date.now() - this.questionPendingSince < resolveQuestionGraceMs() + QUESTION_HOLD_SLACK_MS) {
15994
+ return "waiting";
15995
+ }
15915
15996
  return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
15916
15997
  }
15998
+ // ── Question grace hold ────────────────────────────────────────────
15999
+ /**
16000
+ * Question-pending transitions from the event loop. `armed` starts the
16001
+ * bounded liveness hold + the grace timer; `answered` clears both.
16002
+ * `turn_ended` — the turn's result arrived with the questionnaire still
16003
+ * parked — deliberately KEEPS them: that is the Shape-B silent death
16004
+ * (idle heartbeats stop bumping the activity clock and the reconciler
16005
+ * sleeps the pod minutes later) this hold exists to prevent.
16006
+ */
16007
+ handleUserQuestionSignal(signal) {
16008
+ if (signal.kind === "armed") {
16009
+ this.questionPendingSince = Date.now();
16010
+ this.armQuestionGraceTimer(resolveQuestionGraceMs());
16011
+ this.refreshLoopStatus();
16012
+ } else if (signal.kind === "answered") {
16013
+ this.clearQuestionHold();
16014
+ }
16015
+ }
16016
+ armQuestionGraceTimer(delayMs) {
16017
+ if (this.questionGraceTimer) clearTimeout(this.questionGraceTimer);
16018
+ this.questionGraceTimer = setTimeout(() => this.handleQuestionGraceExpiry(), delayMs);
16019
+ this.questionGraceTimer.unref();
16020
+ }
16021
+ clearQuestionHold() {
16022
+ if (this.questionGraceTimer) {
16023
+ clearTimeout(this.questionGraceTimer);
16024
+ this.questionGraceTimer = null;
16025
+ }
16026
+ if (this.questionPendingSince === null) return;
16027
+ this.questionPendingSince = null;
16028
+ this.refreshLoopStatus();
16029
+ }
16030
+ /**
16031
+ * The grace expired with the questionnaire still unanswered. Un-park the
16032
+ * agent by injecting the timeout prompt as a superseding system message —
16033
+ * the proven supersede path (mid-turn: queue + queryBridge.stop(), teardown
16034
+ * kills the parked dialog and the respawn `--resume`s cleanly; idle: the
16035
+ * inputResolver picks it up). Never synthetic keypresses: Enter would
16036
+ * fabricate an answer and Esc handling is CLI-version-dependent.
16037
+ *
16038
+ * A human actively typing in the Connected-TUI defers the expiry — they may
16039
+ * be mid-answer, and superseding would yank the dialog out from under them.
16040
+ */
16041
+ handleQuestionGraceExpiry() {
16042
+ this.questionGraceTimer = null;
16043
+ if (this.questionPendingSince === null || this.stopped) return;
16044
+ const lastInput = this.connection.lastPtyInputAt;
16045
+ if (lastInput !== null && Date.now() - lastInput < QUESTION_TYPING_RECENT_MS) {
16046
+ this.questionPendingSince = Date.now();
16047
+ this.armQuestionGraceTimer(QUESTION_TYPING_DEFER_MS);
16048
+ return;
16049
+ }
16050
+ process.stderr.write(
16051
+ "[conveyor-agent] Question grace expired with no answer \u2014 un-parking the agent\n"
16052
+ );
16053
+ this.injectMessage({
16054
+ content: QUESTION_TIMEOUT_PROMPT,
16055
+ userId: "system",
16056
+ source: "question_timeout"
16057
+ });
16058
+ }
15917
16059
  /** Mirror the resolved status into the loop-lag buffer so the starvation-proof
15918
16060
  * heartbeat worker reports it too. Returns what it wrote. */
15919
16061
  refreshLoopStatus() {
@@ -15942,6 +16084,7 @@ ${outcome.failures.join("\n")}
15942
16084
  process.stderr.write(`[conveyor-agent] Shutdown: reason=${finalState}
15943
16085
  `);
15944
16086
  this.connection.sendEvent({ type: "shutdown", reason: finalState });
16087
+ this.clearQuestionHold();
15945
16088
  this.portDiscovery.stop();
15946
16089
  this.backgroundWork.clear();
15947
16090
  this.loopLag.stop();
@@ -287,6 +287,10 @@ var AgentConnection = class _AgentConnection {
287
287
  // PTY relay (S5 terminal). Single-slot callbacks, set per PtySession run.
288
288
  ptyInputCallback = null;
289
289
  ptyResizeCallback = null;
290
+ /** Epoch ms of the last relayed keystroke, or null before any input. The
291
+ * question-grace expiry reads it to defer un-parking while a human is
292
+ * actively typing into the Connected-TUI (they may be mid-answer). */
293
+ _lastPtyInputAt = null;
290
294
  constructor(config) {
291
295
  this.config = config;
292
296
  }
@@ -475,6 +479,7 @@ var AgentConnection = class _AgentConnection {
475
479
  });
476
480
  this.socket.on("pty:input", (data) => {
477
481
  if (data.sessionId && data.sessionId !== this.config.sessionId) return;
482
+ this._lastPtyInputAt = Date.now();
478
483
  this.ptyInputCallback?.(data.data);
479
484
  });
480
485
  this.socket.on("pty:resize", (data) => {
@@ -925,6 +930,10 @@ var AgentConnection = class _AgentConnection {
925
930
  }).catch(() => {
926
931
  });
927
932
  }
933
+ /** Epoch ms of the last relayed TUI keystroke, or null before any. */
934
+ get lastPtyInputAt() {
935
+ return this._lastPtyInputAt;
936
+ }
928
937
  /** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
929
938
  onPtyInput(handler) {
930
939
  this.ptyInputCallback = handler;
@@ -5,7 +5,7 @@ import {
5
5
  readWorkspaceBytes,
6
6
  statWorkspacePath,
7
7
  workspacePathExists
8
- } from "./chunk-SLUBOIYE.js";
8
+ } from "./chunk-HFUVAHII.js";
9
9
  import {
10
10
  reportBootMilestone
11
11
  } from "./chunk-GL2DIQEQ.js";
@@ -4,6 +4,7 @@ import {
4
4
  } from "./chunk-W4LZ7R6Z.js";
5
5
 
6
6
  // src/harness/pty/pty-support.ts
7
+ import { randomUUID } from "crypto";
7
8
  import { stat } from "fs/promises";
8
9
  import { tmpdir } from "os";
9
10
 
@@ -153,6 +154,10 @@ function resolvePlanDialogTiming() {
153
154
  windowMs: envMs("CONVEYOR_PTY_PLAN_DIALOG_WINDOW_MS", PLAN_DIALOG_WINDOW_MS)
154
155
  };
155
156
  }
157
+ function ensureSessionTarget(options, want, mintId = randomUUID) {
158
+ if (want || options.sessionId) return options;
159
+ return { ...options, sessionId: mintId() };
160
+ }
156
161
  function turnOptionsFrom(options) {
157
162
  return {
158
163
  canUseTool: options.canUseTool,
@@ -278,6 +283,7 @@ export {
278
283
  resolveSubmitNudgeTiming,
279
284
  resolveSubmitRedeliveryMaxAttempts,
280
285
  resolvePlanDialogTiming,
286
+ ensureSessionTarget,
281
287
  turnOptionsFrom,
282
288
  killPtyWithEscalation,
283
289
  loadPtySpawn,
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-URYBO32C.js";
10
+ } from "./chunk-UHMCBCQL.js";
11
11
  import {
12
12
  DEFAULT_SONNET_MODEL,
13
13
  PtyHarness,
@@ -27,13 +27,13 @@ import {
27
27
  resolveTuiKindFromEnv,
28
28
  runUsageProbe,
29
29
  sampleKeyUsage
30
- } from "./chunk-HYPX6FEX.js";
30
+ } from "./chunk-GUYMLASA.js";
31
31
  import "./chunk-XORJ6SII.js";
32
32
  import {
33
33
  inheritedEnv,
34
34
  resolvePtySpawn,
35
35
  sessionTempBase
36
- } from "./chunk-3F4ZZKCA.js";
36
+ } from "./chunk-W6VILPQE.js";
37
37
  import {
38
38
  AgentConnection,
39
39
  CodespacePortVisibility,
@@ -44,7 +44,7 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-SLUBOIYE.js";
47
+ } from "./chunk-HFUVAHII.js";
48
48
  import "./chunk-GL2DIQEQ.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
50
  import "./chunk-7P6QZHXZ.js";
@@ -1201,7 +1201,7 @@ function wireSpawnChildren(mode, connection, supervisors, logger7) {
1201
1201
 
1202
1202
  // src/cli.ts
1203
1203
  if (process.argv[2] === "boot") {
1204
- const { runBoot } = await import("./boot-SWUVT5B4.js");
1204
+ const { runBoot } = await import("./boot-7XH4FFLO.js");
1205
1205
  process.exit(await runBoot(process.argv.slice(3)));
1206
1206
  }
1207
1207
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1303,7 +1303,7 @@ process.on("unhandledRejection", (reason) => {
1303
1303
  process.exit(1);
1304
1304
  });
1305
1305
  if (process.env.CONVEYOR_MODE === "workbench") {
1306
- const { startWorkbenchServer } = await import("./server-7XH7RYUX.js");
1306
+ const { startWorkbenchServer } = await import("./server-2Z5V3IPA.js");
1307
1307
  const { oomWatchdogOptionsFromEnv } = await import("./oom-watchdog-PAC5OJJG.js");
1308
1308
  const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QBCYO4GI.js");
1309
1309
  const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
@@ -1454,7 +1454,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1454
1454
  process.exit(1);
1455
1455
  }
1456
1456
  if (CONVEYOR_MODE === "serving") {
1457
- const { runServingSession } = await import("./serve-boot-B4BIZMNB.js");
1457
+ const { runServingSession } = await import("./serve-boot-IFUIGFEH.js");
1458
1458
  exitContext.runnerMode = "serving";
1459
1459
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1460
1460
  const outcome = await runServingSession({
package/dist/gate-cli.js CHANGED
@@ -79,7 +79,9 @@ function runGate(argv, options = {}) {
79
79
  try {
80
80
  process.exitCode = await runGate(process.argv.slice(2));
81
81
  } catch (error) {
82
- process.stderr.write(`conveyor-gate: ${error instanceof Error ? error.message : String(error)}
83
- `);
82
+ process.stderr.write(
83
+ `conveyor-gate: ${error instanceof Error ? error.message : String(error)}
84
+ `
85
+ );
84
86
  process.exitCode = 2;
85
87
  }
package/dist/index.d.ts CHANGED
@@ -131,6 +131,10 @@ declare class AgentConnection {
131
131
  private earlyProbeUsage;
132
132
  private ptyInputCallback;
133
133
  private ptyResizeCallback;
134
+ /** Epoch ms of the last relayed keystroke, or null before any input. The
135
+ * question-grace expiry reads it to defer un-parking while a human is
136
+ * actively typing into the Connected-TUI (they may be mid-answer). */
137
+ private _lastPtyInputAt;
134
138
  constructor(config: AgentConnectionConfig);
135
139
  get sessionId(): string;
136
140
  get connected(): boolean;
@@ -282,6 +286,8 @@ declare class AgentConnection {
282
286
  * just stays on the relay transport.
283
287
  */
284
288
  reportPtyStream(port: number | null): void;
289
+ /** Epoch ms of the last relayed TUI keystroke, or null before any. */
290
+ get lastPtyInputAt(): number | null;
285
291
  /** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
286
292
  onPtyInput(handler: (data: string) => void): () => void;
287
293
  /** Subscribe to relayed (reconciled) terminal resizes. Returns an unsubscribe fn. */
@@ -729,6 +735,18 @@ declare class SessionRunner {
729
735
  /** Guards the one-shot `agent_live` boot-milestone report — fired on the
730
736
  * first harness event (createQueryBridge's onEvent hook). */
731
737
  private agentLiveReported;
738
+ /** Epoch ms since a TUI questionnaire has been parked on a human with no
739
+ * answer; null when none is pending. While set (and within the grace +
740
+ * slack bound) `resolveLoopStatus` reports `waiting`, which reaches the API
741
+ * as `active` and keeps the workspace activity clock fresh — the pod must
742
+ * not be slept out from under an unanswered question. Deliberately KEPT
743
+ * when the turn ends with the questionnaire still parked (the Shape-B
744
+ * silent death this hold exists for). */
745
+ private questionPendingSince;
746
+ /** Single-shot unref'd timer driving `handleQuestionGraceExpiry`. The
747
+ * liveness hold above is bounded by timestamp arithmetic, so a lost timer
748
+ * still expires the hold — it just skips the un-park injection. */
749
+ private questionGraceTimer;
732
750
  constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks, deps?: SessionRunnerDependencies);
733
751
  get state(): AgentRunnerStatus;
734
752
  /**
@@ -1016,6 +1034,29 @@ declare class SessionRunner {
1016
1034
  * for why an unknown status still fails open to `active`.
1017
1035
  */
1018
1036
  private resolveLoopStatus;
1037
+ /**
1038
+ * Question-pending transitions from the event loop. `armed` starts the
1039
+ * bounded liveness hold + the grace timer; `answered` clears both.
1040
+ * `turn_ended` — the turn's result arrived with the questionnaire still
1041
+ * parked — deliberately KEEPS them: that is the Shape-B silent death
1042
+ * (idle heartbeats stop bumping the activity clock and the reconciler
1043
+ * sleeps the pod minutes later) this hold exists to prevent.
1044
+ */
1045
+ private handleUserQuestionSignal;
1046
+ private armQuestionGraceTimer;
1047
+ private clearQuestionHold;
1048
+ /**
1049
+ * The grace expired with the questionnaire still unanswered. Un-park the
1050
+ * agent by injecting the timeout prompt as a superseding system message —
1051
+ * the proven supersede path (mid-turn: queue + queryBridge.stop(), teardown
1052
+ * kills the parked dialog and the respawn `--resume`s cleanly; idle: the
1053
+ * inputResolver picks it up). Never synthetic keypresses: Enter would
1054
+ * fabricate an answer and Esc handling is CLI-version-dependent.
1055
+ *
1056
+ * A human actively typing in the Connected-TUI defers the expiry — they may
1057
+ * be mid-answer, and superseding would yank the dialog out from under them.
1058
+ */
1059
+ private handleQuestionGraceExpiry;
1019
1060
  /** Mirror the resolved status into the loop-lag buffer so the starvation-proof
1020
1061
  * heartbeat worker reports it too. Returns what it wrote. */
1021
1062
  private refreshLoopStatus;
@@ -1055,9 +1096,31 @@ interface AgentRunnerConfig {
1055
1096
  */
1056
1097
  packExecution?: _project_shared.PackExecution;
1057
1098
  }
1099
+ /**
1100
+ * Question-pending transitions surfaced to the runner by the event loop
1101
+ * (`applyQuestionTransitions` in execution/event-processor.ts):
1102
+ * - `armed` — the PTY hook observed an AskUserQuestion; a questionnaire is
1103
+ * parked on a human.
1104
+ * - `answered` — the questionnaire resolved (PostToolUse envelope, or a new
1105
+ * assistant record without the question block).
1106
+ * - `turn_ended` — the turn's `result` arrived while the questionnaire was
1107
+ * still parked. The dialog is still on screen with nobody answering — the
1108
+ * silent-death shape the question grace hold exists for — so the runner
1109
+ * must KEEP its pending hold rather than clear it.
1110
+ */
1111
+ type UserQuestionSignal = {
1112
+ kind: "armed";
1113
+ questionText?: string;
1114
+ } | {
1115
+ kind: "answered";
1116
+ } | {
1117
+ kind: "turn_ended";
1118
+ };
1058
1119
  interface AgentRunnerCallbacks {
1059
1120
  onEvent: (event: Record<string, unknown>) => void | Promise<void>;
1060
1121
  onStatusChange: (status: string) => void | Promise<void>;
1122
+ /** Optional — fired on question-pending transitions (see UserQuestionSignal). */
1123
+ onUserQuestion?: (signal: UserQuestionSignal) => void;
1061
1124
  }
1062
1125
 
1063
1126
  interface GithubTokenFileSync {
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-HYPX6FEX.js";
4
+ } from "./chunk-GUYMLASA.js";
5
5
  import "./chunk-XORJ6SII.js";
6
- import "./chunk-3F4ZZKCA.js";
6
+ import "./chunk-W6VILPQE.js";
7
7
  import {
8
8
  AgentConnection,
9
9
  GIT_TIMEOUT_MS,
@@ -17,7 +17,7 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-SLUBOIYE.js";
20
+ } from "./chunk-HFUVAHII.js";
21
21
  import "./chunk-GL2DIQEQ.js";
22
22
  import {
23
23
  runAuthTokenCommand,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WorkspaceCommandSupervisor
3
- } from "./chunk-URYBO32C.js";
3
+ } from "./chunk-UHMCBCQL.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-SLUBOIYE.js";
14
+ } from "./chunk-HFUVAHII.js";
15
15
  import "./chunk-GL2DIQEQ.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-UBDSLM44.js";
4
- import "./chunk-3F4ZZKCA.js";
3
+ } from "./chunk-D57S4UR5.js";
4
+ import "./chunk-W6VILPQE.js";
5
5
  import "./chunk-W4LZ7R6Z.js";
6
6
  import "./chunk-6Q6LQBWO.js";
7
7
  import "./chunk-6W6UZ4SJ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.4",
3
+ "version": "11.0.6",
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",