Skip to content

fix: resume double-refresh — idempotent session_start UI installs - #13

Closed
agnitum2009 wants to merge 12 commits into
openpi-dev:mainfrom
agnitum2009:fix/resume-double-refresh
Closed

fix: resume double-refresh — idempotent session_start UI installs#13
agnitum2009 wants to merge 12 commits into
openpi-dev:mainfrom
agnitum2009:fix/resume-double-refresh

Conversation

@agnitum2009

Copy link
Copy Markdown
Collaborator

Problem

/resume visibly refreshes the whole session content twice before settling. Root cause (compared against omp/oh-my-pi):

  1. Kernel (pi 0.84.1): rebuildChatFromMessages() = chatContainer.clear() + renderSessionEntries() — full rebuild with zero component reuse. omp's equivalent accepts reuseSettledComponents and reuses cached components from a WeakMap, so its replay does not repaint. (Not fixable from an extension package.)
  2. Extension amplifiers (fixable here): session_start re-fires on every resume, and two openpi extensions performed repeated structural updates that force a second full-viewport repaint:
    • subagents/installSubagentNavigation wrapped the editor again each time — stacking another BelowEditorNavigationEditor layer and replacing the editor component.
    • tasks/updateTaskWidget called setWidget unconditionally — destroying and rebuilding the widget component.

Fix

  • subagents: install the editor wrapper once per runtime; later session_start events reuse it.
  • tasks: track the installed widget state; skip setWidget when the widget is already installed in the same shown/hidden state (mirrors the existing idempotency in subagents/workflows strip widgets).

Tests

232 related tests passing (new: repeated session_start does not rebuild an already-installed widget); tsc --noEmit clean; prettier clean.

pi-agent added 12 commits August 12, 2026 11:08
tasks 残留根因(遗忘+机制不全)→ commit-task-sync hook 尝试:
- extensions/commit-task-sync/index.ts:pi.on(tool_result) 检测 bash git commit 成功 →
  pi.on(agent_settled) ctx.ui.notify 提示 agent 同步 tasks(仿 post-edit,fire-and-forget)
- tsc 通过(exit 0)
- docs/knowledge-base/:obsidian vault(residual-root-cause + hook-design + implementation)
- 发现:openpi 有 pi.on(tool_result) 事件(PostToolUse 等价)
双通道提醒:
1. agent_settled → ui.notify(TUI/业主,建议性)
2. context → injectCommitReminder(对话/agent,强制——注入 <commit-task-sync> 块到下轮 messages)
context injection 仿 injectTaskProjection(tasks/index.ts:483 pi.on(context) return {messages})。
commit 检测后下轮注入提示+reset(仅提醒一次)。agent 不能忽略(在对话上下文)。
tsc 通过。
更新 commit-task-sync-implementation.md:
- v1 ui.notify + v2 context injection 演进
- 双通道验证(user TUI ✅ + agent context injection ✅)
- tsc TS2769 修复(event 类型推断)
- openpi hook 机制发现(pi.on tool_result/context + post-edit/injectTaskProjection 范本)
- pi.on 可用事件清单
- PR openpi-dev#6(tt-a1i/openpi)
- 结论:tasks 残留从靠记忆力变机制强制提醒
升级 commit-task-sync(单信号 A)→ multi-signal-sync(MECE 全场景):
- A commit(git commit exit 0,tool_result 检测)
- B verify(tsc/test/verify PASS exit 0,tool_result 检测)
- C authorization(user message 授权语,context 检测)
dual-channel:agent_settled ui.notify + context injectSyncReminder(<multi-signal-sync> 块)
D/E(无变更/取消)无信号 → 收口审计纪律(提醒文本已含)
第一性原理:tasks 完成 = 真实完成信号驱动,非 agent 记忆。
MECE:commit/验证/授权 三信号互斥穷尽(tool_result + context 可检测面)。
tsc 0错(TS2769 修复:event 类型推断)。
…ttled notify)

bug: context 注入时 reset signals → agent_settled notify 不触发(signals 被 context 消费后已空)。
fix: 两个通道独立——signals(context 注入,下轮消费)+ pendingNotify(agent_settled notify,本轮消费)。
addSignal 同时加到两通道;context 消费 signals(注入块);agent_settled 消费 pendingNotify(notify)。
…s Indicator)

问题:ui.notify 瞬时消失(业主看不到驻留)。
fix:agent_settled → ctx.ui.setStatus(footer 驻留状态,跨 render 持续显示——tui.md Pattern 4)。
- 信号轮:setStatus('multi-signal-sync', '⚠️ 完成信号(...)— 请同步 tasks') 驻留
- 下一轮(无新信号):setStatus undefined 清除
- 双通道:对话 context 注入块(agent 提醒)+ footer 驻留状态(user 持续看到)
tsc 0错。
MECE 五类信号(A commit/B verify/C authorization/D 无变更/E 取消)+ 演进(v1-v5)+ 验证记录(A/B/C 三信号 notify 确认触发)+ v5 驻留(footer setStatus)+ openpi 机制发现。
…r gradient + blocked-task notifications

- tasks: completed items stay in the widget struck through (omp todo-HUD
  style); in-progress subject carries a time-driven shimmer sweep with a
  120ms redraw cadence; cross-extension attachment row under the census
  (shared/task-widget-attachment) so completion-signal reminders render in
  the task panel instead of the footer status bar
- subagents: strip moved above the editor (aboveEditor) as a multi-row HUD
  with one row per running subagent, live tool action inline (omp
  currentTool style), unread-settled notice row; footer setStatus removed
- workflows: strip moved above the editor as a multi-row HUD (header +
  per-agent rows), metrics in accent to stay distinguishable from the
  subagents HUD's warning yellow; footer setStatus removed
- working-indicator: new extension overriding pi's braille spinner with a
  flowing truecolor gradient bar (10 frames, 80ms tick) on the Working...
  status line
- goal/multi-signal-sync: blocked goal and blocked task announcements on
  agent_settled (once per blocked stint), fixing silent waits for human
  intervention
Port omp's (oh-my-pi) todo mechanisms that keep human-in-the-loop moments
visible, so a task awaiting an owner decision can no longer sit silently
behind a plausible status:

- reconciliation (omp #reconcileTodosWithSubagents): settled subagents are
  recorded by the subagents extension and drained at agent_settled; a
  successful child whose title matches an open task auto-closes it with the
  child id as evidence, and a blocked task matching a successful child is
  closed as the unblock signal. Failed children stay open by design — the
  decision belongs to the user or the next turn.
- idle recap (omp #runIdleRecap, notify variant): a settled turn with open
  work re-arms a 120s timer; if no new agent turn starts and open/blocked
  tasks remain, a single warning surfaces them (with a blocked count),
  instead of stalling until the user happens to ask. Any activity cancels.
- markdown round-trip (omp /todo export|import): /tasks export writes an
  editable checklist (omp markers [ ], [/], [x], [!], [-]); /tasks import
  replaces the batch wholesale, notes via '-- note' suffix.
- matching (omp normalizeForTodoMatch/todoMatchesAnyDescription): normalize
  then equal, substring fallback with a length floor; CJK digit-spacing
  variants reconcile because all non-alphanumerics including spaces drop.
- single in-progress invariant (omp normalizeInProgressTask): starting a
  task demotes any other in-flight task to pending.
- nextActionableTask exported for recap anchoring (in-flight first, blocked
  excluded).
…tall warning, task light-up

DDD-layered visibility for running subagents, so a plausible 'running' status
can no longer mask a stalled or failing child (first-principles: the surface
must distinguish 在动 / 在等 / 死了 for every in-flight agent):

- domain (pure, fully tested): lastActivityOf/isStalled (5m silence
  threshold), lastIntentOf (live streaming text, then latest assistant text),
  failureStreakOf over the snapshot; STALL_THRESHOLD_MS constant
- application: manager folds every event into lastActivityAt; consecutive
  ToolEnd isError streaks count up and reset on success or settle (pi's
  kernel exposes no retry event, so the streak is the honest proxy)
- UI (HUD rows): live tool → intent fallback (💬) → stall warning
  (⚠ 无活动 Nm); ✗ 连败 N after 2+ consecutive failures
- light-up bridge (omp): subagents publishes running-child descriptions;
  tasks widget highlights a pending task whose subject matches a running
  child (accent icon + bold subject), so 'someone is already doing this' is
  visible before the reconcile closes it
/resume replays the session and re-fires session_start on every extension.
Two structural updates were repeated on each resume, forcing a second
full-viewport repaint after the kernel's own rebuild:

- subagents installSubagentNavigation wrapped the editor again on every
  session_start, stacking another BelowEditorNavigationEditor layer and
  replacing the editor component (structural change → full repaint). Now
  installed once per runtime.
- tasks updateTaskWidget called setWidget unconditionally, destroying and
  rebuilding the widget component on every resume. Now skips when the
  widget is already installed in the same shown/hidden state.

(omp's kernel avoids the first repaint entirely via reuseSettledComponents
component caching; pi 0.84.1 rebuilds the chat from scratch, so the
extension-side amplifiers were the fixable part here.)
…/suggestions

PR openpi-dev#13 made subagents' editor wrapper one-shot, but workflows and
suggestions re-wrap the editor on every session_start too (each /resume
stacks another WorkflowNavigationEditor / NextActionSuggestionEditor layer
and replaces the editor component — a structural change forcing a second
full-viewport repaint). Same one-shot guard applied to both; ui-customization
header/footer re-install is addressed separately.
agnitum2009 pushed a commit to agnitum2009/openpi that referenced this pull request Aug 13, 2026
…/suggestions

PR openpi-dev#13 made subagents' editor wrapper one-shot, but workflows and
suggestions re-wrap the editor on every session_start too (each /resume
stacks another WorkflowNavigationEditor / NextActionSuggestionEditor layer
and replaces the editor component — a structural change forcing a second
full-viewport repaint). Same one-shot guard applied to both; ui-customization
header/footer re-install is addressed separately.
@tt-a1i

tt-a1i commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

这个问题线索是有价值的,我按当前 main 和锁定的 Pi 0.84.1 又核了一遍。不过这版补丁不建议继续合,主要不是冲突,而是它没有命中真实的 /resume 生命周期。

Pi 在 /resume 时不是在同一个 Extension 实例上简单重发 session_startAgentSessionRuntime.switchSession() 会先触发 session_shutdown,随后 InteractiveMode.resetExtensionUI() 清掉 widgets 和 custom editor,销毁旧 runtime,再创建新的 Extension 实例并触发 session_start。因此 navigationInstalledworkflowNavigationInstalledsuggestionEditorInstalledtaskWidgetInstalled 都会重新变成 false,实际 Resume 仍会重新安装。

Tasks 这处还有一个独立回归:widget factory 捕获了第一次安装时的 current snapshot,之后只要显隐状态仍为 true 就直接返回。任务从 pending 变成 in_progress、增加任务或切换 session tree 后,面板可能继续显示旧内容。

新增测试没有覆盖上述生命周期:它只在同一个 fake instance/context 上连续触发两次 session_start,也只检查 setWidget 调用次数;没有 shutdown、UI reset、新 runtime,也没有断言 Resume 后内容是否更新。三个 editor guard 也没有对应测试。

我倾向于关闭这个旧 PR,保留问题,基于当前 main 另开一个小修复:

  • 用真实 session replacement 做回归,确认 wrapper 不叠加、ctx 指向新 Session;
  • Task widget 改为读取 live snapshot,只在显隐变化时安装或卸载;
  • 如果双刷来自 bind 期间的多次 requestRender,更合适的是在 Pi 内核做 UI bind batching/component reuse,或在 OpenPI 里用一个 session-aware editor coordinator 统一组合 wrapper。

我这次只做了源码审查,还没有做 TUI 肉眼复现,所以“当前版本仍可见双刷”暂时不当成已确认;但现有 guard 确定不能作为修复合入。

@tt-a1i

tt-a1i commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

按当前 main 和 Pi 0.84.1 的真实 Resume 生命周期复核后,这版 instance-local guard 不能解决重新绑定,并会让 Task widget 捕获旧快照。先关闭这个累计旧栈 PR;后续会基于最新 main 提交带真实生命周期回归测试的小修复。

@tt-a1i tt-a1i closed this Aug 20, 2026
@agnitum2009
agnitum2009 deleted the fix/resume-double-refresh branch August 25, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants