pi-famulus 0.1.8 → 0.1.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/bin/pi-famulus.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { nativePackageName, resolveNativeManagerPath } from "../src/native-manager.js";
3
+ import { ensureNativeManagerInstalled, nativePackageName } from "../src/native-manager.js";
4
4
 
5
- const binary = resolveNativeManagerPath();
5
+ const binary = ensureNativeManagerInstalled();
6
6
  if (!binary) {
7
7
  const name = nativePackageName(process.platform, process.arch);
8
8
  console.error(name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-famulus",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Keep your main agent's context clean: subagents on cheaper models, long bash in the background, and monitors that wake the agent. One Rust daemon runs the shells.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -54,12 +54,12 @@
54
54
  "pi-famulus": "./bin/pi-famulus.js"
55
55
  },
56
56
  "optionalDependencies": {
57
- "pi-famulus-linux-x64": "0.1.8",
58
- "pi-famulus-linux-arm64": "0.1.8",
59
- "pi-famulus-darwin-x64": "0.1.8",
60
- "pi-famulus-darwin-arm64": "0.1.8",
61
- "pi-famulus-win-x64": "0.1.8",
62
- "pi-famulus-win-arm64": "0.1.8"
57
+ "pi-famulus-linux-x64": "0.1.9",
58
+ "pi-famulus-linux-arm64": "0.1.9",
59
+ "pi-famulus-darwin-x64": "0.1.9",
60
+ "pi-famulus-darwin-arm64": "0.1.9",
61
+ "pi-famulus-win-x64": "0.1.9",
62
+ "pi-famulus-win-arm64": "0.1.9"
63
63
  },
64
64
  "pi": {
65
65
  "extensions": [
package/src/config.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { accessSync, constants, readFileSync, statSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { delimiter, join, win32 } from "node:path";
10
- import { nativePackageName, resolveNativeManagerPath } from "./native-manager.js";
10
+ import { describeNativeLookup, resolveNativeManagerPath } from "./native-manager.js";
11
11
 
12
12
  export interface FamulusConfig {
13
13
  /** Foreground budget for bash before auto-backgrounding (ms). */
@@ -290,11 +290,7 @@ export function describeManagerSearch(
290
290
  const tried: string[] = [];
291
291
  if (config.managerPath) tried.push(`config managerPath ${describeCandidate(config.managerPath)}`);
292
292
  if (env.PI_FAMULUS_MANAGER_PATH) tried.push(`PI_FAMULUS_MANAGER_PATH ${describeCandidate(env.PI_FAMULUS_MANAGER_PATH)}`);
293
- const nativeName = nativePackageName(process.platform, process.arch);
294
- const native = resolveNativeManagerPath();
295
- tried.push(nativeName
296
- ? `npm ${nativeName}${native ? ` ${native}` : " (missing or unusable, ignored)"}`
297
- : `npm native manager (unsupported ${process.platform}/${process.arch})`);
293
+ tried.push(describeNativeLookup());
298
294
  tried.push(describeCandidate(join(home, "bin", MANAGER_FILE_NAME)));
299
295
  tried.push("pi-famulus on PATH");
300
296
  return tried.join("; ");
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ import { createRegistryCommsHost } from "./comms/registry-host";
24
24
  import { createAgentMessageTool, createContactSupervisorTool } from "./comms/tools";
25
25
  import { registerReplyCommand } from "./comms/reply-command";
26
26
  import { describeManagerSearch, getFamulusHome, loadConfig, resolveManagerPath, resolveSubagentConfig } from "./config";
27
+ import { ensureNativeManagerInstalled } from "./native-manager.js";
27
28
  import type { TaskExitInfo } from "./format";
28
29
  import { EXTENSION_VERSION, ManagerClient, type ManagerEvent, type TaskRecord } from "./manager-client";
29
30
  import { createMonitorTool, exitEventFromRecord, MonitorRegistry } from "./monitor";
@@ -99,7 +100,28 @@ function createFamulusRuntime(retiring?: Promise<unknown>): FamulusRuntime {
99
100
  registerBehaviorGuidelines(pi);
100
101
  const home = getFamulusHome();
101
102
  const config = loadConfig(home);
102
- const managerPath = resolveManagerPath(config, home);
103
+ // pi update reinstalls extensions with npm. omit=optional drops the platform
104
+ // package during that install. Fetch it in the background so a slow or
105
+ // unreachable registry cannot stall extension startup.
106
+ let managerPath = resolveManagerPath(config, home);
107
+ let famulusInfo: {
108
+ extensionVersion: string;
109
+ managerPath: string | null;
110
+ describeSearch: () => string;
111
+ getClient: () => ManagerClient | null;
112
+ } | null = null;
113
+ const nativeRestore: Promise<string | null> = managerPath
114
+ ? Promise.resolve(managerPath)
115
+ : Promise.resolve()
116
+ .then(() => ensureNativeManagerInstalled())
117
+ .then((path) => {
118
+ if (path) {
119
+ managerPath = path;
120
+ if (famulusInfo) famulusInfo.managerPath = path;
121
+ }
122
+ return path;
123
+ })
124
+ .catch(() => null);
103
125
  const clock = realClock;
104
126
 
105
127
  // Claude-style transcript pills for notifications (TUI only; no-ops elsewhere).
@@ -392,12 +414,13 @@ function createFamulusRuntime(retiring?: Promise<unknown>): FamulusRuntime {
392
414
  clock,
393
415
  syncWithManager: deps.syncWithManager,
394
416
  });
395
- registerFamulusCommand(pi, {
417
+ famulusInfo = {
396
418
  extensionVersion: EXTENSION_VERSION,
397
419
  managerPath,
398
420
  describeSearch: () => describeManagerSearch(config, home),
399
421
  getClient: () => client,
400
- });
422
+ };
423
+ registerFamulusCommand(pi, famulusInfo);
401
424
  monitorRegistry.onChange(() => {
402
425
  for (const mon of monitorRegistry.listActive()) {
403
426
  const existing = workIndex.get(mon.taskId);
@@ -819,11 +842,18 @@ function createFamulusRuntime(retiring?: Promise<unknown>): FamulusRuntime {
819
842
  startFleetWidget(startCtx);
820
843
 
821
844
  // Connect in the background: a cold manager spawn must not stall the
822
- // first prompt. Tools call ensureAvailable() lazily before use.
845
+ // first prompt. The cache install, if one is running, finishes first so a
846
+ // missing optional package is not reported before it can be restored.
847
+ // Tools call ensureAvailable() lazily before use.
823
848
  const c = client;
824
- void c
825
- .connect()
849
+ void nativeRestore
850
+ .then((path) => {
851
+ if (client !== c || !c) return null;
852
+ if (path) c.useManagerPath(path);
853
+ return c.connect();
854
+ })
826
855
  .then((ok) => {
856
+ if (ok === null || client !== c || !c) return;
827
857
  if (ok) {
828
858
  if (c.protocolLevel() >= 4) void reconcileAgentLeases(c).catch(() => {});
829
859
  return;
@@ -18,7 +18,7 @@ import { famulusPaths } from "./config";
18
18
  import { realClock, type Clock, type ClockTimer } from "./clock";
19
19
 
20
20
  const MAX_FRAME_BYTES = 4 * 1024 * 1024; // 4 MiB (§3.3)
21
- const EXTENSION_VERSION = "0.1.8";
21
+ const EXTENSION_VERSION = "0.1.9";
22
22
  export { EXTENSION_VERSION };
23
23
  // 5: speaks per-kind agent admission and queued acquire.
24
24
  const PROTOCOL = 5;
@@ -285,7 +285,7 @@ function delay(clock: Clock, ms: number): Promise<void> {
285
285
  export class ManagerClient {
286
286
  private readonly home: string;
287
287
  private readonly sessionId: string;
288
- private readonly managerPath: string | null;
288
+ private managerPath: string | null;
289
289
  private readonly piPid: number;
290
290
  private readonly cwd: string | undefined;
291
291
  private readonly log: (message: string) => void;
@@ -327,6 +327,11 @@ export class ManagerClient {
327
327
  return this.state === "connected";
328
328
  }
329
329
 
330
+ /** A binary found after construction. The first path wins. */
331
+ useManagerPath(path: string): void {
332
+ if (!this.managerPath) this.managerPath = path;
333
+ }
334
+
330
335
  /** Maximum protocol level reported by the connected manager; 0 means unknown/legacy. */
331
336
  protocolLevel(): number {
332
337
  return this.protocolLevel_;
@@ -3,7 +3,37 @@ export interface NativeManagerOptions {
3
3
  arch?: string;
4
4
  /** Node package resolver; tests can use createRequire from an isolated consumer. */
5
5
  resolve?: (specifier: string) => string;
6
+ /** Walk node_modules from this file instead of this module. */
7
+ from?: string;
8
+ /** Do not walk above this directory. */
9
+ stopAt?: string;
10
+ /** Versioned platform cache. `false` disables the cache lookup. */
11
+ cacheDir?: string | false;
12
+ }
13
+ export interface NativeCacheOptions {
14
+ version?: string;
15
+ platform?: string;
16
+ arch?: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ home?: string;
19
+ }
20
+ export interface EnsureNativeOptions extends NativeManagerOptions {
21
+ env?: NodeJS.ProcessEnv;
22
+ npmCommand?: string;
23
+ spawn?: (
24
+ command: string,
25
+ args: string[],
26
+ options?: { timeout?: number; windowsHide?: boolean; encoding?: "utf8" },
27
+ ) => { status: number | null; error?: Error };
6
28
  }
7
29
  export function nativePackageName(platform: string, arch: string): string | null;
30
+ export function nativeCacheDir(options?: NativeCacheOptions): string;
8
31
  /** Null when unsupported, missing, corrupt, non-executable, or a different version. */
9
32
  export function resolveNativeManagerPath(options?: NativeManagerOptions): string | null;
33
+ /** `npm <package> <path>` or why the exact platform package was not usable. */
34
+ export function describeNativeLookup(options?: NativeManagerOptions): string;
35
+ /**
36
+ * Install the exact platform package into the versioned cache when it is missing.
37
+ * Returns the binary path, or null when it is still unusable.
38
+ */
39
+ export function ensureNativeManagerInstalled(options?: EnsureNativeOptions): string | null;
@@ -1,5 +1,9 @@
1
- import { accessSync, constants, readFileSync, statSync } from "node:fs";
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, closeSync, constants, mkdirSync, openSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
2
3
  import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
3
7
  import { getSystemErrorMap } from "node:util";
4
8
 
5
9
  // Filesystem failures mean this optional package is unusable, regardless of the syscall code.
@@ -11,8 +15,10 @@ const resolutionErrorCodes = new Set([
11
15
  "ERR_INVALID_MODULE_SPECIFIER", "ERR_UNSUPPORTED_RESOLVE_REQUEST",
12
16
  ]);
13
17
 
18
+ const modulePath = fileURLToPath(import.meta.url);
14
19
  const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
15
20
  const resolveInstalled = createRequire(import.meta.url).resolve;
21
+ const RESTORE_HINT = "not installed; run `npm install --include=optional pi-famulus`";
16
22
 
17
23
  /** The native optional packages are selected by npm's os/cpu constraints. */
18
24
  export function nativePackageName(platform, arch) {
@@ -23,20 +29,263 @@ export function nativePackageName(platform, arch) {
23
29
  return `pi-famulus-${packageOs}-${arch}`;
24
30
  }
25
31
 
32
+ /**
33
+ * Cache for the exact platform package. `pi update --extensions` reinstalls
34
+ * with npm, and `omit=optional` (user or project npmrc) deletes optional
35
+ * dependencies during that install. The cache is outside the famulus runtime
36
+ * home and outside pi's package tree, so the next update does not remove it.
37
+ */
38
+ export function nativeCacheDir({
39
+ version = packageVersion,
40
+ platform = process.platform,
41
+ arch = process.arch,
42
+ env = process.env,
43
+ home = homedir(),
44
+ } = {}) {
45
+ const base = platform === "win32"
46
+ ? join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "pi-famulus", "native")
47
+ : join(env.XDG_CACHE_HOME || join(home, ".cache"), "pi-famulus", "native");
48
+ return join(base, version, `${platform}-${arch}`);
49
+ }
50
+
51
+ function isIgnoredResolveError(error) {
52
+ return error instanceof SyntaxError || systemErrorCodes.has(error?.code) || resolutionErrorCodes.has(error?.code);
53
+ }
54
+
55
+ function executableFile(path) {
56
+ try {
57
+ const stat = statSync(path);
58
+ if (!stat.isFile() || stat.size <= 0) return false;
59
+ accessSync(path, constants.X_OK);
60
+ return true;
61
+ } catch (error) {
62
+ if (isIgnoredResolveError(error)) return false;
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /** Usable exact-version binary inside one package directory, or null. */
68
+ function binaryInPackage(dir, name) {
69
+ let metadata;
70
+ try {
71
+ metadata = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
72
+ } catch (error) {
73
+ if (isIgnoredResolveError(error)) return null;
74
+ throw error;
75
+ }
76
+ if (!metadata || metadata.name !== name || metadata.version !== packageVersion) return null;
77
+ const exported = metadata.exports?.["./bin/pi-famulus"];
78
+ const rel = typeof exported === "string" ? exported : metadata.main;
79
+ if (typeof rel !== "string" || !rel.startsWith("./") || rel.split("/").includes("..")) return null;
80
+ const binary = join(dir, rel);
81
+ const root = dir.endsWith(sep) ? dir : dir + sep;
82
+ if (!binary.startsWith(root)) return null;
83
+ return executableFile(binary) ? binary : null;
84
+ }
85
+
86
+ function walkNodeModules(startFile, stopAt) {
87
+ const dirs = [];
88
+ let dir = dirname(startFile);
89
+ const seen = new Set();
90
+ const boundary = stopAt ? resolve(stopAt) : null;
91
+ while (!seen.has(dir)) {
92
+ seen.add(dir);
93
+ dirs.push(join(dir, "node_modules"));
94
+ if (boundary && (dir === boundary || !dir.startsWith(boundary.endsWith(sep) ? boundary : boundary + sep))) break;
95
+ const parent = dirname(dir);
96
+ if (parent === dir) break;
97
+ dir = parent;
98
+ }
99
+ return dirs;
100
+ }
101
+
102
+ function legacyResolve(name, resolve) {
103
+ const metadata = JSON.parse(readFileSync(resolve(`${name}/package.json`), "utf8"));
104
+ if (!metadata || metadata.name !== name || metadata.version !== packageVersion) return null;
105
+ const binary = resolve(`${name}/bin/pi-famulus`);
106
+ const stat = statSync(binary);
107
+ if (!stat.isFile() || stat.size <= 0) return null;
108
+ accessSync(binary, constants.X_OK);
109
+ return binary;
110
+ }
111
+
112
+ function searchNative({ platform = process.platform, arch = process.arch, from, stopAt, cacheDir } = {}) {
113
+ const name = nativePackageName(platform, arch);
114
+ if (!name) return { name: null, path: null, detail: `unsupported ${platform}/${arch}` };
115
+ let start = from ?? modulePath;
116
+ if (!from) {
117
+ try { start = realpathSync(modulePath); } catch { /* keep the URL path */ }
118
+ }
119
+ const seen = new Set();
120
+ for (const nodeModules of walkNodeModules(start, stopAt)) {
121
+ const dir = join(nodeModules, name);
122
+ if (seen.has(dir)) continue;
123
+ seen.add(dir);
124
+ const binary = binaryInPackage(dir, name);
125
+ if (binary) return { name, path: binary, detail: "" };
126
+ }
127
+ // NODE_PATH and other resolver roots. An explicit `from` is a hermetic walk.
128
+ if (!from) {
129
+ try {
130
+ const resolved = dirname(resolveInstalled(`${name}/package.json`));
131
+ if (!seen.has(resolved)) {
132
+ const binary = binaryInPackage(resolved, name);
133
+ if (binary) return { name, path: binary, detail: "" };
134
+ }
135
+ } catch (error) {
136
+ if (!isIgnoredResolveError(error)) throw error;
137
+ }
138
+ }
139
+ if (cacheDir !== false) {
140
+ const dir = join(cacheDir ?? nativeCacheDir({ platform, arch }), "node_modules", name);
141
+ const binary = binaryInPackage(dir, name);
142
+ if (binary) return { name, path: binary, detail: "" };
143
+ }
144
+ return { name, path: null, detail: RESTORE_HINT };
145
+ }
146
+
26
147
  /** Resolve the exact-version optional package without loading native executable bytes. */
27
- export function resolveNativeManagerPath({ platform = process.platform, arch = process.arch, resolve = resolveInstalled } = {}) {
148
+ export function resolveNativeManagerPath({ platform = process.platform, arch = process.arch, resolve, from, stopAt, cacheDir } = {}) {
28
149
  const name = nativePackageName(platform, arch);
29
150
  if (!name) return null;
151
+ // Injected resolvers stay hermetic: tests use them to simulate one install tree.
152
+ if (resolve) {
153
+ try {
154
+ return legacyResolve(name, resolve);
155
+ } catch (error) {
156
+ if (isIgnoredResolveError(error)) return null;
157
+ throw error;
158
+ }
159
+ }
160
+ return searchNative({ platform, arch, from, stopAt, cacheDir }).path;
161
+ }
162
+
163
+ /** `npm <package> <path>` or why that exact platform package was not usable. */
164
+ export function describeNativeLookup(options) {
165
+ const name = nativePackageName(options?.platform ?? process.platform, options?.arch ?? process.arch);
166
+ if (!name) return `npm native manager (unsupported ${options?.platform ?? process.platform}/${options?.arch ?? process.arch})`;
167
+ if (options?.resolve) {
168
+ const path = resolveNativeManagerPath(options);
169
+ return path ? `npm ${name} ${path}` : `npm ${name} (missing or unusable, ignored)`;
170
+ }
171
+ const found = searchNative(options);
172
+ return found.path ? `npm ${name} ${found.path}` : `npm ${name} (${found.detail})`;
173
+ }
174
+
175
+ function pidAlive(pid) {
30
176
  try {
31
- const metadata = JSON.parse(readFileSync(resolve(`${name}/package.json`), "utf8"));
32
- if (!metadata || metadata.name !== name || metadata.version !== packageVersion) return null;
33
- const binary = resolve(`${name}/bin/pi-famulus`);
34
- const stat = statSync(binary);
35
- if (!stat.isFile() || stat.size <= 0) return null;
36
- accessSync(binary, constants.X_OK);
37
- return binary;
177
+ process.kill(pid, 0);
178
+ return true;
38
179
  } catch (error) {
39
- if (error instanceof SyntaxError || systemErrorCodes.has(error?.code) || resolutionErrorCodes.has(error?.code)) return null;
40
- throw error;
180
+ return error?.code === "EPERM";
181
+ }
182
+ }
183
+
184
+ function sleepMs(ms) {
185
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
186
+ }
187
+
188
+ /** A lock left by a dead process, or held far longer than one npm install, can be replaced. */
189
+ function installLockIsStale(lockPath) {
190
+ try {
191
+ const age = Date.now() - statSync(lockPath).mtimeMs;
192
+ const pid = Number(readFileSync(lockPath, "utf8").trim());
193
+ if (Number.isInteger(pid) && pid > 0) return !pidAlive(pid) || age > 150_000;
194
+ return age > 5_000;
195
+ } catch {
196
+ return true;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * One npm install per cache directory. Another startup either waits until that
202
+ * install publishes a binary or takes over a stale lock. Returns the open lock,
203
+ * or null when waiting itself produced a usable binary.
204
+ */
205
+ function acquireInstallLock(dir, lookup) {
206
+ mkdirSync(dir, { recursive: true });
207
+ const lockPath = `${dir}.install.lock`;
208
+ const deadline = Date.now() + 130_000;
209
+ while (Date.now() <= deadline) {
210
+ try {
211
+ const fd = openSync(lockPath, "wx");
212
+ try { writeSync(fd, String(process.pid)); } catch { /* the fd is still the lock */ }
213
+ return { fd, lockPath };
214
+ } catch (error) {
215
+ if (error?.code !== "EEXIST") throw error;
216
+ const ready = resolveNativeManagerPath(lookup);
217
+ if (ready) return null;
218
+ if (installLockIsStale(lockPath)) {
219
+ rmSync(lockPath, { force: true });
220
+ continue;
221
+ }
222
+ sleepMs(200);
223
+ }
224
+ }
225
+ return null;
226
+ }
227
+
228
+ function releaseInstallLock(lock) {
229
+ if (!lock) return;
230
+ try { closeSync(lock.fd); } catch { /* already closed */ }
231
+ rmSync(lock.lockPath, { force: true });
232
+ }
233
+
234
+ /**
235
+ * Install the exact platform package into the versioned cache when the
236
+ * optional dependency is missing. No-ops when a usable binary is already
237
+ * resolvable. `PI_FAMULUS_NO_NATIVE_FETCH=1`, `PI_OFFLINE=1`, and Vitest skip
238
+ * the install. Directory, manifest, and npm failures return null so startup
239
+ * degrades instead of throwing.
240
+ */
241
+ export function ensureNativeManagerInstalled({
242
+ platform = process.platform,
243
+ arch = process.arch,
244
+ cacheDir,
245
+ env = process.env,
246
+ npmCommand,
247
+ spawn = spawnSync,
248
+ from,
249
+ stopAt,
250
+ } = {}) {
251
+ const lookup = { platform, arch, cacheDir, from, stopAt };
252
+ try {
253
+ const existing = resolveNativeManagerPath(lookup);
254
+ if (existing) return existing;
255
+ if (!nativePackageName(platform, arch)) return null;
256
+ if (env.PI_FAMULUS_NO_NATIVE_FETCH === "1" || env.PI_OFFLINE === "1" || env.VITEST) return null;
257
+ if (cacheDir === false) return null;
258
+ const dir = cacheDir ?? nativeCacheDir({ platform, arch, env });
259
+ const lockedLookup = { ...lookup, cacheDir: dir };
260
+ const lock = acquireInstallLock(dir, lockedLookup);
261
+ if (!lock) return resolveNativeManagerPath(lockedLookup);
262
+ try {
263
+ const manifest = join(dir, "package.json");
264
+ try {
265
+ readFileSync(manifest);
266
+ } catch {
267
+ writeFileSync(manifest, `${JSON.stringify({ private: true, name: "pi-famulus-native-cache", version: "0.0.0" })}\n`);
268
+ }
269
+ const command = npmCommand ?? (platform === "win32" ? "npm.cmd" : "npm");
270
+ const name = nativePackageName(platform, arch);
271
+ let result;
272
+ try {
273
+ result = spawn(command, [
274
+ "install", `${name}@${packageVersion}`,
275
+ "--prefix", dir,
276
+ "--ignore-scripts", "--no-fund", "--no-audit", "--loglevel=error",
277
+ ], { timeout: 120_000, windowsHide: true, encoding: "utf8" });
278
+ } catch {
279
+ return resolveNativeManagerPath(lockedLookup);
280
+ }
281
+ const found = resolveNativeManagerPath(lockedLookup);
282
+ if (found) return found;
283
+ if (!result || result.error || result.status !== 0) return null;
284
+ return null;
285
+ } finally {
286
+ releaseInstallLock(lock);
287
+ }
288
+ } catch {
289
+ return null;
41
290
  }
42
291
  }