@haoyiyin/9router 0.1.3 → 0.1.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haoyiyin/9router",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Pi extension — 9Router provider (OpenCode Free oc/-free models + router models) + fetch/search/transcribe tools",
package/src/command.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { loadConfig, saveConfig, DEFAULT_BASE } from "./config.ts";
3
- import { registerProvider } from "./provider.ts";
3
+ import { registerProvider, isStaleCtxError } from "./provider.ts";
4
4
 
5
5
  // Never contains a real key. If a dialog pre-fills this placeholder as an
6
6
  // editable default, an exact match is treated as "keep the current key".
@@ -41,7 +41,9 @@ export function registerCommand(pi: ExtensionAPI): void {
41
41
  await registerProvider(pi);
42
42
  } catch (err) {
43
43
  ctx.ui.notify(
44
- `9Router saved, but provider re-registration failed: ${err instanceof Error ? err.message : String(err)}`,
44
+ isStaleCtxError(err)
45
+ ? "9Router saved. This extension instance was replaced by a session change/reload; the new instance has re-registered the provider."
46
+ : `9Router saved, but provider re-registration failed: ${err instanceof Error ? err.message : String(err)}`,
45
47
  "warning",
46
48
  );
47
49
  }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import registerFetch from "./fetch.ts";
3
3
  import registerSearch from "./search.ts";
4
4
  import registerTranscribe from "./transcribe.ts";
5
- import { registerProvider } from "./provider.ts";
5
+ import { registerProvider, isStaleCtxError } from "./provider.ts";
6
6
  import { registerCommand } from "./command.ts";
7
7
 
8
8
  export default async function (pi: ExtensionAPI): Promise<void> {
@@ -15,14 +15,27 @@ export default async function (pi: ExtensionAPI): Promise<void> {
15
15
  await registerProvider(pi);
16
16
 
17
17
  let refreshing = false;
18
+ let closed = false;
18
19
  const timer = setInterval(
19
20
  async () => {
20
21
  // ponytail: models refresh by unregister+register; zen cache TTL is
21
22
  // 30m so refetch happens naturally. Overlap guard avoids re-entrancy.
22
- if (refreshing) return;
23
+ if (closed || refreshing) return;
23
24
  refreshing = true;
24
25
  try {
25
26
  await registerProvider(pi);
27
+ } catch (err) {
28
+ // The captured `pi` goes stale after session replacement or
29
+ // extension reload, and pi's assertActive() throw must never
30
+ // escape this callback — it would crash the whole process as an
31
+ // uncaughtException. The new session's extension instance
32
+ // re-registers the provider itself, so stop this timer for good.
33
+ if (isStaleCtxError(err)) {
34
+ closed = true;
35
+ clearInterval(timer);
36
+ }
37
+ // Any other error (network hiccup, bad upstream payload) is
38
+ // swallowed: the next tick retries.
26
39
  } finally {
27
40
  refreshing = false;
28
41
  }
@@ -32,4 +45,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
32
45
  if (typeof (timer as { unref?: () => void }).unref === "function") {
33
46
  (timer as { unref: () => void }).unref();
34
47
  }
48
+ pi.on("session_shutdown", () => {
49
+ closed = true;
50
+ clearInterval(timer);
51
+ });
35
52
  }
package/src/provider.ts CHANGED
@@ -69,6 +69,16 @@ export function getReasoning(model: RouterModel, devModel?: ModelsDevModel): boo
69
69
  return /reasoner|thinking|xhigh|high|max|pro|codex|opus|sonnet/i.test(model.id ?? "");
70
70
  }
71
71
 
72
+ /**
73
+ * True if `err` is pi's stale-extension-ctx error, thrown when a captured
74
+ * `pi`/command ctx is used after session replacement (newSession / fork /
75
+ * switchSession) or extension reload. A stale instance can never recover —
76
+ * the new session's extension instance re-registers everything itself.
77
+ */
78
+ export function isStaleCtxError(err: unknown): boolean {
79
+ return err instanceof Error && /stale after session replacement|ctx is stale/.test(err.message);
80
+ }
81
+
72
82
  export async function registerProvider(pi: ExtensionAPI): Promise<void> {
73
83
  const cfg = loadConfig();
74
84