signetai 0.164.4 → 0.165.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.
package/dist/mcp-stdio.js CHANGED
@@ -43925,6 +43925,7 @@ function buildRememberRequestBody(content, options = {}) {
43925
43925
  validFrom: options.validFrom,
43926
43926
  validUntil: options.validUntil,
43927
43927
  sourceCreatedAt: options.sourceCreatedAt,
43928
+ reviewAfter: options.reviewAfter,
43928
43929
  hints: options.hints,
43929
43930
  transcript: options.transcript,
43930
43931
  structured: normalizeStructuredMemoryPayload(options.structured),
@@ -47358,6 +47359,15 @@ function up105(db) {
47358
47359
  INSERT INTO entities_fts(entities_fts) VALUES ('rebuild');
47359
47360
  `);
47360
47361
  }
47362
+ function hasColumn17(db, table, column) {
47363
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
47364
+ }
47365
+ function up106(db) {
47366
+ if (!hasColumn17(db, "memories", "review_after")) {
47367
+ db.exec("ALTER TABLE memories ADD COLUMN review_after TEXT;");
47368
+ }
47369
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_review_after ON memories(review_after);");
47370
+ }
47361
47371
  var MIGRATIONS = [
47362
47372
  {
47363
47373
  version: 1,
@@ -48190,6 +48200,14 @@ var MIGRATIONS = [
48190
48200
  version: 105,
48191
48201
  name: "agent-scoped-entity-name",
48192
48202
  up: up105
48203
+ },
48204
+ {
48205
+ version: 106,
48206
+ name: "memory-review-after",
48207
+ up: up106,
48208
+ artifacts: {
48209
+ columns: [{ table: "memories", column: "review_after" }]
48210
+ }
48193
48211
  }
48194
48212
  ];
48195
48213
  var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
@@ -50699,8 +50717,8 @@ function txIngestEnvelope(db, mem) {
50699
50717
  importance, type, tags, pinned, is_deleted, extraction_status,
50700
50718
  embedding_model, extraction_model, created_at, updated_at, updated_by,
50701
50719
  source_type, source_id, source_path, runtime_path, idempotency_key, scope, agent_id, visibility,
50702
- memory_kind, evidence_meta)
50703
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(mem.id, mem.content, mem.normalizedContent ?? mem.content, mem.contentHash, mem.who, mem.why, mem.project, mem.importance, mem.type, mem.tags, mem.pinned, mem.isDeleted ?? 0, mem.extractionStatus ?? "none", mem.embeddingModel ?? null, mem.extractionModel ?? null, mem.createdAt, mem.createdAt, mem.updatedBy ?? mem.who, mem.sourceType, mem.sourceId, mem.sourcePath ?? null, mem.runtimePath ?? null, mem.idempotencyKey ?? null, mem.scope ?? null, mem.agentId ?? "default", mem.visibility ?? "global", mem.memoryKind ?? null, mem.evidenceMeta ?? null);
50720
+ memory_kind, evidence_meta, review_after)
50721
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(mem.id, mem.content, mem.normalizedContent ?? mem.content, mem.contentHash, mem.who, mem.why, mem.project, mem.importance, mem.type, mem.tags, mem.pinned, mem.isDeleted ?? 0, mem.extractionStatus ?? "none", mem.embeddingModel ?? null, mem.extractionModel ?? null, mem.createdAt, mem.createdAt, mem.updatedBy ?? mem.who, mem.sourceType, mem.sourceId, mem.sourcePath ?? null, mem.runtimePath ?? null, mem.idempotencyKey ?? null, mem.scope ?? null, mem.agentId ?? "default", mem.visibility ?? "global", mem.memoryKind ?? null, mem.evidenceMeta ?? null, mem.reviewAfter ?? null);
50704
50722
  return mem.id;
50705
50723
  }
50706
50724
  function txForgetMemory(db, input) {
@@ -50949,6 +50967,16 @@ function readString2(record5, key) {
50949
50967
  const trimmed = value.trim();
50950
50968
  return trimmed.length > 0 ? trimmed : null;
50951
50969
  }
50970
+ function readReviewAfter(payload) {
50971
+ const value = readString2(payload, "review_after");
50972
+ if (value === null)
50973
+ return null;
50974
+ const parsed = new Date(value);
50975
+ if (Number.isNaN(parsed.getTime())) {
50976
+ throw new OntologyProposalError("payload.review_after must be a valid ISO timestamp", 400);
50977
+ }
50978
+ return parsed.toISOString();
50979
+ }
50952
50980
  function readNumber(record5, key) {
50953
50981
  const value = record5[key];
50954
50982
  return typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -51243,6 +51271,7 @@ function materializeAttributeMemoryInTx(db, input) {
51243
51271
  sourcePath: input.proposal.source_path,
51244
51272
  agentId: input.agentId,
51245
51273
  visibility: "global",
51274
+ reviewAfter: input.reviewAfter,
51246
51275
  createdAt: now()
51247
51276
  });
51248
51277
  }
@@ -51359,6 +51388,7 @@ function applyAddClaimValue(db, agentId, proposal, payload) {
51359
51388
  const id = crypto.randomUUID();
51360
51389
  const confidence = clamp01(readNumber(payload, "confidence") ?? proposal.confidence);
51361
51390
  const importance = clamp01(readNumber(payload, "importance") ?? confidence);
51391
+ const reviewAfter = readReviewAfter(payload);
51362
51392
  const proposalEvidence = proposalAuditEvidence(proposal);
51363
51393
  db.prepare(`INSERT INTO entity_attributes
51364
51394
  (id, aspect_id, agent_id, kind, content, normalized_content,
@@ -51376,6 +51406,7 @@ function applyAddClaimValue(db, agentId, proposal, payload) {
51376
51406
  content: value,
51377
51407
  normalizedContent: normalized,
51378
51408
  importance,
51409
+ reviewAfter,
51379
51410
  proposal
51380
51411
  });
51381
51412
  return { entityId, aspectId, attributeId: id, memoryId, deduped: false };
@@ -51427,6 +51458,7 @@ function applySetClaimValue(db, agentId, proposal, payload) {
51427
51458
  const id = version2 === 1 ? rootId : crypto.randomUUID();
51428
51459
  const confidence = clamp01(readNumber(payload, "confidence") ?? proposal.confidence);
51429
51460
  const importance = clamp01(readNumber(payload, "importance") ?? confidence);
51461
+ const reviewAfter = readReviewAfter(payload);
51430
51462
  db.prepare(`INSERT INTO entity_attributes
51431
51463
  (id, aspect_id, agent_id, kind, content, normalized_content,
51432
51464
  confidence, importance, status, group_key, claim_key,
@@ -51442,6 +51474,7 @@ function applySetClaimValue(db, agentId, proposal, payload) {
51442
51474
  content: value,
51443
51475
  normalizedContent: normalized,
51444
51476
  importance,
51477
+ reviewAfter,
51445
51478
  proposal
51446
51479
  });
51447
51480
  if (active.length > 0) {
@@ -52331,6 +52364,7 @@ var aspectName = text.describe("The specific domain of knowledge this claim belo
52331
52364
  var aspectId = text.describe("The stable id of an existing aspect.");
52332
52365
  var claimValue = text.describe("A complete atomic assertion that is understandable on its own.");
52333
52366
  var claimKey = text.describe("A stable semantic slot key for versions of the same claim.");
52367
+ var reviewAfter = text.describe("An ISO timestamp after which a future temporal claim must be reviewed. Omit for non-temporal claims.").optional();
52334
52368
  var linkType = exports_external.enum(DEPENDENCY_TYPES2).describe("The dependency type between the two entities.");
52335
52369
  function payload(shape) {
52336
52370
  return exports_external.object(shape).passthrough();
@@ -52352,8 +52386,8 @@ var DREAMING_ONTOLOGY_PAYLOAD_SCHEMAS = {
52352
52386
  ...reasonField
52353
52387
  }),
52354
52388
  create_entity: payload({ name: entityName, type: entityType }),
52355
- add_claim_value: payload({ entityId, aspectId, claimKey, value: claimValue }),
52356
- set_claim_value: payload({ entityId, aspectId, claimKey, value: claimValue }),
52389
+ add_claim_value: payload({ entityId, aspectId, claimKey, value: claimValue, reviewAfter }),
52390
+ set_claim_value: payload({ entityId, aspectId, claimKey, value: claimValue, reviewAfter }),
52357
52391
  supersede_claim_value: payload({
52358
52392
  entityId,
52359
52393
  aspectId,
@@ -52656,7 +52690,13 @@ function toApplicatorPayload(accessor, agentId, operation2, payload2) {
52656
52690
  return null;
52657
52691
  const name = lookupEntityName(accessor, agentId, entityId2);
52658
52692
  const aspect = lookupAspectName(accessor, agentId, entityId2, aspectId2);
52659
- return name === null || aspect === null ? null : { entity: name, aspect, claim_key: claimKey2, value };
52693
+ return name === null || aspect === null ? null : {
52694
+ entity: name,
52695
+ aspect,
52696
+ claim_key: claimKey2,
52697
+ value,
52698
+ ...stringField(payload2, "reviewAfter") ? { review_after: stringField(payload2, "reviewAfter") } : {}
52699
+ };
52660
52700
  }
52661
52701
  case "supersede_claim_value": {
52662
52702
  const entityId2 = stringField(payload2, "entityId");
@@ -52937,6 +52977,77 @@ function readDreamingRunbook(accessor, agentId, limit = 5) {
52937
52977
  });
52938
52978
  }
52939
52979
 
52980
+ // ../../platform/daemon/src/pipeline/memory-review-due.ts
52981
+ function toReviewDueMemory(row) {
52982
+ return {
52983
+ id: row.id,
52984
+ content: row.content,
52985
+ type: row.type,
52986
+ importance: row.importance,
52987
+ reviewAfter: row.review_after,
52988
+ createdAt: row.created_at,
52989
+ agentId: row.agent_id,
52990
+ scope: row.scope,
52991
+ attributeId: row.attribute_id,
52992
+ entityId: row.entity_id,
52993
+ entityName: row.entity_name,
52994
+ aspectId: row.aspect_id,
52995
+ aspectName: row.aspect_name,
52996
+ claimKey: row.claim_key
52997
+ };
52998
+ }
52999
+ function reviewDueQuery(options) {
53000
+ return options.agentId ? {
53001
+ sql: "AND m.agent_id = ?",
53002
+ params: [options.agentId]
53003
+ } : { sql: "", params: [] };
53004
+ }
53005
+ var REVIEW_DUE_SELECT = `
53006
+ SELECT m.id, m.content, m.type, m.importance, m.review_after, m.created_at, m.agent_id, m.scope,
53007
+ ea.id AS attribute_id, a.entity_id, e.name AS entity_name,
53008
+ ea.aspect_id, a.name AS aspect_name, ea.claim_key
53009
+ FROM memories m
53010
+ LEFT JOIN entity_attributes ea
53011
+ ON ea.memory_id = m.id
53012
+ AND ea.agent_id = m.agent_id
53013
+ AND ea.status = 'active'
53014
+ LEFT JOIN entity_aspects a ON a.id = ea.aspect_id AND a.agent_id = ea.agent_id
53015
+ LEFT JOIN entities e ON e.id = a.entity_id AND e.agent_id = ea.agent_id
53016
+ WHERE m.review_after IS NOT NULL
53017
+ AND m.superseded_by IS NULL
53018
+ AND m.is_deleted = 0`;
53019
+ function findExpiredReviewDueMemories(accessor, now2 = new Date, opts = {}) {
53020
+ const limit = opts.limit ?? 50;
53021
+ const query = reviewDueQuery(opts);
53022
+ const rows = accessor.all(`${REVIEW_DUE_SELECT}
53023
+ AND m.review_after < ?
53024
+ ${query.sql}
53025
+ ORDER BY review_after ASC
53026
+ LIMIT ?`, now2.toISOString(), ...query.params, limit);
53027
+ return rows.map(toReviewDueMemory);
53028
+ }
53029
+ function findApproachingReviewDueMemories(accessor, now2 = new Date, opts = {}) {
53030
+ const windowMs = opts.expiringSoonMs ?? 7 * 24 * 60 * 60 * 1000;
53031
+ const limit = opts.limit ?? 50;
53032
+ const query = reviewDueQuery(opts);
53033
+ const horizon = new Date(now2.getTime() + windowMs);
53034
+ const rows = accessor.all(`${REVIEW_DUE_SELECT}
53035
+ AND m.review_after >= ?
53036
+ AND m.review_after <= ?
53037
+ ${query.sql}
53038
+ ORDER BY review_after ASC
53039
+ LIMIT ?`, now2.toISOString(), horizon.toISOString(), ...query.params, limit);
53040
+ return rows.map(toReviewDueMemory);
53041
+ }
53042
+ function collectReviewDueClaims(accessor, now2, options = {}) {
53043
+ const limit = options.limit ?? 50;
53044
+ const expired = findExpiredReviewDueMemories(accessor, now2, { ...options, limit });
53045
+ return {
53046
+ expired,
53047
+ approaching: expired.length >= limit ? [] : findApproachingReviewDueMemories(accessor, now2, { ...options, limit: limit - expired.length })
53048
+ };
53049
+ }
53050
+
52940
53051
  // ../../platform/daemon/src/pipeline/dreaming-capabilities.ts
52941
53052
  var bounded = (value, fallback, max) => Math.min(Math.max(Math.floor(value ?? fallback), 1), max);
52942
53053
  var MAX_EVIDENCE_EXCERPT_CHARS = 2000;
@@ -53251,23 +53362,55 @@ function createDreamingCapabilities(params) {
53251
53362
  }
53252
53363
  return { ok: true, passId: params.passId };
53253
53364
  }),
53254
- capability("attention_list", "List attention", "List attention records (the hygiene queue) by kind and resolution status. Omit agentId to see the whole install's queue (each record carries its owning agentId); pass agentId to narrow to one scope.", true, exports_external.object({
53365
+ capability("attention_list", "List attention", "List attention records by kind and resolution status. Use kind hygiene for the queue, or review_due for expired and approaching temporal claims. Omit agentId to see the whole install; pass agentId to narrow to one scope.", true, exports_external.object({
53255
53366
  agentId: exports_external.string().optional(),
53256
53367
  kind: exports_external.string().optional(),
53257
53368
  status: exports_external.enum(["pending", "resolved"]).optional(),
53258
53369
  limit: exports_external.number().finite().optional()
53259
- }), async ({ agentId: scopeId, kind, status, limit }) => ({
53260
- ok: true,
53261
- items: scopeId !== undefined ? getDreamingAttentionScoped(accessor, scopeId, {
53262
- kind,
53263
- status: status ?? "pending",
53264
- limit: bounded(limit, 20, 100)
53265
- }) : getDreamingAttentionAcrossScopes(accessor, {
53266
- kind,
53267
- status: status ?? "pending",
53268
- limit: bounded(limit, 50, 200)
53269
- })
53270
- })),
53370
+ }), async ({ agentId: scopeId, kind, status, limit }) => {
53371
+ if (kind === "review_due") {
53372
+ if (status === "resolved")
53373
+ return { ok: true, items: [] };
53374
+ const due = accessor.withReadDb((db) => collectReviewDueClaims({ all: (sql, ...params2) => db.prepare(sql).all(...params2) }, new Date, { agentId: scopeId, limit: bounded(limit, scopeId ? 50 : 100, scopeId ? 100 : 200) }));
53375
+ return {
53376
+ ok: true,
53377
+ items: [
53378
+ ...due.expired.map((item) => ({
53379
+ id: item.id,
53380
+ kind: "review_due",
53381
+ status: "pending",
53382
+ subjectRef: `memory:${item.id}`,
53383
+ details: { phase: "expired", ...item },
53384
+ priority: "high",
53385
+ createdAt: item.createdAt,
53386
+ agentId: item.agentId
53387
+ })),
53388
+ ...due.approaching.map((item) => ({
53389
+ id: item.id,
53390
+ kind: "review_due",
53391
+ status: "pending",
53392
+ subjectRef: `memory:${item.id}`,
53393
+ details: { phase: "approaching", ...item },
53394
+ priority: "normal",
53395
+ createdAt: item.createdAt,
53396
+ agentId: item.agentId
53397
+ }))
53398
+ ]
53399
+ };
53400
+ }
53401
+ return {
53402
+ ok: true,
53403
+ items: scopeId !== undefined ? getDreamingAttentionScoped(accessor, scopeId, {
53404
+ kind,
53405
+ status: status ?? "pending",
53406
+ limit: bounded(limit, 20, 100)
53407
+ }) : getDreamingAttentionAcrossScopes(accessor, {
53408
+ kind,
53409
+ status: status ?? "pending",
53410
+ limit: bounded(limit, 50, 200)
53411
+ })
53412
+ };
53413
+ }),
53271
53414
  capability("apply_ontology_ops", "Apply ontology operations", 'Apply every semantic write through the daemon audit seam in one batch, in one agent scope (pass the agentId whose graph you are maintaining — hygiene attention records belong to the agent that flagged them). Ops are processed in array order. Hygiene ops (flag, archive_*, merge_entities) cite provenance: "attention:$<index>" for a flag earlier in the same batch, or "attention:<uuid>" from a prior batch. Content-bearing ops cite evidence with exact quotes from canonical episodic evidence in that scope.', false, exports_external.object({
53272
53415
  agentId: exports_external.string().min(1),
53273
53416
  operations: exports_external.array(DREAMING_ONTOLOGY_OPERATION_SCHEMA).min(1).max(100)
@@ -55319,6 +55462,7 @@ async function createMcpServer(opts) {
55319
55462
  sourceCreatedAt: exports_external.string().optional().describe("Source-system creation time for this memory"),
55320
55463
  validFrom: exports_external.string().optional().describe("Start of validity window for this memory"),
55321
55464
  validUntil: exports_external.string().optional().describe("End of validity window for this memory"),
55465
+ reviewAfter: exports_external.string().optional().describe("ISO timestamp when this temporal claim becomes due for review (issue #945)"),
55322
55466
  transcript: exports_external.string().optional().describe("Raw source text (conversation transcript) to preserve alongside extracted memory"),
55323
55467
  structured: exports_external.object({
55324
55468
  entities: exports_external.array(exports_external.object({
@@ -55370,7 +55514,8 @@ async function createMcpServer(opts) {
55370
55514
  observedAt,
55371
55515
  sourceCreatedAt,
55372
55516
  validFrom,
55373
- validUntil
55517
+ validUntil,
55518
+ reviewAfter: reviewAfter2
55374
55519
  }) => {
55375
55520
  const result = await fetchDaemon(baseUrl, "/api/memory/remember", {
55376
55521
  method: "POST",
@@ -55387,7 +55532,8 @@ async function createMcpServer(opts) {
55387
55532
  observedAt,
55388
55533
  sourceCreatedAt,
55389
55534
  validFrom,
55390
- validUntil
55535
+ validUntil,
55536
+ reviewAfter: reviewAfter2
55391
55537
  })
55392
55538
  });
55393
55539
  if (!result.ok) {
@@ -1,43 +1,43 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.164.4",
3
+ "version": "0.165.0",
4
4
  "assets": [
5
5
  {
6
6
  "name": "signet-darwin-arm64",
7
7
  "platform": "darwin-arm64",
8
- "sha256": "abbd69eccef51280c46829cb3e38eb61bdb879343df0a5dff4164d8317f7c070",
9
- "size": 116912800
8
+ "sha256": "cfa00e0e08296e60030ec256596115e23360e3d113456dd1b4117a17f2a09f26",
9
+ "size": 116929312
10
10
  },
11
11
  {
12
12
  "name": "signet-darwin-x64",
13
13
  "platform": "darwin-x64",
14
- "sha256": "055d857332bb87a719016723faffa58e74f58cc9183fc24f2c2f19207c5eb2f4",
15
- "size": 121539136
14
+ "sha256": "bfa798eefc6a1e502186b32678ee9d0a6c4edef5d14f57aa70fafb1c69f8f5c7",
15
+ "size": 121555520
16
16
  },
17
17
  {
18
18
  "name": "signet-linux-arm64",
19
19
  "platform": "linux-arm64",
20
- "sha256": "4e3b13e685a462475c65080692020de73527d30dccc86b9dfb8f9677ba6062df",
21
- "size": 154151808
20
+ "sha256": "9e5dfbd7c4664cde0ccb882b642fb413e7ee1a1c61821514d7d451c8685a4d9b",
21
+ "size": 154173882
22
22
  },
23
23
  {
24
24
  "name": "signet-linux-x64",
25
25
  "platform": "linux-x64",
26
- "sha256": "691d75e564fd8fd4bea0a5639ef56ca167cf95313a260e7d7e80048dd3c00787",
27
- "size": 154710793
26
+ "sha256": "c0740baf74799c1b229e41a0231360700e0b536adfd538d70ead1fa55d4bc841",
27
+ "size": 154732867
28
28
  },
29
29
  {
30
30
  "name": "signet-win32-x64.exe",
31
31
  "platform": "win32-x64",
32
- "sha256": "03b1fcc14ccf1dd10bf85d8f023015f545ad9616c528fc02b37ca627dbb436ef",
33
- "size": 170844160
32
+ "sha256": "2e3291f8f52a1aec46c9e207c3d58f079046f12e9890b148a3b47197c3c116ee",
33
+ "size": 170866176
34
34
  }
35
35
  ],
36
36
  "components": {
37
37
  "connectors": {
38
- "url": "signet-connectors-0.164.4.tar.gz",
39
- "sha256": "4dc3d1ff69b24c59d751c12df729edc1c6a2bea4a227401815aa602363afe972",
40
- "size": 16156
38
+ "url": "signet-connectors-0.165.0.tar.gz",
39
+ "sha256": "59cfe5b98223652be028e51f99d927681c6cdd6da1b4e6237582e94cae40b071",
40
+ "size": 16248
41
41
  }
42
42
  }
43
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signetai",
3
- "version": "0.164.4",
3
+ "version": "0.165.0",
4
4
  "description": "Signet native CLI installer wrapper",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,10 +65,10 @@
65
65
  "access": "public"
66
66
  },
67
67
  "optionalDependencies": {
68
- "signetai-darwin-arm64": "https://github.com/Signet-AI/signetai/releases/download/v0.164.4/signetai-darwin-arm64-0.164.4.tgz",
69
- "signetai-darwin-x64": "https://github.com/Signet-AI/signetai/releases/download/v0.164.4/signetai-darwin-x64-0.164.4.tgz",
70
- "signetai-linux-arm64": "https://github.com/Signet-AI/signetai/releases/download/v0.164.4/signetai-linux-arm64-0.164.4.tgz",
71
- "signetai-linux-x64": "https://github.com/Signet-AI/signetai/releases/download/v0.164.4/signetai-linux-x64-0.164.4.tgz",
72
- "signetai-win32-x64": "https://github.com/Signet-AI/signetai/releases/download/v0.164.4/signetai-win32-x64-0.164.4.tgz"
68
+ "signetai-darwin-arm64": "https://github.com/Signet-AI/signetai/releases/download/v0.165.0/signetai-darwin-arm64-0.165.0.tgz",
69
+ "signetai-darwin-x64": "https://github.com/Signet-AI/signetai/releases/download/v0.165.0/signetai-darwin-x64-0.165.0.tgz",
70
+ "signetai-linux-arm64": "https://github.com/Signet-AI/signetai/releases/download/v0.165.0/signetai-linux-arm64-0.165.0.tgz",
71
+ "signetai-linux-x64": "https://github.com/Signet-AI/signetai/releases/download/v0.165.0/signetai-linux-x64-0.165.0.tgz",
72
+ "signetai-win32-x64": "https://github.com/Signet-AI/signetai/releases/download/v0.165.0/signetai-win32-x64-0.165.0.tgz"
73
73
  }
74
74
  }