agentroam-runtime-darwin-arm64 0.2.0-preview.12 → 0.2.0-preview.13
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/manifest.json +1 -1
- package/package.json +1 -1
- package/runtime/.next/BUILD_ID +1 -1
- package/runtime/.next/app-build-manifest.json +7 -7
- package/runtime/.next/app-path-routes-manifest.json +1 -1
- package/runtime/.next/build-manifest.json +6 -6
- package/runtime/.next/prerender-manifest.json +1 -1
- package/runtime/.next/react-loadable-manifest.json +1 -1
- package/runtime/.next/required-server-files.json +1 -1
- package/runtime/.next/routes-manifest.json +1 -1
- package/runtime/.next/server/app/_not-found/page.js +1 -1
- package/runtime/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/runtime/.next/server/app/_not-found.html +1 -1
- package/runtime/.next/server/app/_not-found.rsc +1 -1
- package/runtime/.next/server/app/api/agent/abort/route.js +1 -1
- package/runtime/.next/server/app/api/agent/answer/route.js +1 -1
- package/runtime/.next/server/app/api/agent/model/route.js +1 -1
- package/runtime/.next/server/app/api/agent/models/route.js +1 -1
- package/runtime/.next/server/app/api/agent/run/route.js +1 -1
- package/runtime/.next/server/app/api/agent/runtime-health/route.js +1 -1
- package/runtime/.next/server/app/api/agent/steer/route.js +1 -1
- package/runtime/.next/server/app/api/agent/stream/route.js +2 -2
- package/runtime/.next/server/app/api/agent-workspaces/[workspaceId]/sessions/route.js +1 -1
- package/runtime/.next/server/app/api/agent-workspaces/route.js +1 -1
- package/runtime/.next/server/app/api/auth/verify/route.js +1 -1
- package/runtime/.next/server/app/api/mcp/connect/route.js +1 -1
- package/runtime/.next/server/app/api/mcp/disconnect/route.js +1 -1
- package/runtime/.next/server/app/api/mcp/servers/route.js +1 -1
- package/runtime/.next/server/app/api/memory/[key]/route.js +1 -1
- package/runtime/.next/server/app/api/memory/route.js +1 -1
- package/runtime/.next/server/app/api/plugins/load/route.js +1 -1
- package/runtime/.next/server/app/api/projects/[id]/route.js +1 -1
- package/runtime/.next/server/app/api/projects/check/route.js +1 -1
- package/runtime/.next/server/app/api/projects/directories/route.js +1 -1
- package/runtime/.next/server/app/api/projects/roots/route.js +1 -1
- package/runtime/.next/server/app/api/projects/route.js +1 -1
- package/runtime/.next/server/app/api/remote-tools/register/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/[id]/changes/route.js +2 -2
- package/runtime/.next/server/app/api/sessions/[id]/fork/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/[id]/goals/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/[id]/handoff/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/[id]/query-index/route.js +1 -0
- package/runtime/.next/server/app/api/sessions/[id]/query-index/route.js.nft.json +1 -0
- package/runtime/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/runtime/.next/server/app/api/sessions/route.js +1 -1
- package/runtime/.next/server/app/api/skills/route.js +1 -1
- package/runtime/.next/server/app/api/tools/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/login/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/logout/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/revoke-all/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/setup/route.js +1 -1
- package/runtime/.next/server/app/api/web-auth/status/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/bootstrap/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/device-state/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/history/route.js +1 -1
- package/runtime/.next/server/app/api/web-console/preferences/route.js +1 -1
- package/runtime/.next/server/app/web/page.js +3 -4
- package/runtime/.next/server/app/web/page_client-reference-manifest.js +1 -1
- package/runtime/.next/server/app/web.html +1 -1
- package/runtime/.next/server/app/web.rsc +2 -2
- package/runtime/.next/server/app-paths-manifest.json +21 -20
- package/runtime/.next/server/chunks/1322.js +37 -37
- package/runtime/.next/server/chunks/2083.js +7 -7
- package/runtime/.next/server/chunks/5303.js +2 -2
- package/runtime/.next/server/middleware-build-manifest.js +1 -1
- package/runtime/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/runtime/.next/server/pages/404.html +1 -1
- package/runtime/.next/server/pages/500.html +1 -1
- package/runtime/.next/server/server-reference-manifest.json +1 -1
- package/runtime/.next/static/chunks/137.261fab9d606d75d3.js +1 -0
- package/runtime/.next/static/chunks/app/web/page-8e4da33d77868123.js +1 -0
- package/runtime/.next/static/chunks/{main-app-e9c7dfe241898866.js → main-app-3596fd7d2841e9a3.js} +1 -1
- package/runtime/.next/static/chunks/{webpack-aa3dd6c115307361.js → webpack-d22db940097662ef.js} +1 -1
- package/runtime/node_modules/@agent/core/dist/domain/model/entities.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/domain/model/entities.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionHistory.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndex.d.ts +16 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndex.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndex.test.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndex.test.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndexCache.d.ts +11 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndexCache.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndexCache.test.d.ts +2 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/SessionQueryIndexCache.test.d.ts.map +1 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/entities.d.ts +18 -0
- package/runtime/node_modules/@agent/core/dist/domain/session/entities.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/domain/session/index.d.ts +3 -1
- package/runtime/node_modules/@agent/core/dist/domain/session/index.d.ts.map +1 -1
- package/runtime/node_modules/@agent/core/dist/index.js +202 -7
- package/runtime/node_modules/@agent/core/dist/index.js.map +6 -4
- package/runtime/webapp/dist/assets/App-CZspcejP.js +275 -0
- package/runtime/webapp/dist/assets/{index-CTuREapw.js → index-BAH3pT_c.js} +6 -6
- package/runtime/webapp/dist/assets/index-lt5J4xDf.css +1 -0
- package/runtime/webapp/dist/index.html +2 -2
- package/runtime/ws-server.mjs +13 -2
- package/runtime/.next/static/chunks/137.b2c5a562cf28417d.js +0 -1
- package/runtime/.next/static/chunks/app/web/page-2a4cea1bebd50375.js +0 -1
- package/runtime/webapp/dist/assets/App-Bf3AhvuP.js +0 -270
- package/runtime/webapp/dist/assets/index-B1fGLxCu.css +0 -1
- /package/runtime/.next/static/{z8n2mClYDr-eB4JXiAbLG → iaxCayE60RyqJY_Zj-OjS}/_buildManifest.js +0 -0
- /package/runtime/.next/static/{z8n2mClYDr-eB4JXiAbLG → iaxCayE60RyqJY_Zj-OjS}/_ssgManifest.js +0 -0
|
@@ -316,6 +316,116 @@ class PermissionAwareToolExecutor {
|
|
|
316
316
|
return this.delegate.execute(name, args, ctx);
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
|
+
// src/domain/session/SessionQueryIndex.ts
|
|
320
|
+
var ANCHOR_PREFIX = "history-anchor.v1.";
|
|
321
|
+
var MESSAGE_PREFIX = "history-message.v1.";
|
|
322
|
+
var PREVIEW_LIMIT = 120;
|
|
323
|
+
|
|
324
|
+
class StaleSessionAnchorError extends Error {
|
|
325
|
+
code = "STALE_SESSION_ANCHOR";
|
|
326
|
+
constructor() {
|
|
327
|
+
super("Session history changed; refresh the query index");
|
|
328
|
+
this.name = "StaleSessionAnchorError";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function buildSessionQueryIndex(sessionId, messages, revision = computeSessionHistoryRevision(messages)) {
|
|
332
|
+
const visible = visibleSessionMessages(messages);
|
|
333
|
+
let ordinal = 0;
|
|
334
|
+
const entries = visible.flatMap(({ message, visibleIndex }) => {
|
|
335
|
+
if (!isIndexedUserMessage(message))
|
|
336
|
+
return [];
|
|
337
|
+
ordinal += 1;
|
|
338
|
+
return [{
|
|
339
|
+
messageId: sessionHistoryMessageId(visibleIndex, message),
|
|
340
|
+
ordinal,
|
|
341
|
+
preview: queryPreview(message),
|
|
342
|
+
pageToken: encodeSessionHistoryAnchor(revision, visibleIndex)
|
|
343
|
+
}];
|
|
344
|
+
});
|
|
345
|
+
return { sessionId, revision, totalQueries: entries.length, entries };
|
|
346
|
+
}
|
|
347
|
+
function computeSessionHistoryRevision(messages) {
|
|
348
|
+
let hash = 2166136261;
|
|
349
|
+
const visible = messages.filter(isVisibleSessionMessage);
|
|
350
|
+
hash = hashText(hash, String(visible.length));
|
|
351
|
+
for (const message of visible) {
|
|
352
|
+
const content = message.content ?? "";
|
|
353
|
+
const signature = [
|
|
354
|
+
message.role,
|
|
355
|
+
message.name ?? "",
|
|
356
|
+
String(content.length),
|
|
357
|
+
content.slice(0, message.role === "user" ? 128 : 96),
|
|
358
|
+
content.slice(message.role === "user" ? -32 : -96),
|
|
359
|
+
String(message.images?.length ?? message.presentation?.attachments?.length ?? 0),
|
|
360
|
+
(message.toolCalls ?? []).map((call) => call.id).join(",")
|
|
361
|
+
].join("\x1F");
|
|
362
|
+
hash = hashText(hash, signature);
|
|
363
|
+
}
|
|
364
|
+
return `${visible.length.toString(36)}-${(hash >>> 0).toString(36)}`;
|
|
365
|
+
}
|
|
366
|
+
function sessionHistoryMessageId(visibleIndex, message) {
|
|
367
|
+
const content = message.content ?? "";
|
|
368
|
+
const signature = [
|
|
369
|
+
message.role,
|
|
370
|
+
message.name ?? "",
|
|
371
|
+
String(content.length),
|
|
372
|
+
content.slice(0, 128),
|
|
373
|
+
content.slice(-32),
|
|
374
|
+
String(message.images?.length ?? message.presentation?.attachments?.length ?? 0)
|
|
375
|
+
].join("\x1F");
|
|
376
|
+
return `${MESSAGE_PREFIX}${visibleIndex.toString(36)}.${(hashText(2166136261, signature) >>> 0).toString(36)}`;
|
|
377
|
+
}
|
|
378
|
+
function encodeSessionHistoryAnchor(revision, visibleIndex) {
|
|
379
|
+
return `${ANCHOR_PREFIX}${revision}.${visibleIndex.toString(36)}`;
|
|
380
|
+
}
|
|
381
|
+
function decodeSessionHistoryAnchor(token, revision) {
|
|
382
|
+
if (!token.startsWith(ANCHOR_PREFIX))
|
|
383
|
+
throw new StaleSessionAnchorError;
|
|
384
|
+
const payload = token.slice(ANCHOR_PREFIX.length);
|
|
385
|
+
const separator = payload.lastIndexOf(".");
|
|
386
|
+
if (separator <= 0 || payload.slice(0, separator) !== revision) {
|
|
387
|
+
throw new StaleSessionAnchorError;
|
|
388
|
+
}
|
|
389
|
+
const visibleIndex = Number.parseInt(payload.slice(separator + 1), 36);
|
|
390
|
+
if (!Number.isSafeInteger(visibleIndex) || visibleIndex < 0) {
|
|
391
|
+
throw new StaleSessionAnchorError;
|
|
392
|
+
}
|
|
393
|
+
return visibleIndex;
|
|
394
|
+
}
|
|
395
|
+
function visibleSessionMessages(messages) {
|
|
396
|
+
const visible = [];
|
|
397
|
+
for (const message of messages) {
|
|
398
|
+
if (!isVisibleSessionMessage(message))
|
|
399
|
+
continue;
|
|
400
|
+
visible.push({ message, visibleIndex: visible.length });
|
|
401
|
+
}
|
|
402
|
+
return visible;
|
|
403
|
+
}
|
|
404
|
+
function isVisibleSessionMessage(message) {
|
|
405
|
+
return message.role === "user" || message.role === "assistant";
|
|
406
|
+
}
|
|
407
|
+
function isIndexedUserMessage(message) {
|
|
408
|
+
if (message.role !== "user")
|
|
409
|
+
return false;
|
|
410
|
+
if (message.name === "__interrupt__" || message.name === "__compaction_checkpoint__")
|
|
411
|
+
return false;
|
|
412
|
+
return Boolean(message.content.trim() || message.images?.length || message.presentation?.attachments?.some((attachment) => attachment.type === "image"));
|
|
413
|
+
}
|
|
414
|
+
function queryPreview(message) {
|
|
415
|
+
const text = message.content.replace(/\s+/g, " ").trim();
|
|
416
|
+
if (text)
|
|
417
|
+
return text.length > PREVIEW_LIMIT ? `${text.slice(0, PREVIEW_LIMIT - 1)}…` : text;
|
|
418
|
+
return "图片消息";
|
|
419
|
+
}
|
|
420
|
+
function hashText(seed, value) {
|
|
421
|
+
let hash = seed;
|
|
422
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
423
|
+
hash ^= value.charCodeAt(index);
|
|
424
|
+
hash = Math.imul(hash, 16777619);
|
|
425
|
+
}
|
|
426
|
+
return hash;
|
|
427
|
+
}
|
|
428
|
+
|
|
319
429
|
// src/domain/session/SessionHistory.ts
|
|
320
430
|
var DEFAULT_SESSION_HISTORY_PAGE_SIZE = 50;
|
|
321
431
|
var MAX_SESSION_HISTORY_PAGE_SIZE = 100;
|
|
@@ -329,14 +439,17 @@ function paginateSessionHistory(messages, events, query = {}) {
|
|
|
329
439
|
toolResults.set(message.toolCallId, message);
|
|
330
440
|
}
|
|
331
441
|
}
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const start = findTurnBoundaryStart(visibleMessages, nominalStart);
|
|
442
|
+
const revision = computeSessionHistoryRevision(messages);
|
|
443
|
+
const { start, end, kind } = selectHistoryRange(visibleMessages, pageSize, query, revision);
|
|
335
444
|
const selected = visibleMessages.slice(start, end);
|
|
336
445
|
const pageMessages = [];
|
|
337
446
|
const selectedToolCallIds = new Set;
|
|
338
|
-
for (
|
|
339
|
-
|
|
447
|
+
for (let offset = 0;offset < selected.length; offset += 1) {
|
|
448
|
+
const message = selected[offset];
|
|
449
|
+
pageMessages.push({
|
|
450
|
+
...message,
|
|
451
|
+
historyId: sessionHistoryMessageId(start + offset, message)
|
|
452
|
+
});
|
|
340
453
|
for (const toolCall of message.toolCalls ?? []) {
|
|
341
454
|
selectedToolCallIds.add(toolCall.id);
|
|
342
455
|
const result = toolResults.get(toolCall.id);
|
|
@@ -351,10 +464,51 @@ function paginateSessionHistory(messages, events, query = {}) {
|
|
|
351
464
|
nextCursor: start > 0 ? `${CURSOR_PREFIX}${start}` : null,
|
|
352
465
|
hasMore: start > 0,
|
|
353
466
|
pageSize: selected.length,
|
|
354
|
-
totalItems: visibleMessages.length
|
|
467
|
+
totalItems: visibleMessages.length,
|
|
468
|
+
olderCursor: start > 0 ? `${CURSOR_PREFIX}${start}` : null,
|
|
469
|
+
newerCursor: end < visibleMessages.length ? `${CURSOR_PREFIX}${end}` : null,
|
|
470
|
+
kind,
|
|
471
|
+
revision
|
|
355
472
|
}
|
|
356
473
|
};
|
|
357
474
|
}
|
|
475
|
+
function selectHistoryRange(messages, pageSize, query, revision) {
|
|
476
|
+
if (query.anchor) {
|
|
477
|
+
const target = decodeSessionHistoryAnchor(query.anchor, revision);
|
|
478
|
+
if (target >= messages.length || messages[target]?.role !== "user") {
|
|
479
|
+
throw new StaleSessionAnchorError;
|
|
480
|
+
}
|
|
481
|
+
const nominalStart2 = Math.max(0, target - Math.floor(pageSize / 2));
|
|
482
|
+
const start = findTurnBoundaryStart(messages, nominalStart2);
|
|
483
|
+
const requiredPageSize = Math.max(pageSize, target - start + 1);
|
|
484
|
+
return {
|
|
485
|
+
start,
|
|
486
|
+
end: findTurnBoundaryEnd(messages, start, requiredPageSize),
|
|
487
|
+
kind: "anchored"
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (query.after) {
|
|
491
|
+
const start = decodeCursor(query.after, messages.length);
|
|
492
|
+
return {
|
|
493
|
+
start,
|
|
494
|
+
end: findTurnBoundaryEnd(messages, start, pageSize),
|
|
495
|
+
kind: "anchored"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const end = decodeCursor(query.before, messages.length);
|
|
499
|
+
const nominalStart = Math.max(0, end - pageSize);
|
|
500
|
+
return {
|
|
501
|
+
start: findTurnBoundaryStart(messages, nominalStart),
|
|
502
|
+
end,
|
|
503
|
+
kind: "latest"
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function findTurnBoundaryEnd(messages, start, pageSize) {
|
|
507
|
+
let end = Math.min(messages.length, start + pageSize);
|
|
508
|
+
while (end < messages.length && messages[end]?.role !== "user")
|
|
509
|
+
end += 1;
|
|
510
|
+
return end;
|
|
511
|
+
}
|
|
358
512
|
function findTurnBoundaryStart(messages, nominalStart) {
|
|
359
513
|
if (nominalStart <= 0 || messages[nominalStart]?.role === "user") {
|
|
360
514
|
return nominalStart;
|
|
@@ -399,6 +553,42 @@ function selectPageEvents(events, toolCallIds) {
|
|
|
399
553
|
selected.unshift(latestContextUsage);
|
|
400
554
|
return selected;
|
|
401
555
|
}
|
|
556
|
+
// src/domain/session/SessionQueryIndexCache.ts
|
|
557
|
+
class SessionQueryIndexCache {
|
|
558
|
+
capacity;
|
|
559
|
+
entries = new Map;
|
|
560
|
+
constructor(capacity = 24) {
|
|
561
|
+
this.capacity = capacity;
|
|
562
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
563
|
+
throw new Error("Session query index cache capacity must be positive");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
getOrCreate(sessionId, messages) {
|
|
567
|
+
const revision = computeSessionHistoryRevision(messages);
|
|
568
|
+
const cached = this.entries.get(sessionId);
|
|
569
|
+
if (cached?.revision === revision) {
|
|
570
|
+
this.entries.delete(sessionId);
|
|
571
|
+
this.entries.set(sessionId, cached);
|
|
572
|
+
return cached;
|
|
573
|
+
}
|
|
574
|
+
const created = buildSessionQueryIndex(sessionId, messages, revision);
|
|
575
|
+
this.entries.delete(sessionId);
|
|
576
|
+
this.entries.set(sessionId, created);
|
|
577
|
+
while (this.entries.size > this.capacity) {
|
|
578
|
+
const oldest = this.entries.keys().next().value;
|
|
579
|
+
if (oldest === undefined)
|
|
580
|
+
break;
|
|
581
|
+
this.entries.delete(oldest);
|
|
582
|
+
}
|
|
583
|
+
return created;
|
|
584
|
+
}
|
|
585
|
+
invalidate(sessionId) {
|
|
586
|
+
this.entries.delete(sessionId);
|
|
587
|
+
}
|
|
588
|
+
clear() {
|
|
589
|
+
this.entries.clear();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
402
592
|
// src/domain/session/SessionTitle.ts
|
|
403
593
|
var NEW_SESSION_PLACEHOLDER_TITLE = "新会话";
|
|
404
594
|
var AUTO_TITLE_PENDING_METADATA_KEY = "autoTitleFromFirstMessage";
|
|
@@ -12213,6 +12403,7 @@ export {
|
|
|
12213
12403
|
updateQueuedSessionMessage,
|
|
12214
12404
|
toolApprovalDecisionFromAnswer,
|
|
12215
12405
|
sessionQueueItemKind,
|
|
12406
|
+
sessionHistoryMessageId,
|
|
12216
12407
|
reorderQueuedSessionMessages,
|
|
12217
12408
|
reorderQueuedSessionGoals,
|
|
12218
12409
|
registerBuiltinTools,
|
|
@@ -12245,11 +12436,13 @@ export {
|
|
|
12245
12436
|
describeCron,
|
|
12246
12437
|
defaultPinnedCommands,
|
|
12247
12438
|
consumePendingAutoTitle,
|
|
12439
|
+
computeSessionHistoryRevision,
|
|
12248
12440
|
computeNextFireAt,
|
|
12249
12441
|
classifyToolPermission,
|
|
12250
12442
|
classifyFileContent,
|
|
12251
12443
|
cancelSessionGoal,
|
|
12252
12444
|
cancelQueuedSessionMessage,
|
|
12445
|
+
buildSessionQueryIndex,
|
|
12253
12446
|
WriteFileTool,
|
|
12254
12447
|
WebSearchTool,
|
|
12255
12448
|
WebFetchTool,
|
|
@@ -12268,8 +12461,10 @@ export {
|
|
|
12268
12461
|
TerminalSession,
|
|
12269
12462
|
TOOL_PERMISSION_MODES,
|
|
12270
12463
|
TOOL_APPROVAL_OPTIONS,
|
|
12464
|
+
StaleSessionAnchorError,
|
|
12271
12465
|
SkillRegistry,
|
|
12272
12466
|
SkillLoader,
|
|
12467
|
+
SessionQueryIndexCache,
|
|
12273
12468
|
SessionGoalCoordinator,
|
|
12274
12469
|
ScrollbackBuffer,
|
|
12275
12470
|
SQLiteWebConsoleStore,
|
|
@@ -12333,4 +12528,4 @@ export {
|
|
|
12333
12528
|
AUTO_TITLE_PENDING_METADATA_KEY
|
|
12334
12529
|
};
|
|
12335
12530
|
|
|
12336
|
-
//# debugId=
|
|
12531
|
+
//# debugId=06235227BBB1532064756E2164756E21
|