Skip to content

topic-memory

Manages the living content-topics.json — CRUD operations on topics, scoring history, revisit scheduling, performance data integration, and archive management. Use when reading, adding, updating, or archiving topics in the content intelligence system.

ModelSource
sonnetpack: content-pumper
Full Reference

┏━ 🧠 topic-memory ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Living state layer for the content intelligence ┃ ┃ system — reads, writes, archives, and scores ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Manages content-topics.json — the single source of truth for the Topic Brain system. All other skills read from or write to this file via the operations below. Idempotent — safe to call repeatedly.


{
"version": "1.0",
"lastUpdated": "<ISO timestamp>",
"config": {
"autoThreshold": 80,
"checkIntervalHours": 6,
"maxQueueSize": 50,
"scoringWeights": {
"freshness": 0.20,
"searchVolume": 0.20,
"socialBuzz": 0.15,
"polarizationHeat": 0.10,
"brandFit": 0.20,
"competitionGap": 0.15
}
},
"topics": [
{
"id": "<uuid>",
"title": "<string>",
"slug": "<string>",
"category": "<string>",
"verticals": ["<string>"],
"score": 0,
"signals": {
"freshness": 0,
"searchVolume": 0,
"socialBuzz": 0,
"trendVelocity": "rising | stable | falling",
"polarizationHeat": 0,
"brandFit": 0,
"competitionGap": 0
},
"sentiment": {
"polarized": false,
"sides": [
{
"label": "<string>",
"position": "<string>",
"estimatedSize": 0,
"triggers": ["<string>"],
"engagement": 0
}
]
},
"history": {
"firstSeen": "<ISO timestamp>",
"lastScored": "<ISO timestamp>",
"lastWritten": "<ISO timestamp | null>",
"timesWritten": 0,
"nextCheck": "<ISO timestamp>",
"articles": [
{
"url": "<string>",
"publishedAt": "<ISO timestamp>",
"site": "<string>",
"wordCount": 0
}
],
"performance": [
{
"recordedAt": "<ISO timestamp>",
"source": "ga4 | gsc",
"sessions": 0,
"clicks": 0,
"impressions": 0,
"avgPosition": 0,
"ctr": 0
}
]
},
"status": "discovered | queued | writing | published | monitoring | archived",
"targetMode": "evergreen | trending | seasonal",
"targetSites": ["<string>"]
}
],
"archive": []
}

Archive entries use the same structure as topics.


OperationInputAction
add-topictitle, category, verticals, targetMode, targetSitesAppend to topics[] with uuid, default signals (all 0), status=discovered
update-scoretopicIdRecalculate composite score from signals using config.scoringWeights
update-signalstopicId, signals patchMerge signal values, then call update-score
update-sentimenttopicId, sentiment objectReplace sentiment block, set lastScored
record-articletopicId, url, site, wordCountPush to history.articles, increment timesWritten, set lastWritten, call set-next-check
record-performancetopicId, source, metricsPush to history.performance[]
set-next-checktopicIdSet nextCheck = now + config.checkIntervalHours
archive-topictopicIdMove from topics[] to archive[], set status=archived
get-leaderboardReturn topics[] sorted by score descending
get-due-for-checkReturn topics[] where nextCheck < now

discovered
└─▸ queued (score ≥ autoThreshold, within maxQueueSize)
└─▸ writing (assigned to content-pumper)
└─▸ published (article recorded in history.articles)
└─▸ monitoring (tracking performance post-publish)
└─▸ archived (auto-archive rules triggered)

Normalize each signal to 0–100 before weighting:

composite = Σ(normalized_signal × config.scoringWeights[signal_name])

Signals mapped to weights:

  • freshness × 0.20
  • searchVolume × 0.20
  • socialBuzz × 0.15
  • polarizationHeat × 0.10
  • brandFit × 0.20
  • competitionGap × 0.15

trendVelocity is categorical (rising | stable | falling) — not included in the weighted sum but used in auto-archive rules and tie-breaking.


Archive a topic when ALL three conditions are true:

  • history.timesWritten > 3
  • signals.trendVelocity === "falling"
  • signals.socialBuzz < 0.2

Call archive-topic for any topic matching all three. Log the trigger in a note field on the archived entry.


Default path: content-topics.json at project root.

Override via env var CONTENT_TOPICS_PATH. Always read the full file, mutate in memory, write back atomically. Never partial-write.


SkillReadsWrites
trend-scannerconfig.checkIntervalHoursupdate-signals, add-topic
topic-scorertopics[], config.scoringWeightsupdate-score
sentiment-mappertopics[]update-sentiment
content-pumper-pimpget-leaderboard, get-due-for-checkrecord-article, status transitions
content-schedulerget-leaderboardset-next-check