@nklisch/pi-enhanced 0.4.1 → 0.4.3
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/CHANGELOG.md +7 -0
- package/node_modules/@nklisch/pi-astral-pocket/README.md +108 -52
- package/node_modules/@nklisch/pi-astral-pocket/package.json +3 -1
- package/node_modules/@nklisch/pi-astral-pocket/src/config.ts +40 -23
- package/node_modules/@nklisch/pi-astral-pocket/src/controller.ts +78 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/distiller.ts +182 -73
- package/node_modules/@nklisch/pi-astral-pocket/src/guidance.ts +12 -5
- package/node_modules/@nklisch/pi-astral-pocket/src/index.ts +172 -77
- package/node_modules/@nklisch/pi-astral-pocket/src/provider.ts +128 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/scope.ts +33 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/sessions.ts +111 -99
- package/node_modules/@nklisch/pi-astral-pocket/src/store.ts +319 -119
- package/node_modules/@nklisch/pi-astral-pocket/src/tools.ts +43 -13
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.3 — Prepared
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Bundle `@nklisch/pi-plugins` 0.8.3 and select `@nklisch/pi-mcp-adapter` 2.21.0-nklisch.3 so installed MCP calls retain structured facts in model-visible results.
|
|
8
|
+
- Keep all other bundled workspace versions unchanged from 0.4.2.
|
|
9
|
+
|
|
3
10
|
## v0.4.1 — 2026-09-04
|
|
4
11
|
|
|
5
12
|
### Added
|
|
@@ -1,62 +1,79 @@
|
|
|
1
1
|
# @nklisch/pi-astral-pocket
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
modeled on OpenAI Codex's memories system, minus the always-on cost.
|
|
6
|
-
|
|
7
|
-
## What it does
|
|
8
|
-
|
|
9
|
-
When the active model is astra (including mid-session `/model` switches), the
|
|
10
|
-
extension:
|
|
11
|
-
|
|
12
|
-
- **Injects pocket guidance** into the system prompt: when to consult the
|
|
13
|
-
pocket, a budgeted "quick pass" lookup protocol, drift/verification policy,
|
|
14
|
-
and note-taking judgment (durable facts only, never secrets).
|
|
15
|
-
- **Activates two tools**:
|
|
16
|
-
- `pocket_note` — write a durable note (title, body, keywords). The store's
|
|
17
|
-
registry and summary are updated mechanically on every write.
|
|
18
|
-
- `pocket_recall` — keyword-search pocket notes and past **astra** sessions.
|
|
19
|
-
Summarized by default (tool names + truncated args/results); `full: true`
|
|
20
|
-
for larger excerpts.
|
|
21
|
-
- **Runs a bounded distiller pass** on activation: astra sessions idle past a
|
|
22
|
-
threshold (default 6h, max 16 per pass, 30-day window) are distilled into
|
|
23
|
-
pocket notes by a cheap configurable model, then consolidated into the
|
|
24
|
-
injected summary's durable digest. There is no daemon; the pass only runs
|
|
25
|
-
when astra activates. If the distiller is disabled or its model is
|
|
26
|
-
unavailable, the mechanical floor (registry + recent-notes index) keeps
|
|
27
|
-
working.
|
|
28
|
-
|
|
29
|
-
For any other model, the extension is inert: no tools, no injection, no
|
|
30
|
-
distiller.
|
|
3
|
+
Astral Pocket keeps durable Markdown notes across Pi sessions. It activates only
|
|
4
|
+
when the current model is `openai-codex/gpt-6-astra`.
|
|
31
5
|
|
|
32
|
-
|
|
6
|
+
Notes normally belong to the current Git repository. Subdirectories and linked
|
|
7
|
+
worktrees share the same local repository identity. Explicit global notes can
|
|
8
|
+
carry a general preference or a conditional lesson between repositories.
|
|
9
|
+
Foreign repository memories are not injected into the current session.
|
|
33
10
|
|
|
34
|
-
|
|
35
|
-
- `/pocket status` — show enablement, activation, note count, distiller config.
|
|
11
|
+
## Notes and digest
|
|
36
12
|
|
|
37
|
-
|
|
13
|
+
The extension adds two tools while Astra is active:
|
|
38
14
|
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
- `pocket_note` saves a durable note. Its default scope is `project`. The caller
|
|
16
|
+
may set `scope: "global"` only for an intentionally portable preference or
|
|
17
|
+
observation.
|
|
18
|
+
- `pocket_recall` searches current-project and global notes, plus past Astra
|
|
19
|
+
sessions from the current repository. `full: true` returns larger excerpts.
|
|
20
|
+
`scope: "all"` deliberately includes other repositories and labels their
|
|
21
|
+
results as cross-repository precedent. Recall returns at most 20 matches from
|
|
22
|
+
each selected source.
|
|
41
23
|
|
|
24
|
+
Distillation means extracting durable decisions, constraints, preferences, and
|
|
25
|
+
pitfalls from an idle session into a source-linked note. Automatic extraction
|
|
26
|
+
always stays project-scoped. A digest is then rebuilt from the actual notes for
|
|
27
|
+
the current repository. Explicit global notes have a separate, smaller digest.
|
|
28
|
+
The extension never builds a new digest by recursively summarizing an old one.
|
|
29
|
+
|
|
30
|
+
Memory is historical evidence, not an instruction source. The current user
|
|
31
|
+
request and current repository guidance take priority. Verify remembered facts
|
|
32
|
+
when their relevance or freshness is uncertain.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
/pocket status
|
|
38
|
+
/pocket on
|
|
39
|
+
/pocket off
|
|
40
|
+
/pocket distiller on
|
|
41
|
+
/pocket distiller off
|
|
42
|
+
/pocket model openai-codex/gpt-6-astra
|
|
43
|
+
/pocket model reset
|
|
44
|
+
/pocket reasoning minimal
|
|
45
|
+
/pocket reasoning off|minimal|low|medium|high|xhigh|max
|
|
46
|
+
/pocket reasoning reset
|
|
47
|
+
/pocket distill
|
|
48
|
+
/pocket rebuild
|
|
42
49
|
```
|
|
43
|
-
config.json # enabled flag + distiller settings
|
|
44
|
-
SUMMARY.md # injected into astra's prompt; pinned block + durable digest + recent notes
|
|
45
|
-
POCKET.md # searchable registry, one line per note
|
|
46
|
-
notes/ # append-only note files (<timestamp>-<slug>.md)
|
|
47
|
-
distilled.json # distiller bookkeeping
|
|
48
|
-
```
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
`/pocket status` shows the requested and resolved model, requested and effective
|
|
52
|
+
reasoning, whether distillation is enabled, and the last pass outcome. Pi may
|
|
53
|
+
map a requested reasoning level to a model-supported effort; status reports that
|
|
54
|
+
mapping.
|
|
55
|
+
|
|
56
|
+
`/pocket distill` retries changed or previously failed session work and stale
|
|
57
|
+
digests. `/pocket rebuild` also forces the current project and global digest
|
|
58
|
+
caches to be regenerated. Both commands require an active Astra session and an
|
|
59
|
+
enabled distiller.
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
Changing the model or reasoning setting cancels the current pass before starting
|
|
62
|
+
a replacement. Switching away from Astra, disabling the pocket, reloading, or
|
|
63
|
+
shutting down cancels the session-owned pass.
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Configuration lives in `config.json`. All fields are optional. These are the
|
|
68
|
+
defaults:
|
|
53
69
|
|
|
54
70
|
```json
|
|
55
71
|
{
|
|
56
72
|
"enabled": true,
|
|
57
73
|
"distiller": {
|
|
58
74
|
"enabled": true,
|
|
59
|
-
"model":
|
|
75
|
+
"model": "openai-codex/gpt-6-astra",
|
|
76
|
+
"reasoning": "minimal",
|
|
60
77
|
"minIdleHours": 6,
|
|
61
78
|
"maxSessionsPerPass": 16,
|
|
62
79
|
"maxSessionAgeDays": 30
|
|
@@ -64,19 +81,58 @@ distilled.json # distiller bookkeeping
|
|
|
64
81
|
}
|
|
65
82
|
```
|
|
66
83
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
The model must use the exact `provider/modelId` form. Astral Pocket asks Pi's
|
|
85
|
+
model registry for current authentication and headers on every request. It does
|
|
86
|
+
not silently select another provider when that model is unavailable. A malformed
|
|
87
|
+
configuration falls back to defaults without blocking note access.
|
|
88
|
+
|
|
89
|
+
## Storage and recovery
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
Files are stored under `~/.pi/agent/astral-pocket/`, or under
|
|
92
|
+
`$PI_CODING_AGENT_DIR/astral-pocket/` when that variable is set:
|
|
72
93
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
```text
|
|
95
|
+
config.json settings
|
|
96
|
+
notes/ canonical Markdown notes
|
|
97
|
+
digests/ rebuildable per-project and global digest caches
|
|
98
|
+
POCKET.md rebuildable note registry
|
|
99
|
+
SUMMARY.md legacy-compatible derived summary surface
|
|
100
|
+
distilled.json processed source revisions and digest fingerprints
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Canonical note files remain readable if a model call fails or a derived registry
|
|
104
|
+
or digest lags. Changed sessions replace their one stable generated note. If a
|
|
105
|
+
fresh extraction returns `NONE`, that generated note is removed rather than
|
|
106
|
+
leaving superseded knowledge behind. Legacy notes are not rewritten: notes with
|
|
107
|
+
project metadata remain project-scoped, while notes with unknown scope appear
|
|
108
|
+
only in an explicit all-project recall.
|
|
109
|
+
|
|
110
|
+
There is no daemon or global worker. Work runs only on Astra activation or an
|
|
111
|
+
explicit command. Pi's file mutation queue serializes writes inside one process,
|
|
112
|
+
and files are atomically replaced to avoid torn output. Two simultaneous Pi
|
|
113
|
+
processes can still duplicate model calls or publish competing derived snapshots;
|
|
114
|
+
the extension does not claim cross-process locking. A later rebuild recovers the
|
|
115
|
+
registry and digest from canonical notes.
|
|
116
|
+
|
|
117
|
+
## Privacy
|
|
118
|
+
|
|
119
|
+
Session files can contain prompts, tool arguments, and tool results. Distillation
|
|
120
|
+
uses a bounded transcript that omits most tool output and instructs the model to
|
|
121
|
+
exclude credentials, personal data, quoted instructions, rejected proposals,
|
|
122
|
+
and facts already documented in the repository. A prompt cannot guarantee that
|
|
123
|
+
every sensitive value is removed.
|
|
124
|
+
|
|
125
|
+
Choosing a distiller model sends the bounded source material to that model's
|
|
126
|
+
configured provider. Astral Pocket never falls back to a different provider.
|
|
127
|
+
Use `pocket_recall` without `full` first; larger session excerpts can re-surface
|
|
128
|
+
sensitive text from past work. Use all-project recall only when you intentionally
|
|
129
|
+
want foreign repository precedent.
|
|
77
130
|
|
|
78
131
|
## Install
|
|
79
132
|
|
|
80
133
|
```sh
|
|
81
|
-
pi install npm:@nklisch/pi-
|
|
134
|
+
pi install npm:@nklisch/pi-enhanced
|
|
82
135
|
```
|
|
136
|
+
|
|
137
|
+
Astral Pocket is currently distributed as part of Pi Enhanced rather than as a
|
|
138
|
+
standalone npm package.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nklisch/pi-astral-pocket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Model-gated note pocket and session recall for gpt-6-astra",
|
|
6
6
|
"type": "module",
|
|
@@ -40,10 +40,12 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
+
"@earendil-works/pi-ai": "*",
|
|
43
44
|
"@earendil-works/pi-coding-agent": "*",
|
|
44
45
|
"typebox": "*"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": "0.82.0",
|
|
47
49
|
"@earendil-works/pi-coding-agent": "*",
|
|
48
50
|
"@types/node": "^24.0.0",
|
|
49
51
|
"typescript": ">=7.0.0 <8",
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_DISTILLER_MODEL = "openai-codex/gpt-6-astra";
|
|
7
|
+
export const DEFAULT_DISTILLER_REASONING: ModelThinkingLevel = "minimal";
|
|
8
|
+
export const REASONING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
9
|
+
|
|
4
10
|
export interface DistillerConfig {
|
|
5
|
-
/** Master switch for
|
|
11
|
+
/** Master switch for activation-time and explicit distillation. */
|
|
6
12
|
enabled: boolean;
|
|
7
|
-
/**
|
|
8
|
-
model: string
|
|
13
|
+
/** Exact provider/modelId selection. No provider fallback is performed. */
|
|
14
|
+
model: string;
|
|
15
|
+
/** Requested Pi reasoning level; the model may map it to another effective effort. */
|
|
16
|
+
reasoning: ModelThinkingLevel;
|
|
9
17
|
/** Sessions younger than this are still in progress; leave them alone. */
|
|
10
18
|
minIdleHours: number;
|
|
11
|
-
/** Bound on
|
|
19
|
+
/** Bound on extraction calls per pass. */
|
|
12
20
|
maxSessionsPerPass: number;
|
|
13
|
-
/** Sessions older than this are not
|
|
21
|
+
/** Sessions older than this are not automatically distilled. */
|
|
14
22
|
maxSessionAgeDays: number;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
export interface PocketConfig {
|
|
18
|
-
/** Master switch for the whole pocket, toggled by /pocket on|off. */
|
|
19
26
|
enabled: boolean;
|
|
20
27
|
distiller: DistillerConfig;
|
|
21
28
|
}
|
|
@@ -24,22 +31,14 @@ export const DEFAULT_CONFIG: PocketConfig = {
|
|
|
24
31
|
enabled: true,
|
|
25
32
|
distiller: {
|
|
26
33
|
enabled: true,
|
|
27
|
-
model:
|
|
34
|
+
model: DEFAULT_DISTILLER_MODEL,
|
|
35
|
+
reasoning: DEFAULT_DISTILLER_REASONING,
|
|
28
36
|
minIdleHours: 6,
|
|
29
37
|
maxSessionsPerPass: 16,
|
|
30
38
|
maxSessionAgeDays: 30,
|
|
31
39
|
},
|
|
32
40
|
};
|
|
33
41
|
|
|
34
|
-
/** Cheap-model preference order when config.distiller.model is null. The first
|
|
35
|
-
* entry resolvable in the user's model registry wins; if none resolve, the
|
|
36
|
-
* distiller degrades to "mechanical floor only" rather than failing. */
|
|
37
|
-
export const DISTILLER_MODEL_PREFERENCE: readonly string[] = [
|
|
38
|
-
"zai/glm-5.3-flash",
|
|
39
|
-
"openrouter/deepseek-v4-flash-latest",
|
|
40
|
-
"ollama-cloud/glm-5.3-flash",
|
|
41
|
-
];
|
|
42
|
-
|
|
43
42
|
export function configPath(root: string): string {
|
|
44
43
|
return join(root, "config.json");
|
|
45
44
|
}
|
|
@@ -49,9 +48,17 @@ function clampNumber(value: unknown, fallback: number, min: number, max: number)
|
|
|
49
48
|
return Math.min(max, Math.max(min, n));
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
export function isReasoningLevel(value: unknown): value is ModelThinkingLevel {
|
|
52
|
+
return typeof value === "string" && (REASONING_LEVELS as readonly string[]).includes(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isModelSpec(value: unknown): value is string {
|
|
56
|
+
if (typeof value !== "string") return false;
|
|
57
|
+
const slash = value.indexOf("/");
|
|
58
|
+
return slash > 0 && slash < value.length - 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A malformed configuration degrades to defaults so note access remains usable. */
|
|
55
62
|
export function loadConfig(root: string): PocketConfig {
|
|
56
63
|
let raw: Record<string, unknown> = {};
|
|
57
64
|
try {
|
|
@@ -59,12 +66,16 @@ export function loadConfig(root: string): PocketConfig {
|
|
|
59
66
|
} catch {
|
|
60
67
|
return structuredClone(DEFAULT_CONFIG);
|
|
61
68
|
}
|
|
62
|
-
const d =
|
|
69
|
+
const d = typeof raw.distiller === "object" && raw.distiller !== null
|
|
70
|
+
? raw.distiller as Record<string, unknown>
|
|
71
|
+
: {};
|
|
63
72
|
return {
|
|
64
73
|
enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_CONFIG.enabled,
|
|
65
74
|
distiller: {
|
|
66
75
|
enabled: typeof d.enabled === "boolean" ? d.enabled : DEFAULT_CONFIG.distiller.enabled,
|
|
67
|
-
|
|
76
|
+
// Legacy null meant "choose a cheap fallback". It now resets to the explicit Astra default.
|
|
77
|
+
model: isModelSpec(d.model) ? d.model : DEFAULT_DISTILLER_MODEL,
|
|
78
|
+
reasoning: isReasoningLevel(d.reasoning) ? d.reasoning : DEFAULT_DISTILLER_REASONING,
|
|
68
79
|
minIdleHours: clampNumber(d.minIdleHours, DEFAULT_CONFIG.distiller.minIdleHours, 1, 48),
|
|
69
80
|
maxSessionsPerPass: clampNumber(d.maxSessionsPerPass, DEFAULT_CONFIG.distiller.maxSessionsPerPass, 1, 128),
|
|
70
81
|
maxSessionAgeDays: clampNumber(d.maxSessionAgeDays, DEFAULT_CONFIG.distiller.maxSessionAgeDays, 0, 90),
|
|
@@ -72,7 +83,13 @@ export function loadConfig(root: string): PocketConfig {
|
|
|
72
83
|
};
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
function atomicWrite(path: string, contents: string): void {
|
|
87
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
88
|
+
writeFileSync(temporary, contents, "utf8");
|
|
89
|
+
renameSync(temporary, path);
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
export function saveConfig(root: string, config: PocketConfig): void {
|
|
76
93
|
mkdirSync(root, { recursive: true });
|
|
77
|
-
|
|
94
|
+
atomicWrite(configPath(root), `${JSON.stringify(config, null, 2)}\n`);
|
|
78
95
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { DistillerResult } from "./distiller.js";
|
|
2
|
+
|
|
3
|
+
export type PassOutcome =
|
|
4
|
+
| { state: "idle" }
|
|
5
|
+
| { state: "running"; startedAt: string }
|
|
6
|
+
| { state: "completed"; finishedAt: string; result: DistillerResult }
|
|
7
|
+
| { state: "failed"; finishedAt: string; error: string }
|
|
8
|
+
| { state: "cancelled"; finishedAt: string };
|
|
9
|
+
|
|
10
|
+
export type PassReporter = (message: string, level: "info" | "warning" | "error") => void;
|
|
11
|
+
|
|
12
|
+
/** Own one serialized, revocable background pass for this extension instance. */
|
|
13
|
+
export class DistillerController {
|
|
14
|
+
private generation = 0;
|
|
15
|
+
private controller: AbortController | undefined;
|
|
16
|
+
private queue: Promise<void> = Promise.resolve();
|
|
17
|
+
private reporter: PassReporter | undefined;
|
|
18
|
+
private outcome: PassOutcome = { state: "idle" };
|
|
19
|
+
|
|
20
|
+
status(): PassOutcome {
|
|
21
|
+
return this.outcome;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stop(): void {
|
|
25
|
+
this.generation += 1;
|
|
26
|
+
this.reporter = undefined;
|
|
27
|
+
this.controller?.abort();
|
|
28
|
+
this.controller = undefined;
|
|
29
|
+
if (this.outcome.state === "running") {
|
|
30
|
+
this.outcome = { state: "cancelled", finishedAt: new Date().toISOString() };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
start(
|
|
35
|
+
task: (signal: AbortSignal) => Promise<DistillerResult>,
|
|
36
|
+
reporter?: PassReporter,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
this.stop();
|
|
39
|
+
const generation = this.generation;
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
this.controller = controller;
|
|
42
|
+
this.reporter = reporter;
|
|
43
|
+
const run = async () => {
|
|
44
|
+
if (controller.signal.aborted || generation !== this.generation) return;
|
|
45
|
+
this.outcome = { state: "running", startedAt: new Date().toISOString() };
|
|
46
|
+
try {
|
|
47
|
+
const result = await task(controller.signal);
|
|
48
|
+
if (controller.signal.aborted || generation !== this.generation) return;
|
|
49
|
+
this.outcome = { state: "completed", finishedAt: new Date().toISOString(), result };
|
|
50
|
+
if (result.errors.length > 0) {
|
|
51
|
+
this.report(generation, `Astral Pocket distillation finished with ${result.errors.length} error(s); source notes remain available.`, "warning");
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (controller.signal.aborted || generation !== this.generation) return;
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
this.outcome = { state: "failed", finishedAt: new Date().toISOString(), error: message };
|
|
57
|
+
this.report(generation, `Astral Pocket distillation failed: ${message}. Source notes remain available.`, "warning");
|
|
58
|
+
} finally {
|
|
59
|
+
if (generation === this.generation) {
|
|
60
|
+
this.controller = undefined;
|
|
61
|
+
this.reporter = undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
this.queue = this.queue.catch(() => undefined).then(run);
|
|
66
|
+
return this.queue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private report(generation: number, message: string, level: "info" | "warning" | "error"): void {
|
|
70
|
+
if (generation !== this.generation || !this.reporter) return;
|
|
71
|
+
try {
|
|
72
|
+
this.reporter(message, level);
|
|
73
|
+
} catch {
|
|
74
|
+
// Session replacement can revoke a UI sink between the generation check
|
|
75
|
+
// and notification. Reporting failure must never escape this boundary.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|