@mjasnikovs/pi-task 0.38.17 → 0.38.18

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.
@@ -215,5 +215,16 @@ export declare const CONFIG_LOADERS: {
215
215
  * into the config object.
216
216
  */
217
217
  export declare function loadConfig(raw: unknown): PiTaskConfig;
218
+ /**
219
+ * Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
220
+ *
221
+ * The override exists because this module loads the real file at import time,
222
+ * which made every test that reads a config value depend on the developer's own
223
+ * `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
224
+ * 12 tests here while CI (no config file at all) stayed green. The test preload
225
+ * points this at a path under the tmp dir that never exists, so tests always see
226
+ * DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
227
+ */
228
+ export declare const CONFIG_PATH_ENV = "PI_TASK_CONFIG_PATH";
218
229
  export declare function getConfig(): PiTaskConfig;
219
230
  export declare function saveConfig(config: PiTaskConfig): Promise<void>;
@@ -182,7 +182,19 @@ export function loadConfig(raw) {
182
182
  }
183
183
  return out;
184
184
  }
185
- const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
185
+ /**
186
+ * Where the saved config lives. `PI_TASK_CONFIG_PATH` overrides it.
187
+ *
188
+ * The override exists because this module loads the real file at import time,
189
+ * which made every test that reads a config value depend on the developer's own
190
+ * `~/.config/pi-task/config.json` — a machine-local `"debugLogs": "off"` failed
191
+ * 12 tests here while CI (no config file at all) stayed green. The test preload
192
+ * points this at a path under the tmp dir that never exists, so tests always see
193
+ * DEFAULT_CONFIG. Read once at module eval: the preload runs before any import.
194
+ */
195
+ export const CONFIG_PATH_ENV = 'PI_TASK_CONFIG_PATH';
196
+ const CONFIG_PATH = process.env[CONFIG_PATH_ENV]?.trim()
197
+ || path.join(os.homedir(), '.config', 'pi-task', 'config.json');
186
198
  const _g = globalThis;
187
199
  if (!_g.__piTaskConfig) {
188
200
  _g.__piTaskConfig = { config: { ...DEFAULT_CONFIG }, loaded: false };
@@ -115,21 +115,35 @@ export function buildWidgetData(s) {
115
115
  export function startWidget(ctx, getState) {
116
116
  if (!ctx.hasUI)
117
117
  return () => { };
118
+ // `ctx.ui` THROWS once the ctx goes stale (/reload, session replacement), so
119
+ // the theme read belongs INSIDE the guard, not one line above it. render()
120
+ // runs from a timer, where an unguarded throw is an uncaughtException that
121
+ // kills the whole pi process — and a swallowed one would throw again on
122
+ // every tick, so a stale ctx latches and stops the timer. Issue #15.
123
+ let stale = false;
118
124
  const render = () => {
125
+ if (stale)
126
+ return;
119
127
  const s = getState();
120
- const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
121
- const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire
128
+ const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire; needs no ctx
122
129
  try {
130
+ const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
123
131
  ctx.ui.setWidget(WIDGET_KEY, lines);
124
132
  }
125
133
  catch {
126
- /* stale ctx */
134
+ stale = true;
135
+ clearInterval(timer);
136
+ return;
127
137
  }
128
138
  setTaskWidget(plain, s ? buildWidgetData(s) : null);
129
139
  };
130
- render();
140
+ // The timer is created BEFORE the first render so `timer` is always bound
141
+ // when render's catch reaches for it: a ctx already stale on the very first
142
+ // paint now stops the loop there, instead of arming an interval that wakes
143
+ // up and returns early forever.
131
144
  const timer = setInterval(render, WIDGET_REFRESH_MS);
132
145
  timer.unref?.();
146
+ render();
133
147
  return () => {
134
148
  clearInterval(timer);
135
149
  try {
@@ -192,21 +206,32 @@ export function buildAutoLoaderData(s) {
192
206
  export function startAutoLoader(ctx, getState) {
193
207
  if (!ctx.hasUI)
194
208
  return () => { };
209
+ // Same staleness hazard as startWidget: the theme read must sit inside the
210
+ // guard, and a stale ctx stops the timer rather than throwing every tick.
211
+ let stale = false;
195
212
  const render = () => {
213
+ if (stale)
214
+ return;
196
215
  const s = getState();
197
- const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
198
- const plain = s ? buildAutoLoaderLines(s, undefined) : undefined;
216
+ const plain = s ? buildAutoLoaderLines(s, undefined) : undefined; // needs no ctx
199
217
  try {
218
+ const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
200
219
  ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
201
220
  }
202
221
  catch {
203
- /* stale ctx */
222
+ stale = true;
223
+ clearInterval(timer);
224
+ return;
204
225
  }
205
226
  setTaskWidget(plain, s ? buildAutoLoaderData(s) : null);
206
227
  };
207
- render();
228
+ // The timer is created BEFORE the first render so `timer` is always bound
229
+ // when render's catch reaches for it: a ctx already stale on the very first
230
+ // paint now stops the loop there, instead of arming an interval that wakes
231
+ // up and returns early forever.
208
232
  const timer = setInterval(render, WIDGET_REFRESH_MS);
209
233
  timer.unref?.();
234
+ render();
210
235
  return () => {
211
236
  clearInterval(timer);
212
237
  try {
@@ -249,7 +274,14 @@ export function buildImplData(s) {
249
274
  export function flashTerminalWidget(ctx, state, taskId, reason) {
250
275
  if (!ctx.hasUI)
251
276
  return;
252
- const theme = ctx.ui.theme;
277
+ // Same staleness hazard as the render timers: bail rather than throw.
278
+ let theme;
279
+ try {
280
+ theme = ctx.ui.theme;
281
+ }
282
+ catch {
283
+ return;
284
+ }
253
285
  let line;
254
286
  let clearMs;
255
287
  if (state === 'cancelled') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.17",
3
+ "version": "0.38.18",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",