@pasko70/pibo 2.5.2 → 3.0.0

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.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +457 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -209,6 +209,8 @@ export class PiboGatewayServer {
209
209
  for (const app of registry.getWebApps())
210
210
  await app.dispose?.();
211
211
  }
212
+ await Promise.allSettled([...new Set([this.pluginRegistry, this.compatibilityRuntimeRegistry].filter((registry) => Boolean(registry)))]
213
+ .map(async (registry) => await registry.disposeSpeechProviders()));
212
214
  if (this.ownsSessionStore) {
213
215
  this.sessionStore?.close?.();
214
216
  }
@@ -419,6 +421,7 @@ export class PiboGatewayServer {
419
421
  rebindSessionRuntime: (piboSessionId, input) => this.requireRouter().rebindSessionRuntime(piboSessionId, input),
420
422
  getSessionRuntimeStatus: (piboSessionId) => this.requireRouter().getSessionRuntimeStatus(piboSessionId),
421
423
  getSessionStatusSnapshot: (piboSessionId) => this.requireRouter().getSessionStatusSnapshot(piboSessionId),
424
+ getSessionForkCandidates: (piboSessionId) => this.requireRouter().getSessionForkCandidates(piboSessionId),
422
425
  listSessionRuntimeStatuses: () => this.requireRouter().listSessionRuntimeStatuses(),
423
426
  listRuns: (options) => this.requireRouter().listRuns(options),
424
427
  snapshotSignalSession: (piboSessionId) => this.requireRouter().snapshotSignalSession(piboSessionId),
@@ -432,6 +435,11 @@ export class PiboGatewayServer {
432
435
  getCapabilityCatalog: () => this.pluginRegistry.getCapabilityCatalog(),
433
436
  getTranscriptionProviderInfos: () => this.pluginRegistry.getTranscriptionProviderInfos(),
434
437
  transcribe: (providerId, input) => this.pluginRegistry.transcribe(providerId, input),
438
+ getSpeechProviderIds: () => this.pluginRegistry.getSpeechProviderIds(),
439
+ getSpeechProviderInfos: () => this.pluginRegistry.getSpeechProviderInfos(),
440
+ startSpeechSession: (providerId, input, options) => this.pluginRegistry.startSpeechSession(providerId, input, options),
441
+ speakSpeechSession: (sessionId, input) => this.pluginRegistry.speakSpeechSession(sessionId, input),
442
+ stopSpeechSession: (sessionId) => this.pluginRegistry.stopSpeechSession(sessionId),
435
443
  inspectAgentRuntimeInstances: () => this.pluginRegistry.inspectAgentRuntimeInstances(),
436
444
  getAgentRuntimeAuthStatus: (runtimeInstanceId) => this.requireRouter().getAgentRuntimeAuthStatus(runtimeInstanceId),
437
445
  startAgentRuntimeAuth: (runtimeInstanceId, input) => this.requireRouter().startAgentRuntimeAuth(runtimeInstanceId, input),
@@ -6,6 +6,7 @@ import { createPiboChatUserSkillsPlugin } from "../plugins/chat-user-skills.js";
6
6
  import { createPiboChatWebPlugin } from "../plugins/chat-web.js";
7
7
  import { createPiboChatVscodeWebPlugin } from "../plugins/chat-vscode-web.js";
8
8
  import { createPiboContextFilesPlugin } from "../plugins/context-files.js";
9
+ import { createPiboPreviewPlugin } from "../previews/plugin.js";
9
10
  import { createPiboCronPlugin } from "../cron/plugin.js";
10
11
  import { createPiboLoopPlugin } from "../loops/plugin.js";
11
12
  import { createPiboDevAuthPlugin } from "../plugins/dev-auth.js";
@@ -160,6 +161,7 @@ export function createWebPiboPluginRegistry(options = {}) {
160
161
  createPiboChatCustomAgentProfilesPlugin({ agentStorePath: resolvedOptions.chat?.agentStorePath }),
161
162
  createPiboLoopPlugin({ loopStorePath: resolvedOptions.chat?.ralphStorePath, dataStorePath: resolvedOptions.chat?.dataStorePath, dataPayloadRootDir: resolvedOptions.chat?.dataPayloadRootDir }),
162
163
  createPiboContextFilesPlugin(resolvedOptions.contextFiles),
164
+ createPiboPreviewPlugin(),
163
165
  createPiboChatWebPlugin(resolvedOptions.chat),
164
166
  createPiboChatVscodeWebPlugin(),
165
167
  ],
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ export { runGatewayClient } from "./gateway/client.js";
41
41
  export { LOCAL_TUI_CHANNEL_NAME, LocalRoutedTuiClient, createLocalRoutedTuiClient, createLocalRoutedTuiExtension, runLocalRoutedTui, } from "./local/tui.js";
42
42
  export { createWebHostChannel, DEFAULT_WEB_CHANNEL_HOST, DEFAULT_WEB_CHANNEL_PORT, WEB_CHANNEL_NAME } from "./web/channel.js";
43
43
  export { sendGatewayEvent, sendGatewayMessageAndWaitForReply } from "./gateway/request.js";
44
- export { createAgentToolDefinitions, createSubagentToolDefinitions, createSubagentToolName, formatAvailableAgentsForPrompt, listAvailableAgents, PIBO_AGENT_TOOL_NAMES, } from "./subagents/tool.js";
44
+ export { createAgentToolDefinitions, createSubagentToolDefinitions, createSubagentToolName, formatAvailableAgentsForPrompt, listAvailableAgents, normalizePiboAgentSessionName, PIBO_AGENT_SESSION_NAME_MAX_LENGTH, PIBO_AGENT_TOOL_NAMES, } from "./subagents/tool.js";
45
45
  export { PiboSteeringUnavailableError } from "./core/events.js";
46
46
  export { RuntimeSessionBindingConflictError, RuntimeSessionBindingTransitionError, assertRuntimeSessionBindingTransition, createInitialRuntimeSessionBinding, createLegacyPiRuntimeSessionBinding, nextRuntimeSessionBinding, } from "./sessions/runtime-binding.js";
47
47
  export { InMemoryPiboSessionStore, createPiSessionId, createPiboSessionId, createPiboSession, } from "./sessions/store.js";
@@ -299,7 +299,7 @@ export class PiboLoopStore {
299
299
  const enabled = input.enabled === true;
300
300
  const state = {
301
301
  completedIterations: 0,
302
- ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
302
+ ...(mode === 'goal' ? { goalStatus: enabled ? 'active' : 'paused', tokenAccounting: newGoalTokenAccounting(), tokensUsed: 0, activeTimeSeconds: 0, activeTimeRunningAt: null, ...(enabled ? { goalStartedAt: timestamp } : {}) } : {}),
303
303
  ...(input.initialPiboSessionId?.trim() ? { lastPiboSessionId: input.initialPiboSessionId.trim() } : {}),
304
304
  };
305
305
  const job = { id: mode === 'ralph' ? `ralph_${randomUUID()}` : `loop_${randomUUID()}`, mode, name: (input.name ?? defaultName(input.prompt)).trim(), description: input.description?.trim() || undefined, enabled, target, profile: input.profile, prompt: input.prompt, maxIterations: normalizeMaxIterations(input.maxIterations), tokenBudget: normalizeTokenBudget(input.tokenBudget), tokenReserve: normalizeTokenReserve(input.tokenReserve), stopPolicy: normalizeLoopStopPolicy(input.stopPolicy), ...runtimeOptions, ...(resources ? { resources } : {}), state, createdAt: timestamp, updatedAt: timestamp };
@@ -373,7 +373,7 @@ export class PiboLoopStore {
373
373
  createdAt: timestamp,
374
374
  };
375
375
  this.db.prepare('INSERT INTO pibo_ralph_run_facts (id, job_id, run_id, pibo_session_id, type, source, payload_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(fact.id, fact.jobId, null, piboSessionId, fact.type, fact.source, JSON.stringify(fact.payload), fact.createdAt);
376
- const state = { ...job.state, goalStatus: 'active', goalEndedAt: undefined, stopRequestedAt: undefined, cancelRequestedAt: undefined, runningAt: undefined };
376
+ const state = { ...job.state, goalStatus: 'active', goalEndedAt: undefined, stopRequestedAt: undefined, cancelRequestedAt: undefined, activeTimeRunningAt: null, runningAt: undefined };
377
377
  this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 1, state_json = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(state), timestamp, job.id);
378
378
  this.db.exec('COMMIT');
379
379
  return this.getJob(id);
@@ -483,6 +483,7 @@ export class PiboLoopStore {
483
483
  try {
484
484
  const job = this.recordGoalProgress(id, { activeTimeSeconds: seconds }, now);
485
485
  if (job?.mode === 'goal') {
486
+ this.updateJobStateLocked(id, { ...job.state, activeTimeRunningAt: null }, nowIso(now));
486
487
  const row = this.db.prepare('SELECT * FROM pibo_ralph_runs WHERE id = ? AND job_id = ?').get(runId, id);
487
488
  if (row) {
488
489
  const accounting = { ...(parseRunAccounting(row.accounting_json) ?? { tokenAccounting: normalizeLoopTokenAccounting(job.state.tokenAccounting) }), activeTimeSeconds: seconds };
@@ -490,7 +491,7 @@ export class PiboLoopStore {
490
491
  }
491
492
  }
492
493
  this.db.exec('COMMIT');
493
- return job;
494
+ return this.getJob(id) ?? job;
494
495
  }
495
496
  catch (error) {
496
497
  this.db.exec('ROLLBACK');
@@ -681,6 +682,7 @@ export class PiboLoopStore {
681
682
  const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
682
683
  const state = {
683
684
  ...job.state,
685
+ ...(job.mode === 'goal' ? { activeTimeRunningAt: null } : {}),
684
686
  runningAt: undefined,
685
687
  completedIterations,
686
688
  lastRunAt: timestamp,
@@ -789,7 +791,7 @@ export class PiboLoopStore {
789
791
  }
790
792
  }
791
793
  const run = this.createRunLocked(job, timestamp);
792
- const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id, nextAttemptAt: undefined, retryBackoffMs: undefined };
794
+ const state = { ...job.state, ...(job.mode === 'goal' ? { activeTimeRunningAt: timestamp } : {}), runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id, nextAttemptAt: undefined, retryBackoffMs: undefined };
793
795
  this.updateJobStateLocked(job.id, state, timestamp);
794
796
  this.db.exec('COMMIT');
795
797
  return { job: { ...job, state, updatedAt: timestamp }, run };
@@ -4,7 +4,7 @@
4
4
  import { createHash } from 'node:crypto';
5
5
  import { existsSync } from 'node:fs';
6
6
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
- import { homedir } from 'node:os';
7
+ import { homedir, tmpdir } from 'node:os';
8
8
  import { dirname, isAbsolute, join, resolve } from 'node:path';
9
9
  import { ErrorCode, configInvalidJsonError, configMissingFieldError, configNotFoundError, configSearchError, formatCliError, serverNotFoundError, } from './errors.js';
10
10
  export const DEFAULT_MCP_CONFIG_FILE = 'mcp_servers.json';
@@ -207,34 +207,106 @@ export function getDaemonRequestTimeoutMs() {
207
207
  }
208
208
  return DEFAULT_DAEMON_REQUEST_TIMEOUT_SECONDS * 1000;
209
209
  }
210
+ function getDaemonUserKey(platform) {
211
+ const uid = process.getuid?.();
212
+ if (platform !== 'win32' && uid !== undefined) {
213
+ return String(uid);
214
+ }
215
+ const identity = `${homedir()}\0${process.env.USERNAME ?? process.env.USER ?? ''}`;
216
+ return createHash('sha256').update(identity).digest('hex').slice(0, 12);
217
+ }
218
+ function getDaemonServerKey(serverName) {
219
+ // Node replaces unpaired UTF-16 surrogates when encoding a string as UTF-8.
220
+ // Hash those names as explicit code units so distinct JavaScript strings do
221
+ // not collapse onto the same daemon endpoint.
222
+ const hasUnpairedSurrogate = Array.from({ length: serverName.length }, (_, index) => serverName.charCodeAt(index)).some((codeUnit, index, codeUnits) => {
223
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
224
+ const next = codeUnits[index + 1];
225
+ return next === undefined || next < 0xdc00 || next > 0xdfff;
226
+ }
227
+ if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
228
+ const previous = codeUnits[index - 1];
229
+ return previous === undefined || previous < 0xd800 || previous > 0xdbff;
230
+ }
231
+ return false;
232
+ });
233
+ const hash = createHash('sha256');
234
+ if (hasUnpairedSurrogate) {
235
+ hash.update('pibo-mcp-utf16\0');
236
+ const codeUnits = Buffer.allocUnsafe(serverName.length * 2);
237
+ for (let index = 0; index < serverName.length; index += 1) {
238
+ codeUnits.writeUInt16LE(serverName.charCodeAt(index), index * 2);
239
+ }
240
+ hash.update(codeUnits);
241
+ }
242
+ else {
243
+ hash.update(serverName);
244
+ }
245
+ return hash.digest('hex').slice(0, 16);
246
+ }
247
+ function getDaemonFileName(serverName) {
248
+ // Keep every filesystem path bounded. Unix-domain socket limits are much
249
+ // shorter than ordinary filesystem path limits, and server names are input.
250
+ return getDaemonServerKey(serverName);
251
+ }
210
252
  /**
211
- * Get the socket directory for daemon connections
212
- * Uses platform-appropriate temp directory
253
+ * Get the directory for daemon PID files and POSIX sockets.
213
254
  */
214
- export function getSocketDir() {
215
- const uid = process.getuid?.() ?? 'unknown';
216
- // macOS uses /var/folders which is auto-cleaned, Linux uses /tmp
217
- const base = process.platform === 'darwin' ? '/tmp' : '/tmp';
218
- return join(base, `mcp-cli-${uid}`);
255
+ export function getSocketDir(platform = process.platform) {
256
+ return join(tmpdir(), `mcp-cli-${getDaemonUserKey(platform)}`);
219
257
  }
220
258
  /**
221
- * Get socket path for a specific server
259
+ * Whether the platform represents daemon IPC as a filesystem socket.
222
260
  */
223
- export function getSocketPath(serverName) {
224
- return join(getSocketDir(), `${serverName}.sock`);
261
+ export function usesFilesystemSocket(platform = process.platform) {
262
+ return platform !== 'win32';
225
263
  }
226
264
  /**
227
- * Get PID file path for a specific server daemon
265
+ * Get the IPC endpoint for a specific server.
266
+ * POSIX uses Unix domain sockets; Windows uses named pipes.
228
267
  */
229
- export function getPidPath(serverName) {
230
- return join(getSocketDir(), `${serverName}.pid`);
268
+ export function getSocketPath(serverName, platform = process.platform) {
269
+ if (platform === 'win32') {
270
+ const userKey = getDaemonUserKey(platform);
271
+ return `\\\\.\\pipe\\pibo-mcp-${userKey}-${getDaemonServerKey(serverName)}`;
272
+ }
273
+ return join(getSocketDir(platform), `${getDaemonFileName(serverName)}.sock`);
274
+ }
275
+ /**
276
+ * Get PID file path for a specific server daemon.
277
+ */
278
+ export function getPidPath(serverName, platform = process.platform) {
279
+ return join(getSocketDir(platform), `${getDaemonFileName(serverName)}.pid`);
280
+ }
281
+ /**
282
+ * Get the atomic startup-election claim path for a server daemon.
283
+ */
284
+ export function getDaemonClaimPath(serverName, platform = process.platform) {
285
+ return join(getSocketDir(platform), `${getDaemonFileName(serverName)}.lock`);
286
+ }
287
+ /**
288
+ * Get the prefix shared by active client lease files for a server daemon.
289
+ */
290
+ export function getDaemonLeasePrefix(serverName, platform = process.platform) {
291
+ return join(getSocketDir(platform), `${getDaemonFileName(serverName)}.lease-`);
292
+ }
293
+ function normalizeForHash(value) {
294
+ if (Array.isArray(value)) {
295
+ return value.map(normalizeForHash);
296
+ }
297
+ if (value && typeof value === 'object') {
298
+ return Object.fromEntries(Object.entries(value)
299
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
300
+ .map(([key, nestedValue]) => [key, normalizeForHash(nestedValue)]));
301
+ }
302
+ return value;
231
303
  }
232
304
  /**
233
- * Generate a hash of server config for stale detection
234
- * Returns consistent hash for identical configs
305
+ * Generate a hash of server config for stale detection.
306
+ * Nested object key order does not affect the hash.
235
307
  */
236
308
  export function getConfigHash(config) {
237
- const str = JSON.stringify(config, Object.keys(config).sort());
309
+ const str = JSON.stringify(normalizeForHash(config));
238
310
  return createHash('sha256').update(str).digest('hex').slice(0, 16);
239
311
  }
240
312
  /**