@staix/agent-hub 0.12.24 → 0.12.25

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.
@@ -23,6 +23,9 @@ let shellSeq = 0;
23
23
  let forcedFailure = "";
24
24
  let turnGeneration = 0;
25
25
  let sessionId = "";
26
+ let pendingPromptId: string | undefined;
27
+ let activePromptId: string | undefined;
28
+ let settlement: Promise<void> | undefined;
26
29
  const maxSteps = Number(process.env.AGENTHUB_PI_MAX_STEPS ?? 30);
27
30
  let shutdown: (() => void) | undefined;
28
31
  let runtimeCtx: any;
@@ -66,7 +69,30 @@ async function poll(pi: ExtensionAPI): Promise<void> {
66
69
  if (!command) continue;
67
70
  try {
68
71
  if (command.type === "prompt" || command.type === "steer") {
72
+ if (command.type === "prompt") {
73
+ // Let our settled callback return to native Pi before the normal queued successor starts.
74
+ if (settlement) {
75
+ let timer: ReturnType<typeof setTimeout> | undefined;
76
+ try { await Promise.race([settlement, new Promise<void>(resolve => { timer = setTimeout(resolve, 5_000); })]); }
77
+ finally { clearTimeout(timer); }
78
+ }
79
+ if (settlement || runtimeCtx?.isIdle?.() !== true || (command.sessionId !== undefined && command.sessionId !== sessionId) || (command.generation !== undefined && command.generation !== turnGeneration)) {
80
+ await post("/ack", { id: command.id, ok: false, error: "Pi prompt was not inserted because its native session is no longer idle at the expected generation", refusal: { kind: "prompt_not_started", promptId: command.id, sessionId, generation: turnGeneration } });
81
+ continue;
82
+ }
83
+ // Pinned Pi 1.0.1 starts _runAgentPrompt synchronously before its first await.
84
+ // Keep this check and insertion in one JS turn; snapshots cannot reserve native idleness.
85
+ pendingPromptId = command.id;
86
+ }
69
87
  pi.sendMessage({ customType: "agent-hub", content: String(command.message ?? ""), display: true }, { triggerTurn: command.type === "prompt", deliverAs: command.type === "steer" ? "steer" : "followUp" });
88
+ if (command.type === "prompt" && runtimeCtx?.isIdle?.() !== false) {
89
+ // Another extension's async settled hook can make native Pi defer this insertion.
90
+ // Our callback barrier is not an atomic native reservation: keep that effect uncertain.
91
+ if (pendingPromptId === command.id) pendingPromptId = undefined;
92
+ if (activePromptId === command.id) activePromptId = undefined;
93
+ await post("/ack", { id: command.id, ok: false, error: "Pi prompt insertion did not synchronously claim a native turn", refusal: { kind: "prompt_uncertain", promptId: command.id, sessionId, generation: turnGeneration } });
94
+ continue;
95
+ }
70
96
  } else if (command.type === "get_session_state") {
71
97
  const entries = runtimeCtx.sessionManager.getEntries();
72
98
  await post("/ack", { id: command.id, ok: true, result: {
@@ -94,7 +120,7 @@ async function poll(pi: ExtensionAPI): Promise<void> {
94
120
  }
95
121
  }
96
122
  await post("/ack", { id: command.id, ok: true });
97
- } catch (error) { await post("/ack", { id: command.id, ok: false, error: (error as Error).message }); }
123
+ } catch (error) { if (pendingPromptId === command.id) pendingPromptId = undefined; await post("/ack", { id: command.id, ok: false, error: (error as Error).message }); }
98
124
  } catch { await new Promise((resolve) => setTimeout(resolve, 250)); }
99
125
  }
100
126
  }
@@ -117,7 +143,7 @@ export default function(pi: ExtensionAPI): void {
117
143
  const state = ctx.sessionManager.getHeader();
118
144
  sessionId = String(state?.id ?? "");
119
145
  try {
120
- const claimed = await post("/event", { type: "session_start", ownerToken, pid: process.pid, signature: processSignature(process.pid), sessionId: state?.id, sessionFile: ctx.sessionManager.getSessionFile(), approvalTurnAbort: typeof ctx.abort === "function" });
146
+ const claimed = await post("/event", { type: "session_start", ownerToken, pid: process.pid, signature: processSignature(process.pid), sessionId: state?.id, sessionFile: ctx.sessionManager.getSessionFile(), approvalTurnAbort: typeof ctx.abort === "function", fencedPrompt: true });
121
147
  if (claimed?.ok === false) { ctx.shutdown?.(); return; }
122
148
  } catch (error) { ctx.shutdown?.(); throw error; }
123
149
  if (!pollStarted) { pollStarted = true; void poll(pi); }
@@ -127,7 +153,7 @@ export default function(pi: ExtensionAPI): void {
127
153
  const text = message?.content?.filter((c: any) => c?.type === "text").map((c: any) => c.text).join("")?.trim();
128
154
  const failed = !!forcedFailure || message?.stopReason === "error";
129
155
  const cancelled = !forcedFailure && message?.stopReason === "aborted";
130
- await post("/event", { type: "agent_end", generation: turnGeneration, text: text ?? "", failed, cancelled, ...(failed ? { error: forcedFailure || message?.errorMessage || message?.stopReason } : {}) });
156
+ await post("/event", { type: "agent_end", generation: turnGeneration, ...(activePromptId ? { promptId: activePromptId } : {}), text: text ?? "", failed, cancelled, ...(failed ? { error: forcedFailure || message?.errorMessage || message?.stopReason } : {}) });
131
157
  });
132
158
  pi.on("message_end", async (event: any) => {
133
159
  const tokens = assistantTokens(event.message);
@@ -142,7 +168,8 @@ export default function(pi: ExtensionAPI): void {
142
168
  lastActivity = now;
143
169
  try { await post("/event", { type: "activity" }); } catch { /* shutdown owns bridge cleanup */ }
144
170
  });
145
- pi.on("agent_start", async () => { toolSteps = 0; forcedFailure = ""; turnGeneration++; await post("/event", { type: "agent_start", generation: turnGeneration }); });
171
+ // Native retries/continuations emit more starts before the one logical agent_settled.
172
+ pi.on("agent_start", async () => { toolSteps = 0; forcedFailure = ""; turnGeneration++; activePromptId = pendingPromptId ?? activePromptId; pendingPromptId = undefined; await post("/event", { type: "agent_start", generation: turnGeneration, ...(activePromptId ? { promptId: activePromptId } : {}) }); });
146
173
  pi.on("model_select", async (_event: any, ctx: any) => { if (ctx.model?.provider && ctx.model.provider !== "agent-hub-local") ctx.shutdown?.(); });
147
174
  pi.on("before_agent_start", async (event: any, ctx: any) => {
148
175
  if (ctx.model?.provider && ctx.model.provider !== "agent-hub-local") { ctx.abort?.(); return { systemPrompt: event.systemPrompt }; }
@@ -157,10 +184,18 @@ export default function(pi: ExtensionAPI): void {
157
184
  return undefined;
158
185
  });
159
186
  pi.on("agent_settled", async (_event: any, ctx: any) => {
187
+ let release!: () => void;
188
+ const current = new Promise<void>(resolve => { release = resolve; });
189
+ settlement = current;
160
190
  const entries = ctx.sessionManager?.getEntries?.() ?? [];
161
191
  const message = entries.slice().reverse().find((entry: any) => entry.type === "message" && entry.message?.role === "assistant")?.message;
162
192
  const text = message?.content?.filter((c: any) => c?.type === "text").map((c: any) => c.text).join("")?.trim();
163
- await post("/event", { type: "agent_settled", generation: turnGeneration, ...(text ? { text } : {}) });
193
+ try { await post("/event", { type: "agent_settled", generation: turnGeneration, ...(activePromptId ? { promptId: activePromptId } : {}), ...(text ? { text } : {}) }); }
194
+ finally {
195
+ activePromptId = undefined;
196
+ // Release after this callback returns, not inside its awaited POST continuation.
197
+ setTimeout(() => { if (settlement === current) settlement = undefined; release(); }, 0);
198
+ }
164
199
  });
165
200
  pi.on("user_bash", async (event: any) => {
166
201
  const generation = turnGeneration, callSessionId = sessionId;
package/src/ui/index.html CHANGED
@@ -515,7 +515,7 @@ function dropTarget(node, accepted, payload) {
515
515
  void action({...payload, id:card.id});
516
516
  });
517
517
  }
518
- function taskCard(task, card, stage, ids) {
518
+ function taskCard(task, card, stage, ids, overdue) {
519
519
  const row = el('article', undefined, 'task-card'), head = el('div', undefined, 'row-head');
520
520
  row.tabIndex = 0; taskRows.set(task.id,row);
521
521
  const trackCard = on => { const sceneCard=officeCards.get(task.id);if(sceneCard)sceneCard.classList.toggle('office-spotlight',on); };
@@ -539,7 +539,7 @@ function taskCard(task, card, stage, ids) {
539
539
  if(task.external?.provider==='github'){const source=el('a','GitHub #'+task.external.issue);source.href=task.external.url;source.rel='noreferrer';source.target='_blank';row.append(source);}
540
540
  if(task.pinned)row.append(el('p','Pinned','meta'));
541
541
  if(task.plannedStart != null || task.due != null)row.append(el('p','Planned '+dateText(task.plannedStart)+' / due '+dateText(task.due),'meta'));
542
- if(task.due != null && task.due<Date.now() && task.state!=='approved')row.append(badge('Overdue','warn'));
542
+ if(overdue)row.append(badge('Overdue','warn'));
543
543
  if(task.actualDates)row.append(el('p','Actual start '+dateText(task.actualDates.startedAt)+' / review '+dateText(task.actualDates.reviewAt)+' / done '+dateText(task.actualDates.doneAt),'meta'));
544
544
 
545
545
  const controls = el('div', undefined, 'actions');
@@ -567,6 +567,7 @@ function renderBoard(snapshot, peers, ids) {
567
567
  renderTaskProgress(progress);
568
568
  const stages = new Map(progress.stages.map(stage=>[stage.id,stage])), byId = new Map(snapshot.tasks.map(task=>[task.id,task]));
569
569
  const cards = new Map(model.columns.flatMap(column=>[...column.ready,...column.waiting]).map(card=>[card.id,card]));
570
+ const now = Date.now(), overdue = new Set(snapshot.tasks.filter(task=>task.due != null && task.due<now && task.state!=='approved').map(task=>task.id));
570
571
  boardViewState={snapshot,peers,ids,cards};
571
572
  renderPlanning(snapshot);
572
573
  const staged=$('backlog-proposal');staged.disabled=snapshot.boardCapabilities?.backlog!==true || !sameBoardContext(boardContext());
@@ -589,7 +590,7 @@ function renderBoard(snapshot, peers, ids) {
589
590
  }
590
591
  if (!ids.length) empty(target,'No peer status available.');
591
592
  });
592
- update('tasks',[snapshot.tasks,peers,snapshot.boardCapabilities,taskFilter,ownerFilter,releaseFilter,lanes,showAllDone],target=>{
593
+ update('tasks',[snapshot.tasks,peers,snapshot.boardCapabilities,taskFilter,ownerFilter,releaseFilter,lanes,showAllDone,[...overdue]],target=>{
593
594
  taskRows.clear();
594
595
  const visible=snapshot.tasks.filter(task=>(!taskFilter || (taskFilter==='work' ? ['in_progress','changes_requested'].includes(task.state) : task.state===taskFilter)) && (!ownerFilter || task.owner===ownerFilter || task.reviewer===ownerFilter) && (!releaseFilter || task.release===Number(releaseFilter)));
595
596
  const visibleIds=new Set(visible.map(task=>task.id));
@@ -614,7 +615,7 @@ function renderBoard(snapshot, peers, ids) {
614
615
  if(hidden) ready=ready.slice(0,10);
615
616
  for(const [label,list] of [['Ready',ready],['Waiting',waiting]]) {
616
617
  if(column.id==='todo') box.append(el('h4',label+' ('+list.length+')'));
617
- for(const card of list) box.append(taskCard(byId.get(card.id),card,stages.get(card.id),eligible));
618
+ for(const card of list) box.append(taskCard(byId.get(card.id),card,stages.get(card.id),eligible,overdue.has(card.id)));
618
619
  }
619
620
  if(!ready.length && !waiting.length) empty(box,'No tasks');
620
621
  if(hidden){const more=el('button','Show '+hidden+' older Done tasks');more.type='button';more.addEventListener('click',()=>{if(!boardViewState)return;showAllDone=true;renderBoard(boardViewState.snapshot,boardViewState.peers,boardViewState.ids)});box.append(more)}