pi-goal-x 0.18.8 → 0.18.9

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 +56 -6
  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
  }
@@ -3307,8 +3336,8 @@ promptGuidelines: [
3307
3336
 
3308
3337
  pi.on("turn_start", async (_event, ctx) => {
3309
3338
  // Per-turn flag resets (#4 + C9 fix).
3339
+ advanceTurnSeq();
3310
3340
  goalWorkToolCalledThisTurn = false;
3311
- turnStoppedFor = null;
3312
3341
  syncGoalTools();
3313
3342
  beginAccounting();
3314
3343
  updateUI(ctx);
@@ -3316,17 +3345,29 @@ promptGuidelines: [
3316
3345
 
3317
3346
  // #4 + C9 fix + Phase 5 C3: gate in-turn tool calls based on lifecycle state.
3318
3347
  pi.on("tool_call", async (event, ctx) => {
3348
+ const stoppedGoalId = currentTurnStoppedGoalId();
3319
3349
  // Post-stop in-turn block (C9 0ad8 fix): after pause_goal / abort_goal /
3320
3350
  // complete_goal / propose_goal_tweak fires in this turn, block all subsequent tool calls except
3321
3351
  // read-only inspection. Forces the agent to yield the turn instead of "fixing"
3322
3352
  // the situation by creating extra files etc.
3323
- if (turnStoppedFor !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
3353
+ if (stoppedGoalId !== null && !POST_STOP_ALLOWED_TOOL_SET.has(event.toolName)) {
3324
3354
  return {
3325
3355
  block: true,
3326
- reason: `The goal was already stopped earlier in this turn (goalId=${turnStoppedFor}). ` +
3356
+ reason: `The goal was already stopped earlier in this turn (goalId=${stoppedGoalId}). ` +
3327
3357
  `Do not call more tools; end the turn with a brief summary and yield to the user.`,
3328
3358
  };
3329
3359
  }
3360
+ // Stale checkpoint guard: if the turn was triggered by a queued continuation
3361
+ // for a goal that is no longer active/autoContinue, block work tools.
3362
+ if (checkpointGoalId !== null && !isActionableContinuationGoal(checkpointGoalId) && isStaleCheckpointBlockedToolCall(event.toolName)) {
3363
+ // Block the tool call with a stale-checkpoint message.
3364
+ return {
3365
+ block: true,
3366
+ reason: `Cannot call ${event.toolName}: the goal checkpoint that triggered this turn is no longer active. ` +
3367
+ `Goal ${checkpointGoalId} has been paused, cleared, or replaced. ` +
3368
+ `End the turn with a brief summary and yield to the user.`,
3369
+ };
3370
+ }
3330
3371
  // Phase 5 soft gate relaxation: active-goal question block and repeated get_goal
3331
3372
  // block are removed. The agent is trusted to prefer work tools; prompts nudge
3332
3373
  // toward concrete work without hard-stopping the turn.
@@ -3448,11 +3489,13 @@ promptGuidelines: [
3448
3489
  });
3449
3490
 
3450
3491
  pi.on("before_agent_start", async (event, ctx) => {
3492
+ advanceTurnSeq();
3451
3493
  syncGoalTools();
3452
3494
  const currentSystemPrompt = () => ctx.getSystemPrompt?.() || event.systemPrompt;
3453
3495
  const incomingGoalId = extractGoalIdFromInjectedMessage(event.prompt ?? "");
3454
3496
 
3455
3497
  if (confirmationIntent !== null) {
3498
+ checkpointGoalId = null;
3456
3499
  clearContinuationState();
3457
3500
  clearActiveAccounting();
3458
3501
  runningGoalId = null;
@@ -3460,6 +3503,7 @@ promptGuidelines: [
3460
3503
  }
3461
3504
 
3462
3505
  if (tweakDraftingFor !== null) {
3506
+ checkpointGoalId = null;
3463
3507
  clearContinuationState();
3464
3508
  clearActiveAccounting();
3465
3509
  runningGoalId = null;
@@ -3470,8 +3514,12 @@ promptGuidelines: [
3470
3514
  // matches the active goal, abort the whole turn instead of letting the
3471
3515
  // model act on a stale instruction.
3472
3516
  if (incomingGoalId !== null) {
3517
+ // Reconcile from disk to pick up any external state changes before
3518
+ // evaluating whether the checkpoint is actionable.
3519
+ reconcileFocusedGoalFromDisk(ctx);
3520
+ checkpointGoalId = incomingGoalId;
3473
3521
  clearContinuationState();
3474
- if (!state.goal || state.goal.id !== incomingGoalId || (state.goal.status !== "active") || !state.goal.autoContinue) {
3522
+ if (!isActionableContinuationGoal(incomingGoalId)) {
3475
3523
  try {
3476
3524
  ctx.abort?.();
3477
3525
  } catch {}
@@ -3480,10 +3528,12 @@ promptGuidelines: [
3480
3528
  systemPrompt: `${currentSystemPrompt()}\n\n${staleContinuationPrompt(incomingGoalId, state.goal)}`,
3481
3529
  };
3482
3530
  }
3531
+ checkpointGoalId = null;
3483
3532
  } else {
3484
3533
  // A user-driven turn — clear any queued continuation so we don't
3485
3534
  // double-fire after the user's own message returns. Also reset the
3486
3535
  // autoContinue nudge state so the user always gets a fresh chain.
3536
+ checkpointGoalId = null;
3487
3537
  clearContinuationState();
3488
3538
  resetGetGoalNudgeState(state.goal?.id);
3489
3539
  }
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.9",
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",