让扩展正确重载、退出和处理失败
在会话启动时获取资源,退出时幂等释放,理解初始化失败和运行失败的不同边界。
让扩展正确重载、退出和处理失败
第一次运行正常,重载几次后同一事件出现多次,常见原因是旧监听器和定时器没有释放。删除模块缓存只能换掉代码,不能自动追回扩展已经创建的外部资源。
这篇做一个计数监听器,练习明确的启动与释放。计数只是观察资源是否重复启动,不代表真实业务进度。
本文示例固定使用 @earendil-works/pi-coding-agent@1.5.0,源码位于仓库 columns/coding-agent/examples/extension-development/。示例是独立包,先在该目录安装依赖;不需要修改专栏站点依赖。下面的命令均从这个示例目录运行。
初始化只注册,启动时获取资源
extensions/lifecycle.ts 的工厂只注册处理器;真正的计时器在 session_start 创建。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
import { startMonitor } from "../src/monitor.js"
export default function (pi: ExtensionAPI) {
let monitor: ReturnType<typeof startMonitor> | undefined
const dispose = () => {
monitor?.dispose()
monitor = undefined
}
pi.on("session_start", () => {
dispose()
monitor = startMonitor((listener) => pi.events.on("lesson:tick", listener))
})
pi.on("session_shutdown", dispose)
pi.registerCommand("lesson-ticks", {
description: "查看当前扩展实例的计数",
handler: async (_args, ctx) => {
if (ctx.hasUI) {
ctx.ui.notify(String(monitor?.count() ?? 0), "info")
}
},
})
}重复收到启动事件前先释放旧实例,退出时清空引用。pi.events.on 返回取消订阅函数,应与监听器归属于同一个扩展实例。
session_shutdown 既要考虑退出,也要考虑资源重建时的旧实例关闭。不能把它理解为“整个电脑上的智能体以后再也不会运行”。以当前宿主的事件派发为准,清理函数应允许重复调用。
清理本身做成可测试的函数
export function startMonitor(subscribe: (listener: () => void) => () => void) {
let ticks = 0
let stopped = false
const unsubscribe = subscribe(() => {
ticks += 1
})
const timer = setInterval(() => {
ticks += 1
}, 1000)
timer.unref()
return {
count: () => ticks,
dispose() {
if (stopped) {
return
}
stopped = true
clearInterval(timer)
unsubscribe()
},
}
}unref() 只表示这个计时器不会独自维持进程运行,不能代替 clearInterval()。真正释放包括取消计时器、取消订阅,以及生产代码中可能存在的子进程、文件 watcher、连接或临时文件。
这里的取消订阅函数同步且不抛错。如果业务资源关闭会失败,应用 try/finally 或聚合结果,确保一个资源的释放失败不会跳过其他资源;同时记录失败,不能吞掉后宣称全部关闭。
区分三类失败
宿主捕获扩展回调异常,不代表任意后台 Promise 都受保护。裸启动异步任务后应绑定取消、处理 rejection,并在关闭时等待必要收尾。否则异常可能脱离当前会话,甚至成为未处理拒绝。
用重复操作检验生命周期
npx pi --extension ./extensions/lifecycle.ts执行 /lesson-ticks,再用交互模式的 /reload 重载,核对是否只有新的计数器工作。重载前后的业务状态是否应保留,要另做持久化设计;本例计数器刻意在启动时重新计数。
离线测试应至少验证连续两次 dispose() 只取消一次订阅,释放后事件不再增加计数,重复启动不会保留上一实例。这些测试帮助发现资源归属错误,但不能证明真实终端退出、系统信号或子进程全部正确。
扩展重载和插件热重载不同
命令行宿主重新发现扩展,主要目的是更新注册与运行资源。它不保证保留模块变量。后文 Chord 的热重载则有契约、对象引用和状态迁移协议,不能把两者都归结为“清除 import 缓存”。
需要保留的数据先写入适合的存储,需要关闭的资源按所有者释放;把两件事分开,才能判断重载成功意味着哪些结果。
依据与版本
代码以已发布的 1.5.0 包导出、类型声明和加载器为准。在线主分支会继续变化,遇到接口差异时先核对版本。