pi-goal-x 0.18.8 → 0.18.10

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.
Files changed (2) hide show
  1. package/extensions/goal.ts +57 -8
  2. package/package.json +1 -1
@@ -429,8 +429,15 @@ export default function goalExtension(pi: ExtensionAPI): void {
429
429
  // after their successful execute. Once set, pi.on("tool_call") blocks all
430
430
  // subsequent in-turn tool calls except POST_STOP_ALLOWED_TOOLS. This is the
431
431
  // schema fix for "agent keeps writing files after pause_goal".
432
+ // turnSeq: lightweight generation counter, incremented at each turn start.
433
+ // Used to scope turnStoppedFor so stale markers from prior turns or sessions
434
+ // cannot poison a resumed active goal.
435
+ // checkpointGoalId: remembers the incoming goal id from queued continuations.
436
+ // When set but the goal is no longer active/autoContinue, work tools are blocked.
432
437
  let goalWorkToolCalledThisTurn = false;
433
- let turnStoppedFor: string | null = null;
438
+ let turnSeq = 0;
439
+ let turnStoppedFor: { goalId: string; turnSeq: number } | null = null;
440
+ let checkpointGoalId: string | null = null;
434
441
 
435
442
  // #5 post-compaction resync: when a compaction just happened, the next agent
436
443
  // turn gets an extra reminder block. Set in session_compact, consumed
@@ -576,6 +583,28 @@ export default function goalExtension(pi: ExtensionAPI): void {
576
583
  accounting.lastAccountedAt = null;
577
584
  }
578
585
 
586
+ function advanceTurnSeq(): void {
587
+ turnSeq += 1;
588
+ if (turnStoppedFor?.turnSeq !== turnSeq) turnStoppedFor = null;
589
+ }
590
+
591
+ function currentTurnStoppedGoalId(): string | null {
592
+ if (!turnStoppedFor) return null;
593
+ if (turnStoppedFor.turnSeq !== turnSeq) {
594
+ turnStoppedFor = null;
595
+ return null;
596
+ }
597
+ return turnStoppedFor.goalId;
598
+ }
599
+
600
+ function isActionableContinuationGoal(goalId: string | null | undefined): goalId is string {
601
+ return !!goalId && state.goal?.id === goalId && state.goal.status === "active" && state.goal.autoContinue;
602
+ }
603
+
604
+ function isStaleCheckpointBlockedToolCall(toolName: string): boolean {
605
+ return !POST_STOP_ALLOWED_TOOL_SET.has(toolName);
606
+ }
607
+
579
608
  function clearStoppedRuntimeState(): void {
580
609
  clearContinuationState();
581
610
  clearActiveAccounting();
@@ -1285,7 +1314,7 @@ Verification contract:
1285
1314
  continuationTimer = null;
1286
1315
  continuationScheduledFor = null;
1287
1316
  syncGoalTools();
1288
- if (!state.goal || state.goal.id !== goalId || state.goal.status !== "active" || !state.goal.autoContinue) {
1317
+ if (!isActionableContinuationGoal(goalId)) {
1289
1318
  if (continuationQueuedFor === goalId) continuationQueuedFor = null;
1290
1319
  return;
1291
1320
  }
@@ -3267,8 +3296,6 @@ promptGuidelines: [
3267
3296
  },
3268
3297
  }));
3269
3298
 
3270
- syncGoalTools();
3271
-
3272
3299
  pi.on("context", async (event): Promise<{ messages: typeof event.messages } | undefined> => {
3273
3300
  let changed = false;
3274
3301
  const latestGoalEventIndex = new Map<string, number>();
@@ -3307,8 +3334,8 @@ promptGuidelines: [
3307
3334
 
3308
3335
  pi.on("turn_start", async (_event, ctx) => {
3309
3336
  // Per-turn flag resets (#4 + C9 fix).
3337
+ advanceTurnSeq();
3310
3338
  goalWorkToolCalledThisTurn = false;
3311
- turnStoppedFor = null;
3312
3339
  syncGoalTools();
3313
3340
  beginAccounting();
3314
3341
  updateUI(ctx);
@@ -3316,17 +3343,29 @@ promptGuidelines: [
3316
3343
 
3317
3344
  // #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
3318
3345
  pi.on("tool_call", async (event, ctx) => {
3346
+ const stoppedGoalId = currentTurnStoppedGoalId();
3319
3347
  // Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
3320
3348
  // complete_goal / propose_goal_tweak fires in this turn, block all subsequent tool calls except
3321
3349
  // read-only inspection. Forces the agent to yield the turn instead of "fixing"
3322
3350
  // the situation by creating extra files etc.
3323
- if (turnStoppedFor !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
3351
+ if (stoppedGoalId !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
3324
3352
  return {
3325
3353
  block: true,
3326
- reason: `The goal was already stopped earlier in this turn (goalId=${turnStoppedFor}). ` +
3354
+ reason: `The goal was already stopped earlier in this turn (goalId=${stoppedGoalId}). ` +
3327
3355
  `Do not call more tools; end the turn with a brief summary and yield to the user.`,
3328
3356
  };
3329
3357
  }
3358
+ // Stale checkpoint guard: if the turn was triggered by a queued continuation
3359
+ // for a goal that is no longer active/autoContinue, block work tools.
3360
+ if (checkpointGoalId !== null && !isActionableContinuationGoal(checkpointGoalId) && isStaleCheckpointBlockedToolCall(event.toolName)) {
3361
+ // Block the tool call with a stale-checkpoint message.
3362
+ return {
3363
+ block: true,
3364
+ reason: `Cannot call ${event.toolName}: the goal checkpoint that triggered this turn is no longer active. ` +
3365
+ `Goal ${checkpointGoalId} has been paused, cleared, or replaced. ` +
3366
+ `End the turn with a brief summary and yield to the user.`,
3367
+ };
3368
+ }
3330
3369
  // Phase 5 soft gate relaxation: active-goal question block and repeated get_goal
3331
3370
  // block are removed. The agent is trusted to prefer work tools; prompts nudge
3332
3371
  // toward concrete work without hard-stopping the turn.
@@ -3408,6 +3447,7 @@ promptGuidelines: [
3408
3447
 
3409
3448
  pi.on("session_start", async (event, ctx) => {
3410
3449
  loadState(ctx);
3450
+ syncGoalTools();
3411
3451
  syncTerminalInputPause(ctx);
3412
3452
  if (event.reason === "resume" && !state.goal && openGoals().length > 1 && ctx.hasUI) {
3413
3453
  await focusGoalCommand(ctx);
@@ -3448,11 +3488,13 @@ promptGuidelines: [
3448
3488
  });
3449
3489
 
3450
3490
  pi.on("before_agent_start", async (event, ctx) => {
3491
+ advanceTurnSeq();
3451
3492
  syncGoalTools();
3452
3493
  const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
3453
3494
  const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
3454
3495
 
3455
3496
  if (confirmationIntent !== null) {
3497
+ checkpointGoalId = null;
3456
3498
  clearContinuationState();
3457
3499
  clearActiveAccounting();
3458
3500
  runningGoalId = null;
@@ -3460,6 +3502,7 @@ promptGuidelines: [
3460
3502
  }
3461
3503
 
3462
3504
  if (tweakDraftingFor !== null) {
3505
+ checkpointGoalId = null;
3463
3506
  clearContinuationState();
3464
3507
  clearActiveAccounting();
3465
3508
  runningGoalId = null;
@@ -3470,8 +3513,12 @@ promptGuidelines: [
3470
3513
  // matches the active goal, abort the whole turn instead of letting the
3471
3514
  // model act on a stale instruction.
3472
3515
  if (incomingGoalId !== null) {
3516
+ // Reconcile from disk to pick up any external state changes before
3517
+ // evaluating whether the checkpoint is actionable.
3518
+ reconcileFocusedGoalFromDisk(ctx);
3519
+ checkpointGoalId = incomingGoalId;
3473
3520
  clearContinuationState();
3474
- if (!state.goal || state.goal.id !== incomingGoalId || (state.goal.status !== "active") || !state.goal.autoContinue) {
3521
+ if (!isActionableContinuationGoal(incomingGoalId)) {
3475
3522
  try {
3476
3523
  ctx.abort?.();
3477
3524
  } catch {}
@@ -3480,10 +3527,12 @@ promptGuidelines: [
3480
3527
  systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, state.goal)}`,
3481
3528
  };
3482
3529
  }
3530
+ checkpointGoalId = null;
3483
3531
  } else {
3484
3532
  // A user-driven turn — clear any queued continuation so we don't
3485
3533
  // double-fire after the user's own message returns. Also reset the
3486
3534
  // autoContinue nudge state so the user always gets a fresh chain.
3535
+ checkpointGoalId = null;
3487
3536
  clearContinuationState();
3488
3537
  resetGetGoalNudgeState(state.goal?.id);
3489
3538
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-x",
3
- "version": "0.18.8",
3
+ "version": "0.18.10",
4
4
  "description": "Goal mode extension for pi: persistent long-running objectives, /goal-set drafting, Sisyphus prompt style, autoContinue, and an above-editor status overlay. Fork of @capyup/pi-goal.",
5
5
  "license": "MIT",
6
6
  "author": "pi-goal-x contributors",