@astrosheep/pi-goal-next 0.1.2 → 0.1.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/README.md CHANGED
@@ -61,7 +61,7 @@ The prompt text is copied byte-for-byte from Codex Goal (`continuation.md`, `bud
61
61
 
62
62
  ## Limits and accounting
63
63
 
64
- - The default maximum is 25 continuation turns (`maxContinuations`). Change it with `/goal turns N`.
64
+ - The default maximum is 25 continuation turns (`maxContinuations`) per run. Change it with `/goal turns N`. `/goal resume` resets the run's continuation count and immediately schedules work when idle. It preserves the objective, journal history, cumulative usage, and token budget. If the token budget is exhausted or the continuation allowance is zero, resume reports the limit instead of claiming success.
65
65
  - A token budget is unset by default. Set one at creation with `/goal --tokens N[k|M] ...` or later with `/goal budget N`.
66
66
  - Usage is attributed by message entry id, separately for assistant and tool-result messages. Duplicate entry ids are ignored.
67
67
  - Missing provider usage is recorded as unknown, never treated as confirmed zero.
@@ -88,7 +88,7 @@ The blocked audit is prompt-level only: the runtime does not count blocking turn
88
88
 
89
89
  `/goal` or `/goal status` · `/goal [--tokens N[k|M]] <objective>` (create; refuses while unfinished) · `/goal edit <objective>` · `/goal pause` · `/goal resume` · `/goal clear` · `/goal budget <tokens|none>` · `/goal turns <max-continuations>`.
90
90
 
91
- A successful `create` or `resume` calls `continuation.onSettled()` so an idle session starts pursuing immediately (Codex starts the turn directly). A successful `edit` sends `objectiveUpdatedPrompt(goal)` with `triggerTurn: true`; `pause` and `clear` send nothing (the active-status check stops continuation).
91
+ A successful `resume` atomically journals `resetContinuations: true` on the user transition to active, resetting the run's continuation count while preserving the objective, usage, budget, and historical entries. It also accepts an already-active goal. Exhausted token budgets and zero continuation allowances are reported without resuming. A successful `create` or `resume` calls `continuation.onSettled()` so an idle session starts pursuing immediately (Codex starts the turn directly). A successful `edit` sends `objectiveUpdatedPrompt(goal)` with `triggerTurn: true`; `pause` and `clear` send nothing (the active-status check stops continuation).
92
92
 
93
93
  ## Defaults
94
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-goal-next",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Persistent autonomous goals for pi, with Codex-verbatim goal semantics: continuation prompts, self-audited completion, budgets, and CAS-journaled state.",
5
5
  "type": "module",
6
6
  "files": [
package/src/commands.ts CHANGED
@@ -49,7 +49,13 @@ export function registerGoalCommands(piLike: CommandPiLike, deps: CommandDeps):
49
49
  case "clear": return finish(await goalCommit.commit({ type: "clear" }, revision), "Goal cleared.");
50
50
  case "pause": return finish(await goalCommit.commit({ type: "transition", to: "paused", by: "user", userRequest: '"/goal pause"' }, revision), "Goal paused.");
51
51
  case "resume": {
52
- const r = await goalCommit.commit({ type: "transition", to: "active", by: "user" }, revision);
52
+ if (!current) return "Cannot resume: no goal exists.";
53
+ const goal = current.goal;
54
+ if (goal.status === "complete") return "Cannot resume: goal is complete.";
55
+ const used = goal.usage.input + goal.usage.output + goal.usage.cacheRead + goal.usage.cacheWrite;
56
+ if (goal.tokenBudget !== null && used >= goal.tokenBudget) return "Cannot resume: token budget exhausted. Adjust /goal budget first; cumulative usage is preserved.";
57
+ if (goal.maxContinuations === 0) return "Cannot resume: continuation allowance is zero. Adjust /goal turns first.";
58
+ const r = await goalCommit.commit({ type: "transition", to: "active", by: "user", resetContinuations: true }, revision);
53
59
  if (r.kind === "ok") await deps.kick();
54
60
  return r.kind === "ok" ? "Goal resumed." : "Goal update failed.";
55
61
  }
@@ -39,7 +39,7 @@ export function createGoalCommit(store: GoalStore) {
39
39
  switch (intent.type) {
40
40
  case "create": return { type: "goal.created", version: 1, seq, goal: next! };
41
41
  case "clear": return { type: "goal.cleared", version: 1, seq };
42
- case "transition": return { type: "goal.transition", version: 1, seq, from: previous!.status, to: intent.to, by: intent.by, ...(intent.userRequest ? { userRequest: intent.userRequest } : {}) };
42
+ case "transition": return { type: "goal.transition", version: 1, seq, from: previous!.status, to: intent.to, by: intent.by, ...(intent.userRequest ? { userRequest: intent.userRequest } : {}), ...(intent.resetContinuations ? { resetContinuations: true } : {}) };
43
43
  case "usage": return { type: "goal.usage", version: 1, seq, input: intent.input ?? null, output: intent.output ?? null, cacheRead: intent.cacheRead ?? null, cacheWrite: intent.cacheWrite ?? null, unknownMessages: intent.unknownMessages ?? 0 };
44
44
  case "continuation_sent": return { type: "goal.continuation_sent", version: 1, seq, generation: intent.generation };
45
45
  case "stale_turn": return { type: "goal.stale_turn", version: 1, seq, generation: intent.generation };
package/src/goal.ts CHANGED
@@ -7,7 +7,7 @@ export type Goal = {
7
7
  };
8
8
  export type Entry =
9
9
  | { type: "goal.created"; version: 1; seq: number; goal: Goal }
10
- | { type: "goal.transition"; version: 1; seq: number; from: Status; to: Status; by: Actor; userRequest?: string }
10
+ | { type: "goal.transition"; version: 1; seq: number; from: Status; to: Status; by: Actor; userRequest?: string; resetContinuations?: boolean }
11
11
  | { type: "goal.cleared"; version: 1; seq: number }
12
12
  | { type: "goal.usage"; version: 1; seq: number; input: number | null; output: number | null; cacheRead: number | null; cacheWrite: number | null; unknownMessages: number }
13
13
  | { type: "goal.continuation_sent"; version: 1; seq: number; generation: number }
@@ -15,7 +15,7 @@ export type Entry =
15
15
  | { type: "goal.limit_config"; version: 1; seq: number; tokenBudget: number | null; maxContinuations: number };
16
16
  export type Intent =
17
17
  | { type: "create"; id: string; objective: string; tokenBudget?: number | null; maxContinuations?: number }
18
- | { type: "transition"; to: Status; by: Actor; userRequest?: string }
18
+ | { type: "transition"; to: Status; by: Actor; userRequest?: string; resetContinuations?: boolean }
19
19
  | { type: "clear" }
20
20
  | { type: "usage"; input?: number | null; output?: number | null; cacheRead?: number | null; cacheWrite?: number | null; unknownMessages?: number }
21
21
  | { type: "continuation_sent"; generation: number }
@@ -55,8 +55,11 @@ export function transition(state: Goal | null, intent: Intent): Goal | null {
55
55
  if (intent.to === "paused" && !intent.userRequest?.trim()) throw new GoalError("forbidden", "paused requires user request evidence");
56
56
  if (intent.by === "agent" && !["complete", "blocked"].includes(intent.to)) throw new GoalError("forbidden", "agent cannot set this status");
57
57
  if (intent.by === "agent" && intent.to === "complete" && state.status !== "active") throw new GoalError("forbidden", "agent can complete only an active goal");
58
- if (state.status === "complete" || intent.to === state.status) throw new GoalError("illegal", "illegal status transition");
59
- return { ...state, status: intent.to };
58
+ const reset = intent.resetContinuations === true;
59
+ if (reset && (intent.by !== "user" || intent.to !== "active")) throw new GoalError("forbidden", "only user resume can reset continuations");
60
+ if (reset && state.tokenBudget !== null && usageKeys.reduce((sum, key) => sum + state.usage[key], 0) >= state.tokenBudget) throw new GoalError("budget", "token budget exhausted; adjust /goal budget before resuming");
61
+ if (state.status === "complete" || (intent.to === state.status && !reset)) throw new GoalError("illegal", "illegal status transition");
62
+ return { ...state, status: intent.to, ...(reset ? { continuationSeq: 0 } : {}) };
60
63
  }
61
64
  if (intent.type === "limit_config") {
62
65
  if (!Number.isInteger(intent.maxContinuations) || intent.maxContinuations < 0 || (intent.tokenBudget !== null && (!Number.isFinite(intent.tokenBudget) || intent.tokenBudget < 0))) throw new GoalError("invalid", "invalid limits");
@@ -95,7 +98,7 @@ export function fold(entries: readonly Entry[]): Goal | null {
95
98
  if (!entry.type.startsWith("goal.")) continue;
96
99
  if (entry.type === "goal.created") state = { ...entry.goal };
97
100
  else if (entry.type === "goal.cleared") state = null;
98
- else if (state && entry.type === "goal.transition") state = transition(state, { type: "transition", to: entry.to, by: entry.by, userRequest: entry.userRequest });
101
+ else if (state && entry.type === "goal.transition") state = transition(state, { type: "transition", to: entry.to, by: entry.by, userRequest: entry.userRequest, resetContinuations: entry.resetContinuations });
99
102
  else if (state && entry.type === "goal.limit_config") state = transition(state, { type: "limit_config", tokenBudget: entry.tokenBudget, maxContinuations: entry.maxContinuations });
100
103
  else if (state && entry.type === "goal.continuation_sent") state = transition(state, { type: "continuation_sent", generation: entry.generation });
101
104
  else if (state && entry.type === "goal.usage") state = transition(state, { type: "usage", input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite, unknownMessages: entry.unknownMessages });
package/src/lifecycle.ts CHANGED
@@ -29,7 +29,7 @@ function messageFromEvent(event: any, ctx: any): Message | null {
29
29
  return m?.role === message.role;
30
30
  });
31
31
  const m = candidate?.message ?? candidate ?? message;
32
- const entryId = m?.id ?? m?.entryId ?? message.id ?? message.entryId;
32
+ const entryId = candidate?.id ?? candidate?.entryId ?? m?.id ?? m?.entryId ?? message.id ?? message.entryId;
33
33
  // Entry identity is heuristic-by-position because branch entries may omit message ids.
34
34
  return typeof entryId === "string"
35
35
  ? { entryId, role: message.role, usage: m?.usage ?? message.usage, toolName: m?.toolName ?? message.toolName, stopReason: m?.stopReason ?? message.stopReason }
@@ -95,6 +95,9 @@ export function registerLifecycle(pi: PiEvents, deps: LifecycleDeps): void {
95
95
  await before(ctx);
96
96
  const snapshot = deps.goalCommit.current();
97
97
  if (!snapshot) { steeredGoalId = null; steered = false; return; }
98
+ // Cancellation gates every automatic send, including budget-limit steering.
99
+ if (lastAssistantStop === "error" || lastAssistantStop === "aborted") return;
100
+ if (snapshot.goal.status !== "active" && snapshot.goal.status !== "budget_limited") return;
98
101
  const verdict = deps.accounting.settleTurn(snapshot.goal);
99
102
  if (verdict.kind !== "ok") {
100
103
  const latest = deps.goalCommit.current();
@@ -108,8 +111,6 @@ export function registerLifecycle(pi: PiEvents, deps: LifecycleDeps): void {
108
111
  steered = true;
109
112
  deps.send({ customType: "pi-goal-next/budget_limit", content: budgetLimitPrompt(goal), display: false, details: { goalId: goal.id } }, { triggerTurn: true });
110
113
  }
111
- // Error/aborted turns produced no completed work; only continue after a normally-finished turn (Codex parity).
112
- if (lastAssistantStop === "error" || lastAssistantStop === "aborted") return;
113
114
  await deps.continuation.onSettled();
114
115
  });
115
116
  pi.on("message_start", async (event: MessageStartEvent, ctx: any) => { await before(ctx); await deps.continuation.onMessageStart(event?.message ?? event); });