dsh-context-compression-improved 0.3.0 → 0.4.0-beta.1

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 (41) hide show
  1. package/.githooks/pre-push +37 -0
  2. package/package.json +3 -2
  3. package/packages/selector/cordis.patch.yml +12 -5
  4. package/packages/selector/lib/client.d.ts +24 -0
  5. package/packages/selector/lib/client.js +506 -5
  6. package/packages/selector/lib/config.js +27 -4
  7. package/packages/selector/lib/index.d.ts +7 -0
  8. package/packages/selector/lib/index.js +229 -1
  9. package/packages/selector/lib/pruner.d.ts +254 -0
  10. package/packages/selector/lib/pruner.js +714 -25
  11. package/packages/selector/package.json +0 -1
  12. package/packages/selector/src/client/EstimatorControls.tsx +101 -0
  13. package/packages/selector/src/client/ReviewOverlay.tsx +320 -0
  14. package/packages/selector/src/client/index.ts +17 -0
  15. package/packages/selector/src/client/locales.ts +38 -0
  16. package/packages/selector/src/client/preset-options.ts +1 -0
  17. package/packages/selector/src/client/review-scope.ts +16 -0
  18. package/packages/selector/src/client/settings-section.tsx +17 -8
  19. package/packages/selector/src/index.ts +308 -0
  20. package/packages/selector/src/profiles.ts +28 -1
  21. package/packages/selector/src/pruner/state.ts +27 -0
  22. package/packages/selector/src/pruner.ts +430 -10
  23. package/packages/selector/src/runtime/audit.ts +27 -0
  24. package/packages/selector/src/runtime/config.ts +33 -1
  25. package/packages/selector/src/runtime/tokenpilot/estimator.ts +60 -13
  26. package/packages/selector/src/runtime/tokenpilot/proposal.ts +223 -0
  27. package/packages/selector/src/runtime/tokenpilot/review-queue.ts +231 -0
  28. package/packages/selector/src/runtime/tokenpilot/review-storage.ts +122 -0
  29. package/packages/selector/src/runtime/types.ts +17 -0
  30. package/packages/selector/tests/code-skeleton.client.spec.ts +3 -2
  31. package/packages/selector/tests/custom-contract.client.spec.ts +3 -2
  32. package/packages/selector/tests/preset-options-write.client.spec.ts +34 -1
  33. package/packages/selector/tests/review-overlay.client.spec.tsx +118 -0
  34. package/packages/selector/tests/review-routes.host.spec.ts +290 -0
  35. package/packages/selector/tests/runtime/audit.spec.ts +44 -0
  36. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +23 -0
  37. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +5 -0
  38. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +199 -0
  39. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +313 -0
  40. package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +168 -0
  41. package/packages/selector/tests/settings-seat.client.spec.ts +5 -4
@@ -33,6 +33,302 @@ const ESTIMATOR_CATALOG_ROUTES = [
33
33
  '/api/dsh-context-compression-improved/estimator-catalog',
34
34
  ] as const
35
35
 
36
+ // TokenPilot-inspired R4: the review pipeline's client↔runtime channel, dual
37
+ // prefixed like the catalog route so 0.1.2 clients keep working.
38
+ const REVIEW_QUEUE_ROUTES = [
39
+ '/endpoint/dsh-context-compression-improved/review-queue',
40
+ '/api/dsh-context-compression-improved/review-queue',
41
+ ] as const
42
+ const REVIEW_DECIDE_ROUTES = [
43
+ '/endpoint/dsh-context-compression-improved/review-decide',
44
+ '/api/dsh-context-compression-improved/review-decide',
45
+ ] as const
46
+
47
+ /** The review faces of the pruner service the routes consume. */
48
+ interface ReviewPrunerLike {
49
+ listReviewProposals(session: unknown): readonly {
50
+ readonly id: string
51
+ readonly kind: string
52
+ readonly items: readonly {
53
+ readonly seq: number
54
+ readonly kind: string
55
+ readonly component: string
56
+ readonly tokensBefore: number
57
+ readonly tokensAfter: number
58
+ }[]
59
+ readonly benefit: {
60
+ readonly recoveredTokens: number
61
+ readonly penaltyTokens: number
62
+ readonly paybackTurns?: number
63
+ readonly expectedSaving?: number
64
+ }
65
+ readonly enqueuedTurn: number
66
+ readonly lastTurnIndex: number
67
+ }[]
68
+ decideReviewProposal(
69
+ session: unknown,
70
+ proposalId: string,
71
+ decision: 'approved' | 'rejected' | 'ignored',
72
+ ): { ok: true } | { ok: false, reason: string } | undefined
73
+ /** Aggregate pending read; absent on older builds (routes then degrade to 503). */
74
+ listAllReviewProposals?(): readonly {
75
+ readonly sessionId: string
76
+ readonly proposals: readonly {
77
+ readonly id: string
78
+ readonly kind: string
79
+ readonly items: readonly { readonly seq: number, readonly kind: string, readonly component: string, readonly tokensBefore: number, readonly tokensAfter: number }[]
80
+ readonly benefit: { readonly recoveredTokens: number, readonly penaltyTokens: number, readonly paybackTurns?: number, readonly expectedSaving?: number }
81
+ readonly enqueuedTurn: number
82
+ readonly lastTurnIndex: number
83
+ }[]
84
+ }[]
85
+ reviewSummary?(session: unknown): {
86
+ readonly autoApplied: number
87
+ readonly reviewApplied: number
88
+ readonly expired: number
89
+ readonly voided: number
90
+ }
91
+ }
92
+
93
+ /** Minimal face of the agents service: session id → agent (carrying the session). */
94
+ interface AgentsServiceLike {
95
+ get?(id: unknown): { session?: unknown } | undefined
96
+ }
97
+
98
+ function reviewPrunerOf(readService: (name: string) => unknown): ReviewPrunerLike | undefined {
99
+ const candidate = readService('toolResultPruner') as {
100
+ listReviewProposals?: unknown
101
+ decideReviewProposal?: unknown
102
+ } | undefined
103
+ return typeof candidate?.listReviewProposals === 'function'
104
+ && typeof candidate?.decideReviewProposal === 'function'
105
+ ? candidate as unknown as ReviewPrunerLike
106
+ : undefined
107
+ }
108
+
109
+ function sessionFor(readService: (name: string) => unknown, sessionId: string): unknown {
110
+ const agents = readService('agents') as AgentsServiceLike | undefined
111
+ return typeof agents?.get === 'function' ? agents.get(sessionId)?.session : undefined
112
+ }
113
+
114
+ function reviewJson(res: unknown, status: number, body: unknown): void {
115
+ const resTyped = res as {
116
+ writeHead: (code: number, headers?: Record<string, string>) => void
117
+ end: (body?: string) => void
118
+ }
119
+ resTyped.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
120
+ resTyped.end(JSON.stringify(body))
121
+ }
122
+
123
+ function readRequestBody(req: unknown): Promise<string> {
124
+ return new Promise((resolve, reject) => {
125
+ const typed = req as {
126
+ on?: (event: string, listener: (chunk?: Buffer) => void) => void
127
+ }
128
+ let data = ''
129
+ try {
130
+ typed.on?.('data', chunk => {
131
+ data += String(chunk ?? '')
132
+ if (data.length > 64 * 1024) {
133
+ data = ''
134
+ resolve('')
135
+ }
136
+ })
137
+ typed.on?.('end', () => resolve(data))
138
+ typed.on?.('error', reject)
139
+ } catch (error) {
140
+ reject(error instanceof Error ? error : new Error(String(error)))
141
+ }
142
+ })
143
+ }
144
+
145
+ /**
146
+ * Serve the review pipeline's two HTTP routes (best effort, mirroring the
147
+ * estimator-catalog registration):
148
+ *
149
+ * - `GET .../review-queue?sessionId=…` → the session's pending proposals with
150
+ * their benefit numbers. Sanitized by construction: the queue never holds
151
+ * message content, and the response carries ids/seqs/counts only (digests
152
+ * stay in the runtime — the client cannot need them).
153
+ * - `POST .../review-decide` `{sessionId, proposalId, decision}` → one human
154
+ * decision. Invalid body → 400; unknown/not-pending proposal → 404; review
155
+ * mode off for the session → 503.
156
+ */
157
+ function registerReviewQueueRoutes(ctx: Context): void {
158
+ const readService = (name: string): unknown => {
159
+ try {
160
+ return (ctx as unknown as { get: (service: string) => unknown }).get(name)
161
+ } catch {
162
+ return undefined
163
+ }
164
+ }
165
+ const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
166
+ console[level](message, ...args)
167
+ }
168
+ const registered = (): void => {
169
+ log('info', 'context-compression review queue routes registered: %s / %s', REVIEW_QUEUE_ROUTES.join(', '), REVIEW_DECIDE_ROUTES.join(', '))
170
+ }
171
+
172
+ type SanitizedProposal = {
173
+ sessionId: string
174
+ id: string
175
+ kind: string
176
+ items: readonly { seq: number, kind: string, component: string, tokensBefore: number, tokensAfter: number }[]
177
+ benefit: {
178
+ recoveredTokens: number
179
+ penaltyTokens: number
180
+ paybackTurns?: number
181
+ expectedSaving?: number
182
+ }
183
+ enqueuedTurn: number
184
+ lastTurnIndex: number
185
+ }
186
+
187
+ const getHandler = (req: unknown, res: unknown): void => {
188
+ const pruner = reviewPrunerOf(readService)
189
+ if (pruner === undefined || pruner.listAllReviewProposals === undefined) {
190
+ reviewJson(res, 503, { ok: false, error: 'review pipeline unavailable' })
191
+ return
192
+ }
193
+ let sessionId = ''
194
+ try {
195
+ const url = new URL(String((req as { url?: string }).url ?? ''), 'http://localhost')
196
+ sessionId = url.searchParams.get('sessionId') ?? ''
197
+ } catch {
198
+ sessionId = ''
199
+ }
200
+ const sanitize = (
201
+ sid: string,
202
+ proposal: {
203
+ id: string
204
+ kind: string
205
+ items: readonly { seq: number, kind: string, component: string, tokensBefore: number, tokensAfter: number }[]
206
+ benefit: { recoveredTokens: number, penaltyTokens: number, paybackTurns?: number, expectedSaving?: number }
207
+ enqueuedTurn: number
208
+ lastTurnIndex: number
209
+ },
210
+ ): SanitizedProposal => ({
211
+ sessionId: sid,
212
+ id: proposal.id,
213
+ kind: proposal.kind,
214
+ items: proposal.items.map(item => ({
215
+ seq: item.seq,
216
+ kind: item.kind,
217
+ component: item.component,
218
+ tokensBefore: item.tokensBefore,
219
+ tokensAfter: item.tokensAfter,
220
+ })),
221
+ benefit: {
222
+ recoveredTokens: proposal.benefit.recoveredTokens,
223
+ penaltyTokens: proposal.benefit.penaltyTokens,
224
+ ...(proposal.benefit.paybackTurns === undefined ? {} : { paybackTurns: proposal.benefit.paybackTurns }),
225
+ ...(proposal.benefit.expectedSaving === undefined ? {} : { expectedSaving: proposal.benefit.expectedSaving }),
226
+ },
227
+ enqueuedTurn: proposal.enqueuedTurn,
228
+ lastTurnIndex: proposal.lastTurnIndex,
229
+ })
230
+
231
+ // Without a sessionId the read aggregates every session with live
232
+ // proposals — the client carries no session id of its own.
233
+ if (sessionId === '') {
234
+ const pending = pruner.listAllReviewProposals()
235
+ .flatMap(entry => entry.proposals.map(proposal => sanitize(entry.sessionId, proposal)))
236
+ reviewJson(res, 200, { ok: true, total: pending.length, pending })
237
+ return
238
+ }
239
+
240
+ const session = sessionFor(readService, sessionId)
241
+ if (session === undefined) {
242
+ reviewJson(res, 404, { ok: false, error: 'unknown session' })
243
+ return
244
+ }
245
+ const pending = pruner.listReviewProposals(session).map(proposal => sanitize(sessionId, proposal))
246
+ const summary = pruner.reviewSummary?.(session)
247
+ reviewJson(res, 200, {
248
+ ok: true,
249
+ sessionId,
250
+ total: pending.length,
251
+ pending,
252
+ ...(summary === undefined ? {} : { summary }),
253
+ })
254
+ }
255
+
256
+ const decideHandler = async (req: unknown, res: unknown): Promise<void> => {
257
+ const pruner = reviewPrunerOf(readService)
258
+ if (pruner === undefined) {
259
+ reviewJson(res, 503, { ok: false, error: 'review pipeline unavailable' })
260
+ return
261
+ }
262
+ let body: unknown
263
+ try {
264
+ body = JSON.parse(await readRequestBody(req))
265
+ } catch {
266
+ body = undefined
267
+ }
268
+ if (typeof body !== 'object' || body === null) {
269
+ reviewJson(res, 400, { ok: false, error: 'invalid JSON body' })
270
+ return
271
+ }
272
+ const record = body as { sessionId?: unknown, proposalId?: unknown, decision?: unknown }
273
+ if (typeof record.sessionId !== 'string' || record.sessionId === ''
274
+ || typeof record.proposalId !== 'string' || record.proposalId === '') {
275
+ reviewJson(res, 400, { ok: false, error: 'sessionId and proposalId are required' })
276
+ return
277
+ }
278
+ if (record.decision !== 'approved' && record.decision !== 'rejected' && record.decision !== 'ignored') {
279
+ reviewJson(res, 400, { ok: false, error: 'decision must be approved, rejected, or ignored' })
280
+ return
281
+ }
282
+ const session = sessionFor(readService, record.sessionId)
283
+ if (session === undefined) {
284
+ reviewJson(res, 404, { ok: false, error: 'unknown session' })
285
+ return
286
+ }
287
+ const outcome = pruner.decideReviewProposal(session, record.proposalId, record.decision)
288
+ if (outcome === undefined) {
289
+ reviewJson(res, 503, { ok: false, error: 'review mode is off for this session' })
290
+ return
291
+ }
292
+ if (!outcome.ok) {
293
+ reviewJson(res, 404, { ok: false, error: outcome.reason })
294
+ return
295
+ }
296
+ reviewJson(res, 200, { ok: true, sessionId: record.sessionId, proposalId: record.proposalId, decision: record.decision })
297
+ }
298
+
299
+ const register = (webServer: WebServerLike): void => {
300
+ const table: ReadonlyArray<{ path: string, handler: (req: unknown, res: unknown) => unknown }> = [
301
+ ...[...REVIEW_QUEUE_ROUTES].map(path => ({ path, handler: getHandler })),
302
+ ...[...REVIEW_DECIDE_ROUTES].map(path => ({ path, handler: (req: unknown, res: unknown) => { void decideHandler(req, res) } })),
303
+ ]
304
+ const disposers = table
305
+ .map(entry => webServer.register({ kind: 'exact', path: entry.path, handler: entry.handler }))
306
+ .filter((off): off is () => void => typeof off === 'function')
307
+ ctx.effect(
308
+ () => () => { for (const off of disposers) off() },
309
+ 'contextCompressionSelector.review routes',
310
+ )
311
+ registered()
312
+ }
313
+
314
+ const active = asWebServer(readService('webServer'))
315
+ if (active !== undefined) {
316
+ register(active)
317
+ return
318
+ }
319
+
320
+ ctx.inject(['webServer'], (injected) => {
321
+ const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
322
+ if (webServer === undefined) {
323
+ log('warn', 'context-compression webServer exposes no register() — review routes not registered')
324
+ return
325
+ }
326
+ register(webServer)
327
+ })
328
+
329
+ log('warn', 'context-compression webServer not active yet — review routes pending: %s', REVIEW_QUEUE_ROUTES.join(', '))
330
+ }
331
+
36
332
  /**
37
333
  * The one service the catalog route actually needs. `llm` and
38
334
  * `agentDefaultModel` are payload enrichment the handler resolves per request,
@@ -209,12 +505,20 @@ export interface Config {
209
505
  * covers both arrival orders.
210
506
  */
211
507
  estimatorCatalogRoute?: boolean
508
+ /**
509
+ * Register the review pipeline's HTTP routes (pending-queue read + decide
510
+ * write) on this row. Same Bundle opt-in semantics as
511
+ * `estimatorCatalogRoute`; without the routes the floating window has no
512
+ * transport and simply never appears.
513
+ */
514
+ reviewQueueRoute?: boolean
212
515
  }
213
516
 
214
517
  /** Loader validation for the standalone Bundle opt-in. */
215
518
  export const Config: z<Config> = z.object({
216
519
  presetOverlay: z.boolean().default(false),
217
520
  estimatorCatalogRoute: z.boolean().default(false),
521
+ reviewQueueRoute: z.boolean().default(false),
218
522
  })
219
523
 
220
524
  /** Register the persisted default read by the currently mounted root pruner. */
@@ -234,6 +538,10 @@ export function apply(ctx: Context, config: Config = {}): void {
234
538
  // service, because the route is host-wide rather than per-pruner-instance.
235
539
  if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
236
540
 
541
+ // TokenPilot-inspired R4: the review pipeline's client transport (see the
542
+ // config JSDoc for the opt-in semantics).
543
+ if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx)
544
+
237
545
  if (config.presetOverlay !== true) return
238
546
 
239
547
  ctx.inject(['agentPresets'], (presetsCtx) => {
@@ -135,6 +135,11 @@ export interface PresetOptionsSettings {
135
135
  readonly estimatorBaseUrl?: string
136
136
  readonly estimatorApiKey?: string
137
137
  readonly estimatorTimeoutMs?: number
138
+ /** Review-mode overrides (beta); mirrors the runtime PresetOptions.reviewMode. */
139
+ readonly reviewMode?: boolean
140
+ readonly reviewTimeoutTurns?: number
141
+ readonly cacheHitDiscountAlpha?: number
142
+ readonly reviewHighImpactTokens?: number
138
143
  }
139
144
 
140
145
  /**
@@ -148,9 +153,10 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
148
153
  const allowed = new Set([
149
154
  'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
150
155
  'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
156
+ 'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
151
157
  ])
152
158
  if (Object.keys(value).some(key => !allowed.has(key))) return undefined
153
- for (const key of ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'] as const) {
159
+ for (const key of ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'reviewMode'] as const) {
154
160
  const entry = value[key]
155
161
  if (entry !== undefined && typeof entry !== 'boolean') return undefined
156
162
  }
@@ -168,6 +174,23 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
168
174
  || estimatorTimeoutMs < 100 || estimatorTimeoutMs > 60_000)) {
169
175
  return undefined
170
176
  }
177
+ const reviewTimeoutTurns = value.reviewTimeoutTurns
178
+ if (reviewTimeoutTurns !== undefined
179
+ && (typeof reviewTimeoutTurns !== 'number' || !Number.isSafeInteger(reviewTimeoutTurns) || reviewTimeoutTurns < 1)) {
180
+ return undefined
181
+ }
182
+ const cacheHitDiscountAlpha = value.cacheHitDiscountAlpha
183
+ if (cacheHitDiscountAlpha !== undefined
184
+ && (typeof cacheHitDiscountAlpha !== 'number' || !Number.isFinite(cacheHitDiscountAlpha)
185
+ || cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) {
186
+ return undefined
187
+ }
188
+ const reviewHighImpactTokens = value.reviewHighImpactTokens
189
+ if (reviewHighImpactTokens !== undefined
190
+ && (typeof reviewHighImpactTokens !== 'number' || !Number.isSafeInteger(reviewHighImpactTokens)
191
+ || reviewHighImpactTokens < 0)) {
192
+ return undefined
193
+ }
171
194
  const decoded: {
172
195
  -readonly [K in keyof PresetOptionsSettings]: PresetOptionsSettings[K]
173
196
  } = {}
@@ -181,6 +204,10 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
181
204
  if (value.estimatorBaseUrl !== undefined) decoded.estimatorBaseUrl = value.estimatorBaseUrl as string
182
205
  if (value.estimatorApiKey !== undefined) decoded.estimatorApiKey = value.estimatorApiKey as string
183
206
  if (estimatorTimeoutMs !== undefined) decoded.estimatorTimeoutMs = estimatorTimeoutMs as number
207
+ if (value.reviewMode !== undefined) decoded.reviewMode = value.reviewMode as boolean
208
+ if (reviewTimeoutTurns !== undefined) decoded.reviewTimeoutTurns = reviewTimeoutTurns as number
209
+ if (cacheHitDiscountAlpha !== undefined) decoded.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
210
+ if (reviewHighImpactTokens !== undefined) decoded.reviewHighImpactTokens = reviewHighImpactTokens as number
184
211
  return decoded
185
212
  }
186
213
 
@@ -8,11 +8,24 @@
8
8
  import type { Session } from '@deepseek-ai/dsh-session'
9
9
  import type { DedupeTable } from '../runtime/tokenpilot/dedup.ts'
10
10
  import type { EstimatorFailures } from '../runtime/tokenpilot/estimator.ts'
11
+ import type { ReviewQueue, ReviewQueueStore } from '../runtime/tokenpilot/review-queue.ts'
11
12
  import type {
12
13
  ContextCompressionSettings,
13
14
  ResolvedConfig,
14
15
  } from '../runtime/types.ts'
15
16
 
17
+ /** Four-state per-session outcome counters behind the floating-window summary row. */
18
+ export interface ReviewSessionSummary {
19
+ /** Rewrites that landed through the automatic path while review mode served this session. */
20
+ autoApplied: number
21
+ /** Approved proposals whose merged batch executed with an applied receipt. */
22
+ reviewApplied: number
23
+ /** Pending proposals that expired unhandled at a turn boundary. */
24
+ expired: number
25
+ /** Approved proposals voided at the apply point (digest mismatch et al). */
26
+ voided: number
27
+ }
28
+
16
29
  /** Mutable per-session state bag used inside {@link ToolResultPruner}. */
17
30
  export interface PrunerState {
18
31
  /** Resolved immutable deployment configuration. */
@@ -40,4 +53,18 @@ export interface PrunerState {
40
53
  readonly tailTrimBoundaryAttempts: WeakMap<Session, object>
41
54
  /** Last effective policy audit key emitted for each Session. */
42
55
  readonly policyResolutionAudits: WeakMap<Session, string>
56
+ /**
57
+ * TokenPilot-inspired R4: shared review-queue store. Starts as the in-memory
58
+ * fail-open fallback; swapped to the storageDomain-backed adapter when (and
59
+ * if) that seam opens successfully.
60
+ */
61
+ reviewStore: ReviewQueueStore
62
+ /** Per-session review queue carrying the frozen timeout policy. */
63
+ readonly reviewQueues: WeakMap<Session, ReviewQueue>
64
+ /** Last observed turn index per Session: the monotonic clock for review expiries. */
65
+ readonly reviewClocks: WeakMap<Session, number>
66
+ /** Estimator-reported remaining turns Ŝ per Session; advisory only. */
67
+ readonly estimatorRemainingTurns: WeakMap<Session, number>
68
+ /** Four-state outcome counters per Session (floating-window summary row). */
69
+ readonly reviewSummaries: WeakMap<Session, ReviewSessionSummary>
43
70
  }