@mjasnikovs/pi-task 0.13.0 → 0.13.2

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.
@@ -1,4 +1,4 @@
1
- import { broadcast as wsBroadcast } from './broadcast.js';
1
+ import { broadcast as wsBroadcast, hasConnectedClients } from './broadcast.js';
2
2
  import { pushNotify } from './push.js';
3
3
  import { setPrompt, clearPrompt } from './session-state.js';
4
4
  const g = globalThis;
@@ -56,7 +56,8 @@ export class SessionUI {
56
56
  };
57
57
  setPrompt(prompt);
58
58
  // Reaches a backgrounded/suspended phone, which the in-page UI can't.
59
- void pushNotify('pi needs your input', spec.question, 'pi-prompt').catch(() => { });
59
+ if (!hasConnectedClients())
60
+ void pushNotify('pi needs your input', spec.question, 'pi-prompt').catch(() => { });
60
61
  // Local: resolves to a value/undefined, or undefined on abort. Swallow
61
62
  // the rejection some implementations throw on abort so it never leaks.
62
63
  const local = this.ctx.hasUI ?
@@ -3,3 +3,4 @@ export declare function addClient(ws: WebSocket): void;
3
3
  export declare function removeClient(ws: WebSocket): void;
4
4
  export declare function broadcast(msg: unknown): void;
5
5
  export declare function sendTo(ws: WebSocket, msg: unknown): void;
6
+ export declare function hasConnectedClients(): boolean;
@@ -22,3 +22,10 @@ export function sendTo(ws, msg) {
22
22
  ws.send(JSON.stringify(msg));
23
23
  }
24
24
  }
25
+ export function hasConnectedClients() {
26
+ for (const ws of clients) {
27
+ if (ws.readyState === ws.OPEN)
28
+ return true;
29
+ }
30
+ return false;
31
+ }
@@ -1,6 +1,7 @@
1
1
  import { setAgentIdle } from './state.js';
2
2
  import { pushNotify } from './push.js';
3
3
  import { publishNotify } from './bridge.js';
4
+ import { hasConnectedClients } from './broadcast.js';
4
5
  import { agentStart, appendText, textEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
5
6
  /** Mirror pi agent events into the authoritative SessionState. Each handler
6
7
  * drives a mutator, which updates the snapshot AND broadcasts the live delta. */
@@ -22,7 +23,8 @@ export function setupEvents(pi) {
22
23
  if (errorMessage || ae.reason === 'error') {
23
24
  const message = errorMessage || 'Request failed';
24
25
  addError(message);
25
- void pushNotify('Agent error', message, 'pi-error').catch(() => { });
26
+ if (!hasConnectedClients())
27
+ void pushNotify('Agent error', message, 'pi-error').catch(() => { });
26
28
  }
27
29
  }
28
30
  });
@@ -41,7 +43,8 @@ export function setupEvents(pi) {
41
43
  pi.on('agent_end', (_event, ctx) => {
42
44
  setAgentIdle(true);
43
45
  agentEnd(ctx.getContextUsage());
44
- void pushNotify('Task finished', '', 'pi-end').catch(() => { });
46
+ if (!hasConnectedClients())
47
+ void pushNotify('Task finished', '', 'pi-end').catch(() => { });
45
48
  });
46
49
  pi.on('input', (event, _ctx) => {
47
50
  if (event.source === 'interactive' && typeof event.text === 'string') {
@@ -121,6 +121,21 @@ export async function startServer(onMessage, getHtml) {
121
121
  handle.onFirstConnect = null;
122
122
  // One authoritative snapshot — the client replaces its whole view with it.
123
123
  sendTo(ws, snapshot());
124
+ // Heartbeat: ping every 30 s; terminate if the client doesn't respond.
125
+ // This catches phones that close/sleep without sending a TCP FIN, so the
126
+ // server's client-set stays accurate and push notifications fire correctly.
127
+ let alive = true;
128
+ const heartbeat = setInterval(() => {
129
+ if (!alive) {
130
+ ws.terminate();
131
+ return;
132
+ }
133
+ alive = false;
134
+ ws.ping();
135
+ }, 10_000);
136
+ ws.on('pong', () => {
137
+ alive = true;
138
+ });
124
139
  ws.on('message', data => {
125
140
  let msg;
126
141
  try {
@@ -142,6 +157,7 @@ export async function startServer(onMessage, getHtml) {
142
157
  onMessage(msg.text);
143
158
  });
144
159
  ws.on('close', () => {
160
+ clearInterval(heartbeat);
145
161
  removeClient(ws);
146
162
  });
147
163
  });
@@ -210,18 +210,20 @@ export function validateSpecShape(spec) {
210
210
  }
211
211
  // ─── Title derivation ────────────────────────────────────────────────────────
212
212
  export function deriveTitle(refined) {
213
+ const stripBold = (s) => s.replace(/^\*+|\*+$/g, '').trim();
214
+ const truncate = (s) => (s.length > 119 ? s.slice(0, 119) + '…' : s);
213
215
  const lines = refined.split('\n');
214
216
  for (let i = 0; i < lines.length; i++) {
215
- const stripped = lines[i].trim().replace(/^#+\s+/, '');
217
+ const stripped = stripBold(lines[i].trim().replace(/^#+\s+/, ''));
216
218
  if (/^GOAL\s*:?\s*$/i.test(stripped)) {
217
219
  for (let j = i + 1; j < lines.length; j++) {
218
220
  const line = lines[j].trim();
219
221
  if (line.length === 0)
220
222
  continue;
221
- const headerCheck = line.replace(/^#+\s+/, '');
223
+ const headerCheck = stripBold(line.replace(/^#+\s+/, ''));
222
224
  if (/^(CONSTRAINTS|KNOWN-UNKNOWNS)\s*:?\s*$/i.test(headerCheck))
223
225
  break;
224
- return line;
226
+ return truncate(line);
225
227
  }
226
228
  break;
227
229
  }
@@ -230,10 +232,10 @@ export function deriveTitle(refined) {
230
232
  let line = raw.trim();
231
233
  if (line.length === 0)
232
234
  continue;
233
- line = line.replace(/^#+\s+/, '').replace(/^GOAL\s*:?\s*/i, '');
235
+ line = stripBold(line.replace(/^#+\s+/, '')).replace(/^GOAL\s*:?\s*/i, '');
234
236
  if (line.length === 0)
235
237
  continue;
236
- return line;
238
+ return truncate(line);
237
239
  }
238
240
  return '(untitled)';
239
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",