@mocito/pi-goal 0.1.10 → 0.1.11

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.11 - 2026-07-17
6
+
7
+ ### Fixed
8
+
9
+ - Preserve accumulated wall-clock time when `/goal resume` is used on an already-active goal.
10
+ - Derive persisted `piGoalVersion` metadata from the package version so release versions cannot drift.
11
+ - Ignore malformed persisted mutations instead of allowing invalid status, budget, or accounting data to corrupt reconstructed goal state.
12
+
5
13
  ## 0.1.10 - 2026-07-01
6
14
 
7
15
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mocito/pi-goal",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Persistent long-running goals for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,9 +56,9 @@
56
56
  "@earendil-works/pi-ai": "^0.80.0",
57
57
  "@earendil-works/pi-coding-agent": "^0.80.0",
58
58
  "@earendil-works/pi-tui": "^0.80.0",
59
- "@types/node": "^25.9.3",
60
- "typebox": "^1.2.10",
61
- "tsx": "^4.22.4",
59
+ "@types/node": "^26.1.0",
60
+ "typebox": "^1.3.4",
61
+ "tsx": "^4.23.0",
62
62
  "typescript": "^6.0.3"
63
63
  },
64
64
  "publishConfig": {
package/src/commands.ts CHANGED
@@ -94,9 +94,10 @@ async function editGoal(pi: ExtensionAPI, ctx: ExtensionCommandContext, runtime:
94
94
  function setGoalStatus(pi: ExtensionAPI, ctx: ExtensionCommandContext, runtime: CommandRuntime, status: "paused" | "active"): void {
95
95
  const current = accountCurrent(pi, ctx, runtime.getGoal());
96
96
  if (!current) return ctx.ui.notify("No goal set.", "error");
97
- const time = status === "active" ? current.timeUsedSeconds : realizedTimeUsed(current);
98
- const activeStartedAt = status === "active" ? nowIso() : undefined;
99
- const mutation = statusMutation(current, status, time, activeStartedAt, transitionMeta(status === "active" ? "command:/goal resume" : "command:/goal pause", current, time, nowIso()));
97
+ const transitionedAt = nowIso();
98
+ const time = realizedTimeUsed(current, Date.parse(transitionedAt));
99
+ const activeStartedAt = status === "active" ? transitionedAt : undefined;
100
+ const mutation = statusMutation(current, status, time, activeStartedAt, transitionMeta(status === "active" ? "command:/goal resume" : "command:/goal pause", current, time, transitionedAt));
100
101
  appendGoalMutation(pi, mutation);
101
102
  runtime.setGoal(applyGoalMutation(current, mutation));
102
103
  runtime.afterGoalChanged(ctx, status === "active" ? "Goal resumed." : "Goal paused.");
package/src/metadata.ts CHANGED
@@ -1,7 +1,15 @@
1
+ import { createRequire } from "node:module";
1
2
  import type { BranchEntry, GoalMutationMeta, GoalState } from "./types.js";
2
3
  import { secondsBetween } from "./utils.js";
3
4
 
4
- export const PI_GOAL_VERSION = "0.1.7";
5
+ const packageJson = createRequire(import.meta.url)("../package.json") as { version?: unknown };
6
+
7
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
8
+ throw new Error("pi-goal package version is missing or invalid.");
9
+ }
10
+
11
+ /** Package version that produced the persisted metadata; schema versioning is separate. */
12
+ export const PI_GOAL_VERSION = packageJson.version;
5
13
 
6
14
  export function withPiGoalVersion(meta: GoalMutationMeta = {}): GoalMutationMeta {
7
15
  return { piGoalVersion: PI_GOAL_VERSION, ...meta };
package/src/state.ts CHANGED
@@ -70,10 +70,50 @@ export function applyGoalMutation(current: GoalState | null, mutation: GoalMutat
70
70
  }
71
71
 
72
72
  function isKnownMutation(value: Partial<GoalMutation> | undefined): value is GoalMutation {
73
- if (!value || value.schemaVersion !== GOAL_SCHEMA_VERSION || typeof value.kind !== "string" || typeof value.at !== "string") return false;
74
- if (["create", "replace"].includes(value.kind)) return typeof value.goalId === "string" && typeof (value as any).objective === "string";
75
- if (["edit", "status", "budget", "account"].includes(value.kind)) return typeof value.goalId === "string";
76
- return value.kind === "clear";
73
+ if (!value || value.schemaVersion !== GOAL_SCHEMA_VERSION || typeof value.kind !== "string" || !validTimestamp(value.at)) return false;
74
+ const mutation = value as Record<string, unknown>;
75
+ if (value.kind === "clear") return optionalNonNegativeInteger(mutation.timeUsedSeconds);
76
+ if (typeof mutation.goalId !== "string" || mutation.goalId.length === 0) return false;
77
+ switch (value.kind) {
78
+ case "create":
79
+ case "replace":
80
+ return typeof mutation.objective === "string" && optionalPositiveInteger(mutation.tokenBudget);
81
+ case "edit":
82
+ return typeof mutation.objective === "string" && (mutation.status === undefined || isGoalStatus(mutation.status));
83
+ case "status":
84
+ return isGoalStatus(mutation.status)
85
+ && optionalNonNegativeInteger(mutation.timeUsedSeconds)
86
+ && (mutation.activeStartedAt === undefined || validTimestamp(mutation.activeStartedAt));
87
+ case "budget":
88
+ return optionalPositiveInteger(mutation.tokenBudget);
89
+ case "account":
90
+ return nonNegativeInteger(mutation.tokens)
91
+ && Array.isArray(mutation.entryIds)
92
+ && mutation.entryIds.every((id) => typeof id === "string");
93
+ default:
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function isGoalStatus(value: unknown): value is GoalStatus {
99
+ return typeof value === "string" && ["active", "paused", "blocked", "usage_limited", "budget_limited", "complete"].includes(value);
100
+ }
101
+
102
+ function validTimestamp(value: unknown): value is string {
103
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
104
+ }
105
+
106
+ function nonNegativeInteger(value: unknown): value is number {
107
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
108
+ }
109
+
110
+ function optionalNonNegativeInteger(value: unknown): boolean {
111
+ return value === undefined || nonNegativeInteger(value);
112
+ }
113
+
114
+ function optionalPositiveInteger(value: unknown): boolean {
115
+ return value === undefined
116
+ || (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0);
77
117
  }
78
118
 
79
119
  export function statusMutation(goal: GoalState, status: GoalStatus, timeUsedSeconds: number, activeStartedAt?: string, meta?: GoalMutationMeta): GoalMutation {