@openeryc/pi-coding-agent 0.75.45 → 0.75.46

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.
@@ -30,6 +30,7 @@ import { parseGitUrl } from "../../utils/git.js";
30
30
  import { getCwdRelativePath } from "../../utils/paths.js";
31
31
  import { getPiUserAgent } from "../../utils/pi-user-agent.js";
32
32
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
33
+ import { sleep } from "../../utils/sleep.js";
33
34
  import { ensureTool } from "../../utils/tools-manager.js";
34
35
  import { checkForNewPiVersion } from "../../utils/version-check.js";
35
36
  import { ArminComponent } from "./components/armin.js";
@@ -172,6 +173,15 @@ export class InteractiveMode {
172
173
  compactionQueuedMessages = [];
173
174
  // Shutdown state
174
175
  shutdownRequested = false;
176
+ // Goal runner state: auto-continues working on a session goal across turns,
177
+ // retries on errors, and survives process restarts via session persistence.
178
+ goalRunnerEnabled = true;
179
+ goalRunnerAbortController = undefined;
180
+ goalRunnerRetryAttempt = 0;
181
+ goalRunnerStatusDisplayed = false;
182
+ GOAL_RUNNER_MAX_RETRIES = 10;
183
+ GOAL_RUNNER_BASE_DELAY_MS = 3000;
184
+ GOAL_RUNNER_TURN_DELAY_MS = 800;
175
185
  // Extension UI state
176
186
  extensionSelector = undefined;
177
187
  extensionInput = undefined;
@@ -507,6 +517,16 @@ export class InteractiveMode {
507
517
  */
508
518
  async run() {
509
519
  await this.init();
520
+ // If resuming a session that has a goal, auto-continue from where it left off.
521
+ // This handles crash recovery and manual resume scenarios.
522
+ const resumedGoal = this.session.sessionGoal;
523
+ if (resumedGoal && this.session.state.messages.length > 0) {
524
+ this.goalRunnerEnabled = true;
525
+ this.chatContainer.addChild(new Spacer(1));
526
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Resuming goal: "${resumedGoal}" (${keyText("app.interrupt")} to pause)`), 1, 0));
527
+ this.ui.requestRender();
528
+ await this.goalRunnerContinuationLoop(resumedGoal);
529
+ }
510
530
  // Start version check asynchronously
511
531
  checkForNewPiVersion(this.version).then((newRelease) => {
512
532
  if (newRelease) {
@@ -562,6 +582,7 @@ export class InteractiveMode {
562
582
  // Main interactive loop
563
583
  while (true) {
564
584
  const userInput = await this.getUserInput();
585
+ this.goalRunnerEnabled = true; // Re-enable on any user input
565
586
  try {
566
587
  await this.session.prompt(userInput);
567
588
  }
@@ -569,6 +590,13 @@ export class InteractiveMode {
569
590
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
570
591
  this.showError(errorMessage);
571
592
  }
593
+ // After the prompt completes (or errors), check if goal runner should auto-continue.
594
+ if (this.goalRunnerEnabled) {
595
+ const goal = this.session.sessionGoal;
596
+ if (goal) {
597
+ await this.goalRunnerContinuationLoop(goal);
598
+ }
599
+ }
572
600
  }
573
601
  }
574
602
  async checkForPackageUpdates() {
@@ -1884,6 +1912,14 @@ export class InteractiveMode {
1884
1912
  if (this.session.isStreaming) {
1885
1913
  this.restoreQueuedMessagesToEditor({ abort: true });
1886
1914
  }
1915
+ else if (this.goalRunnerAbortController) {
1916
+ // Pause goal runner without clearing the goal
1917
+ this.goalRunnerEnabled = false;
1918
+ this.goalRunnerAbortController.abort();
1919
+ this.chatContainer.addChild(new Spacer(1));
1920
+ this.chatContainer.addChild(new Text(theme.fg("dim", "Goal runner paused. Use /goal on to resume."), 1, 0));
1921
+ this.ui.requestRender();
1922
+ }
1887
1923
  else if (this.session.isBashRunning) {
1888
1924
  this.session.abortBash();
1889
1925
  }
@@ -4365,9 +4401,12 @@ export class InteractiveMode {
4365
4401
  const goal = text.replace(/^\/goal\s*/, "").trim();
4366
4402
  if (!goal) {
4367
4403
  const currentGoal = this.session.sessionGoal;
4404
+ const runnerStatus = this.goalRunnerEnabled ? "enabled" : "paused";
4368
4405
  if (currentGoal) {
4369
4406
  this.chatContainer.addChild(new Spacer(1));
4370
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal: ${currentGoal}`), 1, 0));
4407
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal: ${currentGoal} (goal runner: ${runnerStatus})`), 1, 0));
4408
+ this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal on - enable goal runner`), 1, 0));
4409
+ this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal off - pause goal runner`), 1, 0));
4371
4410
  }
4372
4411
  else {
4373
4412
  this.showWarning("Usage: /goal <goal description>");
@@ -4375,11 +4414,92 @@ export class InteractiveMode {
4375
4414
  this.ui.requestRender();
4376
4415
  return;
4377
4416
  }
4417
+ const lower = goal.toLowerCase();
4418
+ if (lower === "on" || lower === "off") {
4419
+ this.goalRunnerEnabled = lower === "on";
4420
+ const currentGoal = this.session.sessionGoal;
4421
+ if (currentGoal) {
4422
+ const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
4423
+ this.chatContainer.addChild(new Spacer(1));
4424
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Goal runner ${label} for goal: "${currentGoal}"`), 1, 0));
4425
+ }
4426
+ else {
4427
+ this.showWarning("No goal is set. Use /goal <description> to set one first.");
4428
+ }
4429
+ this.ui.requestRender();
4430
+ return;
4431
+ }
4378
4432
  this.session.setSessionGoal(goal);
4433
+ this.goalRunnerEnabled = true;
4379
4434
  this.chatContainer.addChild(new Spacer(1));
4380
4435
  this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: ${goal}`), 1, 0));
4381
4436
  this.ui.requestRender();
4382
4437
  }
4438
+ /**
4439
+ * Goal runner continuation loop.
4440
+ * After each turn completes, auto-prompts the agent to continue pursuing the goal.
4441
+ * Handles errors with exponential backoff and respects user interruption.
4442
+ * Survives crashes: on next startup, the persisted session goal triggers auto-resume.
4443
+ */
4444
+ async goalRunnerContinuationLoop(goal) {
4445
+ // Abort any previous goal runner controller
4446
+ this.goalRunnerAbortController?.abort();
4447
+ this.goalRunnerAbortController = new AbortController();
4448
+ const signal = this.goalRunnerAbortController.signal;
4449
+ try {
4450
+ while (this.goalRunnerEnabled && this.session.sessionGoal) {
4451
+ if (signal.aborted)
4452
+ break;
4453
+ // Small delay between turns so the user can see what happened
4454
+ try {
4455
+ await sleep(this.GOAL_RUNNER_TURN_DELAY_MS, signal);
4456
+ }
4457
+ catch {
4458
+ break;
4459
+ }
4460
+ if (signal.aborted)
4461
+ break;
4462
+ // Skip if the agent is already streaming or compacting
4463
+ if (this.session.isStreaming || this.session.isCompacting) {
4464
+ continue;
4465
+ }
4466
+ // Show status on first iteration
4467
+ if (!this.goalRunnerStatusDisplayed) {
4468
+ this.goalRunnerStatusDisplayed = true;
4469
+ this.showStatus(`Pursuing goal: "${goal}" (${keyText("app.interrupt")} to pause)`);
4470
+ }
4471
+ try {
4472
+ await this.session.prompt(`Continue pursuing the goal: "${goal}". If you have fully completed this goal, report what was accomplished. If you cannot make further progress, explain what blocked you.`);
4473
+ // Successful turn completed, reset retry counter
4474
+ this.goalRunnerRetryAttempt = 0;
4475
+ }
4476
+ catch (error) {
4477
+ this.goalRunnerRetryAttempt++;
4478
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4479
+ if (this.goalRunnerRetryAttempt >= this.GOAL_RUNNER_MAX_RETRIES) {
4480
+ this.goalRunnerEnabled = false;
4481
+ this.showError(`Goal runner paused after ${this.GOAL_RUNNER_MAX_RETRIES} consecutive errors. ` +
4482
+ `Last error: ${errorMessage}. Use /goal on to resume.`);
4483
+ break;
4484
+ }
4485
+ // Exponential backoff
4486
+ const delay = this.GOAL_RUNNER_BASE_DELAY_MS * 2 ** (this.goalRunnerRetryAttempt - 1);
4487
+ this.showStatus(`Goal runner error, retrying in ${Math.ceil(delay / 1000)}s ` +
4488
+ `(${this.goalRunnerRetryAttempt}/${this.GOAL_RUNNER_MAX_RETRIES})... (${keyText("app.interrupt")} to pause)`);
4489
+ try {
4490
+ await sleep(delay, signal);
4491
+ }
4492
+ catch {
4493
+ break;
4494
+ }
4495
+ }
4496
+ }
4497
+ }
4498
+ finally {
4499
+ this.goalRunnerAbortController = undefined;
4500
+ this.goalRunnerStatusDisplayed = false;
4501
+ }
4502
+ }
4383
4503
  handleSessionCommand() {
4384
4504
  const stats = this.session.getSessionStats();
4385
4505
  const sessionName = this.sessionManager.getSessionName();