@signalridge/pi-goal 1.4.2 → 1.4.4
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/CHANGELOG.md +15 -0
- package/README.md +11 -6
- package/package.json +8 -8
- package/src/accounting.ts +37 -3
- package/src/commands.ts +123 -17
- package/src/goal.ts +1 -1
- package/src/lifecycle.ts +327 -25
- package/src/menu.ts +1 -1
- package/src/persistence.ts +12 -0
- package/src/prompts.ts +8 -1
- package/src/queue.ts +2 -3
- package/src/run-protocol.ts +15 -0
- package/src/runtime.ts +258 -24
- package/src/tools.ts +16 -4
- package/src/wait.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @signalridge/pi-goal
|
|
2
2
|
|
|
3
|
+
## 1.4.4
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- ef0158d: Persist goal wait reasons and deadlines, restore scheduled wakes, and resume recorded waits on real input without an extra prompt. Keep deliberate pauses stopped and cancel stale wait timers.
|
|
7
|
+
- 4d300c7: Wake waiting goals only after real input is accepted, exclude unrelated usage from their budgets, preserve cumulative usage across compaction and session-tree navigation, and restore deadline timers across pause, resume, and queue transitions. Recheck overdue waits without overriding restrictive tool policies, reset safety only for delivered input, and reduce repeated goal-binding context.
|
|
8
|
+
|
|
9
|
+
## 1.4.3
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 8190516: Extend the tested Pi host compatibility range through 0.87.x while retaining supported older hosts. Validate the published extensions against Pi 0.87.1 and adapt changed provider, session-context, and lifecycle contracts where necessary.
|
|
13
|
+
- 8190516: Add Pi 0.99.1 to the supported host peer ranges while retaining Pi 0.84–0.87 compatibility. Align Pi development dependency pins with 0.99.1 for validation against the new host.
|
|
14
|
+
- Updated dependencies [8190516]
|
|
15
|
+
- Updated dependencies [8190516]
|
|
16
|
+
- @signalridge/pi-ui@1.3.2
|
|
17
|
+
|
|
3
18
|
## 1.4.2
|
|
4
19
|
### Patch Changes
|
|
5
20
|
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Goal mode uses Codex-like persistence instructions and sends guarded continuatio
|
|
|
38
38
|
|
|
39
39
|
## Install
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
The declared Pi peer range covers `0.84.x`–`0.87.x` and `0.99.1+` within the 0.99 minor; the current development host is `0.99.1`. See the [compatibility review](https://github.com/signalridge/pi-extensions/blob/main/docs/pi-0.99-compatibility.md) for verification details. The `agent_settled` lifecycle event this extension depends on landed in `0.80.6`.
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
pi install npm:@signalridge/pi-goal
|
|
@@ -141,11 +141,11 @@ Tool visibility is a baseline, not ownership of Pi's global active-tool list. Pl
|
|
|
141
141
|
navigate, Enter selects or submits, Escape goes Back, and Ctrl+C closes the full flow.
|
|
142
142
|
- In RPC mode, bare `/goal` and `/goal status` report the current summary through an observable notification without opening terminal UI. Pi exposes no extension-command output channel in print or JSON mode, so those routes reject with an explicit unsupported-mode error instead of misreporting stderr as status output.
|
|
143
143
|
- Menu-driven Replace, Clear, Prioritize, Skip, and Drop last actions preview the exact affected goals and require confirmation. Existing direct routes remain immediate for compatibility and automation.
|
|
144
|
-
- `/goal <goal_to_complete>` starts goal mode. If another unfinished goal exists, Pi asks for confirmation before replacing it with a new active goal and resetting its usage counters.
|
|
144
|
+
- `/goal <goal_to_complete>` starts goal mode. If another unfinished goal exists, Pi asks for confirmation before replacing it with a new active goal and resetting its usage counters. A synchronous kickoff-submission failure clears a new goal or restores the prior goal; a previously active goal is restored as paused.
|
|
145
145
|
- `/goal --tokens 100k <goal_to_complete>` starts or replaces goal mode with a token budget. `k` and `m` suffixes are accepted, for example `100k` or `1.5m`.
|
|
146
|
-
- `/goal edit <goal_to_complete>` updates the existing goal objective without resetting usage counters. A successful active edit rotates the stale-turn guard and starts a fresh safety epoch. Paused, blocked, and usage-limited goals stay stopped and retain their safety state until resume. A budget-limited goal reactivates only when `edit --tokens` raises its budget above current usage.
|
|
147
|
-
- `/goal pause` stops prompt injection and auto-continuation
|
|
148
|
-
- `/goal resume` resumes a paused, blocked, usage-limited, or budget-limited goal when its token budget allows it, rotates the stale-turn guard id, resets the automatic-response/repeat safety epoch when the queued resume prompt starts, clears a safety-pause cause, and reports the new finite epoch or explicit Unlimited state. Objective, cumulative usage, elapsed time, and queue are preserved. If prompt
|
|
146
|
+
- `/goal edit <goal_to_complete>` updates the existing goal objective without resetting usage counters. A successful active edit rotates the stale-turn guard and starts a fresh safety epoch. Paused, blocked, and usage-limited goals stay stopped and retain their safety state until resume. A budget-limited goal reactivates only when `edit --tokens` raises its budget above current usage. A synchronous prompt-submission failure restores the exact previous safety counters/cause; it restores a budget-limited goal or restores and pauses a previously active goal.
|
|
147
|
+
- `/goal pause` stops prompt injection and auto-continuation and keeps the goal for later resume. It aborts an active goal's current turn; pausing an already-waiting goal does not interrupt unrelated work. Pausing a waiting goal cancels its deadline and input wake; only `/goal resume` can restart that deliberate pause.
|
|
148
|
+
- `/goal resume` resumes a paused, blocked, usage-limited, or budget-limited goal when its token budget allows it, rotates the stale-turn guard id, resets the automatic-response/repeat safety epoch when the queued resume prompt starts, clears a safety-pause cause, and reports the new finite epoch or explicit Unlimited state. Objective, cumulative usage, elapsed time, and queue are preserved. If prompt submission throws synchronously, the original stopped state, guard id, counters, fingerprint, and cause are restored.
|
|
149
149
|
- `/goal clear` clears the current goal or the entire ordered queue, status, pending continuation/transition, and legacy persisted state for the current working directory without aborting unrelated in-flight work.
|
|
150
150
|
|
|
151
151
|
With `experimental.goals: true`:
|
|
@@ -163,7 +163,11 @@ Goal objectives are limited to 4,000 characters. Put longer instructions in a fi
|
|
|
163
163
|
|
|
164
164
|
## Session and reload behavior
|
|
165
165
|
|
|
166
|
-
Goal state is stored as Pi session state, similar to Codex's thread-owned goals. `/reload` and reopening the same Pi session can restore that session's unfinished goal. An active restored goal already at or above its finite automatic-work limit pauses before another provider request and reports that progress is saved; use `/goal` to review and continue. With `"after-first-goal"`, an unfinished restore marks the tools unlocked in the new extension runtime, but it does not widen an active-tool set already restricted by an earlier lifecycle handler; an active goal instead restores as paused when either terminal tool is missing. If no unfinished goal remains, a fresh runtime starts locked again. Active elapsed time is checkpointed before shutdown and restarted after reload, so offline and stopped wall-clock time is excluded. Automatic-response counts, repeat fingerprints, and safety-pause causes persist across reload and compaction.
|
|
166
|
+
Goal state is stored as Pi session state, similar to Codex's thread-owned goals. `/reload` and reopening the same Pi session can restore that session's unfinished goal. An active restored goal already at or above its finite automatic-work limit pauses before another provider request and reports that progress is saved; use `/goal` to review and continue. With `"after-first-goal"`, an unfinished restore marks the tools unlocked in the new extension runtime, but it does not widen an active-tool set already restricted by an earlier lifecycle handler; an active goal instead restores as paused when either terminal tool is missing. If no unfinished goal remains, a fresh runtime starts locked again. Active elapsed time is checkpointed before shutdown and restarted after reload, so offline and stopped wall-clock time is excluded. Automatic-response counts, repeat fingerprints, and safety-pause causes persist across reload and compaction. An accepted non-`/goal` user/RPC input resets the safety epoch only while the goal is active and reclassifies the in-flight run as manual; input consumed by another extension or rejected before a turn starts does not reset it. Navigating the session tree checkpoints the source goal's active time, restores the destination branch's own Goal state, and accounts only for assistant work after that branch's last Goal-state checkpoint. Pending queue transitions resume after navigation settles; leaving a managed Goal branch closes that run. Starting a new Pi session in the same working directory does not inherit the old goal.
|
|
167
|
+
|
|
168
|
+
A `goal_wait` pause stores its reason and optional absolute wake deadline in the same session entry. On reload or session resume, a future deadline is re-armed; an overdue deadline gets one resume attempt after Pi is idle and pending messages have drained. No timer runs while Pi is closed. A wait without a deadline stays quiet until real interactive or RPC input is accepted for delivery, or `/goal resume` is used. Input consumed by another extension or rejected before the turn starts leaves the wait intact. Accepted real input resumes only a recorded wait and supplies the turn itself, without an extra resume prompt. A queued follow-up wakes the wait when delivered, not when queued. If delivery stays inside an existing agent run, the context hook supplies the rotated goal ID before its next response; it does not add another user turn. Extension-generated input and `/goal` commands do not count as real-input wake triggers. When Pi cannot distinguish real and extension input at delivery (for example, identical queued text or cross-transformed messages), the goal stays waiting rather than accepting a possible extension wake. That ambiguity remains attached to the waiting goal, including its deadline, until explicit `/goal resume` or reload; later input is not used to guess which earlier message was delivered. Automatic wakes do not override a restrictive active-tool policy; an overdue deadline rechecks readiness at most once per minute until Goal tools are available, while a settled boundary may wake sooner. An input whose delivery has not been confirmed also defers the deadline rather than racing a second prompt; if that input was rejected, resolve the failure and use `/goal resume` explicitly.
|
|
169
|
+
|
|
170
|
+
Ordinary pauses, safety pauses, blockers, usage limits, exhausted budgets, and frozen queues do not auto-resume. Older paused entries without wait metadata remain paused; the extension does not infer a wait from transcript text. `/goal pause`, edit, clear, and replacement cancel the old wait. A synchronous deadline prompt-submission failure restores the stopped goal and wait metadata without a timer retry loop; use `/goal resume` or reload after resolving the problem. Pi's extension API does not acknowledge asynchronous prompt acceptance, so a later authentication or model failure cannot be rolled back by the extension; if no turn starts, inspect `/goal status` and explicitly pause/resume or reload after fixing the failure. Objective, cumulative usage, and the configured finite or Unlimited automatic-work setting are retained.
|
|
167
171
|
|
|
168
172
|
Ordered queues use the same canonical `goal-state` session entry as single goals. Every item owns independent usage and safety state. Shelving, priority displacement, automatic advancement, and later reactivation preserve that item's epoch rather than granting more automatic work. The legacy `{ goal }` shape remains valid, and missing safety fields normalize to zero/defaults. Queue fields are written only when needed. Sessions created by the former standalone `pi-goals` experiment can migrate their last `goals-state` array and pending `unshift` intent when the branch has never written a canonical `goal-state`; any canonical entry, including an explicit clear, takes precedence so old plural state cannot be resurrected.
|
|
169
173
|
|
|
@@ -180,6 +184,7 @@ Older versions wrote unfinished goals to `~/.pi/agent/pi-goal-state.json` keyed
|
|
|
180
184
|
- `active 3m · automatic Unlimited` — explicit Unlimited automatic work.
|
|
181
185
|
- `paused · automatic limit 25/25` — the automatic-work limit paused the goal; `/goal` opens the recovery preview.
|
|
182
186
|
- `paused · automatic 12/25` — another pause reason stopped work while preserving the finite epoch.
|
|
187
|
+
- `waiting · automatic Unlimited` — a recorded `goal_wait` pause; `/goal status` shows its reason and any deadline.
|
|
183
188
|
- `blocked · automatic 12/25` — progress requires user or external action, or a terminal non-usage error stopped work.
|
|
184
189
|
- `usage · automatic 12/25` — the provider or account usage limit stopped work.
|
|
185
190
|
- `budget 100k/100k · automatic 12/25` — the user-configured token budget was reached; auto-continuation stops.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalridge/pi-goal",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Pi extension for autonomous /goal completion with opt-in experimental ordered queues.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,22 +36,22 @@
|
|
|
36
36
|
"lint": "biome check ."
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@earendil-works/pi-ai": "^0.84.0 || ^0.85.0",
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0",
|
|
41
|
-
"@earendil-works/pi-tui": "^0.84.0 || ^0.85.0",
|
|
39
|
+
"@earendil-works/pi-ai": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1",
|
|
41
|
+
"@earendil-works/pi-tui": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1",
|
|
42
42
|
"typebox": "^1.3.11"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@biomejs/biome": "2.5.7",
|
|
46
|
-
"@earendil-works/pi-ai": "0.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
48
|
-
"@earendil-works/pi-tui": "0.
|
|
46
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
48
|
+
"@earendil-works/pi-tui": "0.99.1",
|
|
49
49
|
"typebox": "1.3.11",
|
|
50
50
|
"typescript": "7.0.2"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@narumitw/pi-tui-kit": "^0.54.0",
|
|
54
|
-
"@signalridge/pi-ui": "^1.3.
|
|
54
|
+
"@signalridge/pi-ui": "^1.3.2"
|
|
55
55
|
},
|
|
56
56
|
"repository": {
|
|
57
57
|
"type": "git",
|
package/src/accounting.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Usage } from "@earendil-works/pi-ai";
|
|
|
3
3
|
export interface GoalAccountingState {
|
|
4
4
|
status: string;
|
|
5
5
|
baselineTokens: number;
|
|
6
|
+
/** Usage retained from before the current session-token baseline. */
|
|
7
|
+
usageOffset?: number;
|
|
6
8
|
tokensUsed: number;
|
|
7
9
|
timeUsedSeconds: number;
|
|
8
10
|
activeStartedAt?: number;
|
|
@@ -29,15 +31,36 @@ export function checkpointGoalActiveTime(goal: GoalAccountingState, now: number,
|
|
|
29
31
|
goal.activeStartedAt = continueClock ? now : undefined;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
export function rebaseGoalUsage<T extends GoalAccountingState>(goal: T, totalTokens: number): T {
|
|
35
|
+
const total = nonNegativeFiniteNumber(totalTokens);
|
|
36
|
+
const used = nonNegativeFiniteNumber(goal.tokensUsed);
|
|
37
|
+
return {
|
|
38
|
+
...goal,
|
|
39
|
+
baselineTokens: Math.max(0, total - used),
|
|
40
|
+
usageOffset: total < used ? used - total : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
export function updateGoalUsage(
|
|
33
45
|
goal: GoalAccountingState,
|
|
34
46
|
ctx: UsageContext,
|
|
35
47
|
continueClock = goal.status === "active",
|
|
36
48
|
) {
|
|
37
49
|
const now = Date.now();
|
|
38
|
-
const
|
|
39
|
-
goal.baselineTokens = baselineTokens;
|
|
40
|
-
goal.tokensUsed =
|
|
50
|
+
const total = currentTokenTotal(ctx);
|
|
51
|
+
goal.baselineTokens = nonNegativeFiniteNumber(goal.baselineTokens);
|
|
52
|
+
goal.tokensUsed = nonNegativeFiniteNumber(goal.tokensUsed);
|
|
53
|
+
goal.usageOffset = Math.min(goal.tokensUsed, nonNegativeFiniteNumber(goal.usageOffset)) || undefined;
|
|
54
|
+
const previousTotal = goal.baselineTokens + goal.tokensUsed - (goal.usageOffset ?? 0);
|
|
55
|
+
if (total < previousTotal) {
|
|
56
|
+
const rebased = rebaseGoalUsage(goal, total);
|
|
57
|
+
goal.baselineTokens = rebased.baselineTokens;
|
|
58
|
+
goal.usageOffset = rebased.usageOffset;
|
|
59
|
+
}
|
|
60
|
+
goal.tokensUsed = Math.min(
|
|
61
|
+
Number.MAX_SAFE_INTEGER,
|
|
62
|
+
(goal.usageOffset ?? 0) + Math.max(0, total - goal.baselineTokens),
|
|
63
|
+
);
|
|
41
64
|
checkpointGoalActiveTime(goal, now, continueClock);
|
|
42
65
|
goal.updatedAt = now;
|
|
43
66
|
}
|
|
@@ -100,3 +123,14 @@ export function currentTokenTotal(ctx: UsageContext): number {
|
|
|
100
123
|
const sessionManager = ctx.sessionManager as { getBranch?: () => unknown[] } | undefined;
|
|
101
124
|
return cumulativeAssistantTokens(sessionManager?.getBranch?.() ?? []);
|
|
102
125
|
}
|
|
126
|
+
|
|
127
|
+
export function assistantTokensAfterGoalState(ctx: UsageContext, customType = "goal-state") {
|
|
128
|
+
const sessionManager = ctx.sessionManager as { getBranch?: () => unknown[] } | undefined;
|
|
129
|
+
const branch = sessionManager?.getBranch?.() ?? [];
|
|
130
|
+
let lastStateIndex = -1;
|
|
131
|
+
for (let index = 0; index < branch.length; index++) {
|
|
132
|
+
const entry = branch[index] as { type?: unknown; customType?: unknown };
|
|
133
|
+
if (entry?.type === "custom" && entry.customType === customType) lastStateIndex = index;
|
|
134
|
+
}
|
|
135
|
+
return lastStateIndex < 0 ? 0 : cumulativeAssistantTokens(branch.slice(lastStateIndex + 1));
|
|
136
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkpointGoalActiveTime, currentTokenTotal, formatTokenCount } from "./accounting.js";
|
|
1
|
+
import { checkpointGoalActiveTime, currentTokenTotal, formatTokenCount, rebaseGoalUsage } from "./accounting.js";
|
|
2
2
|
import { validateObjective } from "./command.js";
|
|
3
3
|
import { notifyTerminal, safeGoalMenuText } from "./errors.js";
|
|
4
4
|
import type { ActiveGoal } from "./persistence.js";
|
|
@@ -24,10 +24,12 @@ import {
|
|
|
24
24
|
isResumableGoalStatus,
|
|
25
25
|
nextGoalInstance,
|
|
26
26
|
queueGoalSafetyReset,
|
|
27
|
+
resetGoalSafetyEpoch,
|
|
27
28
|
STATUS_KEY,
|
|
28
29
|
type StatusContext,
|
|
29
30
|
stoppedStatusLabel,
|
|
30
31
|
transitionGoal,
|
|
32
|
+
type WaitingInputWake,
|
|
31
33
|
} from "./runtime.js";
|
|
32
34
|
|
|
33
35
|
// User-command mutations are kept separate from Pi event wiring. Every controller
|
|
@@ -56,6 +58,7 @@ export class GoalCommandController {
|
|
|
56
58
|
|
|
57
59
|
const existingGoal = this.runtime.activeGoal?.status !== "complete" ? this.runtime.activeGoal : undefined;
|
|
58
60
|
const existingQueuedGoals = [...this.runtime.queuedGoals];
|
|
61
|
+
const existingReplacementUsage = this.runtime.pendingReplacementUsage;
|
|
59
62
|
const existingQueueIdentity = goalQueueIdentity(
|
|
60
63
|
this.runtime.activeGoal,
|
|
61
64
|
this.runtime.queuedGoals,
|
|
@@ -106,6 +109,14 @@ export class GoalCommandController {
|
|
|
106
109
|
this.runtime.pendingQueueAction = undefined;
|
|
107
110
|
this.runtime.activeGoal = createGoal(objective, tokenBudget, currentTokenTotal(ctx));
|
|
108
111
|
const startedGoal = this.runtime.activeGoal;
|
|
112
|
+
this.runtime.pendingReplacementUsage =
|
|
113
|
+
this.runtime.agentRunGoalId !== undefined
|
|
114
|
+
? { runGoalId: this.runtime.agentRunGoalId, replacementGoalId: startedGoal.id }
|
|
115
|
+
: undefined;
|
|
116
|
+
if (this.runtime.pendingReplacementUsage) startedGoal.usageBaselinePending = true;
|
|
117
|
+
this.runtime.ambiguousWaitInputGoalId = undefined;
|
|
118
|
+
this.runtime.pendingDirectInputs = [];
|
|
119
|
+
this.runtime.pendingNonGoalInputs = [];
|
|
109
120
|
onActivated?.(startedGoal);
|
|
110
121
|
this.runtime.persistGoal(startedGoal);
|
|
111
122
|
if (this.runtime.activeGoal?.id !== startedGoal.id || this.runtime.activeGoal.status !== "active") {
|
|
@@ -126,7 +137,7 @@ export class GoalCommandController {
|
|
|
126
137
|
rolledBackStartedGoal = true;
|
|
127
138
|
if (existingGoal) {
|
|
128
139
|
this.runtime.queuedGoals = existingQueuedGoals;
|
|
129
|
-
this.runtime.recordGoalUsage(existingGoal, ctx);
|
|
140
|
+
if (existingGoal.status === "active") this.runtime.recordGoalUsage(existingGoal, ctx);
|
|
130
141
|
if (existingGoal.status === "active") {
|
|
131
142
|
this.runtime.stopActiveGoal(ctx, {
|
|
132
143
|
kind: "activation_rollback",
|
|
@@ -149,7 +160,9 @@ export class GoalCommandController {
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
if (rolledBackStartedGoal) {
|
|
163
|
+
this.runtime.pendingReplacementUsage = existingReplacementUsage;
|
|
152
164
|
this.runtime.toolPolicy.restore(goalToolVisibilityBeforeActivation);
|
|
165
|
+
this.runtime.scheduleGoalWaitWake(ctx);
|
|
153
166
|
}
|
|
154
167
|
return;
|
|
155
168
|
}
|
|
@@ -274,6 +287,7 @@ export class GoalCommandController {
|
|
|
274
287
|
return this.dispatchPendingQueueActionIfSettled(ctx);
|
|
275
288
|
}
|
|
276
289
|
const goal = this.runtime.activeGoal;
|
|
290
|
+
this.runtime.scheduleGoalWaitWake(ctx);
|
|
277
291
|
if (goal?.status !== "active") return false;
|
|
278
292
|
this.runtime.requestContinuation(goal);
|
|
279
293
|
return this.runtime.dispatchContinuationIfSettled(ctx);
|
|
@@ -330,6 +344,7 @@ export class GoalCommandController {
|
|
|
330
344
|
if (blocksStaleGoalToolCalls(this.runtime.activeGoal.status)) {
|
|
331
345
|
this.runtime.blockStaleGoalToolCalls();
|
|
332
346
|
}
|
|
347
|
+
this.runtime.scheduleGoalWaitWake(ctx);
|
|
333
348
|
notifyTerminal(
|
|
334
349
|
ctx.ui,
|
|
335
350
|
`${reason === "complete" ? "Goal complete" : "Goal skipped"}: ${previousText}. Next goal remains ${this.runtime.activeGoal.status}: ${this.runtime.activeGoal.text}`,
|
|
@@ -388,8 +403,12 @@ export class GoalCommandController {
|
|
|
388
403
|
notifyTerminal(ctx.ui, "No active goal.", "info");
|
|
389
404
|
return;
|
|
390
405
|
}
|
|
391
|
-
if (this.runtime.activeGoal.status !== "active") {
|
|
392
|
-
notifyTerminal(
|
|
406
|
+
if (this.runtime.activeGoal.status !== "active" && !this.runtime.activeGoal.wait) {
|
|
407
|
+
notifyTerminal(
|
|
408
|
+
ctx.ui,
|
|
409
|
+
`Goal is ${this.runtime.activeGoal.status}; only active or waiting goals can be paused.`,
|
|
410
|
+
"warning",
|
|
411
|
+
);
|
|
393
412
|
return;
|
|
394
413
|
}
|
|
395
414
|
const stoppedGoal = this.runtime.stopActiveGoal(ctx, {
|
|
@@ -399,7 +418,74 @@ export class GoalCommandController {
|
|
|
399
418
|
if (stoppedGoal) notifyTerminal(ctx.ui, `Goal paused: ${stoppedGoal.text}`, "info");
|
|
400
419
|
}
|
|
401
420
|
|
|
402
|
-
|
|
421
|
+
private canWakeWaitingGoal() {
|
|
422
|
+
const goal = this.runtime.activeGoal;
|
|
423
|
+
return Boolean(
|
|
424
|
+
!this.runtime.queueFrozen &&
|
|
425
|
+
!this.runtime.pendingQueueAction &&
|
|
426
|
+
goal?.status === "paused" &&
|
|
427
|
+
goal.wait &&
|
|
428
|
+
this.runtime.ambiguousWaitInputGoalId !== goal.id &&
|
|
429
|
+
this.runtime.toolPolicy.toolsAvailable() &&
|
|
430
|
+
(goal.tokenBudget === undefined || goal.tokensUsed < goal.tokenBudget),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
prepareWaitingGoalOnInput(ctx: StatusContext): WaitingInputWake | undefined {
|
|
435
|
+
const goal = this.runtime.activeGoal;
|
|
436
|
+
if (!goal || !this.canWakeWaitingGoal()) return undefined;
|
|
437
|
+
// Reserve the prompt binding without changing the persisted wait. Pi can
|
|
438
|
+
// still reject images after before_agent_start, before any message starts.
|
|
439
|
+
const resumedGoal = resetGoalSafetyEpoch(
|
|
440
|
+
transitionGoal(nextGoalInstance(rebaseGoalUsage(goal, currentTokenTotal(ctx))), "active"),
|
|
441
|
+
);
|
|
442
|
+
return { waitingGoal: goal, resumedGoal, generation: this.runtime.menuGeneration };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
commitWaitingGoalOnInput(ctx: StatusContext, ticket: WaitingInputWake, inRun = false) {
|
|
446
|
+
if (
|
|
447
|
+
ticket.generation !== this.runtime.menuGeneration ||
|
|
448
|
+
this.runtime.activeGoal !== ticket.waitingGoal ||
|
|
449
|
+
!this.canWakeWaitingGoal()
|
|
450
|
+
)
|
|
451
|
+
return false;
|
|
452
|
+
const previousRunGoalId = this.runtime.agentRunGoalId;
|
|
453
|
+
const resumedGoal = resetGoalSafetyEpoch(
|
|
454
|
+
transitionGoal(
|
|
455
|
+
{
|
|
456
|
+
...rebaseGoalUsage(ticket.waitingGoal, currentTokenTotal(ctx)),
|
|
457
|
+
id: ticket.resumedGoal.id,
|
|
458
|
+
},
|
|
459
|
+
"active",
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
this.runtime.pendingInputWake = undefined;
|
|
463
|
+
this.runtime.cancelContinuationWork();
|
|
464
|
+
this.runtime.clearGoalRecovery();
|
|
465
|
+
this.runtime.clearBudgetWrapUp();
|
|
466
|
+
this.runtime.clearStaleGoalToolCallBlock();
|
|
467
|
+
this.runtime.pendingDirectInputs = this.runtime.pendingDirectInputs
|
|
468
|
+
.filter((pending) => pending.realInput && pending.goalId === ticket.waitingGoal.id)
|
|
469
|
+
.map((pending) => ({ ...pending, goalId: resumedGoal.id }));
|
|
470
|
+
this.runtime.pendingNonGoalInputs = this.runtime.pendingNonGoalInputs.filter((pending) => pending.realInput);
|
|
471
|
+
this.runtime.activeGoal = resumedGoal;
|
|
472
|
+
if (inRun) {
|
|
473
|
+
this.runtime.inputWakeGoalId = resumedGoal.id;
|
|
474
|
+
this.runtime.inputWakeNeedsPrompt = previousRunGoalId !== ticket.waitingGoal.id;
|
|
475
|
+
}
|
|
476
|
+
this.runtime.beginAgentRun(resumedGoal.id, "manual");
|
|
477
|
+
this.runtime.persistGoal(resumedGoal);
|
|
478
|
+
if (ticket.generation !== this.runtime.menuGeneration || this.runtime.activeGoal !== resumedGoal) return false;
|
|
479
|
+
this.runtime.updateStatus(ctx, resumedGoal);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
resumeWaitingGoalOnInput(ctx: StatusContext, inRun = false) {
|
|
484
|
+
const ticket = this.prepareWaitingGoalOnInput(ctx);
|
|
485
|
+
return ticket ? this.commitWaitingGoalOnInput(ctx, ticket, inRun) : false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private prepareGoalResume(ctx: StatusContext, resetOnPrompt: boolean) {
|
|
403
489
|
if (!this.runtime.activeGoal) {
|
|
404
490
|
notifyTerminal(ctx.ui, "No active goal.", "info");
|
|
405
491
|
return;
|
|
@@ -427,19 +513,29 @@ export class GoalCommandController {
|
|
|
427
513
|
return;
|
|
428
514
|
}
|
|
429
515
|
const stoppedGoal = this.runtime.activeGoal;
|
|
430
|
-
|
|
516
|
+
this.runtime.ambiguousWaitInputGoalId = undefined;
|
|
517
|
+
this.runtime.pendingDirectInputs = [];
|
|
518
|
+
this.runtime.pendingNonGoalInputs = [];
|
|
431
519
|
this.runtime.cancelContinuationWork();
|
|
432
520
|
this.runtime.clearGoalRecovery();
|
|
433
521
|
this.runtime.clearBudgetWrapUp();
|
|
434
522
|
this.runtime.clearStaleGoalToolCallBlock();
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
523
|
+
const rebased = rebaseGoalUsage({ ...stoppedGoal, usageBaselinePending: undefined }, currentTokenTotal(ctx));
|
|
524
|
+
const active = transitionGoal(nextGoalInstance(rebased), "active");
|
|
525
|
+
const resumedGoal = resetOnPrompt ? queueGoalSafetyReset(active) : resetGoalSafetyEpoch(active);
|
|
526
|
+
this.runtime.activeGoal = resumedGoal;
|
|
527
|
+
this.runtime.persistGoal(resumedGoal);
|
|
528
|
+
// Canonical state publication can synchronously pause or replace the goal.
|
|
529
|
+
if (this.runtime.activeGoal !== resumedGoal || resumedGoal.status !== "active") return;
|
|
530
|
+
this.runtime.updateStatus(ctx, resumedGoal);
|
|
531
|
+
return { stoppedGoal, resumedGoal, goalToolVisibilityBeforeActivation };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async resumeGoal(ctx: StatusContext, source: "explicit" | "deadline" = "explicit") {
|
|
535
|
+
const prepared = this.prepareGoalResume(ctx, true);
|
|
536
|
+
if (!prepared) return;
|
|
537
|
+
const { stoppedGoal, resumedGoal, goalToolVisibilityBeforeActivation } = prepared;
|
|
538
|
+
const stoppedStatus = stoppedGoal.status;
|
|
443
539
|
const sent = await this.runtime.sendOwnedGoalPrompt(
|
|
444
540
|
ctx,
|
|
445
541
|
resumedGoal.id,
|
|
@@ -454,6 +550,9 @@ export class GoalCommandController {
|
|
|
454
550
|
this.runtime.blockStaleGoalToolCalls();
|
|
455
551
|
}
|
|
456
552
|
this.runtime.toolPolicy.restore(goalToolVisibilityBeforeActivation);
|
|
553
|
+
if (source === "explicit" && stoppedGoal.wait?.resumeAt !== undefined) {
|
|
554
|
+
this.runtime.scheduleGoalWaitWake(ctx);
|
|
555
|
+
}
|
|
457
556
|
}
|
|
458
557
|
return;
|
|
459
558
|
}
|
|
@@ -497,7 +596,7 @@ export class GoalCommandController {
|
|
|
497
596
|
return;
|
|
498
597
|
}
|
|
499
598
|
|
|
500
|
-
this.runtime.recordGoalUsage(this.runtime.activeGoal, ctx);
|
|
599
|
+
if (this.runtime.activeGoal.status === "active") this.runtime.recordGoalUsage(this.runtime.activeGoal, ctx);
|
|
501
600
|
const previousGoal = { ...this.runtime.activeGoal };
|
|
502
601
|
this.runtime.cancelContinuationWork();
|
|
503
602
|
this.runtime.clearGoalRecovery();
|
|
@@ -512,7 +611,12 @@ export class GoalCommandController {
|
|
|
512
611
|
},
|
|
513
612
|
editedGoalStatus(previousStatus),
|
|
514
613
|
);
|
|
515
|
-
const nextGoal =
|
|
614
|
+
const nextGoal =
|
|
615
|
+
transitionedGoal.status === "active"
|
|
616
|
+
? queueGoalSafetyReset(
|
|
617
|
+
previousStatus === "active" ? transitionedGoal : rebaseGoalUsage(transitionedGoal, currentTokenTotal(ctx)),
|
|
618
|
+
)
|
|
619
|
+
: transitionedGoal;
|
|
516
620
|
const goalToolVisibilityBeforeActivation =
|
|
517
621
|
nextGoal.status === "active" ? this.runtime.toolPolicy.snapshot() : undefined;
|
|
518
622
|
if (nextGoal.status === "active") {
|
|
@@ -574,7 +678,7 @@ export class GoalCommandController {
|
|
|
574
678
|
this.reportGoalStatus(ctx, message);
|
|
575
679
|
return;
|
|
576
680
|
}
|
|
577
|
-
if (!this.runtime.queueFrozen) {
|
|
681
|
+
if (!this.runtime.queueFrozen && this.runtime.activeGoal.status === "active") {
|
|
578
682
|
this.runtime.recordGoalUsage(this.runtime.activeGoal, ctx);
|
|
579
683
|
this.runtime.persistGoal(this.runtime.activeGoal);
|
|
580
684
|
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
@@ -640,6 +744,7 @@ export class GoalCommandController {
|
|
|
640
744
|
this.runtime.pauseGoalForUnavailableTools(ctx, true, !displacedUsageFinalized);
|
|
641
745
|
} else {
|
|
642
746
|
this.runtime.persistGoal(currentGoal);
|
|
747
|
+
this.runtime.scheduleGoalWaitWake(ctx);
|
|
643
748
|
}
|
|
644
749
|
}
|
|
645
750
|
return false;
|
|
@@ -685,6 +790,7 @@ export class GoalCommandController {
|
|
|
685
790
|
this.runtime.updateStatus(ctx, this.runtime.activeGoal);
|
|
686
791
|
}
|
|
687
792
|
this.runtime.toolPolicy.restore(visibilityBeforeActivation);
|
|
793
|
+
this.runtime.scheduleGoalWaitWake(ctx);
|
|
688
794
|
return false;
|
|
689
795
|
}
|
|
690
796
|
notifyTerminal(ctx.ui, `Goal prioritized: ${objective}`, "info");
|
package/src/goal.ts
CHANGED
|
@@ -21,7 +21,7 @@ function registerGoalRuntime(pi: ExtensionAPI, options: GoalOptions = {}) {
|
|
|
21
21
|
// the real one. Wired here because the controller owns it and the runtime,
|
|
22
22
|
// which arms the timer, deliberately does not depend on the controller.
|
|
23
23
|
runtime.onGoalWaitElapsed = (ctx) => {
|
|
24
|
-
void commands.resumeGoal(ctx);
|
|
24
|
+
void commands.resumeGoal(ctx, "deadline");
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
// Keep registration order explicit: managed-run bus listeners exist before tools,
|