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.
- package/extensions/goal.ts +57 -8
- package/package.json +1 -1
package/extensions/goal.ts
CHANGED
|
@@ -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
|
|
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 (!
|
|
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 (
|
|
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=${
|
|
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 (!
|
|
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.
|
|
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",
|