@deepseek-ai/dsh-session-title 0.1.2-alpha.3 → 0.1.2-alpha.4
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/lib/index.js +10 -8
- package/lib/invariant.js +28 -7
- package/lib/types/index.d.ts +2 -1
- package/lib/types/index.js +10 -8
- package/lib/types/invariant.js +37 -8
- package/lib/types/types.d.ts +5 -4
- package/package.json +16 -16
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
import { z as z$1 } from "zod";
|
|
4
4
|
import { isAgentLoopRequest } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { assertNever, deepFreeze } from "@deepseek-ai/dsh-util-values";
|
|
6
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
6
7
|
//#region lib/types/normalize.js
|
|
7
8
|
/** Title text normalization and UTF-8-safe truncation. */
|
|
8
9
|
/** Operating-system-command escape sequences, including unterminated tails. */
|
|
@@ -133,13 +134,13 @@ const EMPTY_TITLE_INPUT = {
|
|
|
133
134
|
lastSeq: null
|
|
134
135
|
};
|
|
135
136
|
const sessionTitleUserMessageSchema = z$1.object({
|
|
136
|
-
seq: z$1.number().int().nonnegative(),
|
|
137
|
+
seq: z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
137
138
|
text: z$1.string()
|
|
138
139
|
}).strict();
|
|
139
140
|
const titleInputStateSchema = z$1.object({
|
|
140
141
|
first: sessionTitleUserMessageSchema.nullable(),
|
|
141
142
|
count: z$1.number().int().nonnegative(),
|
|
142
|
-
lastSeq: z$1.number().int().nonnegative().nullable()
|
|
143
|
+
lastSeq: z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq).nullable()
|
|
143
144
|
}).strict().superRefine((state, context) => {
|
|
144
145
|
const empty = state.first === null && state.lastSeq === null && state.count === 0;
|
|
145
146
|
const populated = state.first !== null && state.lastSeq !== null && state.count > 0 && state.first.seq <= state.lastSeq;
|
|
@@ -278,7 +279,7 @@ var SessionTitleService = class extends Service {
|
|
|
278
279
|
* @returns latest title snapshot, or `undefined` before eligible input.
|
|
279
280
|
*/
|
|
280
281
|
get(session) {
|
|
281
|
-
return foldSessionTitle(session.
|
|
282
|
+
return foldSessionTitle(session.snapshotEvents());
|
|
282
283
|
}
|
|
283
284
|
/**
|
|
284
285
|
* Accept an explicit user title. Appends a `session/title` event with the
|
|
@@ -452,7 +453,7 @@ var SessionTitleService = class extends Service {
|
|
|
452
453
|
this.assertCurrent(session, work);
|
|
453
454
|
await this.ensureFallback(session);
|
|
454
455
|
this.assertCurrent(session, work);
|
|
455
|
-
const messages = collectSessionTitleMessages(session.
|
|
456
|
+
const messages = collectSessionTitleMessages(session.snapshotEvents(), work.throughSeq);
|
|
456
457
|
const result = await work.registration.provider.generate({
|
|
457
458
|
session,
|
|
458
459
|
messages,
|
|
@@ -488,10 +489,11 @@ var SessionTitleService = class extends Service {
|
|
|
488
489
|
const order = new Map(messages.map((message, index) => [message.seq, index]));
|
|
489
490
|
let previous = -1;
|
|
490
491
|
for (const seq of candidate.messageSeqs) {
|
|
491
|
-
if (typeof seq !== "number") throw new Error("session-title provider messageSeqs must be unique, ordered seqs from the request");
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
492
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) throw new Error("session-title provider messageSeqs must be unique, ordered seqs from the request");
|
|
493
|
+
const sessionSeq = SessionSeq(seq);
|
|
494
|
+
const index = order.get(sessionSeq);
|
|
495
|
+
if (index === void 0 || index <= previous) throw new Error("session-title provider messageSeqs must be unique, ordered seqs from the request");
|
|
496
|
+
messageSeqs.push(sessionSeq);
|
|
495
497
|
previous = index;
|
|
496
498
|
}
|
|
497
499
|
const modelCandidate = candidate.model;
|
package/lib/invariant.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
1
2
|
//#region lib/types/invariant.js
|
|
2
3
|
/**
|
|
3
4
|
* Package-owned invariant companion for `@deepseek-ai/dsh-session-title`.
|
|
@@ -16,16 +17,36 @@ const inject = ["invariants"];
|
|
|
16
17
|
* relationship every appended `session/title` event must keep, whichever
|
|
17
18
|
* writer produced it.
|
|
18
19
|
*/
|
|
20
|
+
function validate(session, event, fail) {
|
|
21
|
+
const { source, messageSeqs } = event.data;
|
|
22
|
+
if (messageSeqs.length === 0 !== (source.kind === "user")) {
|
|
23
|
+
const requirement = source.kind === "user" ? "cite no message seqs" : "cite at least one message seq";
|
|
24
|
+
fail(`session/title event ${String(event.seq)} with source "${source.kind}" must ${requirement}; got ${String(messageSeqs.length)}`);
|
|
25
|
+
}
|
|
26
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const seq of messageSeqs) {
|
|
28
|
+
let checked;
|
|
29
|
+
try {
|
|
30
|
+
checked = SessionSeq(seq);
|
|
31
|
+
} catch {
|
|
32
|
+
fail(`session/title event ${String(event.seq)} has an invalid message seq ${String(seq)}`);
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(checked)) fail(`session/title event ${String(event.seq)} repeats message seq ${checked}`);
|
|
35
|
+
seen.add(checked);
|
|
36
|
+
const cited = checked < event.seq ? session.eventAt(checked) : void 0;
|
|
37
|
+
if (cited?.type !== "user/message" || cited.data.source.kind !== "user") fail(`session/title event ${String(event.seq)} message seq ${checked} must name an earlier human user/message`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
19
40
|
const install = Object.assign((ctx, fail) => {
|
|
41
|
+
const validateExisting = (session) => {
|
|
42
|
+
for (const event of session.snapshotEvents()) if (event.type === "session/title") validate(session, event, fail);
|
|
43
|
+
};
|
|
44
|
+
ctx.sessions.list().forEach(validateExisting);
|
|
45
|
+
ctx.on("session/created", validateExisting, { global: true });
|
|
20
46
|
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
21
47
|
if (eventName !== "session/event") return;
|
|
22
|
-
const [, event] = args;
|
|
23
|
-
if (event.type
|
|
24
|
-
const { source, messageSeqs } = event.data;
|
|
25
|
-
if (messageSeqs.length === 0 !== (source.kind === "user")) {
|
|
26
|
-
const requirement = source.kind === "user" ? "cite no message seqs" : "cite at least one message seq";
|
|
27
|
-
fail(`session/title event ${String(event.seq)} with source "${source.kind}" must ${requirement}; got ${String(messageSeqs.length)}`);
|
|
28
|
-
}
|
|
48
|
+
const [session, event] = args;
|
|
49
|
+
if (event.type === "session/title") validate(session, event, fail);
|
|
29
50
|
}, { global: true });
|
|
30
51
|
}, { inject: ["sessions"] });
|
|
31
52
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
7
7
|
import { z as zod } from 'zod';
|
|
8
8
|
import type { Branded } from '@deepseek-ai/dsh-brand';
|
|
9
9
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
10
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
10
11
|
export type { SessionTitleEventData, SessionTitleModelProvenance, SessionTitleSnapshot, SessionTitleSource, SessionTitleUserMessage, TitleProjection, } from './types.ts';
|
|
11
12
|
import type { SessionTitleEventData, SessionTitleModelProvenance, SessionTitleSnapshot, SessionTitleUserMessage } from './types.ts';
|
|
12
13
|
/** Identifies one session-title provider registration. */
|
|
@@ -68,7 +69,7 @@ export interface SessionTitleProviderResult {
|
|
|
68
69
|
/** Proposed title text. */
|
|
69
70
|
readonly title: string;
|
|
70
71
|
/** Exact seqs from `request.messages` used by this result. */
|
|
71
|
-
readonly messageSeqs: readonly
|
|
72
|
+
readonly messageSeqs: readonly SessionSeq[];
|
|
72
73
|
/** Auxiliary LLM route, when generation used a model. */
|
|
73
74
|
readonly model?: SessionTitleModelProvenance;
|
|
74
75
|
}
|
package/lib/types/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
7
7
|
import { z as zod } from 'zod';
|
|
8
8
|
import { isAgentLoopRequest } from '@deepseek-ai/dsh-llm';
|
|
9
9
|
import { assertNever, deepFreeze } from '@deepseek-ai/dsh-util-values';
|
|
10
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
10
11
|
import { fallbackSessionTitle, normalizeSessionTitle } from "./normalize.js";
|
|
11
12
|
/**
|
|
12
13
|
* Brand a raw provider id.
|
|
@@ -75,13 +76,13 @@ function titleSnapshotFromState(state) {
|
|
|
75
76
|
}
|
|
76
77
|
const EMPTY_TITLE_INPUT = { first: null, count: 0, lastSeq: null };
|
|
77
78
|
const sessionTitleUserMessageSchema = zod.object({
|
|
78
|
-
seq: zod.number().int().nonnegative(),
|
|
79
|
+
seq: zod.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
79
80
|
text: zod.string(),
|
|
80
81
|
}).strict();
|
|
81
82
|
const titleInputStateSchema = zod.object({
|
|
82
83
|
first: sessionTitleUserMessageSchema.nullable(),
|
|
83
84
|
count: zod.number().int().nonnegative(),
|
|
84
|
-
lastSeq: zod.number().int().nonnegative().nullable(),
|
|
85
|
+
lastSeq: zod.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq).nullable(),
|
|
85
86
|
}).strict().superRefine((state, context) => {
|
|
86
87
|
const empty = state.first === null && state.lastSeq === null && state.count === 0;
|
|
87
88
|
const populated = state.first !== null
|
|
@@ -235,7 +236,7 @@ export class SessionTitleService extends Service {
|
|
|
235
236
|
* @returns latest title snapshot, or `undefined` before eligible input.
|
|
236
237
|
*/
|
|
237
238
|
get(session) {
|
|
238
|
-
return foldSessionTitle(session.
|
|
239
|
+
return foldSessionTitle(session.snapshotEvents());
|
|
239
240
|
}
|
|
240
241
|
/**
|
|
241
242
|
* Accept an explicit user title. Appends a `session/title` event with the
|
|
@@ -439,7 +440,7 @@ export class SessionTitleService extends Service {
|
|
|
439
440
|
this.assertCurrent(session, work);
|
|
440
441
|
await this.ensureFallback(session);
|
|
441
442
|
this.assertCurrent(session, work);
|
|
442
|
-
const messages = collectSessionTitleMessages(session.
|
|
443
|
+
const messages = collectSessionTitleMessages(session.snapshotEvents(), work.throughSeq);
|
|
443
444
|
const result = await work.registration.provider.generate({
|
|
444
445
|
session,
|
|
445
446
|
messages,
|
|
@@ -483,14 +484,15 @@ export class SessionTitleService extends Service {
|
|
|
483
484
|
const order = new Map(messages.map((message, index) => [message.seq, index]));
|
|
484
485
|
let previous = -1;
|
|
485
486
|
for (const seq of candidate.messageSeqs) {
|
|
486
|
-
if (typeof seq !== 'number') {
|
|
487
|
+
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0) {
|
|
487
488
|
throw new Error('session-title provider messageSeqs must be unique, ordered seqs from the request');
|
|
488
489
|
}
|
|
489
|
-
const
|
|
490
|
-
|
|
490
|
+
const sessionSeq = SessionSeq(seq);
|
|
491
|
+
const index = order.get(sessionSeq);
|
|
492
|
+
if (index === undefined || index <= previous) {
|
|
491
493
|
throw new Error('session-title provider messageSeqs must be unique, ordered seqs from the request');
|
|
492
494
|
}
|
|
493
|
-
messageSeqs.push(
|
|
495
|
+
messageSeqs.push(sessionSeq);
|
|
494
496
|
previous = index;
|
|
495
497
|
}
|
|
496
498
|
const modelCandidate = candidate.model;
|
package/lib/types/invariant.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Package-owned invariant companion for `@deepseek-ai/dsh-session-title`.
|
|
3
3
|
* @module @deepseek-ai/dsh-session-title/invariant
|
|
4
4
|
*/
|
|
5
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
5
6
|
const PACKAGE_NAME = '@deepseek-ai/dsh-session-title';
|
|
6
7
|
/** Cordis companion plugin name. */
|
|
7
8
|
export const name = 'session-title-invariant';
|
|
@@ -15,20 +16,48 @@ export const inject = ['invariants'];
|
|
|
15
16
|
* relationship every appended `session/title` event must keep, whichever
|
|
16
17
|
* writer produced it.
|
|
17
18
|
*/
|
|
19
|
+
function validate(session, event, fail) {
|
|
20
|
+
const { source, messageSeqs } = event.data;
|
|
21
|
+
if ((messageSeqs.length === 0) !== (source.kind === 'user')) {
|
|
22
|
+
const requirement = source.kind === 'user' ? 'cite no message seqs' : 'cite at least one message seq';
|
|
23
|
+
fail(`session/title event ${String(event.seq)} with source "${source.kind}" must ${requirement}; got ${String(messageSeqs.length)}`);
|
|
24
|
+
}
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const seq of messageSeqs) {
|
|
27
|
+
let checked;
|
|
28
|
+
try {
|
|
29
|
+
checked = SessionSeq(seq);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
fail(`session/title event ${String(event.seq)} has an invalid message seq ${String(seq)}`);
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(checked)) {
|
|
35
|
+
fail(`session/title event ${String(event.seq)} repeats message seq ${checked}`);
|
|
36
|
+
}
|
|
37
|
+
seen.add(checked);
|
|
38
|
+
const cited = checked < event.seq ? session.eventAt(checked) : undefined;
|
|
39
|
+
if (cited?.type !== 'user/message' || cited.data.source.kind !== 'user') {
|
|
40
|
+
fail(`session/title event ${String(event.seq)} message seq ${checked} must name an earlier human user/message`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
18
44
|
const install = Object.assign((ctx, fail) => {
|
|
45
|
+
const validateExisting = (session) => {
|
|
46
|
+
for (const event of session.snapshotEvents()) {
|
|
47
|
+
if (event.type === 'session/title')
|
|
48
|
+
validate(session, event, fail);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
ctx.sessions.list().forEach(validateExisting);
|
|
52
|
+
ctx.on('session/created', validateExisting, { global: true });
|
|
19
53
|
// internal/dispatch interception rejects the append before publication
|
|
20
54
|
// (the session/event listener would only observe the already-committed log).
|
|
21
55
|
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
22
56
|
if (eventName !== 'session/event')
|
|
23
57
|
return;
|
|
24
|
-
const [, event] = args;
|
|
25
|
-
if (event.type
|
|
26
|
-
|
|
27
|
-
const { source, messageSeqs } = event.data;
|
|
28
|
-
if ((messageSeqs.length === 0) !== (source.kind === 'user')) {
|
|
29
|
-
const requirement = source.kind === 'user' ? 'cite no message seqs' : 'cite at least one message seq';
|
|
30
|
-
fail(`session/title event ${String(event.seq)} with source "${source.kind}" must ${requirement}; got ${String(messageSeqs.length)}`);
|
|
31
|
-
}
|
|
58
|
+
const [session, event] = args;
|
|
59
|
+
if (event.type === 'session/title')
|
|
60
|
+
validate(session, event, fail);
|
|
32
61
|
}, { global: true });
|
|
33
62
|
}, { inject: ['sessions'] });
|
|
34
63
|
/**
|
package/lib/types/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export {};
|
|
11
11
|
import type { Branded } from '@deepseek-ai/dsh-brand';
|
|
12
|
+
import type { OptionalSessionSeq, SessionSeq } from '@deepseek-ai/dsh-session/types';
|
|
12
13
|
/** Identifies one session-title provider registration. */
|
|
13
14
|
export type SessionTitleProviderId = Branded<'SessionTitleProviderId'>;
|
|
14
15
|
/** Exact auxiliary model route that produced a title. */
|
|
@@ -34,14 +35,14 @@ export interface SessionTitleEventData {
|
|
|
34
35
|
/** Normalized non-empty title text. */
|
|
35
36
|
readonly title: string;
|
|
36
37
|
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
|
|
37
|
-
readonly messageSeqs:
|
|
38
|
+
readonly messageSeqs: SessionSeq[];
|
|
38
39
|
/** Whether the built-in fallback, a registered provider, or the user supplied the title. */
|
|
39
40
|
readonly source: SessionTitleSource;
|
|
40
41
|
}
|
|
41
42
|
/** Latest folded title plus the title event's durable envelope facts. */
|
|
42
43
|
export interface SessionTitleSnapshot extends SessionTitleEventData {
|
|
43
44
|
/** Seq of the latest `session/title` event. */
|
|
44
|
-
readonly eventSeq:
|
|
45
|
+
readonly eventSeq: SessionSeq;
|
|
45
46
|
/** Timestamp of the latest `session/title` event. */
|
|
46
47
|
readonly updatedAt: number;
|
|
47
48
|
}
|
|
@@ -50,7 +51,7 @@ export type TitleProjection = SessionTitleSnapshot;
|
|
|
50
51
|
/** One eligible human text message exposed to title providers. */
|
|
51
52
|
export interface SessionTitleUserMessage {
|
|
52
53
|
/** Source `user/message` event seq. */
|
|
53
|
-
readonly seq:
|
|
54
|
+
readonly seq: SessionSeq;
|
|
54
55
|
/** Exact concatenated text-block content. */
|
|
55
56
|
readonly text: string;
|
|
56
57
|
}
|
|
@@ -61,7 +62,7 @@ export interface TitleInputState {
|
|
|
61
62
|
/** Total eligible messages folded so far. */
|
|
62
63
|
readonly count: number;
|
|
63
64
|
/** Seq of the newest eligible message, or null before any. */
|
|
64
|
-
readonly lastSeq:
|
|
65
|
+
readonly lastSeq: OptionalSessionSeq;
|
|
65
66
|
}
|
|
66
67
|
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
67
68
|
interface SessionProjectionStateMap {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session-title",
|
|
3
3
|
"description": "Log-backed session title service and provider registry for the DeepSeek Harness",
|
|
4
|
-
"version": "0.1.2-alpha.
|
|
4
|
+
"version": "0.1.2-alpha.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -42,27 +42,27 @@
|
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
45
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.4",
|
|
46
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.4",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.4",
|
|
48
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.4",
|
|
49
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.4",
|
|
50
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.4"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"zod": "^4.4.3",
|
|
54
54
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
55
|
-
"@deepseek-ai/dsh-util-values": "^0.1.2-alpha.
|
|
55
|
+
"@deepseek-ai/dsh-util-values": "^0.1.2-alpha.4"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
59
|
-
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.
|
|
60
|
-
"@deepseek-ai/dsh-agent-loop": "^0.1.2-alpha.
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/dsh-session
|
|
59
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.4",
|
|
60
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.2-alpha.4",
|
|
61
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.4",
|
|
62
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.4",
|
|
63
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-alpha.4",
|
|
64
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.4",
|
|
65
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.4",
|
|
66
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.4"
|
|
67
67
|
}
|
|
68
68
|
}
|