@signalridge/pi-welcome 1.3.7 → 1.3.9
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 +12 -0
- package/README.md +32 -16
- package/package.json +3 -3
- package/src/index.ts +93 -91
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.9
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 4d37546: Point package documentation to the maintained compatibility guide instead of removed historical review snapshots.
|
|
7
|
+
|
|
8
|
+
## 1.3.8
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 3364cc0: Include credential resolution in the Codex compaction deadline and promptly stop waiting on cancellation without interrupting host-owned token persistence. Preserve configured-package inventory for default-autoload exclusion-only resource filters.
|
|
12
|
+
- 3364cc0: Align guidance deduplication and setup profiles with host context selection. Render live welcome inventory after resource binding, restore cards from active branch facts after compaction, and use trust-resolved package and compaction settings. Keep Herdr idle during cache warming, validate blocked notifications, and allocate polling only for active TUI sessions.
|
|
13
|
+
- 3364cc0: Adopt Pi 1.0.4's host-provided dependency contract: declare imported Pi and TypeBox modules as `*` peers rather than installing duplicate runtime modules, and align Pi development pins with the repository's tested 1.0.4 baseline. The wildcard identifies host module ownership; it does not promise compatibility with every Pi version.
|
|
14
|
+
|
|
3
15
|
## 1.3.7
|
|
4
16
|
### Patch Changes
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# pi-welcome
|
|
2
2
|
|
|
3
3
|
TUI-only Signalridge startup card: one rounded panel with the current
|
|
4
|
-
repository, model, context budget, key hints, and
|
|
5
|
-
uses the active Pi theme, persists a bounded custom entry
|
|
6
|
-
|
|
4
|
+
repository, model, context budget, key hints, and a live resource inventory. It
|
|
5
|
+
uses the active Pi theme, persists workspace facts in a bounded custom entry,
|
|
6
|
+
and collapses to a width-safe compact summary on narrow terminals. Resume keeps
|
|
7
|
+
the original workspace facts but refreshes inventory when creating the renderer.
|
|
8
|
+
A card summarized by compaction is restored from the active branch, never from
|
|
9
|
+
an abandoned branch.
|
|
7
10
|
|
|
8
11
|
```
|
|
9
12
|
╭─────────────────────────────────────────────────────────────────────────╮
|
|
@@ -15,24 +18,24 @@ redraws it, and collapses to a width-safe compact summary on narrow terminals.
|
|
|
15
18
|
│ Session: (new) │
|
|
16
19
|
│ Model: openai-codex / gpt-5.6-luna · thinking max │
|
|
17
20
|
│ Budget: 400K · compacts at 270K │
|
|
18
|
-
│
|
|
21
|
+
│ pi v1.0.4 │
|
|
19
22
|
│ │
|
|
23
|
+
│ Inventory: live at render │
|
|
20
24
|
│ Context: AGENTS.md │
|
|
21
25
|
│ Skills: commit, release, review +12 │
|
|
22
26
|
│ Prompts: /plan, /ship +4 │
|
|
23
|
-
│
|
|
24
|
-
│
|
|
27
|
+
│ Configured packages: btw, goal, statusline +18 │
|
|
28
|
+
│ Available themes: dark, light, system +2 │
|
|
25
29
|
│ Tools: 18 active of 42 │
|
|
26
30
|
│ │
|
|
27
31
|
╰─────────────────────────────────────────────────────────────────────────╯
|
|
28
32
|
```
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
brand line says nothing a returning user does not already know.
|
|
34
|
+
The card leads with a compact Pi mark and the running host version.
|
|
32
35
|
|
|
33
36
|
## Install
|
|
34
37
|
|
|
35
|
-
The
|
|
38
|
+
The current tested host is Pi `1.0.4`. Host-provided dependencies use `"*"` peers to identify module ownership, not to guarantee compatibility with every Pi version. See the [compatibility guide](https://github.com/signalridge/pi-extensions/blob/main/docs/compatibility.md) for verification details.
|
|
36
39
|
|
|
37
40
|
```bash
|
|
38
41
|
pi install npm:@signalridge/pi-welcome
|
|
@@ -76,13 +79,26 @@ Skills and prompts come from `getCommands()`, **not** from `loadSkills()`. An
|
|
|
76
79
|
extension may contribute skill paths through `resources_discover`, which is
|
|
77
80
|
where most of them come from in practice, and the standalone loader cannot see
|
|
78
81
|
those: on a session showing dozens of skills it returns zero. `getCommands()`
|
|
79
|
-
reports what Pi actually registered.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
reports what Pi actually registered. Collection happens at renderer component
|
|
83
|
+
creation, after `bindExtensions()` finishes `resources_discover`, not during
|
|
84
|
+
`session_start` before discovery. The inventory is labeled **live at render**;
|
|
85
|
+
it is not a persisted startup snapshot.
|
|
86
|
+
|
|
87
|
+
**Configured packages** comes from `pi.getSettings()`, which already resolves
|
|
88
|
+
project trust and merges configuration. Packages filtered to no resources are
|
|
89
|
+
omitted; skills-only packages may appear. This is configuration, not evidence
|
|
90
|
+
that a package's extensions loaded, and does not enumerate standalone extension
|
|
91
|
+
files. **Available themes** comes from `ctx.ui.getAllThemes()` in the TUI,
|
|
92
|
+
including built-in and discovered themes rather than only a user themes folder.
|
|
93
|
+
Context filenames come from `loadProjectContextFiles`, and tool counts from
|
|
94
|
+
`getAllTools()` / `getActiveTools()`. Nothing here constructs or reloads a second
|
|
95
|
+
resource loader: its `reload()` re-executes every extension factory and would
|
|
96
|
+
duplicate tools, listeners, timers, and child processes.
|
|
97
|
+
|
|
98
|
+
The persisted Budget row uses effective trusted settings, resolving the exact
|
|
99
|
+
`provider/modelId` compaction override, ordinary reserve, then Pi's default
|
|
100
|
+
reserve. Zero is valid; invalid settings do not produce a guessed threshold.
|
|
101
|
+
When automatic compaction is disabled, there is no **compacts at** suffix.
|
|
86
102
|
|
|
87
103
|
## Untrusted text
|
|
88
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalridge/pi-welcome",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
4
4
|
"description": "Startup resource and repository summary card for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"lifecycle": "stable"
|
|
10
10
|
},
|
|
11
11
|
"peerDependencies": {
|
|
12
|
-
"@earendil-works/pi-coding-agent": "
|
|
13
|
-
"@earendil-works/pi-tui": "
|
|
12
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
13
|
+
"@earendil-works/pi-tui": "*"
|
|
14
14
|
},
|
|
15
15
|
"pi": {
|
|
16
16
|
"extensions": [
|
package/src/index.ts
CHANGED
|
@@ -9,20 +9,19 @@
|
|
|
9
9
|
* header, a key-hint block, and one `[Section]` per resource kind with a blank
|
|
10
10
|
* line between each; stacking this card underneath produced two unrelated
|
|
11
11
|
* designs and about forty-five lines of them. So the card carries the pieces
|
|
12
|
-
* worth keeping — the key hints and a
|
|
12
|
+
* worth keeping — the key hints and a live resource inventory — and `quietStartup`
|
|
13
13
|
* is turned on in settings to silence the rest. With `quietStartup: false` Pi
|
|
14
14
|
* still prints its own block and the two appear together again.
|
|
15
15
|
*
|
|
16
16
|
* The card is a custom entry, not a widget: it lands in the transcript, scrolls
|
|
17
|
-
* away as the conversation grows, and survives a reload.
|
|
18
|
-
* session_start and stored in the entry
|
|
19
|
-
*
|
|
17
|
+
* away as the conversation grows, and survives a reload. Workspace facts are
|
|
18
|
+
* captured at session_start and stored in the entry; inventory is collected at
|
|
19
|
+
* component creation, after Pi binds resources, including on resume.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
24
23
|
import { homedir } from "node:os";
|
|
25
|
-
import { basename
|
|
24
|
+
import { basename } from "node:path";
|
|
26
25
|
|
|
27
26
|
import {
|
|
28
27
|
type ExtensionAPI,
|
|
@@ -30,6 +29,7 @@ import {
|
|
|
30
29
|
getAgentDir,
|
|
31
30
|
keyText,
|
|
32
31
|
loadProjectContextFiles,
|
|
32
|
+
SettingsManager,
|
|
33
33
|
type Theme,
|
|
34
34
|
VERSION,
|
|
35
35
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -124,17 +124,17 @@ function gitSummary(cwd: string): string | undefined {
|
|
|
124
124
|
return added || removed ? `${branch} [+${added} -${removed}]` : branch;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
* settings, so this reads the same file pi does — the trigger point is worth the
|
|
130
|
-
* one stat() because it, not the raw window, is what a long session runs into.
|
|
131
|
-
*/
|
|
132
|
-
function readReserveTokens(): number | undefined {
|
|
127
|
+
/** Resolve trusted, merged settings with Pi's own defaults and model validation. */
|
|
128
|
+
function compactionTrigger(pi: ExtensionAPI, ctx: ExtensionContext): number | undefined {
|
|
133
129
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
const settings = pi.getSettings?.();
|
|
131
|
+
if (!settings || !ctx.model) return undefined;
|
|
132
|
+
const compaction = SettingsManager.inMemory(settings).getCompactionSettings(ctx.model);
|
|
133
|
+
const trigger = ctx.model.contextWindow - compaction.reserveTokens;
|
|
134
|
+
return compaction.enabled && trigger > 0 ? trigger : undefined;
|
|
137
135
|
} catch {
|
|
136
|
+
// An invalid configuration (or older host without settings) is not a known
|
|
137
|
+
// automatic trigger. Do not invent a budget from raw files or invalid values.
|
|
138
138
|
return undefined;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -199,52 +199,33 @@ export function collectHints(): string[] {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
* Read from the same two settings files Pi reads, plus the extension
|
|
206
|
-
* directories it scans. Pi labels a package-sourced extension by its package
|
|
207
|
-
* name and a file-sourced one by its path, so the scope and the `pi-` prefix
|
|
208
|
-
* come off to leave the part that identifies it.
|
|
209
|
-
*/
|
|
210
|
-
function collectExtensionNames(cwd: string): string[] {
|
|
211
|
-
const names: string[] = [];
|
|
212
|
-
for (const settingsFile of [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")]) {
|
|
213
|
-
try {
|
|
214
|
-
const packages = JSON.parse(readFileSync(settingsFile, "utf8"))?.packages;
|
|
215
|
-
if (!Array.isArray(packages)) continue;
|
|
216
|
-
for (const entry of packages) {
|
|
217
|
-
const source = typeof entry === "string" ? entry : entry?.source;
|
|
218
|
-
if (typeof source !== "string") continue;
|
|
219
|
-
const spec = source.replace(/^npm:/u, "");
|
|
220
|
-
// `@scope/pi-name@1.2.3` and `./local/path` both reduce to a bare name.
|
|
221
|
-
const bare = (spec.startsWith("@") ? (spec.split("/")[1] ?? spec) : basename(spec)).replace(/@.*$/u, "");
|
|
222
|
-
names.push(bare.replace(/^pi-/u, ""));
|
|
223
|
-
}
|
|
224
|
-
} catch {
|
|
225
|
-
// Discovery is best-effort: a malformed manifest contributes no names
|
|
226
|
-
// rather than blanking the whole card.
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
for (const directory of [join(getAgentDir(), "extensions"), join(cwd, ".pi", "extensions")]) {
|
|
230
|
-
try {
|
|
231
|
-
for (const entry of readdirSync(directory)) {
|
|
232
|
-
names.push(entry.replace(/\.[cm]?[jt]s$/u, "").replace(/^pi-/u, ""));
|
|
233
|
-
}
|
|
234
|
-
} catch {
|
|
235
|
-
// An extensions directory that does not exist is the normal case.
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return tidy(names);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Custom theme names, as Pi's own `[Themes]` section lists them. */
|
|
242
|
-
function collectThemeNames(): string[] {
|
|
202
|
+
/** Configured packages are not proof of loaded extensions (skills-only is valid). */
|
|
203
|
+
function collectPackageNames(pi: ExtensionAPI): string[] {
|
|
243
204
|
try {
|
|
244
205
|
return tidy(
|
|
245
|
-
|
|
246
|
-
.filter((entry) =>
|
|
247
|
-
|
|
206
|
+
(pi.getSettings?.().packages ?? [])
|
|
207
|
+
.filter((entry) => {
|
|
208
|
+
if (typeof entry === "string") return true;
|
|
209
|
+
// Respect packages explicitly filtered to no resources. A skills-only
|
|
210
|
+
// package still belongs here, even with extensions: [].
|
|
211
|
+
return (["extensions", "skills", "prompts", "themes"] as const).some((kind) => {
|
|
212
|
+
const patterns = entry[kind];
|
|
213
|
+
if (patterns === undefined) return entry.autoload !== false;
|
|
214
|
+
if (patterns.length === 0) return false;
|
|
215
|
+
// Default autoload starts with all resources when filters contain
|
|
216
|
+
// only exclusions; autoload:false starts empty and needs an include.
|
|
217
|
+
return (
|
|
218
|
+
entry.autoload !== false ||
|
|
219
|
+
patterns.some((pattern) => !pattern.startsWith("!") && !pattern.startsWith("-"))
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
})
|
|
223
|
+
.map((entry) => {
|
|
224
|
+
const source = typeof entry === "string" ? entry : entry.source;
|
|
225
|
+
const spec = source.replace(/^npm:/u, "");
|
|
226
|
+
const bare = (spec.startsWith("@") ? (spec.split("/")[1] ?? spec) : basename(spec)).replace(/@.*$/u, "");
|
|
227
|
+
return bare.replace(/^pi-/u, "");
|
|
228
|
+
}),
|
|
248
229
|
);
|
|
249
230
|
} catch {
|
|
250
231
|
return [];
|
|
@@ -252,8 +233,8 @@ function collectThemeNames(): string[] {
|
|
|
252
233
|
}
|
|
253
234
|
|
|
254
235
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
236
|
+
* Live registered skills/prompts/tools, available themes, selected context
|
|
237
|
+
* filenames, and configured packages. Not a claim that every package loaded.
|
|
257
238
|
*
|
|
258
239
|
* With `quietStartup` on those sections are gone and nothing brings them back
|
|
259
240
|
* (`/reload` re-runs the same suppressed listing), so this card is the only
|
|
@@ -265,7 +246,7 @@ function collectThemeNames(): string[] {
|
|
|
265
246
|
* — it reports zero on a session showing dozens. `getCommands()` reports what
|
|
266
247
|
* Pi actually registered, which is the honest answer to "what loaded".
|
|
267
248
|
*/
|
|
268
|
-
export function collectResources(pi: ExtensionAPI, cwd: string): [string, string][] {
|
|
249
|
+
export function collectResources(pi: ExtensionAPI, cwd: string, ui?: ExtensionContext["ui"]): [string, string][] {
|
|
269
250
|
const rows: [string, string][] = [];
|
|
270
251
|
const push = (label: string, value: string | undefined) => {
|
|
271
252
|
if (value) rows.push([label, value]);
|
|
@@ -283,15 +264,23 @@ export function collectResources(pi: ExtensionAPI, cwd: string): [string, string
|
|
|
283
264
|
try {
|
|
284
265
|
const commands = pi.getCommands?.() ?? [];
|
|
285
266
|
const named = (source: string, prefix = "") =>
|
|
286
|
-
tidy(
|
|
267
|
+
tidy(
|
|
268
|
+
commands
|
|
269
|
+
.filter((command) => command.source === source)
|
|
270
|
+
.map((command) => `${prefix}${source === "skill" ? command.name.replace(/^skill:/u, "") : command.name}`),
|
|
271
|
+
);
|
|
287
272
|
push("Skills", nameList(named("skill")));
|
|
288
273
|
push("Prompts", nameList(named("prompt", "/")));
|
|
289
274
|
} catch {
|
|
290
275
|
// A host without these accessors simply contributes no rows.
|
|
291
276
|
}
|
|
292
277
|
|
|
293
|
-
push("
|
|
294
|
-
|
|
278
|
+
push("Configured packages", nameList(collectPackageNames(pi)));
|
|
279
|
+
try {
|
|
280
|
+
push("Available themes", nameList(tidy((ui?.getAllThemes() ?? []).map((theme) => theme.name))));
|
|
281
|
+
} catch {
|
|
282
|
+
// Themes are a TUI surface; an older host may not provide it.
|
|
283
|
+
}
|
|
295
284
|
|
|
296
285
|
try {
|
|
297
286
|
const all = pi.getAllTools?.() ?? [];
|
|
@@ -336,20 +325,12 @@ function collect(pi: ExtensionAPI, ctx: ExtensionContext): WelcomeData {
|
|
|
336
325
|
if (typeof window === "number" && window > 0) {
|
|
337
326
|
// Pi compacts at contextWindow - reserveTokens; surfacing the trigger up
|
|
338
327
|
// front is the one number that actually governs a long session.
|
|
339
|
-
const
|
|
340
|
-
const trigger =
|
|
341
|
-
typeof reserve === "number" && reserve > 0 && reserve < window
|
|
342
|
-
? ` · compacts at ${formatTokens(window - reserve)}`
|
|
343
|
-
: "";
|
|
328
|
+
const threshold = compactionTrigger(pi, ctx);
|
|
329
|
+
const trigger = threshold !== undefined ? ` · compacts at ${formatTokens(threshold)}` : "";
|
|
344
330
|
push("Budget", `${formatTokens(window)}${trigger}`);
|
|
345
331
|
}
|
|
346
332
|
}
|
|
347
333
|
|
|
348
|
-
// GAP is the sentinel for a blank row: session facts above, what pi loaded
|
|
349
|
-
// below, so the card reads as two groups rather than one wall.
|
|
350
|
-
const resources = collectResources(pi, ctx.cwd);
|
|
351
|
-
if (resources.length > 0) rows.push(GAP, ...resources);
|
|
352
|
-
|
|
353
334
|
const hints = collectHints();
|
|
354
335
|
return hints.length > 0 ? { rows, hints } : { rows };
|
|
355
336
|
}
|
|
@@ -494,31 +475,52 @@ export class WelcomeCard implements Component {
|
|
|
494
475
|
|
|
495
476
|
// ─── Extension Entry ───────────────────────────────────────────────────────────
|
|
496
477
|
|
|
478
|
+
const INVENTORY_LABELS = new Set([
|
|
479
|
+
"Inventory",
|
|
480
|
+
"Context",
|
|
481
|
+
"Skills",
|
|
482
|
+
"Prompts",
|
|
483
|
+
"Extensions",
|
|
484
|
+
"Themes",
|
|
485
|
+
"Tools",
|
|
486
|
+
"Configured packages",
|
|
487
|
+
"Available themes",
|
|
488
|
+
]);
|
|
489
|
+
|
|
490
|
+
function withLiveInventory(data: WelcomeData, pi: ExtensionAPI, ctx: ExtensionContext): WelcomeData {
|
|
491
|
+
// Remove legacy persisted inventory too, while preserving original workspace
|
|
492
|
+
// facts on resume. Do not rewrite the stored entry during rendering.
|
|
493
|
+
const rows = data.rows.filter(([label]) => label !== "" && !INVENTORY_LABELS.has(label));
|
|
494
|
+
const resources = collectResources(pi, ctx.cwd, ctx.ui);
|
|
495
|
+
if (resources.length > 0) rows.push(GAP, ["Inventory", "live at render"], ...resources);
|
|
496
|
+
return { ...data, rows };
|
|
497
|
+
}
|
|
498
|
+
|
|
497
499
|
export default function (pi: ExtensionAPI) {
|
|
500
|
+
let currentContext: ExtensionContext | undefined;
|
|
498
501
|
pi.registerEntryRenderer<WelcomeData>(ENTRY_TYPE, (entry, _options, theme) => {
|
|
499
502
|
const data = entry.data;
|
|
500
503
|
if (!data?.rows?.length) return undefined;
|
|
501
|
-
return new WelcomeCard(data, theme);
|
|
504
|
+
return new WelcomeCard(currentContext ? withLiveInventory(data, pi, currentContext) : data, theme);
|
|
502
505
|
});
|
|
503
506
|
|
|
504
507
|
pi.on("session_start", async (_event, ctx) => {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// A resumed session already carries its card; appending on every
|
|
508
|
-
// session_start would stack a fresh one on top at each reload.
|
|
509
|
-
try {
|
|
510
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
511
|
-
if (entry.type === "custom" && entry.customType === ENTRY_TYPE) {
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
} catch {
|
|
516
|
-
// Unreadable entries mean we cannot prove the card was already written.
|
|
517
|
-
// Falling through re-appends it, which beats never showing it at all.
|
|
518
|
-
}
|
|
508
|
+
currentContext = ctx.mode === "tui" ? ctx : undefined;
|
|
509
|
+
if (!currentContext) return;
|
|
519
510
|
|
|
520
511
|
try {
|
|
521
|
-
|
|
512
|
+
// The transcript uses the active, compaction-aware projection, not every
|
|
513
|
+
// entry in the file. An off-branch or summarized card is not visible.
|
|
514
|
+
const visible = ctx.sessionManager.buildContextEntries();
|
|
515
|
+
if (visible.some((entry) => entry.type === "custom" && entry.customType === ENTRY_TYPE)) return;
|
|
516
|
+
|
|
517
|
+
// Restore facts from this branch if compaction summarized its card. Never
|
|
518
|
+
// borrow facts from an abandoned sibling branch.
|
|
519
|
+
const original = ctx.sessionManager
|
|
520
|
+
.getBranch()
|
|
521
|
+
.find((entry) => entry.type === "custom" && entry.customType === ENTRY_TYPE);
|
|
522
|
+
const data = original?.type === "custom" ? (original.data as WelcomeData | undefined) : undefined;
|
|
523
|
+
pi.appendEntry<WelcomeData>(ENTRY_TYPE, data?.rows?.length ? data : collect(pi, ctx));
|
|
522
524
|
} catch {
|
|
523
525
|
// A missing card is not worth failing a session start over.
|
|
524
526
|
}
|